import Foundation
import NucleicProtocol

// On Linux, URLSession (the relay REST leg) lives in FoundationNetworking.
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

/// `nucleic-smoke` — a headless mesh client for exercising a runner end to end
/// (docs/ANTIMATTER_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]
@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
    }

    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
            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]")
        }

        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)

        // 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

        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)")
                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 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 {
                    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 .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
            }
        }
    }

    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)
    }
}
