import Foundation
import IOKit.ps
import NucleicCore

#if canImport(FoundationModels)
import FoundationModels

/// One idea's triage verdict, produced by the model via guided generation — the
/// schema is enforced by Foundation Models, so the model can't return malformed text.
@available(macOS 26, *)
@Generable
private struct TriageVerdict {
    @Guide(description: "The idea's number, exactly as labeled in the list")
    let number: Int
    @Guide(description: "Exactly one of: CRITICAL, HIGH, MEDIUM, LOW")
    let level: String
    @Guide(description: "Why it ranks here, in 6 words or fewer")
    let reason: String
}

/// The full ranked backlog: every idea, ordered most-impactful first.
@available(macOS 26, *)
@Generable
private struct TriageRanking {
    @Guide(description: "Every idea exactly once, ordered MOST impactful first")
    let items: [TriageVerdict]
}
#endif

/// Which model backs the soft-AI features. Persisted in UserDefaults; read live by
/// the provider and bound by the Settings picker.
enum IntelligenceChoice: String, CaseIterable, Identifiable {
    case onDevice
    case privateCloudCompute
    case energyAware
    case off

    var id: String { rawValue }

    var label: String {
        switch self {
        case .onDevice: "Apple On-Device"
        case .privateCloudCompute: "Private Cloud Compute"
        case .energyAware: "Energy Aware"
        case .off: "Off (local summary)"
        }
    }

    var detail: String {
        switch self {
        case .onDevice: "Runs entirely on this Mac. Private and offline."
        case .privateCloudCompute: "Apple's server models with the same privacy guarantees; needs a network connection."
        case .energyAware: "On-device while plugged into wall power, and Private Cloud Compute on battery. Laptops only."
        case .off: "No model — a quick locally-assembled summary, and randomly named chats."
        }
    }

    /// The concrete model this choice backs onto right now. Every choice resolves to
    /// itself except ``energyAware``, which reads the live power source: the on-device
    /// model while on AC wall power (and on desktops, which are always "on AC"), and
    /// Private Cloud Compute on battery. Callers route on this, so the routing switches
    /// never see ``energyAware``.
    var resolved: IntelligenceChoice {
        switch self {
        case .energyAware: PowerSource.onACPower ? .onDevice : .privateCloudCompute
        default: self
        }
    }

    static let storageKey = "nucleic.intelligenceModel"

    static var current: IntelligenceChoice {
        IntelligenceChoice(rawValue: UserDefaults.standard.string(forKey: storageKey) ?? "") ?? .onDevice
    }
}

/// `IntelligenceProviding` backed by Apple Foundation Models, routing to the
/// on-device or Private Cloud Compute model per the user's setting and falling
/// back to the local heuristic when the model is off / unavailable / errors.
struct AppleIntelligenceProvider: IntelligenceProviding {
    func summarize(session: Session, events: [AgentEvent]) async -> String {
        let digest = HeuristicSummary.sessionDigest(events)
        guard events.contains(where: { if case .userText = $0.kind { return true } else { return false } })
        else {
            return HeuristicSummary.text(session: session, events: events)
        }
        let instructions = """
            You write a glanceable status summary of a coding session, for someone \
            switching between many sessions. The digest is your only source of truth.

            Every bullet must convey PURPOSE — what the work accomplished and why — \
            not the mechanical step. Lead with the outcome (the bug fixed, the feature \
            added, the thing made clearer), NOT the mechanical step like "Edited a \
            file" or "Ran swift test". Tie the agent's actions back to the user's \
            request to express intent. Fold several related steps into the one goal \
            they served, rather than listing separate file edits. Never quote raw \
            shell commands, flags, or file-by-file steps (say "explored the project \
            structure", not "ran cd"). Describe ONLY work shown in the digest — never \
            copy these instructions or their illustrative phrases into your output.

            The agent's reply is evidence — mine it for the PURPOSE of the work, but \
            never report the repo's contents, or work done by anyone else, as the \
            agent's accomplishments. Output ONLY this Markdown, omitting any section \
            with nothing to say:

            **Done:**
            - <purpose-level outcomes across the whole session>
            **Just now:**
            - <what the most recent run accomplished, and why>
            **Next:**
            - <pending or clearly-suggested next steps>

            Keep every bullet under ~12 words, goal-first and concrete. No preamble, \
            no code fences. If the whole-session work is the same as the most recent run \
            (e.g. only one turn so far), omit **Done:** and show only **Just now:** and \
            **Next:**.
            """
        // Session summary card — completion-tier: shown as a session wraps up.
        if let text = await generate(priority: .completion, label: "Session summary", instructions: instructions, prompt: digest) {
            return text.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        return HeuristicSummary.text(session: session, events: events)
    }

    func sessionName(fromFirstMessage message: String) async -> String? {
        let instructions = """
            You generate chat titles. Given a user's request, reply with ONLY a 3–6 word \
            Title Case label naming the task. Name the SPECIFIC thing being acted on — the \
            concrete file, component, or feature — not a vague category. Prefer "Ship Icon \
            Adjustment" over "UI Adjustment", "Fix Login Button Tap" over "Fix Bug". No \
            quotes, no punctuation, no explanation, no sentences. Never answer the request itself.
            """
        // Few-shot the format inline; small on-device models otherwise tend to answer
        // the request instead of titling it. The vague→specific pairs steer it off
        // generic labels like "UI Adjustment".
        let prompt = """
            Request: "Fix the login button not responding"
            Title: Fix Login Button Tap
            Request: "make the toolbar a bit nicer"
            Title: Toolbar Styling Polish
            Request: "what's in this repo"
            Title: Explore Repository Contents
            Request: "\(message)"
            Title:
            """
        // Chat naming — interactive-tier: the user just opened this chat and is
        // watching for its title, so it preempts queued background work.
        if let raw = await generate(priority: .interactive, label: "Chat name", instructions: instructions, prompt: prompt),
            let name = Self.sanitizeName(raw), HeuristicTitle.looksLikeTitle(name) {
            return name
        }
        // The model refused or rambled — derive a title from the message instead.
        return HeuristicTitle.fromMessage(message)
    }

    func summarizeTodos(_ items: [String], group: String) async -> String {
        let cleaned = items
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
        guard !cleaned.isEmpty else { return "" }
        // One idea needs no summary — show it verbatim (matches the heuristic).
        guard cleaned.count > 1 else { return HeuristicSummary.todoGist(cleaned) }
        let instructions = """
            You summarize a developer's short list of captured to-do ideas into ONE \
            glanceable sentence (max ~18 words) naming the common themes. The list is \
            your only source of truth — never invent work. No preamble, no bullet list, \
            no quotes — just the sentence.
            """
        let prompt = cleaned.map { "- \($0)" }.joined(separator: "\n")
        // To-do group gisting — background-tier backlog work, coalesced per group: a burst
        // of edits to one group collapses to its newest gist, while other groups (and
        // other projects) keep their own waiting work.
        if let text = await generate(priority: .background, coalesceKey: "todo-gist:\(group)", label: "To-do group gist", instructions: instructions, prompt: prompt) {
            let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
            if !trimmed.isEmpty { return trimmed }
        }
        return HeuristicSummary.todoGist(cleaned)
    }

    func summarizeTodo(_ text: String) async -> String {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return "" }
        // Already short — no summary adds value (the caller folds an unchanged result
        // into "no distinct summary").
        guard trimmed.split(whereSeparator: { $0 == " " || $0.isNewline }).count > 8 else {
            return HeuristicSummary.todoLine(trimmed)
        }
        let instructions = """
            You compress a developer's to-do note into a SHORT imperative title \
            (max ~10 words) naming the core task. The note is your only source of \
            truth — never invent detail. No quotes, no preamble, no trailing \
            punctuation — output only the title.
            """
        // Per-idea to-do compression — background-tier backlog work.
        if let out = await generate(priority: .background, label: "To-do summary", instructions: instructions, prompt: trimmed) {
            let line = out.trimmingCharacters(in: .whitespacesAndNewlines)
                .split(whereSeparator: \.isNewline).first.map(String.init) ?? ""
            if !line.isEmpty { return line }
        }
        return HeuristicSummary.todoLine(trimmed)
    }

    func classifyTurn(lastReply: String) async -> TurnDisposition {
        let trimmed = lastReply.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return .completed }
        // The ask, if any, lands at the end — show the model just the tail, both to
        // focus it and to keep the prompt small for the on-device model.
        let tail = String(trimmed.suffix(600))
        let instructions = """
            A coding agent just finished a turn. From the END of its message, decide \
            whether it is WAITING on the user or DONE. Reply with exactly one word: \
            AWAITING or DONE.
            AWAITING — it asks a question, offers options, or needs the user to \
            confirm, decide, or answer before it can continue.
            DONE — it reports completed work or gives a final answer and needs nothing \
            back from the user.
            If you are genuinely unsure, answer DONE.
            """
        // Turn classification (awaiting vs. done) — completion-tier session wrap-up.
        if let raw = await generate(priority: .completion, label: "Turn status", instructions: instructions, prompt: tail) {
            let upper = raw.uppercased()
            if upper.contains("AWAIT") { return .awaitingInput }
            if upper.contains("DONE") { return .completed }
        }
        // Off / unavailable / ambiguous — fall back to the shape-based heuristic.
        return HeuristicTurnClassifier.classify(lastReply)
    }

    func triageTodos(_ items: [TriageInput], group: String) async -> [TriageItem] {
        guard !items.isEmpty else { return [] }
        // One idea has nothing to rank against — skip the model, just label it.
        guard items.count > 1 else { return HeuristicTriage.triage(items) }
        let instructions = """
            You triage a developer's backlog of captured ideas by IMPACT — how much \
            lasting value finishing the work delivers — NOT by how recent, easy, or \
            small it is. Push genuinely urgent or risky work (crashes, data loss, \
            security holes, outages, hard blockers) to the very top even when its scope \
            is tiny. Order EVERY idea from most to least impactful, and assign each a \
            level. Levels are RELATIVE to this list: spread them so the top sits clearly \
            above the bottom — never give everything the same level. Reserve CRITICAL \
            for genuinely urgent or risky work.
            """
        let prompt = items.enumerated()
            .map { "\($0.offset + 1). \($0.element.text)" }
            .joined(separator: "\n")
        #if canImport(FoundationModels)
        // Backlog triage — background-tier: the lowest priority, yielding to chat
        // naming and session completion when the device model is busy. Coalesced per
        // project so a burst of edits in one project collapses to its newest pass while
        // every other project's queued triage is left untouched.
        if #available(macOS 26, *),
           let ranking = await rankTriage(priority: .background, coalesceKey: "backlog-triage:\(group)", label: "Backlog triage", instructions: instructions, prompt: prompt),
           !ranking.items.isEmpty {
            let verdicts = ranking.items.map {
                HeuristicTriage.ModelVerdict(number: $0.number, level: $0.level, reason: $0.reason)
            }
            return HeuristicTriage.fromModelRanking(verdicts, items: items)
        }
        #endif
        // Off / unavailable / errored — deterministic heuristic.
        return HeuristicTriage.triage(items)
    }

    func summarizeBashCommand(_ command: String) async -> String {
        // Map step: one command → its PURPOSE, not the command echoed back ("Ran the test
        // suite", not "Ran swift test"). The per-command gists accumulate in the block's
        // running list, which `mergeBashSummaries` condenses.
        let instructions = """
            You summarize a single shell command a coding agent just ran, for a glanceable \
            collapsed view. Say in ONE short line what the command ACCOMPLISHED — its purpose — \
            and NEVER repeat the raw command, its flags, or file paths. The line MUST describe \
            what THIS command did: `swift test` → "Ran the test suite", `git commit` → "Staged \
            and committed the changes", and a read-only inspection like `ls`, `cat`, `find`, or \
            `grep` → "Listed the project files" or "Searched the codebase". Do not borrow an \
            example that doesn't fit the command. Lead with a past-tense verb (Ran, Built, \
            Listed, Searched, Committed, Checked…). When one specific name is the clearest way to \
            say what happened — a subcommand, build target, file, or symbol — you MAY wrap that \
            single token in `backticks`, e.g. "Rebuilt the `nucleic-local` app"; never echo the \
            whole command that way. Under ~10 words. No preamble, bullets, or code fences — just \
            the line.
            """
        // Collapsed tool-call summaries are background-tier — they fire often, so they must
        // yield to user-facing work on the device model. Fall back to the command itself
        // (the heuristic gist) when the model is off/unavailable or just parrots it back.
        guard let raw = await generate(priority: .bashSummary, label: "Command summary", instructions: instructions, prompt: command) else {
            return command
        }
        let line = firstLine(of: raw)
        if line.isEmpty || HeuristicSummary.echoesCommands(line, commands: [command]) { return command }
        return line
    }

    func mergeBashSummaries(_ summaries: [String]) async -> String {
        // Reduce step: condense the per-command gists into one shorter line. A lone gist
        // is already the line; only merge when there's more than one to fold.
        let fallback = HeuristicSummary.joinBashSummaries(summaries)
        guard summaries.count > 1 else { return summaries.first.map(firstLine) ?? fallback }
        let instructions = """
            You merge a coding agent's per-command summaries of one run of shell commands into \
            ONE shorter line for a glanceable collapsed view. State what the whole run \
            ACCOMPLISHED using ONLY what the summaries below say — never invent steps they don't \
            mention. Build + test summaries merge to "Built and tested the app"; staging + commit \
            to "Staged and committed the changes"; a run of inspections (listing, reading, \
            searching) to "Looked through the project files". Lead with a past-tense verb. You MAY \
            wrap a single specific name (a target, file, or symbol) in `backticks` when it sharpens \
            the line; never echo a whole command. Under ~12 words. No preamble, bullets, or code \
            fences — just the line.
            """
        let prompt = "Per-command summaries, in order:\n" + summaries.map { "- \($0)" }.joined(separator: "\n")
        guard let raw = await generate(priority: .bashSummary, label: "Command summary", instructions: instructions, prompt: prompt) else {
            return fallback
        }
        // On-device models, uncertain, parrot an instruction's example verbatim ("Built and
        // tested the app" for a run of `ls`/`find`). Such a line shares no content word with the
        // gists it was given — drop it for the deterministic merge rather than show invented work.
        let line = firstLine(of: raw)
        if line.isEmpty || !HeuristicSummary.isGrounded(line, in: summaries) { return fallback }
        return line
    }

    func summarizeToolFamily(_ calls: [ToolCall], worktreeRoot: String?) async -> [String] {
        // A non-Bash eligible family (Grep/Glob/… under the "all tool calls" scope): merge
        // the redundant calls into one line. Falls back to the deterministic merge when the
        // model is off/unavailable.
        let fallback = HeuristicSummary.families(calls).map { HeuristicSummary.line(for: $0, relativeTo: worktreeRoot) }
        let verb = HeuristicSummary.families(calls).first?.verb ?? ""
        // A Grep family ("Searched for …") gets a search-tailored prompt: distill what the
        // agent was *looking for* across its patterns into one natural line, naming the
        // concept rather than re-listing raw regex. The other families keep the generic
        // list-merge below.
        let isSearch = !calls.isEmpty && calls.allSatisfy { $0.name == "Grep" }
        let instructions = isSearch ? """
            You summarize a run of code searches a coding agent just made, for a glanceable \
            collapsed view. Each line below is one regex pattern it searched for. Distill what \
            the agent was looking for into ONE short line beginning with "Searched for" — name \
            the concept, not the raw regex. Several related patterns (auth, login, session, \
            token) become "Searched for authentication and session handling"; a few unrelated \
            ones stay a short list. Under ~12 words. No preamble, bullets, code fences, \
            backticks, or regex syntax — just the line.
            """ : """
            You summarize a run of \(verb.lowercased()) tool calls a coding agent just made, for \
            a glanceable collapsed view. Merge the redundant calls into ONE line — three file \
            reads become "Read auth.swift, store.swift, view.swift", not three lines. State the \
            PURPOSE only when a bare list would be ambiguous. Output a single short line under \
            ~12 words, leading with the verb (\(verb)). No preamble, no bullets, no code fences, \
            no numbering — just the line.
            """
        // Ground the model in the agent's own actions rather than letting it invent work.
        let prompt = calls.compactMap { HeuristicSummary.actionDescription($0) }.joined(separator: "\n")
        guard let raw = await generate(priority: .background, label: isSearch ? "Search summary" : "Tool summary", instructions: instructions, prompt: prompt) else {
            return fallback
        }
        let lines = raw.split(whereSeparator: \.isNewline)
            .map { $0.trimmingCharacters(in: CharacterSet(charactersIn: " -•*\t")) }
            .filter { !$0.isEmpty }
        return lines.isEmpty ? fallback : lines
    }

    func updateTrunkSummary(previous: String, with digest: String) async -> String {
        // Incremental fold (NVRSION §6): rewrite the running trunk summary to absorb just the latest
        // landed turn. Each call sees one small increment over the prior summary, so the on-device
        // model keeps its accuracy where summarizing the whole accumulated diff at once would drift.
        let prev = previous.trimmingCharacters(in: .whitespacesAndNewlines)
        let digestTrimmed = digest.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !digestTrimmed.isEmpty else { return prev }
        let instructions = """
            You maintain a short, running summary of the changes accumulating on a shared coding \
            "trunk" before they ship to the real branch as one commit. You are given the summary SO \
            FAR and a description of the LATEST change that just landed. Return an UPDATED summary \
            that folds the latest change into the prior one: a concise, plain-language description of \
            everything the trunk now contains, phrased as the body of a commit message. Lead with \
            imperative verbs (Add, Fix, Refactor, Remove, Update…), group related work into one \
            clause, and keep it to a single line well under 200 characters. Preserve earlier work \
            still described in the summary; never invent changes the inputs don't mention. No \
            preamble, bullets, quotes, or code fences — output only the summary text.
            """
        let prompt = """
            Summary so far:
            \(prev.isEmpty ? "(nothing yet — this is the first change)" : prev)

            Latest change just landed:
            \(digestTrimmed)

            Updated summary:
            """
        // Trunk-summary folds are background-tier — they fire at turn end, never block the user, and
        // are already serialized per project by AppStore, so no coalescing is needed.
        if let raw = await generate(priority: .background, label: "Trunk summary", instructions: instructions, prompt: prompt) {
            let line = firstLine(of: raw)
            if !line.isEmpty { return line }
        }
        // Off / unavailable / empty — fall back to the deterministic accumulating fold.
        return HeuristicSummary.foldTrunkSummary(previous: previous, digest: digest)
    }

    /// The first non-empty line of a model reply, stripped of leading list/bullet markers
    /// — the single glanceable line these collapsed summaries want.
    private func firstLine(of raw: String) -> String {
        raw.split(whereSeparator: \.isNewline)
            .map { $0.trimmingCharacters(in: CharacterSet(charactersIn: " -•*\t")) }
            .first { !$0.isEmpty } ?? ""
    }

    /// Runs one prompt through the selected Foundation Models model. Returns nil if
    /// off, unavailable, or on any error (callers fall back to the heuristic).
    ///
    /// On-device requests pass through the shared ``AFMRequestQueue`` at `priority`, so a
    /// burst of soft-AI work can't saturate the Neural Engine — user-facing generations
    /// (chat naming) are released ahead of background ones (triage). Private Cloud
    /// Compute runs on Apple's servers, not this device, so it isn't queued.
    ///
    /// `coalesceKey`, when given, routes the on-device request through the queue's
    /// coalescing path: a newer keyed request supersedes any earlier still-waiting one of
    /// the same key (a busy group's redundant gists collapse to the latest). A superseded
    /// request returns nil — the same "no result" the caller already handles.
    private func generate(priority: AFMRequestQueue.Priority, coalesceKey: String? = nil, label: String, instructions: String, prompt: String) async -> String? {
        #if canImport(FoundationModels)
        // Carry the model's reply onto the request's history entry (trimmed), so clicking the
        // finished row in the inspector reveals the output it produced — the Bash summary line.
        let resultText: @Sendable (String?) -> String? = {
            $0?.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        // Route on the resolved choice so `.energyAware` lands on on-device (AC) or PCC
        // (battery) — it never reaches these cases itself.
        switch IntelligenceChoice.current.resolved {
        case .onDevice:
            // SystemLanguageModel is available from macOS 26.
            if #available(macOS 26, *) {
                let model = SystemLanguageModel.default
                guard model.isAvailable else { return nil }
                return await runQueued(.shared, priority: priority, coalesceKey: coalesceKey,
                                       label: label, prompt: prompt, resultText: resultText) {
                    try? await LanguageModelSession(model: model, instructions: instructions)
                        .respond(to: prompt).content
                }
            }
        case .privateCloudCompute:
            // Private Cloud Compute (and the generic session init) need macOS 27. It runs on
            // Apple's servers, so it routes through the unbounded cloud queue — visible in the
            // inspector, but never resource-constrained the way the on-device queue is.
            if #available(macOS 27, *) {
                guard PrivateCloudComputeLanguageModel().isAvailable else { return nil }
                return await runQueued(.sharedCloud, priority: priority, coalesceKey: coalesceKey,
                                       label: label, prompt: prompt, resultText: resultText) {
                    try? await LanguageModelSession(model: PrivateCloudComputeLanguageModel(),
                                                    instructions: instructions)
                        .respond(to: prompt).content
                }
            }
        case .energyAware, .off:
            return nil
        }
        #endif
        return nil
    }

    /// Routes `operation` through `queue` (the on-device or Private Cloud Compute lane) at
    /// `priority`. With a `coalesceKey`, takes the queue's coalescing path so a burst of
    /// redundant keyed background work collapses to its newest still-waiting instance; a
    /// superseded pass flattens to nil — the same "no result" every caller already falls back
    /// on. (The cloud queue never makes anything wait, so coalescing is a no-op there.)
    ///
    /// `resultText`, when given, derives the request's recorded output from its result — for
    /// the text path, the model's own reply — so the inspector's expanded detail can show what
    /// it produced. Omitted for structured work, whose result isn't text.
    private func runQueued<T: Sendable>(
        _ queue: AFMRequestQueue, priority: AFMRequestQueue.Priority, coalesceKey: String?,
        label: String, prompt: String,
        resultText: (@Sendable (T?) -> String?)? = nil,
        _ operation: @Sendable () async -> T?
    ) async -> T? {
        if let coalesceKey {
            return await queue.runCoalescing(priority: priority, coalesceKey: coalesceKey,
                                             label: label, detail: prompt,
                                             resultText: resultText, operation) ?? nil
        }
        return await queue.run(priority: priority, label: label, detail: prompt,
                               resultText: resultText, operation)
    }

    #if canImport(FoundationModels)
    /// Asks the selected model to rank + level the backlog using guided generation, so
    /// it returns a validated structured list (not free-form text the on-device model
    /// formats unreliably). Returns nil if off / unavailable / on error.
    ///
    /// `coalesceKey`, when given, routes the on-device request through the queue's
    /// coalescing path: a burst of backlog triages (several ideas added/edited in quick
    /// succession) collapses to a single pass, since each re-triages the whole current
    /// backlog and supersedes any earlier still-queued one. A superseded pass returns nil,
    /// which the caller treats as "no ranking" — harmless, as `AppStore`'s triageToken
    /// guard already discards a stale pass's verdicts.
    @available(macOS 26, *)
    private func rankTriage(priority: AFMRequestQueue.Priority, coalesceKey: String? = nil, label: String, instructions: String, prompt: String) async -> TriageRanking? {
        // Route on the resolved choice — `.energyAware` becomes on-device or PCC per power.
        switch IntelligenceChoice.current.resolved {
        case .onDevice:
            let model = SystemLanguageModel.default
            guard model.isAvailable else { return nil }
            // Both a superseded pass and a model nil flatten to "no ranking" — exactly what
            // the caller wants in either case.
            return await runQueued(.shared, priority: priority, coalesceKey: coalesceKey,
                                   label: label, prompt: prompt) {
                try? await LanguageModelSession(model: model, instructions: instructions)
                    .respond(to: prompt, generating: TriageRanking.self).content
            }
        case .privateCloudCompute:
            if #available(macOS 27, *) {
                guard PrivateCloudComputeLanguageModel().isAvailable else { return nil }
                return await runQueued(.sharedCloud, priority: priority, coalesceKey: coalesceKey,
                                       label: label, prompt: prompt) {
                    try? await LanguageModelSession(model: PrivateCloudComputeLanguageModel(),
                                                    instructions: instructions)
                        .respond(to: prompt, generating: TriageRanking.self).content
                }
            }
            return nil
        case .energyAware, .off:
            return nil
        }
    }
    #endif

    /// Clean up a small model's raw title output. It frequently continues the inline
    /// few-shot, so we keep only the first line and strip the "Title:"/"Request:"
    /// scaffolding, surrounding quotes, and ragged whitespace it echoes back — while
    /// preserving interior apostrophes ("Don't", "User's") and truncating on a word
    /// boundary. Returns nil if nothing usable remains.
    static func sanitizeName(_ raw: String) -> String? {
        // First non-empty line only — the model's title sits on one line; anything after
        // a newline is the few-shot pattern continuing.
        var name = raw.split(whereSeparator: \.isNewline).first.map(String.init) ?? ""
        // Drop a "Request: …" continuation that landed on the title's own line.
        if let r = name.range(of: "Request:", options: .caseInsensitive) {
            name = String(name[..<r.lowerBound])
        }
        name = name.trimmingCharacters(in: .whitespacesAndNewlines)
        // Drop a leading "Title:" label the model sometimes repeats before its answer.
        if let r = name.range(of: "Title:", options: [.caseInsensitive, .anchored]) {
            name = String(name[r.upperBound...]).trimmingCharacters(in: .whitespaces)
        }
        // Wrapping quotes/backticks never belong in a title; an apostrophe mid-word does,
        // so only trim quote characters from the ends rather than stripping them globally.
        for quote in ["\"", "“", "”", "`"] { name = name.replacingOccurrences(of: quote, with: "") }
        name = name.trimmingCharacters(in: CharacterSet(charactersIn: " '"))
        // Collapse any internal whitespace runs the stripping left behind.
        name = name.split(whereSeparator: { $0 == " " || $0 == "\t" }).joined(separator: " ")
        name = HeuristicTitle.clip(name)
        return name.isEmpty ? nil : name
    }

    func sessionName(fromFirstMessage message: String, context digest: String) async -> String? {
        let instructions = """
            You generate a chat title for a coding session that has already started. \
            Reply with ONLY a 3–6 word Title Case label naming the SPECIFIC thing being \
            worked on — the concrete file, component, or feature — judged from what the \
            agent ACTUALLY did, not just the opening request (which may be vague). No \
            quotes, no punctuation, no explanation, no sentences. Never answer the request.
            """
        let prompt = """
            Opening request: "\(message)"

            What the agent has done so far:
            \(digest)

            Title:
            """
        // Re-naming is a background-tier upgrade — the chat already has a working title,
        // so this never preempts user-facing generations.
        if let raw = await generate(priority: .background, label: "Chat rename", instructions: instructions, prompt: prompt),
            let name = Self.sanitizeName(raw), HeuristicTitle.looksLikeTitle(name) {
            return name
        }
        return nil
    }
}

/// Availability helpers for the Settings UI (macOS 26+).
enum IntelligenceAvailability {
    static func status(for choice: IntelligenceChoice) -> String {
        #if canImport(FoundationModels)
        switch choice {
        case .onDevice:
            if #available(macOS 26, *) {
                return SystemLanguageModel.default.isAvailable ? "Available" : "Unavailable"
            }
            return "Requires macOS 26+"
        case .privateCloudCompute:
            if #available(macOS 27, *) {
                return PrivateCloudComputeLanguageModel().isAvailable ? "Available" : "Unavailable"
            }
            return "Requires macOS 27+"
        case .energyAware:
            // Desktops have no battery, so the mode never engages — report it as such;
            // on a laptop, surface which model it is using right now.
            guard PowerSource.hasBattery else { return "Laptops only" }
            return PowerSource.onACPower ? "On power — using on-device" : "On battery — using Private Cloud Compute"
        case .off:
            return "—"
        }
        #else
        return "Unavailable"
        #endif
    }
}

/// The Mac's power state, read live from IOKit, for the Energy Aware intelligence mode:
/// it picks the on-device model on AC wall power and Private Cloud Compute on battery.
enum PowerSource {
    /// Every power-source description in a fresh IOKit snapshot. These are "Get" lookups
    /// (unowned), so they take the unretained value; the snapshot/list are "Copy" (owned).
    private static func descriptions() -> [[String: Any]] {
        guard let snapshot = IOPSCopyPowerSourcesInfo()?.takeRetainedValue() else { return [] }
        let list = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as [CFTypeRef]
        return list.compactMap {
            IOPSGetPowerSourceDescription(snapshot, $0)?.takeUnretainedValue() as? [String: Any]
        }
    }

    /// The internal battery's description, if this Mac has one.
    private static var internalBattery: [String: Any]? {
        descriptions().first { $0[kIOPSTypeKey] as? String == kIOPSInternalBatteryType }
    }

    /// True when this Mac has an internal battery — i.e. it's a laptop. Desktops have none,
    /// which is how the Energy Aware mode is gated off for them.
    static var hasBattery: Bool { internalBattery != nil }

    /// True when running on AC wall power (charger plugged in). Read from the battery's own
    /// power-source state (`AC Power` vs `Battery Power`), which flips live as the charger is
    /// plugged/unplugged. Desktops, with no battery, always report AC.
    static var onACPower: Bool {
        guard let battery = internalBattery else { return true }
        return battery[kIOPSPowerSourceStateKey] as? String == kIOPSACPowerValue
    }
}

/// A coarse read of whether this Mac can comfortably run the on-device model on EVERY
/// tool-call group (the "All tool calls" summary scope), which fires the model far more
/// often than Bash-only. Gauged from the chip: an M4 Pro / Max / Ultra, or any later
/// generation, clears the bar. Used only to advise in Settings — it gates nothing.
enum DeviceCapability {
    /// The CPU brand string, e.g. "Apple M4 Pro" or "Apple M1" (empty if unreadable).
    static var chipName: String {
        var size = 0
        guard sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0) == 0, size > 0 else { return "" }
        var buffer = [CChar](repeating: 0, count: size)
        guard sysctlbyname("machdep.cpu.brand_string", &buffer, &size, nil, 0) == 0 else { return "" }
        return String(cString: buffer).trimmingCharacters(in: .whitespaces)
    }

    /// True when the chip is an M4 Pro/Max/Ultra or a newer generation — strong enough
    /// that summarizing every tool call on-device is reasonable.
    static var handlesHeavySummaries: Bool {
        guard let (generation, tier) = parse(chipName) else { return false }
        return generation >= 5 || (generation == 4 && tier >= .pro)
    }

    /// A one-line recommendation for the heavy "all tool calls" scope on this machine.
    static var heavySummaryAdvice: String {
        handlesHeavySummaries
            ? "Your machine should be able to handle this."
            : "This is not recommended for your machine."
    }

    private enum Tier: Int, Comparable { case base, pro, max, ultra
        static func < (a: Tier, b: Tier) -> Bool { a.rawValue < b.rawValue } }

    /// (generation, tier) parsed from an Apple-silicon brand string; nil for Intel/unknown.
    private static func parse(_ brand: String) -> (generation: Int, tier: Tier)? {
        guard let match = brand.range(of: #"M(\d+)"#, options: .regularExpression),
              let generation = Int(brand[match].dropFirst()) else { return nil }
        let lower = brand.lowercased()
        let tier: Tier = lower.contains("ultra") ? .ultra
            : lower.contains("max") ? .max
            : lower.contains("pro") ? .pro : .base
        return (generation, tier)
    }
}
