import SwiftUI
import NucleicCore

/// A lightweight, main-actor observable that polls the on-device model queue so the
/// toolbar can show a glanceable "is the model busy" indicator without every view
/// having to poll the actor itself. The queue is an actor (not observable), so a small
/// poll loop is the honest way to track it. Brisk while there's activity — each
/// generation lasts only a second or two — and lazy once the queue empties, so a quiet
/// app barely touches the actor.
@MainActor
@Observable
final class AFMActivityMonitor {
    /// Shared so the freeze toggle (in the queue panel) and the indicator (in the sidebar
    /// switcher) read and write one source of truth — the freeze shows on screen the same
    /// frame it's flipped, instead of waiting for the next poll to notice it.
    static let shared = AFMActivityMonitor()

    private(set) var running = 0
    private(set) var waiting = 0
    /// The queue is frozen — slot hand-off is paused for inspection (see
    /// `AFMRequestQueue/setFrozen(_:)`). Drives the switcher's snowflake/glow.
    private(set) var frozen = false

    /// The on-device model is generating right now.
    var isBusy: Bool { running > 0 }
    /// Anything running, queued, or frozen — drives whether the toolbar indicator lights up.
    var hasActivity: Bool { running > 0 || waiting > 0 || frozen }

    /// Freeze or resume both queues. Flips `frozen` synchronously so the snowflake/glow update
    /// this frame, then pushes it to both actors (which actually pause slot hand-off). Freezing
    /// the cloud queue lines its requests up as waiters instead of firing them, so the merged
    /// inspector holds still; resuming drains them all at once (its cap is unbounded). This
    /// monitor is the only thing that ever changes either actor's freeze, so the three never
    /// drift — which is why the poll below doesn't reconcile `frozen` and a section switch
    /// can't drop it.
    func setFrozen(_ value: Bool) {
        frozen = value
        Task {
            await AFMRequestQueue.shared.setFrozen(value)
            await AFMRequestQueue.sharedCloud.setFrozen(value)
        }
    }

    /// Drives the poll loop for as long as the hosting view's `.task` is alive; cancels
    /// cleanly when that view goes away. Tracks only the live counts — `frozen` is owned by
    /// ``setFrozen(_:)`` so it survives the poll cadence and view teardown.
    func run() async {
        while !Task.isCancelled {
            // Both lanes feed the indicator: in PCC mode the on-device queue sits idle while
            // all the work is in the cloud queue, so summing keeps the toolbar honest.
            let onDevice = await AFMRequestQueue.shared.snapshot()
            let cloud = await AFMRequestQueue.sharedCloud.snapshot()
            running = onDevice.running.count + cloud.running.count
            waiting = onDevice.waiting.count + cloud.waiting.count
            try? await Task.sleep(for: .milliseconds(hasActivity ? 400 : 1500))
        }
    }
}

/// An inline inspector for the Apple Foundation Model request queues, rendered as a sidebar
/// mode (see `SidebarMode`): which soft-AI generations are running right now, and which are
/// waiting behind them (in release order — highest priority first, ties FIFO). On-device work
/// shares the single Neural Engine, so it runs one request at a time and a burst of background
/// work queues up; Private Cloud Compute runs on Apple's servers and isn't resource-constrained,
/// so its requests fire concurrently. Both lanes are merged here, with PCC rows marked by a
/// cloud glyph.
///
/// Each row expands to reveal what that slot is actually doing (the prompt the model is
/// processing) and how long it's been running or waiting. Polls while shown, since the
/// actor isn't observable; the loop cancels when the mode switches away.
struct SidebarIntelligencePanel: View {
    /// Cap for the card list — eight session cards, matching the Recents section. The pane
    /// shrinks to fit fewer cards and scrolls within this when there are more (see `SidebarPane`).
    let bodyHeight: CGFloat
    @Environment(\.appPalette) private var palette

    @State private var snapshot = AFMRequestQueue.Snapshot(running: [], waiting: [], history: [])
    @State private var loaded = false
    /// Which rows are expanded, keyed by each request's cross-queue `traceID` so the state
    /// survives the poll rebuilding the `Request` values each tick (and the on-device and
    /// cloud queues' independent ids can't collide).
    @State private var expanded: Set<String> = []
    /// The shared freeze/activity state — the single source of truth for the freeze, so the
    /// header button here and the switcher's snowflake stay in lock-step and the freeze
    /// persists across sidebar mode switches.
    private let afm = AFMActivityMonitor.shared
    /// The instant the queue was frozen, used to hold the live "running for…" / "waiting…"
    /// timers still while frozen so even the elapsed counters stop moving. `nil` when running.
    @State private var frozenAt: Date?

    var body: some View {
        SidebarPane(icon: "sparkles", summary: footerSummary, maxBodyHeight: bodyHeight) {
            freezeButton
        } content: {
            // A ticking `now` so the "running for…" / "waiting…" timers advance smoothly
            // between polls (the poll only refreshes the queue contents). Frozen, we pin
            // `now` to the freeze instant so the counters hold still too.
            TimelineView(.periodic(from: Date(), by: 0.5)) { context in
                content(now: frozenAt ?? context.date)
            }
        }
        .task { await poll() }
    }

    // The queue moves fast (each generation is a second or two), so poll briskly. The freeze
    // persists across sidebar mode switches (owned by the shared monitor, not this view), so
    // the panel can be torn down and rebuilt while the queue stays frozen.
    private func poll() async {
        while !Task.isCancelled {
            snapshot = await Self.mergedSnapshot()
            // Keep the timer-pin in step with the (shared, persistent) freeze, including when
            // the panel is reopened while the queue is still frozen.
            if afm.frozen, frozenAt == nil { frozenAt = Date() }
            else if !afm.frozen { frozenAt = nil }
            loaded = true
            try? await Task.sleep(for: .milliseconds(400))
        }
    }

    /// Merges the on-device and cloud queues into one view for the inspector: running sorted
    /// oldest-first, waiting in release order (priority then arrival), history most-recent
    /// first and bounded. Each request keeps its `isCloud` flag, so the rows can mark PCC work.
    /// Both queues freeze together (see `AFMActivityMonitor/setFrozen(_:)`), so either flag
    /// reflects the paused state.
    private static func mergedSnapshot() async -> AFMRequestQueue.Snapshot {
        let onDevice = await AFMRequestQueue.shared.snapshot()
        let cloud = await AFMRequestQueue.sharedCloud.snapshot()
        let running = (onDevice.running + cloud.running)
            .sorted { ($0.startedAt ?? .distantPast) < ($1.startedAt ?? .distantPast) }
        let waiting = (onDevice.waiting + cloud.waiting).sorted {
            $0.priority != $1.priority ? $0.priority > $1.priority : $0.enqueuedAt < $1.enqueuedAt
        }
        let history = Array((onDevice.history + cloud.history)
            .sorted { ($0.finishedAt ?? .distantPast) > ($1.finishedAt ?? .distantPast) }
            .prefix(50))
        return AFMRequestQueue.Snapshot(running: running, waiting: waiting, history: history,
                                        frozen: onDevice.frozen || cloud.frozen)
    }

    // MARK: Header

    /// Pauses (and resumes) the on-device model queue so the user can read what's lined up
    /// without rows being released and shuffling underneath them. Highlighted while frozen.
    private var freezeButton: some View {
        Button(action: toggleFreeze) {
            HStack(spacing: 4) {
                Image(systemName: "snowflake")
                Text(afm.frozen ? "Frozen" : "Freeze")
            }
            .font(.caption.weight(.medium))
            .padding(.horizontal, 8).padding(.vertical, 3)
            .background((afm.frozen ? palette.frozen : .clear).opacity(afm.frozen ? 0.18 : 0),
                        in: .capsule)
            .foregroundStyle(afm.frozen ? palette.frozen : .secondary)
            .contentShape(.capsule)
        }
        .buttonStyle(.plain)
        .help(afm.frozen
            ? "Resume model processing"
            : "Pause processing so you can inspect the queue without it moving")
    }

    private func toggleFreeze() {
        let value = !afm.frozen
        frozenAt = value ? Date() : nil
        afm.setFrozen(value)
    }

    // MARK: Content

    @ViewBuilder
    private func content(now: Date) -> some View {
        if !loaded {
            SidebarPanePlaceholder { ProgressView() }
        } else if snapshot.isEmpty && snapshot.history.isEmpty {
            SidebarPanePlaceholder {
                VStack(spacing: 8) {
                    Image(systemName: "sparkles")
                        .font(.title).foregroundStyle(.secondary)
                    Text("Queue is idle").font(.headline)
                    Text("Soft-AI work — chat names, summaries, backlog triage — runs through "
                        + "the on-device or Private Cloud Compute model. Nothing is running right now.")
                        .font(.caption).foregroundStyle(.secondary)
                        .multilineTextAlignment(.center)
                        .frame(maxWidth: 280)
                }
            }
        } else {
            VStack(alignment: .leading, spacing: 16) {
                if !snapshot.running.isEmpty {
                    section("Running") {
                        ForEach(snapshot.running, id: \.traceID) { request in
                            row(request, kind: .running, position: nil, now: now)
                        }
                    }
                }
                if !snapshot.waiting.isEmpty {
                    section("Waiting") {
                        ForEach(Array(snapshot.waiting.enumerated()), id: \.element.traceID) { index, request in
                            row(request, kind: .waiting, position: index + 1, now: now)
                        }
                    }
                }
                if !snapshot.history.isEmpty {
                    section("History") {
                        ForEach(snapshot.history, id: \.traceID) { request in
                            row(request, kind: .history, position: nil, now: now)
                        }
                    }
                }
            }
            // No top inset — the header provides the standard gap below the divider.
            .padding(.bottom, 12).padding(.horizontal, 8)
        }
    }

    private func row(_ request: AFMRequestQueue.Request, kind: AFMRequestRow.Kind,
                     position: Int?, now: Date) -> some View {
        AFMRequestRow(
            request: request, kind: kind, position: position, now: now,
            isExpanded: expanded.contains(request.traceID),
            palette: palette,
            onToggle: {
                withAnimation(.easeInOut(duration: 0.18)) {
                    if expanded.contains(request.traceID) { expanded.remove(request.traceID) }
                    else { expanded.insert(request.traceID) }
                }
            })
    }

    @ViewBuilder
    private func section(_ title: String, @ViewBuilder _ body: () -> some View) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(title.uppercased())
                .font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
            body()
        }
    }

    private var footerSummary: String {
        let running = snapshot.running.count
        let waiting = snapshot.waiting.count
        if running == 0 && waiting == 0 { return "Idle" }
        let r = "\(running) running"
        return waiting == 0 ? r : "\(r) · \(waiting) waiting"
    }
}

/// One request in the queue, expandable to reveal what that slot is actually doing.
/// Collapsed: a spinner (running), hourglass (waiting), or checkmark (finished), the work's
/// short label, and a priority badge. Expanded: how long it ran or has been waiting, what
/// its priority tier means, and a preview of the prompt the model is processing. Finished
/// rows are rendered grayed out, since they're a record of past work, not live activity.
private struct AFMRequestRow: View {
    /// Which queue lane this row belongs to — drives its icon, status line, and (for
    /// history) the grayed-out treatment.
    enum Kind { case running, waiting, history }

    let request: AFMRequestQueue.Request
    let kind: Kind
    /// 1-based position in the waiting line (nil for running and history rows).
    let position: Int?
    let now: Date
    let isExpanded: Bool
    let palette: AppPalette
    let onToggle: () -> Void

    private var isRunning: Bool { kind == .running }
    private var isHistory: Bool { kind == .history }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Button(action: onToggle) {
                HStack(spacing: 10) {
                    icon
                    Text(request.label.isEmpty ? "AI request" : request.label)
                        .font(.callout).lineLimit(1)
                    Spacer(minLength: 8)
                    if request.isCloud {
                        // Cloud outline marks a Private Cloud Compute call — it ran on Apple's
                        // servers, not this device's Neural Engine.
                        Image(systemName: "cloud")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                            .help("Ran in Private Cloud Compute")
                    }
                    PriorityBadge(priority: request.priority)
                    Image(systemName: "chevron.right")
                        .font(.caption2.weight(.semibold))
                        .foregroundStyle(.tertiary)
                        .rotationEffect(.degrees(isExpanded ? 90 : 0))
                }
                .contentShape(Rectangle())
            }
            .buttonStyle(.plain)

            if isExpanded { detail }
        }
        // No card box — a flush, full-width row like the session entries, so its label runs
        // as wide as theirs (the panel supplies the matching horizontal inset).
        .padding(.vertical, 6)
        .frame(maxWidth: .infinity, alignment: .leading)
        // Finished work is past, not live — mute the whole row so the eye lands on the
        // running and waiting sections above it.
        .opacity(isHistory ? 0.55 : 1)
    }

    @ViewBuilder
    private var icon: some View {
        switch kind {
        case .running:
            ProgressView().controlSize(.small).frame(width: 16)
        case .waiting:
            Image(systemName: "hourglass")
                .foregroundStyle(palette.attention).frame(width: 16)
        case .history:
            Image(systemName: "checkmark.circle")
                .foregroundStyle(.secondary).frame(width: 16)
        }
    }

    @ViewBuilder
    private var detail: some View {
        VStack(alignment: .leading, spacing: 8) {
            Divider().padding(.top, 10).padding(.bottom, 2)

            Label(statusText, systemImage: statusIcon)
                .font(.caption).foregroundStyle(statusTint)

            if request.isCloud {
                Label("Private Cloud Compute — runs on Apple's servers, off this device.",
                      systemImage: "cloud")
                    .font(.caption).foregroundStyle(.secondary)
            }

            Text(priorityExplanation)
                .font(.caption).foregroundStyle(.secondary)

            Text("Working on")
                .font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
                .padding(.top, 2)
            ScrollView {
                Text(detailText)
                    .font(.caption.monospaced())
                    .foregroundStyle(.secondary)
                    .textSelection(.enabled)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
            .frame(maxHeight: 120)
            .padding(8)
            .background(AppTheme.background, in: .rect(cornerRadius: 6))

            // What the model produced — shown once the request finishes and recorded a text
            // output (a Bash summary line). Absent while it's still running, and for structured
            // work that returns no text.
            if let output = outputText {
                Text("Output")
                    .font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
                    .padding(.top, 2)
                ScrollView {
                    Text(output)
                        .font(.caption.monospaced())
                        .foregroundStyle(.primary)
                        .textSelection(.enabled)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .frame(maxHeight: 120)
                .padding(8)
                .background(AppTheme.background, in: .rect(cornerRadius: 6))
            }
        }
        .padding(.top, 2)
    }

    /// "Running for 3s" / "Waiting 5s · position 2" / "Completed · ran for 2s" — the status
    /// line, live for running and waiting rows and a final record for history rows.
    private var statusText: String {
        if isHistory {
            guard let ran = request.ranFor else { return "Completed" }
            return ran < 1 ? "Completed" : "Completed · ran for \(Self.elapsed(ran))"
        }
        if let running = request.runningFor(now: now) {
            return running < 1 ? "Running" : "Running for \(Self.elapsed(running))"
        }
        let queued = request.queuedFor(now: now)
        let base = queued < 1 ? "Waiting" : "Waiting \(Self.elapsed(queued))"
        if let position { return "\(base) · position \(position)" }
        return base
    }

    private var statusIcon: String {
        switch kind {
        case .running: "bolt.fill"
        case .waiting: "hourglass"
        case .history: "checkmark.circle"
        }
    }

    private var statusTint: Color {
        switch kind {
        case .running: palette.accent
        case .waiting: palette.attention
        case .history: .secondary
        }
    }

    private var priorityExplanation: String {
        switch request.priority {
        case .interactive:
            "User-facing work you're waiting on right now — it preempts queued background work."
        case .bashSummary:
            "Summarizing command output for a running turn — runs ahead of background backlog work."
        case .completion:
            "Wrapping up a finished turn — runs ahead of background backlog work."
        case .background:
            "Background backlog work you're not actively watching."
        }
    }

    /// The prompt the model is processing, clamped so a huge digest can't blow out the
    /// panel; the box scrolls and the text is selectable for the rest.
    private var detailText: String {
        let trimmed = request.detail.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return "No prompt detail for this request." }
        return trimmed.count > 1500 ? String(trimmed.prefix(1500)) + "…" : trimmed
    }

    /// The model's reply for a finished request, clamped like the prompt box. nil when the
    /// request produced no text (still running, or structured work) so the Output section
    /// stays hidden.
    private var outputText: String? {
        guard let trimmed = request.result?.trimmingCharacters(in: .whitespacesAndNewlines),
              !trimmed.isEmpty else { return nil }
        return trimmed.count > 1500 ? String(trimmed.prefix(1500)) + "…" : trimmed
    }

    /// "just-now"-free elapsed formatting for durations ≥ 1s: "8s", "1m 04s".
    private static func elapsed(_ t: TimeInterval) -> String {
        let total = Int(t)
        if total < 60 { return "\(total)s" }
        return "\(total / 60)m \(String(format: "%02d", total % 60))s"
    }
}

/// A small colored capsule naming a request's priority tier.
private struct PriorityBadge: View {
    @Environment(\.appPalette) private var palette
    let priority: AFMRequestQueue.Priority

    var body: some View {
        Text(priority.label)
            .font(.caption2.weight(.semibold))
            .padding(.horizontal, 8).padding(.vertical, 3)
            .background(tint.opacity(0.18), in: .capsule)
            .foregroundStyle(tint)
    }

    // Interactive work is user-facing (accent); background backlog work is muted.
    private var tint: Color {
        switch priority {
        case .interactive: palette.accent
        case .bashSummary: palette.active
        case .completion: palette.success
        case .background: .secondary
        }
    }
}
