import Foundation
import NucleicProtocol

/// Stateful, platform-neutral transcript projection for non-Swift renderers.
///
/// The Windows DLL feeds this the same `HostMsg` JSON it sends to the C# state store.  Messages
/// which change a transcript produce a single minimal splice: preserve the common prefix and
/// suffix, delete the changed middle, and insert its replacement.  That shape maps directly onto
/// WinUI's `ItemsRepeater` without exposing Swift implementation types across the ABI.
public final class TranscriptProjectionABI {
    private final class SessionState {
        var events: [AgentEvent] = []
        var items: [TranscriptItem] = []
        let projector = IncrementalTranscriptProjection()
    }

    private var sessions: [SessionID: SessionState] = [:]

    public init() {}

    /// Apply one host message and return canonical, versioned render-diff JSON.
    ///
    /// Messages unrelated to transcripts return `{"splices":[],"version":1}`. Snapshot, live
    /// event, full-history chunk, and revert messages update only their named session.
    public func apply(_ message: HostMsg) throws -> String {
        let update: (SessionID, [AgentEvent])?
        switch message {
        case .snapshot(let snapshot):
            update = (snapshot.summary.sessionID, snapshot.recentEvents)
        case .events(let batch):
            update = (batch.sessionID, batch.events)
        case .transcriptChunk(let chunk):
            update = (chunk.sessionID, chunk.events)
        case .transcriptReverted(let reverted):
            let state = sessions[reverted.sessionID] ?? SessionState()
            sessions[reverted.sessionID] = state
            if reverted.throughSeq == 0 {
                state.events.removeAll()
            } else {
                state.events.removeAll { $0.seq > reverted.throughSeq }
            }
            return try renderDiff(sessionID: reverted.sessionID, state: state)
        default:
            return JSONValue.object([
                "version": .number(1),
                "splices": .array([]),
            ]).canonicalString()
        }

        guard let (sessionID, incoming) = update else {
            preconditionFailure("transcript update switch must assign an update")
        }
        let state = sessions[sessionID] ?? SessionState()
        sessions[sessionID] = state
        state.events = Self.merged(state.events, incoming)
        return try renderDiff(sessionID: sessionID, state: state)
    }

    private func renderDiff(sessionID: SessionID, state: SessionState) throws -> String {
        let previous = state.items
        let next = state.projector.items(
            for: state.events, showRaw: false, showLockEvents: true)
        state.items = next

        var prefix = 0
        while prefix < previous.count, prefix < next.count, previous[prefix] == next[prefix] {
            prefix += 1
        }

        var suffix = 0
        while suffix < previous.count - prefix, suffix < next.count - prefix,
              previous[previous.count - suffix - 1] == next[next.count - suffix - 1] {
            suffix += 1
        }

        let splices: [JSONValue]
        if prefix == previous.count, prefix == next.count {
            splices = []
        } else {
            let inserted = next[prefix..<(next.count - suffix)].map(Self.json)
            splices = [.object([
                "start": .number(Double(prefix)),
                "deleteCount": .number(Double(previous.count - prefix - suffix)),
                "items": .array(inserted),
            ])]
        }

        return JSONValue.object([
            "version": .number(1),
            "sessionID": .string(sessionID.rawValue),
            "splices": .array(splices),
        ]).canonicalString()
    }

    private static func merged(_ old: [AgentEvent], _ incoming: [AgentEvent]) -> [AgentEvent] {
        var bySeq = Dictionary(uniqueKeysWithValues: old.map { ($0.seq, $0) })
        for event in incoming { bySeq[event.seq] = event }
        return bySeq.values.sorted { $0.seq < $1.seq }
    }

    // MARK: Render DTO

    private static func json(_ item: TranscriptItem) -> JSONValue {
        var value: [String: JSONValue] = [
            "id": .string(item.id),
            // JSON has no UInt64 type; keep the stable scroll/order anchor exact even above
            // JavaScript's 53-bit integer ceiling. C# parses this decimal string as UInt64.
            "seq": .string(String(item.seq)),
        ]
        switch item.kind {
        case .message(let role, let text):
            value["type"] = .string("message")
            value["role"] = .string(role == .user ? "user" : "assistant")
            value["text"] = .string(text)
        case .thinking(let text):
            value["type"] = .string("thinking")
            value["text"] = .string(text)
        case .tool(let group):
            value["type"] = .string("tool")
            value["tool"] = json(group)
        case .toolBlock(let groups):
            value["type"] = .string("toolBlock")
            value["tools"] = .array(groups.map(json))
        case .sessionStarted(let model, let cwd):
            value["type"] = .string("sessionStarted")
            value["model"] = .string(model)
            value["cwd"] = .string(cwd)
        case .usage(let usage):
            value["type"] = .string("usage")
            value["usage"] = protocolJSON(usage)
        case .rateLimit(let limit):
            value["type"] = .string("rateLimit")
            value["rateLimit"] = protocolJSON(limit)
        case .turnBoundary:
            value["type"] = .string("turnBoundary")
        case .approval(let toolName):
            value["type"] = .string("approval")
            value["toolName"] = .string(toolName)
        case .runFinished(let outcome):
            value["type"] = .string("runFinished")
            value["outcome"] = .string(outcome.rawValue)
        case .error(let message):
            value["type"] = .string("error")
            value["message"] = .string(message)
        case .note(let text, let icon, let lockEvent, let lock, let processStall):
            value["type"] = .string("note")
            value["text"] = .string(text)
            value["lockEvent"] = .bool(lockEvent)
            if let icon { value["icon"] = .string(icon) }
            if let lock { value["lock"] = protocolJSON(lock) }
            if let processStall { value["processStall"] = protocolJSON(processStall) }
        case .raw(let type, let body):
            value["type"] = .string("raw")
            value["nativeType"] = .string(type)
            value["body"] = .string(body)
        }
        return .object(value)
    }

    private static func json(_ group: ToolGroup) -> JSONValue {
        var value: [String: JSONValue] = [
            "toolCallID": .string(group.toolCallID),
            "name": .string(group.name),
            "input": group.input,
            "isError": .bool(group.isError),
            "finished": .bool(group.finished),
            "isOrchestration": .bool(group.isOrchestration),
            "fileChanges": .array(group.fileChanges.map {
                .object(["path": .string($0.path), "kind": .string($0.change.rawValue)])
            }),
            "lockLines": .array(group.lockLines.map(protocolJSON)),
            "children": .array(group.children.map(json)),
        ]
        if let result = group.result { value["result"] = result }
        if let command = group.bashCommand { value["bashCommand"] = .string(command) }
        return .object(value)
    }

    private static func protocolJSON<T: Encodable>(_ value: T) -> JSONValue {
        // These are existing NucleicProtocol Codable values. Encoding cannot fail for their
        // concrete cases; retaining a null fallback keeps this renderer-only adapter total if a
        // future optional telemetry field gains an unsupported value.
        guard let data = try? ProtocolJSON.encode(value),
              let json = try? JSONValue(parsing: data) else { return .null }
        return json
    }
}
