import process from 'node:process'
import { createInterface } from 'node:readline'
import { Command } from 'commander'
import { blocksText, reDecode } from '../../daemon/decoders/reparse.ts'
import { logger } from '../../core/logger.ts'
import { DAEMON_BASE_URL } from '../../core/paths.ts'
import { formatCost, formatDuration, formatTime } from '../tui/table.ts'

type ReplayOptions = {
  mock: boolean
  step: boolean
  verify: boolean
  filter?: string[]
}

type ActionSummary = {
  id: string
  kind: string
  sourceAgent: string
  ts: number
  durMs: number
  costUsd: number
  // biome-ignore lint/suspicious/noExplicitAny: payload shape is per-kind.
  payload: any
  rawReq?: string | null
  rawRes?: string | null
}

type RunDetail = {
  run: {
    id: string
    threadId: string
    agent: string
    status: string
    startedAt: number
    durMs: number | null
    costUsd: number
    actionCount: number
  }
  actions: ActionSummary[]
}

export const replayCommand = new Command('replay')
  .description('Replay a run locally from stored data (OSS: mock mode only).')
  .argument('<run-id>', 'Run ID to replay.')
  .option(
    '--mock',
    '(default in OSS) use stored responses instead of calling the real provider.',
    true,
  )
  .option('--step', 'Step through actions interactively (enter = next, q = quit).', false)
  .option(
    '--verify',
    'Re-decode each call from its stored raw bytes and check it matches the captured payload.',
    false,
  )
  .option('--filter <kinds...>', 'Replay only certain action kinds (e.g. model_call).')
  .action(async (runId: string, options: ReplayOptions) => {
    // Fetch with raw so replay is deterministic against the captured wire
    // bytes when present (the truth for replay); falls back to the
    // normalized payload for older traces.
    const url = `${DAEMON_BASE_URL}/api/runs/${encodeURIComponent(runId)}?raw=1`
    let res: Response
    try {
      res = await fetch(url, { signal: AbortSignal.timeout(3000) })
    } catch {
      logger.print('Daemon not reachable. Start it with `openthomas wire` or `openthomas daemon`.')
      process.exitCode = 1
      return
    }
    if (res.status === 404) {
      logger.print(`Run not found: ${runId}`)
      process.exitCode = 1
      return
    }
    if (!res.ok) {
      logger.print(`Daemon error: HTTP ${res.status}`)
      process.exitCode = 1
      return
    }

    const detail = (await res.json()) as RunDetail
    const r = detail.run

    let actions = detail.actions
    if (options.filter?.length) {
      const keep = new Set(options.filter)
      actions = actions.filter((a) => keep.has(a.kind))
    }

    logger.print(
      `Replaying ${r.id} · ${r.agent} · ${actions.length} action${actions.length === 1 ? '' : 's'}` +
        ' (mock — stored responses, no upstream calls)',
    )
    logger.print('')

    if (actions.length === 0) {
      logger.print('(nothing to replay)')
      return
    }

    for (let i = 0; i < actions.length; i++) {
      const a = actions[i]
      if (!a) continue
      logger.print(`━━ step ${i + 1}/${actions.length} · ${a.kind} · ${formatTime(a.ts)} ━━`)
      renderStep(a)
      if (options.verify) await verifyStep(a)
      logger.print('')

      if (options.step && i < actions.length - 1) {
        const cmd = await prompt('  [enter] next · [q] quit ')
        if (cmd === 'quit') {
          logger.print('(stopped)')
          return
        }
      }
    }

    logger.print(
      `done — replayed ${actions.length} action${actions.length === 1 ? '' : 's'}, ` +
        `${formatCost(r.costUsd ?? sumCost(actions))} total`,
    )
  })

async function verifyStep(a: ActionSummary): Promise<void> {
  if (a.kind !== 'model_call') return
  if (a.rawRes == null) {
    logger.print('    verify: skipped (no raw response stored)')
    return
  }
  const p = a.payload ?? {}
  const enc = new TextEncoder()
  const result = await reDecode({
    protocol: typeof p.protocol === 'string' ? p.protocol : '',
    rawReq: a.rawReq != null ? enc.encode(a.rawReq) : null,
    rawRes: enc.encode(a.rawRes),
    stream: p.stream === true,
  })
  if (!result.ok) {
    logger.print(`    verify: skipped (${result.reason})`)
    return
  }
  const reText = blocksText(result.value.response)
  const storedText = blocksText(Array.isArray(p.response) ? p.response : [])
  if (reText === storedText) {
    logger.print('    verify: ✓ re-decode byte-exact match')
  } else {
    logger.print('    verify: ✗ re-decode differs from stored payload')
  }
}

function renderStep(a: ActionSummary): void {
  if (a.kind === 'model_call') {
    const p = a.payload ?? {}
    logger.print(`  → ${p.model ?? '?'}  ${p.endpoint ?? ''}`)
    const sys = typeof p.systemPrompt === 'string' ? p.systemPrompt : ''
    if (sys) logger.print(`    system: ${truncate(sys, 160)}`)
    if (Array.isArray(p.messages) && p.messages.length > 0) {
      logger.print(
        `    request: ${p.messages.length} message(s); last → ${truncate(lastUserText(p.messages), 200)}`,
      )
    }
    logger.print(
      `  ← response${a.rawRes != null ? ' (from stored wire bytes)' : ' (from stored payload)'}:`,
    )
    renderResponseBlocks(p.response)
    if (p.error) logger.print(`    error: ${truncate(String(p.error), 200)}`)
    if (a.costUsd > 0) {
      logger.print(`    cost: ${formatCost(a.costUsd)}  (${formatDuration(a.durMs)})`)
    }
  } else if (a.kind === 'mcp_call') {
    const p = a.payload ?? {}
    const arrow = p.direction === 'request' ? '→' : p.direction === 'response' ? '←' : '·'
    logger.print(`  ${arrow} mcp ${p.server ?? '?'}${p.method ? ` · ${p.method}` : ''}`)
    if (p.params !== undefined)
      logger.print(`    params: ${truncate(JSON.stringify(p.params), 200)}`)
    if (p.result !== undefined)
      logger.print(`    result: ${truncate(JSON.stringify(p.result), 200)}`)
  } else {
    logger.print(`  ${truncate(JSON.stringify(a.payload), 200)}`)
  }
}

function renderResponseBlocks(response: unknown): void {
  if (!Array.isArray(response) || response.length === 0) {
    logger.print('    (no response blocks)')
    return
  }
  for (const b of response) {
    // biome-ignore lint/suspicious/noExplicitAny: block shape is per-type.
    const block: any = b
    if (block?.type === 'text') {
      logger.print(`    [text] ${truncate(block.text ?? '', 300)}`)
    } else if (block?.type === 'tool_use') {
      logger.print(`    [tool_use ${block.name}] ${truncate(JSON.stringify(block.input), 200)}`)
    } else if (block?.type === 'thinking') {
      logger.print(`    [thinking] ${truncate(block.text ?? '', 200)}`)
    } else {
      logger.print(`    [${block?.type ?? 'unknown'}]`)
    }
  }
}

function lastUserText(messages: unknown[]): string {
  for (let i = messages.length - 1; i >= 0; i--) {
    const m = messages[i] as { role?: string; content?: unknown }
    if (m?.role !== 'user') continue
    if (typeof m.content === 'string') return m.content
    if (Array.isArray(m.content)) {
      const text = m.content
        .map((c) =>
          c && typeof c === 'object' && (c as { type?: string }).type === 'text'
            ? ((c as { text?: string }).text ?? '')
            : '',
        )
        .filter(Boolean)
        .join(' ')
      if (text) return text
    }
  }
  return '(no user text)'
}

function sumCost(actions: ActionSummary[]): number {
  return actions.reduce((s, a) => s + (a.costUsd ?? 0), 0)
}

function truncate(s: string, n: number): string {
  if (s.length <= n) return s
  return `${s.slice(0, n)}… (+${s.length - n} chars)`
}

function prompt(question: string): Promise<'next' | 'quit'> {
  const rl = createInterface({ input: process.stdin, output: process.stdout })
  return new Promise((resolve) => {
    rl.question(question, (ans) => {
      rl.close()
      resolve(ans.trim().toLowerCase() === 'q' ? 'quit' : 'next')
    })
  })
}
