// Re-decode a captured call from its stored raw bytes — the foundation for
// deterministic replay ("does the decoder still produce the same normalized
// payload from the same wire bytes?") and for re-running old traces through a
// newer decoder. Reuses the same SSE parsers the live decoders use, so the
// streaming paths can't drift from capture.
//
// Coverage: streamed responses for all three protocols (the dominant real
// case — Claude Code, Codex, OpenClaw all stream) plus non-streamed Anthropic
// (its `content` array is already NormalizedBlock-shaped). Non-streamed
// OpenAI assembles blocks inline in its decoder, so reDecode reports it as
// unsupported rather than risk a divergent re-implementation.

import type { NormalizedBlock } from '../../core/packet.ts'
import { parseAnthropicStream } from './anthropic/sse.ts'
import { collectOutputBlocks, parseOpenAIResponsesStream } from './openai/responses-sse.ts'
import { inputToMessages } from './openai/responses.ts'
import { parseOpenAIChatStream } from './openai/sse.ts'

// biome-ignore lint/suspicious/noExplicitAny: third-party JSON shapes.
type Any = any

export type ReDecoded = {
  model: string
  messages: unknown[]
  systemPrompt?: string
  response: NormalizedBlock[]
}

export type ReDecodeResult = { ok: true; value: ReDecoded } | { ok: false; reason: string }

function streamFromBytes(bytes: Uint8Array): ReadableStream<Uint8Array> {
  return new ReadableStream<Uint8Array>({
    start(controller) {
      controller.enqueue(bytes)
      controller.close()
    },
  })
}

function parseJson(bytes: Uint8Array | null): Any {
  if (!bytes) return null
  try {
    return JSON.parse(new TextDecoder().decode(bytes))
  } catch {
    return null
  }
}

/** Re-derive the normalized request fields + response blocks from stored raw
 *  bytes. `stream` / `protocol` come from the stored payload metadata. */
export async function reDecode(args: {
  protocol: string
  rawReq: Uint8Array | null
  rawRes: Uint8Array | null
  stream: boolean
}): Promise<ReDecodeResult> {
  const { protocol, rawReq, rawRes, stream } = args
  if (!rawRes) return { ok: false, reason: 'no raw response captured' }

  const req = parseJson(rawReq) ?? {}
  let messages: unknown[] = []
  let systemPrompt: string | undefined
  let model = ''
  let response: NormalizedBlock[] = []

  try {
    if (protocol === 'anthropic') {
      messages = Array.isArray(req.messages) ? req.messages : []
      systemPrompt = typeof req.system === 'string' ? req.system : undefined
      model = typeof req.model === 'string' ? req.model : ''
      if (stream) {
        const parsed = await parseAnthropicStream(streamFromBytes(rawRes))
        response = parsed.blocks
        if (parsed.model) model = parsed.model
      } else {
        const json = parseJson(rawRes)
        if (Array.isArray(json?.content)) response = json.content as NormalizedBlock[]
        if (typeof json?.model === 'string') model = json.model
      }
    } else if (protocol === 'openai-chat') {
      messages = Array.isArray(req.messages) ? req.messages : []
      model = typeof req.model === 'string' ? req.model : ''
      if (!stream) return { ok: false, reason: 're-decode supports streamed openai-chat only' }
      const parsed = await parseOpenAIChatStream(streamFromBytes(rawRes))
      response = parsed.blocks
      if (parsed.model) model = parsed.model
    } else if (protocol === 'openai-responses') {
      messages = inputToMessages(req.input)
      systemPrompt = typeof req.instructions === 'string' ? req.instructions : undefined
      model = typeof req.model === 'string' ? req.model : ''
      if (stream) {
        const parsed = await parseOpenAIResponsesStream(streamFromBytes(rawRes))
        response = parsed.blocks
        if (parsed.model) model = parsed.model
      } else {
        const json = parseJson(rawRes)
        response = collectOutputBlocks(json?.output)
        if (typeof json?.model === 'string') model = json.model
      }
    } else {
      return { ok: false, reason: `re-decode not available for protocol "${protocol}"` }
    }
  } catch (err) {
    return { ok: false, reason: `re-decode error: ${(err as Error).message}` }
  }

  return { ok: true, value: { model, messages, systemPrompt, response } }
}

/** Flatten response blocks to their text, for a content comparison that
 *  ignores incidental ordering of metadata. */
export function blocksText(blocks: NormalizedBlock[]): string {
  const parts: string[] = []
  for (const b of blocks) {
    if (b.type === 'text' || b.type === 'thinking') parts.push(b.text)
    else if (b.type === 'tool_use') parts.push(`tool_use:${b.name}:${JSON.stringify(b.input)}`)
  }
  return parts.join('\n')
}
