import Foundation
import NucleicProtocol

// The runner's replacement for Apple Foundation Models (docs/COVALENCE_RUNNER.md §5, item 6):
// one `IntelligenceProviding` implementation that renders each call into a
// `WireIntelligenceRequest`, runs it through a text-generation backend — the mesh-wide AFM
// queue (`nucleic.runner.intelligenceMode = mesh`) or a one-shot agent CLI (`agent`) — and
// validates the answer with the same heuristics-as-guardrails discipline the AFM provider
// uses, falling back to `HeuristicIntelligence` whenever the backend can't serve.

// MARK: - Backends

/// Where a delegated intelligence request gets its text generated. `nil` means "couldn't
/// serve" (no eligible device, deadline, CLI failure) — the provider falls back to heuristics.
public protocol IntelligenceGenerationBackend: Sendable {
    func run(_ request: WireIntelligenceRequest) async -> WireIntelligenceResult?
}

/// Mode `mesh`: hand the request to the mesh-wide AFM queue, which places it on a connected
/// Apple-Intelligence-capable device per the priority/device-class rules.
public struct MeshIntelligenceBackend: IntelligenceGenerationBackend {
    public let queue: MeshIntelligenceQueue
    public init(queue: MeshIntelligenceQueue) { self.queue = queue }
    public func run(_ request: WireIntelligenceRequest) async -> WireIntelligenceResult? {
        await queue.submit(request)
    }
}

/// Mode `agent`: render the shared template and run it as ONE non-interactive agent turn
/// (`claude -p … --model <small SKU> --output-format text`) — the credential mesh already put
/// the agent's login on the runner. Work is serialized through a private `AFMRequestQueue` so
/// a burst of soft-AI jobs can't fork a pile of CLI processes, and each invocation is killed
/// at its deadline so a hung CLI can't wedge the queue.
public struct AgentCLIIntelligenceBackend: IntelligenceGenerationBackend {
    public let executable: String
    public let model: String
    /// Serializes CLI invocations (and orders a backlog by the wire priority).
    private let queue: AFMRequestQueue

    public init(executable: String = "claude", model: String = "haiku") {
        self.executable = executable
        self.model = model
        self.queue = AFMRequestQueue(maxConcurrent: 1)
    }

    public func run(_ request: WireIntelligenceRequest) async -> WireIntelligenceResult? {
        guard let job = IntelligenceDelegate.job(for: request) else {
            return WireIntelligenceResult(
                id: request.id, error: "unsupported kind: \(request.kind.rawValue)")
        }
        let deadline = request.deadlineSeconds ?? 60
        let executable = self.executable
        let model = self.model
        let text = await queue.run(
            priority: AFMRequestQueue.Priority(wire: request.priority),
            label: "Agent \(request.kind.rawValue)", detail: job.prompt,
            resultText: { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
        ) {
            await Self.oneShot(executable: executable, model: model, job: job, deadline: deadline)
        }
        guard let text else { return nil }
        return WireIntelligenceResult(id: request.id, outputs: [text])
    }

    /// One print-mode turn: instructions + prompt as the single prompt argument, plain-text
    /// output, SIGKILL at the deadline. Any failure (CLI missing, non-zero exit, empty reply)
    /// is nil — the provider's heuristic fallback covers it.
    private static func oneShot(
        executable: String, model: String, job: IntelligenceDelegate.Job, deadline: Double
    ) async -> String? {
        let spec = ProcessSpec(
            executable: executable,
            args: [
                "-p", "\(job.instructions)\n\n\(job.prompt)",
                "--model", model, "--output-format", "text",
            ],
            cwd: NSTemporaryDirectory(), stdinMode: .closed)
        guard let handle = try? await ProcessHost().launch(spec) else { return nil }
        let killer = Task {
            try? await Task.sleep(for: .seconds(deadline))
            guard !Task.isCancelled else { return }
            handle.sendSignal(SIGKILL)
        }
        defer { killer.cancel() }
        var lines: [String] = []
        do {
            for try await line in handle.stdoutLines {
                lines.append(String(decoding: line, as: UTF8.self))
            }
        } catch {}
        guard await handle.wait() == 0 else { return nil }
        let text = lines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
        return text.isEmpty ? nil : text
    }
}

extension AFMRequestQueue.Priority {
    /// The local-queue slot for a wire priority tier; unknown/omitted tiers run as background.
    public init(wire: IntelligencePriority?) {
        switch wire ?? .background {
        case .interactive: self = .interactive
        case .bashSummary: self = .bashSummary
        case .completion: self = .completion
        default: self = .background
        }
    }
}

// MARK: - Device-side executor

/// Runs one delegated request on THIS device's provider (the executor half of §5): render the
/// shared template, generate through `IntelligenceProviding.generateText` at the wire-carried
/// priority, and answer — with `error` set whenever no model could serve, so the delegating
/// host falls back to heuristics immediately instead of waiting out its deadline.
public enum IntelligenceDelegateExecutor {
    public static func execute(
        _ request: WireIntelligenceRequest, provider: any IntelligenceProviding
    ) async -> WireIntelligenceResult {
        guard let job = IntelligenceDelegate.job(for: request) else {
            return WireIntelligenceResult(
                id: request.id, error: "unsupported kind: \(request.kind.rawValue)")
        }
        let text = await provider.generateText(
            instructions: job.instructions, prompt: job.prompt,
            priority: AFMRequestQueue.Priority(wire: request.priority))
        guard let text, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            return WireIntelligenceResult(id: request.id, error: "no local model available")
        }
        return WireIntelligenceResult(id: request.id, outputs: [text])
    }
}

// MARK: - Provider

/// `IntelligenceProviding` over a delegated text backend — the runner's `agent` and `mesh`
/// intelligence modes (§5 modes 1–2). Every method mirrors `AppleIntelligenceProvider`'s
/// shape: encode → generate → validate with the deterministic heuristics → fall back to the
/// model-free answer. `auditHostExecReason` stays fail-closed: no backend answer is
/// `.notChecked`, and a garbled verdict token is `.unclear` — never a fabricated "consistent".
public struct DelegatedIntelligenceProvider: IntelligenceProviding {
    private let backend: any IntelligenceGenerationBackend

    public init(backend: any IntelligenceGenerationBackend) {
        self.backend = backend
    }

    /// Mode `mesh`: delegate to Apple-Intelligence-capable mesh devices via the queue.
    public static func mesh(queue: MeshIntelligenceQueue) -> DelegatedIntelligenceProvider {
        DelegatedIntelligenceProvider(backend: MeshIntelligenceBackend(queue: queue))
    }

    /// Mode `agent` (the doc's `AgentIntelligenceProvider`): a small/cheap agent SKU behind
    /// strict prompt templates.
    public static func agent(
        executable: String = "claude", model: String = "haiku"
    ) -> DelegatedIntelligenceProvider {
        DelegatedIntelligenceProvider(
            backend: AgentCLIIntelligenceBackend(executable: executable, model: model))
    }

    /// How long each tier is worth waiting on before the heuristic renders instead. Interactive
    /// work is watched live, so it gives up fastest; background work can afford a long queue.
    static func deadline(for priority: IntelligencePriority) -> Double {
        switch priority {
        case .interactive: 20
        case .bashSummary: 30
        case .completion: 45
        default: 90
        }
    }

    /// Encode one call, run it, and hand back the raw output text — nil whenever the backend
    /// couldn't serve (every caller falls back to its deterministic answer).
    private func generate(
        _ kind: IntelligenceRequestKind, inputs: [String], context: String? = nil,
        priority: IntelligencePriority
    ) async -> String? {
        let request = WireIntelligenceRequest(
            id: UUID().uuidString, kind: kind, inputs: inputs, context: context,
            deadlineSeconds: Self.deadline(for: priority), priority: priority)
        guard let result = await backend.run(request), result.error == nil else { return nil }
        // Unwrap again on receipt even though a current phone worker already did: the mesh is
        // mixed-version (a peer on an older build still sends the raw tool-calling envelope), and
        // the agent-CLI backend never unwraps at all. `unwrap` leaves plain text alone, so running
        // it twice costs nothing and closes both gaps.
        return ModelOutput.usableText(result.outputs.joined(separator: "\n"))
    }

    /// The first non-empty line of a reply, stripped of leading list/bullet markers — the
    /// single glanceable line most kinds want (shared with the AFM provider).
    private func firstLine(of raw: String) -> String {
        HeuristicTitle.firstModelLine(of: raw)
    }

    // MARK: IntelligenceProviding

    public func summarize(session: Session, events: [AgentEvent]) async -> String {
        guard events.contains(where: {
            if case .userText = $0.kind { return true } else { return false }
        }) else {
            return HeuristicSummary.text(session: session, events: events)
        }
        let digest = HeuristicSummary.sessionDigest(events)
        if let text = await generate(.summarizeSession, inputs: [digest], priority: .completion) {
            return text
        }
        return HeuristicSummary.text(session: session, events: events)
    }

    public func sessionName(fromFirstMessage message: String) async -> String? {
        if let raw = await generate(.sessionName, inputs: [message], priority: .interactive),
           let name = HeuristicTitle.sanitizeModelTitle(raw), HeuristicTitle.looksLikeTitle(name) {
            return name
        }
        return HeuristicTitle.fromMessage(message)
    }

    public func sessionName(fromFirstMessage message: String, context digest: String) async -> String? {
        if let raw = await generate(
            .sessionName, inputs: [message], context: digest, priority: .background),
           let name = HeuristicTitle.sanitizeModelTitle(raw), HeuristicTitle.looksLikeTitle(name) {
            return name
        }
        return nil
    }

    public func summarizeTodos(_ items: [String], group: String) async -> String {
        let cleaned = items
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
        guard !cleaned.isEmpty else { return "" }
        guard cleaned.count > 1 else { return HeuristicSummary.todoGist(cleaned) }
        // `group` drives only per-group coalescing on the AFM path; the template doesn't read
        // it, so it stays off the wire.
        if let text = await generate(.summarizeTodos, inputs: cleaned, priority: .background) {
            let line = firstLine(of: text)
            if !line.isEmpty { return line }
        }
        return HeuristicSummary.todoGist(cleaned)
    }

    public func summarizeTodo(_ text: String) async -> String {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return "" }
        guard trimmed.split(whereSeparator: { $0 == " " || $0.isNewline }).count > 8 else {
            return HeuristicSummary.todoLine(trimmed)
        }
        if let out = await generate(.summarizeTodo, inputs: [trimmed], priority: .background) {
            let line = firstLine(of: out)
            if !line.isEmpty { return line }
        }
        return HeuristicSummary.todoLine(trimmed)
    }

    public func classifyTurn(lastReply: String) async -> TurnDisposition {
        let trimmed = lastReply.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return .completed }
        let tail = String(trimmed.suffix(600))
        if let raw = await generate(.classifyTurn, inputs: [tail], priority: .completion) {
            let upper = raw.uppercased()
            if upper.contains("AWAIT") { return .awaitingInput }
            if upper.contains("DONE") { return .completed }
        }
        return HeuristicTurnClassifier.classify(lastReply)
    }

    public func triageTodos(_ items: [TriageInput], group: String) async -> [TriageItem] {
        guard !items.isEmpty else { return [] }
        guard items.count > 1 else { return HeuristicTriage.triage(items) }
        // Like `summarizeTodos`, `group` only keys AFM-side coalescing — not sent.
        guard let raw = await generate(
            .triageTodos, inputs: items.map(\.text), priority: .background)
        else {
            return HeuristicTriage.triage(items)
        }
        // Parse the strict "N|LEVEL|reason" lines the template demands; anything malformed is
        // skipped, and `fromModelRanking` itself falls back to the heuristic ranking when
        // nothing valid survives.
        let verdicts = raw.split(whereSeparator: \.isNewline).compactMap { line -> HeuristicTriage.ModelVerdict? in
            let parts = line.split(separator: "|", maxSplits: 2, omittingEmptySubsequences: false)
                .map { $0.trimmingCharacters(in: .whitespaces) }
            guard parts.count >= 2,
                  let number = Int(parts[0].trimmingCharacters(in: CharacterSet(charactersIn: ".) ")))
            else { return nil }
            return HeuristicTriage.ModelVerdict(
                number: number, level: parts[1], reason: parts.count > 2 ? parts[2] : "")
        }
        guard !verdicts.isEmpty else { return HeuristicTriage.triage(items) }
        return HeuristicTriage.fromModelRanking(verdicts, items: items)
    }

    public func summarizeBashCommand(_ command: String) async -> String {
        guard let raw = await generate(
            .summarizeBashCommand, inputs: [command], priority: .bashSummary)
        else { return command }
        let line = firstLine(of: raw)
        // Same guardrail as the AFM provider: a parroted command isn't a summary.
        if line.isEmpty || HeuristicSummary.echoesCommands(line, commands: [command]) {
            return command
        }
        return line
    }

    public func mergeBashSummaries(_ summaries: [String]) async -> String {
        let fallback = HeuristicSummary.joinBashSummaries(summaries)
        guard summaries.count > 1 else { return summaries.first.map(firstLine) ?? fallback }
        guard let raw = await generate(
            .mergeBashSummaries, inputs: summaries, priority: .bashSummary)
        else { return fallback }
        // Reject an ungrounded line (a parroted instruction example) for the deterministic merge.
        let line = firstLine(of: raw)
        if line.isEmpty || !HeuristicSummary.isGrounded(line, in: summaries) { return fallback }
        return line
    }

    public func summarizeToolFamily(_ calls: [ToolCall], worktreeRoot: String?) async -> [String] {
        let families = HeuristicSummary.families(calls)
        let fallback = families.map { HeuristicSummary.line(for: $0, relativeTo: worktreeRoot) }
        let actions = calls.compactMap { HeuristicSummary.actionDescription($0) }
        guard !actions.isEmpty else { return fallback }
        // The template routes on the verb: an all-Grep family takes the search-tailored prompt.
        let isSearch = calls.allSatisfy { $0.name == "Grep" }
        let verb = isSearch ? IntelligenceDelegate.searchFamilyVerb : (families.first?.verb ?? "")
        guard let raw = await generate(
            .summarizeToolFamily, inputs: actions, context: verb, priority: .background)
        else { return fallback }
        let lines = raw.split(whereSeparator: \.isNewline)
            .map { $0.trimmingCharacters(in: CharacterSet(charactersIn: " -•*\t")) }
            .filter { !$0.isEmpty }
        return lines.isEmpty ? fallback : lines
    }

    public func updateTrunkSummary(previous: String, with digest: String) async -> String {
        let prev = previous.trimmingCharacters(in: .whitespacesAndNewlines)
        let digestTrimmed = digest.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !digestTrimmed.isEmpty else { return prev }
        if let raw = await generate(
            .updateTrunkSummary, inputs: [prev, digestTrimmed], priority: .background) {
            let line = firstLine(of: raw)
            if !line.isEmpty { return line }
        }
        return HeuristicSummary.foldTrunkSummary(previous: previous, digest: digest)
    }

    public func classifyPromptPurpose(prompt: String) async -> PurposeVerdict {
        let trimmed = prompt.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return .general }
        let heuristic = HeuristicPurposeClassifier.classify(trimmed)
        // An unambiguous keyword verdict skips the model entirely (the fast path the AFM
        // provider takes too — the layering must not differ between providers).
        if heuristic.confidence == .high { return heuristic }
        let head = String(trimmed.prefix(600))
        if let raw = await generate(
            .classifyPromptPurpose, inputs: [head], priority: .interactive),
            let verdict = PurposeVerdict.fromModelLine(raw, source: .agent) {
            // Accept a confident model answer outright; a LOW answer only corroborates —
            // it must agree with the heuristic to beat it.
            if verdict.confidence > .low || verdict.purpose == heuristic.purpose {
                return verdict
            }
        }
        return heuristic
    }

    public func auditHostExecReason(command: String, reason: String) async -> HostExecReasonAudit {
        let cmd = command.trimmingCharacters(in: .whitespacesAndNewlines)
        let why = reason.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !cmd.isEmpty, !why.isEmpty else { return .notChecked }
        // Fail-closed both ways: no answer → `.notChecked` (never a spoofable clean bill), and
        // an unrecognized verdict token → `.unclear` via `trust(fromModelToken:)`.
        guard let raw = await generate(
            .auditHostExecReason, inputs: [cmd, why], priority: .interactive)
        else { return .notChecked }
        let line = firstLine(of: raw)
        guard !line.isEmpty else { return .notChecked }
        let parts = line.split(separator: "|", maxSplits: 1, omittingEmptySubsequences: false)
            .map { $0.trimmingCharacters(in: .whitespaces) }
        return HostExecReasonAudit(
            trust: HostExecReasonAudit.trust(fromModelToken: parts[0]),
            rationale: parts.count > 1 ? parts[1] : "")
    }
}
