import Foundation

/// Translates `codex exec --json` stdout lines into normalized event kinds (ADAPTERS §3).
/// This is the **unattended** surface and is a *distinct schema* from the app-server
/// (`CodexAppServerDecoder`): events are tagged by a top-level `type` with **dotted** names
/// (`thread.started`, `item.completed`) and every field is **snake_case** — confirmed against
/// codex-cli 0.141.0. Lenient by contract: unknown shapes become `.raw`.
public final class CodexExecDecoder {
    public struct Decoded: Sendable, Equatable {
        public let nativeType: String?
        public let kind: AgentEvent.Kind
        public init(nativeType: String?, kind: AgentEvent.Kind) {
            self.nativeType = nativeType
            self.kind = kind
        }
    }

    private enum Phase { case started, updated, completed }
    private var startedToolCallIDs: Set<String> = []

    public init() {}

    public func decode(line: Data) -> [Decoded] {
        guard !line.isEmpty else { return [] }
        guard let root = try? JSONValue(parsing: line), root.objectValue != nil else {
            let text = String(decoding: line, as: UTF8.self)
            return [Decoded(nativeType: nil, kind: .raw(Raw(native: .string(text))))]
        }
        switch root["type"]?.stringValue {
        case "thread.started":
            let started = SessionStarted(
                backendSessionID: root["thread_id"]?.stringValue ?? "",
                model: "", cwd: "", toolNames: [], nativeTranscriptPath: nil)
            return [Decoded(nativeType: "thread.started", kind: .sessionStarted(started))]
        case "turn.started":
            return []
        case "turn.completed":
            return decodeTurnCompleted(root)
        case "turn.failed":
            return [decodeError(root["error"] ?? root, native: "turn.failed")]
        case "item.started":
            return decodeItem(root["item"], phase: .started)
        case "item.updated":
            return decodeItem(root["item"], phase: .updated)
        case "item.completed":
            return decodeItem(root["item"], phase: .completed)
        case "error":
            return [decodeError(root, native: "error")]
        case let other:
            return [Decoded(nativeType: other, kind: .raw(Raw(native: root)))]
        }
    }

    private func decodeTurnCompleted(_ root: JSONValue) -> [Decoded] {
        var out: [Decoded] = []
        if let u = root["usage"] {
            let input = u["input_tokens"]?.intValue
            let cached = u["cached_input_tokens"]?.intValue
            let context = (input != nil || cached != nil) ? (input ?? 0) + (cached ?? 0) : nil
            let usage = Usage(
                inputTokens: input, cachedInputTokens: cached, cacheCreationInputTokens: nil,
                outputTokens: u["output_tokens"]?.intValue,
                reasoningTokens: u["reasoning_output_tokens"]?.intValue,
                costUSD: nil, contextInputTokens: context)
            out.append(Decoded(nativeType: "turn.completed", kind: .usage(usage)))
        }
        out.append(
            Decoded(
                nativeType: "turn.completed",
                kind: .turnCompleted(TurnCompleted(stopReason: "completed", usage: nil))))
        return out
    }

    private func decodeItem(_ item: JSONValue?, phase: Phase) -> [Decoded] {
        guard let item, let type = item["type"]?.stringValue else {
            return item.map { [Decoded(nativeType: "item", kind: .raw(Raw(native: $0)))] } ?? []
        }
        switch type {
        case "agent_message":
            guard phase == .completed else { return [] }
            return [
                Decoded(
                    nativeType: "item/agent_message",
                    kind: .assistantText(
                        TextChunk(
                            messageID: item["id"]?.stringValue ?? "msg-unknown",
                            text: item["text"]?.stringValue ?? "", isPartial: false)))
            ]

        case "reasoning":
            guard phase == .completed else { return [] }
            return [
                Decoded(
                    nativeType: "item/reasoning",
                    kind: .thinking(
                        TextChunk(
                            messageID: item["id"]?.stringValue ?? "msg-unknown",
                            text: item["text"]?.stringValue ?? "", isPartial: false)))
            ]

        case "command_execution":
            return decodeCommand(item, phase: phase)

        case "file_change":
            return decodeFileChange(item, phase: phase)

        case "mcp_tool_call":
            return decodeMCP(item, phase: phase)

        case "web_search":
            let id = item["id"]?.stringValue ?? "web-unknown"
            let call = ToolCall(
                toolCallID: id, name: "WebSearch",
                input: .object(["query": .string(item["query"]?.stringValue ?? "")]))
            guard phase == .completed else {
                return startedToolCallIDs.insert(id).inserted
                    ? [Decoded(nativeType: "item/web_search", kind: .toolCallStarted(call))] : []
            }
            return [Decoded(nativeType: "item/web_search", kind: .toolCallCompleted(call))]

        case "error":
            guard phase == .completed else { return [] }
            return [decodeError(item, native: "item/error")]

        default:
            guard phase == .completed else { return [] }
            return [Decoded(nativeType: "item/\(type)", kind: .raw(Raw(native: item)))]
        }
    }

    /// `command_execution` → a `Bash` call (normalized) so it shares the command-summary UI.
    private func decodeCommand(_ item: JSONValue, phase: Phase) -> [Decoded] {
        let id = item["id"]?.stringValue ?? "cmd-unknown"
        let call = ToolCall(
            toolCallID: id, name: "Bash",
            input: .object(["command": .string(item["command"]?.stringValue ?? "")]))
        var out: [Decoded] = []
        if startedToolCallIDs.insert(id).inserted {
            out.append(Decoded(nativeType: "item/command_execution", kind: .toolCallStarted(call)))
        }
        guard phase == .completed else { return out }
        out.append(Decoded(nativeType: "item/command_execution", kind: .toolCallCompleted(call)))
        let exitCode = item["exit_code"]?.intValue
        let isError = (item["status"]?.stringValue == "failed") || (exitCode != nil && exitCode != 0)
        out.append(
            Decoded(
                nativeType: "item/command_execution",
                kind: .toolResult(
                    ToolResult(
                        toolCallID: id, content: item["aggregated_output"] ?? .string(""),
                        isError: isError))))
        return out
    }

    /// `file_change` (native + authoritative). The exec schema's `kind` is a plain string
    /// (`"add"|"delete"|"update"`) — unlike app-server's tagged object.
    private func decodeFileChange(_ item: JSONValue, phase: Phase) -> [Decoded] {
        guard phase == .completed, item["status"]?.stringValue != "failed" else { return [] }
        let id = item["id"]?.stringValue
        return (item["changes"]?.arrayValue ?? []).compactMap { change in
            guard let path = change["path"]?.stringValue else { return nil }
            let kind: FileChange.ChangeKind
            switch change["kind"]?.stringValue {
            case "add": kind = .add
            case "delete": kind = .delete
            default: kind = .update
            }
            return Decoded(
                nativeType: "item/file_change",
                kind: .fileChange(FileChange(path: path, kind: kind, toolCallID: id, diffHint: nil)))
        }
    }

    private func decodeMCP(_ item: JSONValue, phase: Phase) -> [Decoded] {
        let id = item["id"]?.stringValue ?? "mcp-unknown"
        let call = ToolCall(
            toolCallID: id,
            name: "mcp__\(item["server"]?.stringValue ?? "server")__\(item["tool"]?.stringValue ?? "tool")",
            input: item["arguments"] ?? .object([:]))
        var out: [Decoded] = []
        if startedToolCallIDs.insert(id).inserted {
            out.append(Decoded(nativeType: "item/mcp_tool_call", kind: .toolCallStarted(call)))
        }
        guard phase == .completed else { return out }
        out.append(Decoded(nativeType: "item/mcp_tool_call", kind: .toolCallCompleted(call)))
        out.append(
            Decoded(
                nativeType: "item/mcp_tool_call",
                kind: .toolResult(
                    ToolResult(
                        toolCallID: id, content: item["result"] ?? .null,
                        isError: item["status"]?.stringValue == "failed"))))
        return out
    }

    private func decodeError(_ obj: JSONValue, native: String) -> Decoded {
        let message = obj["message"]?.stringValue ?? obj["error"]?.stringValue ?? "Error"
        return Decoded(
            nativeType: native,
            kind: .error(AgentError(recoverable: false, message: message, native: obj)))
    }
}
