import Foundation
import NucleicCore

/// M0 spike (PLAN milestone 1): spawn `claude` in stream-json, normalize into
/// `AgentEvent`s, print to console, write the canonical JSONL transcript, and
/// optionally capture the raw native stream side-by-side as a fixture
/// (OBSERVABILITY A.5 — capture format == fixture format).
@main
struct Spike {
    struct Options {
        var prompt: String?
        var cwd = FileManager.default.currentDirectoryPath
        var model: String?
        var resume: String?
        var executable = "claude"
        var includePartialMessages = false
        var capture = false
        var keepStdin = false
        var approvalMode: ApprovalMode = .interactive
        var allowedTools = ["Read", "Glob", "Grep"]
        var outputDirectory: String?
    }

    enum ApprovalMode: String {
        case interactive
        case autoAllow = "auto-allow"
        case autoDeny = "auto-deny"
    }

    static func main() async {
        let options: Options
        do {
            options = try parse(Array(CommandLine.arguments.dropFirst()))
        } catch let error as SpikeError {
            FileHandle.standardError.write(Data((error.message + "\n\n" + usage).utf8))
            exit(64)
        } catch {
            FileHandle.standardError.write(Data(usage.utf8))
            exit(64)
        }

        guard options.prompt != nil || options.resume != nil else {
            FileHandle.standardError.write(
                Data(("--prompt (or --resume) is required\n\n" + usage).utf8))
            exit(64)
        }

        let sessionID = SessionID.generate()
        let outputBase = options.outputDirectory.map { URL(fileURLWithPath: $0) }
            ?? URL(fileURLWithPath: options.cwd)
                .appendingPathComponent(".nucleic-spike/sessions/\(sessionID.rawValue)")

        let configuration = ClaudeCodeBackend.Configuration(
            executable: options.executable,
            includePartialMessages: options.includePartialMessages,
            allowedTools: options.allowedTools,
            closeStdinAfterPrompt: !options.keepStdin,
            captureDirectory: options.capture ? outputBase.appendingPathComponent("capture") : nil)
        let backend = ClaudeCodeBackend(configuration: configuration)

        let header = SessionHeader(
            sessionID: sessionID,
            backend: .claudeCode,
            worktree: options.cwd,
            model: options.model,
            createdAt: Date())
        let transcriptURL = outputBase.appendingPathComponent("transcript.jsonl")
        let writer: TranscriptWriter
        do {
            writer = try TranscriptWriter(url: transcriptURL, header: header)
        } catch {
            fail("Cannot create transcript at \(transcriptURL.path): \(error)")
        }

        print("session \(sessionID.short)  cwd \(options.cwd)")
        print("transcript \(transcriptURL.path)")
        if options.capture {
            print("capture \(outputBase.appendingPathComponent("capture").path)")
        }
        print(String(repeating: "─", count: 72))

        let stream: AsyncThrowingStream<AgentEvent, Error>
        if let resume = options.resume {
            stream = backend.resume(
                ResumeSpec(
                    sessionID: sessionID, backendSessionID: resume, worktree: options.cwd,
                    prompt: options.prompt.map { AgentInput(text: $0) }))
        } else {
            stream = backend.start(
                RunSpec(
                    sessionID: sessionID,
                    worktree: options.cwd,
                    prompt: AgentInput(text: options.prompt ?? ""),
                    model: options.model))
        }

        var normalized: [AgentEvent] = []
        var outcome: RunFinished.Outcome = .errored
        do {
            for try await event in stream {
                let canonical = try await writer.append(event)
                normalized.append(canonical)
                render(canonical)

                if case .approvalRequested(let request) = event.kind {
                    let decision = decide(request, mode: options.approvalMode)
                    try await backend.respond(to: request.id, decision)
                }
                if case .runFinished(let finished) = event.kind {
                    outcome = finished.outcome
                }
            }
        } catch {
            fail("stream error: \(error)")
        }

        try? await writer.sync()
        try? await writer.close()

        if options.capture {
            let normalizedURL = outputBase.appendingPathComponent("capture/normalized.json")
            let encoder = JSONEncoder()
            encoder.outputFormatting = [.sortedKeys, .prettyPrinted, .withoutEscapingSlashes]
            encoder.dateEncodingStrategy = .iso8601
            if let data = try? encoder.encode(normalized) {
                try? data.write(to: normalizedURL)
            }
        }

        print(String(repeating: "─", count: 72))
        print("outcome: \(outcome.rawValue)  events: \(normalized.count)")
        exit(outcome == .completed ? 0 : 2)
    }

    // MARK: - Approval handling

    static func decide(_ request: ApprovalRequest, mode: ApprovalMode) -> Decision {
        switch mode {
        case .autoAllow:
            print("  ⚖️  auto-allow: \(request.title)")
            return .allow()
        case .autoDeny:
            print("  ⚖️  auto-deny: \(request.title)")
            return .deny(reason: "Denied by spike policy")
        case .interactive:
            print("  ⚖️  APPROVAL [\(request.risk.rawValue)] \(request.title)")
            print("     input: \(request.input.canonicalString().prefix(200))")
            while true {
                print("     [a]llow / [d]eny / [A]lways this tool / [c]ancel run > ", terminator: "")
                switch readLine(strippingNewline: true) {
                case "a": return .allow()
                case "d": return .deny(reason: "Denied by user")
                case "A": return .allowAlways(.toolName)
                case "c": return .cancelRun
                case nil: return .deny(reason: "No TTY to answer approval")
                default: continue
                }
            }
        }
    }

    // MARK: - Rendering

    static func render(_ event: AgentEvent) {
        let prefix = String(format: "%4d", event.seq)
        switch event.kind {
        case .sessionStarted(let started):
            print("\(prefix) ▶ session started  backend-id=\(started.backendSessionID)  model=\(started.model)")
        case .userText(let chunk):
            print("\(prefix) 🙋 \(chunk.text)")
        case .assistantText(let chunk):
            print("\(prefix) \(chunk.isPartial ? "┆" : "💬") \(chunk.text)")
        case .thinking(let chunk):
            print("\(prefix) 🧠 \(chunk.isPartial ? "┆ " : "")\(chunk.text)")
        case .toolCallStarted(let call):
            print("\(prefix) 🔧 \(call.name) started  id=\(call.toolCallID)")
        case .toolCallInputDelta:
            break  // too noisy for the console; lands in the transcript regardless
        case .toolCallCompleted(let call):
            print("\(prefix) 🔧 \(call.name) \(call.input.canonicalString().prefix(120))")
        case .toolResult(let result):
            let summary = result.content.canonicalString().prefix(120)
            print("\(prefix) \(result.isError ? "❌" : "✅") result \(summary)")
        case .fileChange(let change):
            print("\(prefix) 📄 \(change.kind.rawValue) \(change.path) (inferred)")
        case .approvalRequested(let request):
            print("\(prefix) ⏸ approval requested: \(request.title)")
        case .approvalResolved(let resolved):
            print("\(prefix) ▶ approval resolved by \(resolved.decidedBy)")
        case .usage(let usage):
            let cost = usage.costUSD.map { String(format: "$%.4f", $0) } ?? "n/a"
            print("\(prefix) 📊 in=\(usage.inputTokens ?? 0) cacheRead=\(usage.cachedInputTokens ?? 0) cacheWrite=\(usage.cacheCreationInputTokens ?? 0) out=\(usage.outputTokens ?? 0) cost=\(cost)")
        case .rateLimit(let rl):
            let resets = rl.resetsAt.map { " resets=\($0)" } ?? ""
            print("\(prefix) ⏳ rate-limit \(rl.rateLimitType ?? "?") status=\(rl.status ?? "?")\(resets)")
        case .turnCompleted:
            print("\(prefix) ── turn completed")
        case .runFinished(let finished):
            print("\(prefix) ■ run finished: \(finished.outcome.rawValue) (\(finished.durationMs ?? 0)ms)")
        case .error(let error):
            print("\(prefix) ⚠️ \(error.recoverable ? "recoverable" : "fatal"): \(error.message)")
        case .note(let note):
            print("\(prefix) 📝 \(note.text)")
        case .raw(let raw):
            print("\(prefix) ❓ raw[\(event.nativeType ?? "?")] \(raw.native.canonicalString().prefix(120))")
        }
    }

    // MARK: - Arguments

    struct SpikeError: Error {
        let message: String
    }

    static func parse(_ arguments: [String]) throws -> Options {
        var options = Options()
        var iterator = arguments.makeIterator()
        func value(for flag: String) throws -> String {
            guard let v = iterator.next() else { throw SpikeError(message: "missing value for \(flag)") }
            return v
        }
        while let argument = iterator.next() {
            switch argument {
            case "--prompt", "-p": options.prompt = try value(for: argument)
            case "--cwd", "-C": options.cwd = try value(for: argument)
            case "--model", "-m": options.model = try value(for: argument)
            case "--resume": options.resume = try value(for: argument)
            case "--claude": options.executable = try value(for: argument)
            case "--partial": options.includePartialMessages = true
            case "--capture": options.capture = true
            case "--keep-stdin": options.keepStdin = true
            case "--out": options.outputDirectory = try value(for: argument)
            case "--approvals":
                let raw = try value(for: argument)
                guard let mode = ApprovalMode(rawValue: raw) else {
                    throw SpikeError(message: "unknown approval mode \(raw)")
                }
                options.approvalMode = mode
            case "--allowed-tools":
                options.allowedTools = try value(for: argument)
                    .split(separator: ",").map(String.init)
            case "--help", "-h":
                print(usage)
                exit(0)
            default:
                throw SpikeError(message: "unknown argument \(argument)")
            }
        }
        return options
    }

    static let usage = """
        nucleic-spike — M0: run one Claude Code session through the normalized event pipeline

        USAGE: nucleic-spike --prompt "..." [options]

        OPTIONS:
          --prompt, -p <text>      Initial prompt (required unless --resume)
          --cwd, -C <path>         Working directory for the agent (default: .)
          --model, -m <id>         Model override
          --resume <session-id>    Native resume of a previous backend session
          --partial                Enable --include-partial-messages (token deltas)
          --capture                Record native.ndjson + normalized.json fixtures
          --approvals <mode>       interactive | auto-allow | auto-deny (default: interactive)
          --allowed-tools <list>   Comma-separated pre-allowed tools (default: Read,Glob,Grep)
          --out <dir>              Output directory (default: ./.nucleic-spike/sessions/<id>)
          --claude <path>          Claude executable override (default: claude on PATH)
        """

    static func fail(_ message: String) -> Never {
        FileHandle.standardError.write(Data((message + "\n").utf8))
        exit(2)
    }
}
