import Foundation
import NucleicProtocol

// On Linux, URLSession (the relay REST leg) lives in FoundationNetworking.
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
// `exit(_:)` reaches this file through the platform C library, which Foundation re-exports on
// Darwin and Linux but not on Windows (docs/WINDOWS_PORT.md §14 — smoke is the Windows e2e driver).
#if canImport(WinSDK)
import WinSDK
#endif

/// `nucleic-smoke` — a headless mesh client for exercising a runner end to end
/// (docs/COVALENCE_RUNNER.md §0.2 item 2: the "first real agent turn on a Linux runner").
///
/// It plays the iPhone's part programmatically: pair with a host from its pairing code
/// (`nucleic://pair?d=…`, the `--print-pairing` line), dial through the relay, start a chat in
/// a project, stream the transcript, answer every approval (allow — this is a smoke tool for
/// disposable environments, not a production client), and exit 0 when the turn completes.
/// Depends only on NucleicProtocol, so it builds anywhere the wire layer does — including the
/// static musl Linux build.
///
///     nucleic-smoke '<pairing-code>' --prompt "run `echo hi` in the shell" [--project name]
///                   [--model sku] [--timeout 300] [--deny] [--expect-intelligence]
///
/// It also plays an **intelligence worker** (COVALENCE_RUNNER §5, item 6): it advertises
/// `canProvideIntelligence` (desktop class, so every priority tier is eligible) and answers any
/// delegated `intelligenceRequest` with deterministic canned output — no model needed; the point
/// is proving the wire path. With `--expect-intelligence` the exercise PASSES when a delegated
/// `sessionName` job round-trips end to end: request received here, canned title answered, and
/// the host's `sessionUpdated` comes back carrying that exact title. The agent turn itself may
/// error (a bare container has no `claude`); the naming path is what's under test.
@main
struct Smoke {
    struct Options {
        var pairingCode = ""
        var prompt = "Reply with exactly: ok"
        var project: String?          // project name; nil ⇒ the host's first project
        var model: String?
        var timeoutSeconds: Double = 300
        var deny = false              // answer approvals with deny instead of allow
        /// PASS on a delegated-intelligence round trip instead of on turn completion.
        var expectIntelligence = false
    }

    /// The canned title the worker answers a delegated `sessionName` job with — and the exact
    /// string `--expect-intelligence` waits to see come back in a `sessionUpdated`.
    static let cannedSessionTitle = "Mesh Delegation Proven"

    static func main() async {
        var options = Options()
        var args = Array(CommandLine.arguments.dropFirst())
        while !args.isEmpty {
            let arg = args.removeFirst()
            switch arg {
            case "--prompt": options.prompt = args.isEmpty ? options.prompt : args.removeFirst()
            case "--project": options.project = args.isEmpty ? nil : args.removeFirst()
            case "--model": options.model = args.isEmpty ? nil : args.removeFirst()
            case "--timeout":
                options.timeoutSeconds = Double(args.isEmpty ? "" : args.removeFirst()) ?? 300
            case "--deny": options.deny = true
            case "--expect-intelligence": options.expectIntelligence = true
            default:
                if options.pairingCode.isEmpty, arg.contains("d=") { options.pairingCode = arg }
                else { fail("unknown argument: \(arg)") }
            }
        }
        guard !options.pairingCode.isEmpty else {
            fail("usage: nucleic-smoke '<nucleic://pair?d=…>' [--prompt …] [--project …] "
                + "[--model …] [--timeout seconds] [--deny] [--expect-intelligence]")
        }

        let payload: PairingPayload
        do { payload = try PairingPayload(qrString: options.pairingCode) }
        catch { fail("could not decode the pairing code: \(error)") }
        guard let membership = payload.relayMembershipToken, !membership.isEmpty else {
            fail("pairing code carries no relay membership — this tool dials relay-only")
        }

        log("pairing with “\(payload.hostName)” via relay \(RelayAPI.baseURL(payload.relayURL))")
        let channel: RelayFrameChannel
        do {
            channel = try await RelayFrameChannel.dial(
                base: RelayAPI.baseURL(payload.relayURL), membershipToken: membership)
        } catch { fail("relay dial failed: \(error)") }

        let identity = DeviceIdentity()
        let client = SyncClient(
            channel: channel, identity: identity, hostStaticKey: payload.hostStaticKey,
            mode: .pair(secret: payload.pairingSecret),
            deviceID: DeviceIdentity.hostID(ofStaticKey: identity.staticPublicKey),
            deviceLabel: "nucleic-smoke", scopeClaim: .control,
            // Offer this process as a mesh AFM worker (COVALENCE_RUNNER §5): desktop class so
            // every priority tier may land here, with a fake top-rank chip so a mesh-mode
            // runner's queue prefers it. Answers are canned — the wire path is what's tested.
            clientCaps: WireClientCapabilities(
                mesh: 1, canProvideIntelligence: true,
                intelligenceProfile: IntelligenceWorkerProfile(
                    deviceClass: .desktop, chip: "Apple M4 Max")))

        // Hard deadline for the whole exercise — a hung relay/turn must not wedge CI.
        let timeout = Task {
            try? await Task.sleep(for: .seconds(options.timeoutSeconds))
            log("timed out after \(Int(options.timeoutSeconds))s")
            exit(3)
        }
        defer { timeout.cancel() }

        await run(client: client, options: options)
    }

    // MARK: - The exercise

    private static func run(client: SyncClient, options: Options) async {
        var startedChat = false
        var sessionID: SessionID?
        var sawAssistantActivity = false
        var approvalsAnswered = 0
        var intelligenceServed = 0

        for await event in await client.start() {
            switch event {
            case .connecting:
                break
            case .ready(let welcome):
                log("paired: host “\(welcome.host.hostName)” (\(welcome.hostKind ?? "mac")) "
                    + "scope=\(welcome.grantedScope) canCreateProjects=\(welcome.capabilities.canCreateProjects)")
                // Credential settling (COVALENCE_RUNNER §6): a host that ingests sealed
                // credentials pushes its `credentialNeeded` right after registration. Delay the
                // dashboard pull so that push is consumed (and answered) BEFORE the dashboard
                // arrives and triggers `startChat` — the turn then runs on the credentials we
                // just provisioned, which is exactly what the E2E proves. The events queue
                // while we sleep; ordering stays deterministic.
                if welcome.capabilities.canReceiveSealedCredentials {
                    log("host ingests sealed credentials — pausing for its credentialNeeded")
                    try? await Task.sleep(for: .seconds(3))
                }
                await client.send(.listDashboard)
            case .dashboard(let dashboard):
                guard !startedChat else { break }
                let project = options.project
                    .flatMap { name in dashboard.projects.first { $0.name == name } }
                    ?? dashboard.projects.first
                guard let project else {
                    log("host has no projects — seed one (NUCLEIC_RUNNER_PROJECTS) or pass --project")
                    exit(2)
                }
                startedChat = true
                log("starting chat in “\(project.name)” — prompt: \(options.prompt)")
                await client.send(.startChat(StartChatRequest(
                    projectID: project.id, message: options.prompt, model: options.model,
                    useWorktree: true, auto: false)))
            case .sessionUpdated(let summary):
                // The delegated-naming proof (--expect-intelligence): the canned title we
                // answered the sessionName job with came back as the session's actual name —
                // the job round-tripped submit → queue → this worker → rename → broadcast.
                if options.expectIntelligence, summary.title == cannedSessionTitle {
                    log("PASS — delegated intelligence round-tripped: session renamed to "
                        + "“\(summary.title)” (\(intelligenceServed) request(s) served)")
                    exit(0)
                }
                // The chat we started is the session that appears (or changes) after startChat.
                if sessionID == nil, startedChat, summary.status.hasTurnInFlight {
                    sessionID = summary.sessionID
                    log("session \(summary.sessionID.rawValue) — subscribing")
                    await client.send(.subscribe(Subscribe(
                        sessionID: summary.sessionID, sinceSeq: nil, verbosity: .full)))
                    break
                }
                guard summary.sessionID == sessionID else { break }
                log("status: \(summary.status.rawValue)")
                if summary.status == .error {
                    // Under --expect-intelligence the turn is allowed to die (a bare container
                    // has no agent CLI) — the naming push can still land after the error.
                    if options.expectIntelligence {
                        log("session errored (tolerated — waiting on the naming round trip)")
                        break
                    }
                    log("FAIL — session errored")
                    exit(1)
                }
                // Turn over: the agent came back to us (or the run closed) after real activity.
                if sawAssistantActivity, !summary.status.hasTurnInFlight, summary.status != .idle {
                    log("PASS — turn completed (status \(summary.status.rawValue), "
                        + "\(approvalsAnswered) approval(s) answered over the wire)")
                    exit(0)
                }
            case .snapshot(let snapshot):
                for approval in snapshot.pendingApprovals {
                    answer(approval, client: client, deny: options.deny, count: &approvalsAnswered)
                }
            case .events(let batch):
                guard batch.sessionID == sessionID else { break }
                for event in batch.events {
                    describe(event)
                    switch event.kind {
                    case .assistantText, .toolCallStarted, .turnCompleted, .runFinished:
                        sawAssistantActivity = true
                    default: break
                    }
                }
            case .approvalRequested(let approval):
                answer(approval, client: client, deny: options.deny, count: &approvalsAnswered)
            case .intelligenceRequest(let request):
                // A mesh-mode runner delegated one AFM job here (COVALENCE_RUNNER §5). Render
                // the shared template (proves the kind decodes) and answer canned output.
                intelligenceServed += 1
                await serveIntelligence(request, client: client)
            case .credentialNeeded(let need):
                // The runner lacks credentials (COVALENCE_RUNNER §6) — play the holding
                // device: read the local files, seal each requested kind to the runner's key,
                // and provision. This is how a real device answers; here it's the E2E harness.
                await provideCredentials(need, client: client)
                // Give the landing a beat before the (still-queued) dashboard triggers the chat.
                try? await Task.sleep(for: .seconds(1))
            case .wireError(let error):
                log("wire error: \(error.code) — \(error.message)")
            case .failed(let message):
                log("connection failed: \(message)")
                exit(1)
            case .closed:
                log("connection closed before the turn completed")
                exit(1)
            default:
                break
            }
        }
    }

    /// Answer one delegated intelligence job with deterministic canned output — the worker
    /// half of the mesh AFM queue, minus the model. `sessionName` answers the canned title the
    /// `--expect-intelligence` proof watches for; `classifyTurn` answers the template's DONE
    /// token; anything else gets a one-line placeholder. An unknown kind answers with `error`
    /// set, exactly as a real executor does.
    private static func serveIntelligence(
        _ request: WireIntelligenceRequest, client: SyncClient
    ) async {
        let priority = request.priority?.rawValue ?? "background"
        guard IntelligenceDelegate.job(for: request) != nil else {
            log("intelligence request \(request.kind.rawValue) (\(priority)) — unknown kind, answering error")
            await client.send(.intelligenceResult(WireIntelligenceResult(
                id: request.id, error: "unsupported kind: \(request.kind.rawValue)")))
            return
        }
        let output: String
        switch request.kind {
        case .sessionName: output = cannedSessionTitle
        case .classifyTurn: output = "DONE"
        default: output = "Delegated \(request.kind.rawValue) output"
        }
        log("intelligence request \(request.kind.rawValue) (\(priority)) — answering “\(output)”")
        await client.send(.intelligenceResult(WireIntelligenceResult(
            id: request.id, outputs: [output])))
    }

    /// Answer a `credentialNeeded`: seal every requested kind we can satisfy from this
    /// machine's own files to the runner's sealing key (AAD = the kind string, so a box can't
    /// be replayed as another kind) and send one provision envelope.
    private static func provideCredentials(_ need: WireCredentialNeed, client: SyncClient) async {
        let home = FileManager.default.homeDirectoryForCurrentUser
        var records: [SealedCredentialRecord] = []
        for kind in need.kinds {
            let plaintext: Data?
            switch kind {
            case .claudeOAuth:
                plaintext = try? Data(contentsOf: home.appendingPathComponent(".claude/.credentials.json"))
            case .githubToken:
                plaintext = ProcessInfo.processInfo.environment["GITHUB_TOKEN"].map { Data($0.utf8) }
            default:
                plaintext = nil
            }
            guard let plaintext else {
                log("credential \(kind.rawValue): not held here — skipping")
                continue
            }
            do {
                let box = try SealedCredentialBox.seal(
                    plaintext, to: need.sealingPublicKey,
                    additionalData: Data(kind.rawValue.utf8))
                records.append(SealedCredentialRecord(kind: kind, updatedAt: Date(), box: box))
                log("credential \(kind.rawValue): sealed to the runner's key")
            } catch {
                log("credential \(kind.rawValue): seal failed: \(error)")
            }
        }
        guard !records.isEmpty else { return }
        await client.send(.credentialProvision(SealedCredentialEnvelope(records: records)))
        log("provisioned \(records.count) credential(s) over the wire")
    }

    private static func answer(
        _ approval: ApprovalRequest, client: SyncClient, deny: Bool, count: inout Int
    ) {
        count += 1
        log("approval requested: \(approval.toolName) — “\(approval.title)” → \(deny ? "deny" : "allow")")
        let decision: Decision = deny ? .deny(reason: "nucleic-smoke --deny") : .allow()
        Task { await client.send(.approvalRespond(approval.id, decision)) }
    }

    private static func describe(_ event: AgentEvent) {
        switch event.kind {
        case .assistantText(let chunk) where !chunk.isPartial:
            log("assistant: \(chunk.text.prefix(200))")
        case .toolCallStarted(let call):
            log("tool: \(call.name)")
        case .turnCompleted:
            log("event: turnCompleted")
        case .runFinished(let finished):
            log("event: runFinished (\(finished.outcome.rawValue))")
        case .error(let error):
            log("agent error: \(error.message)")
        default:
            break
        }
    }

    private static func log(_ message: String) {
        print("nucleic-smoke: \(message)")
        // Line-buffered stdout would hold output back when piped; the whole point is watching.
        // (`fflush(nil)` flushes every open stream — glibc's `stdout` var trips Swift 6's
        // shared-mutable-state check.)
        fflush(nil)
    }

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