import SwiftUI
import NucleicCore

/// The sidebar "Control" mode (see `SidebarMode`): a cross-session view of the whole Nucleic
/// Control subsystem, gathered from `AppStore.controlSnapshot()`. Four stacked sections —
///
///  1. **Container** — health of the shared `nucleic-control` sandbox (running/stopped, resources,
///     how many Control sessions and projects it serves).
///  2. **Autoship** — each Control session's merge-queue state (armed → queued → merging →
///     shipped / failed).
///  3. **File locks** — which sessions hold a lock on each file and which are queued behind them
///     (with the deadlock banner + release controls), exactly as the old Interlock pane.
///  4. **Activity** — the git-interceptor feed: the merges/commits/rebases the in-container shim
///     observed with certainty, newest-first.
///
/// All four are Control-only by design (locking, autoship, the container, and the interceptor are
/// one bundle), so this panel is the single place that surfaces them. It polls while shown — the
/// container probe and lock footprints aren't observable — and the loop cancels on a mode switch.
struct SidebarControlPanel: 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(AppStore.self) private var store
    @Environment(\.appPalette) private var palette

    @State private var snapshot: ControlSnapshot?
    @State private var confirmingReleaseAll = false

    var body: some View {
        SidebarPane(icon: "atom", summary: summary, maxBodyHeight: bodyHeight) {
            releaseAllButton
        } content: {
            content
        }
        .task { await poll() }
        .confirmationDialog(
            "Release all file locks?",
            isPresented: $confirmingReleaseAll, titleVisibility: .visible
        ) {
            Button("Release All Locks", role: .destructive) { releaseAll() }
            Button("Cancel", role: .cancel) {}
        } message: {
            Text("Clears every session's hold on its changed files so all queued agents "
                + "can proceed. Each lock re-arms once that session merges or edits again.")
        }
    }

    // Poll the aggregate snapshot while the pane is open. Cancels automatically on dismiss.
    private func poll() async {
        while !Task.isCancelled {
            snapshot = await store.controlSnapshot()
            try? await Task.sleep(for: .seconds(2))
        }
    }

    // MARK: - Header

    /// One-line health rollup for the pane header: container state plus any live lock/ship counts.
    private var summary: String {
        guard let snapshot else { return "Loading…" }
        let c = snapshot.container
        if !c.serviceEnabled { return "Container service off" }
        var bits = [c.running ? "Container running" : (c.exists ? "Container stopped" : "No container")]
        let holders = holderIDs(snapshot).count
        if holders > 0 {
            let n = snapshot.locks.files.count
            bits.append("\(holders) holding \(n) file\(n == 1 ? "" : "s")")
        }
        let inFlight = snapshot.autoship.filter { $0.isInFlight }.count
        if inFlight > 0 { bits.append("\(inFlight) shipping") }
        return bits.joined(separator: " · ")
    }

    /// Sessions currently holding at least one file lock.
    private func holderIDs(_ snapshot: ControlSnapshot) -> [SessionID] {
        var seen = Set<SessionID>()
        return snapshot.locks.files
            .flatMap { $0.holders.map(\.sessionID) }
            .filter { seen.insert($0).inserted }
    }

    /// Force-releases every session's locks at once. Shown only while some session is holding one.
    @ViewBuilder
    private var releaseAllButton: some View {
        if let snapshot, !holderIDs(snapshot).isEmpty {
            Button("Release All", systemImage: "lock.open") {
                confirmingReleaseAll = true
            }
            .buttonStyle(.borderless).font(.caption).controlSize(.small)
            .help("Force-release every session's file locks so all queued agents can proceed")
        }
    }

    // MARK: - Body

    @ViewBuilder
    private var content: some View {
        if let snapshot {
            VStack(alignment: .leading, spacing: 16) {
                containerSection(snapshot.container)
                if !snapshot.autoship.isEmpty { autoshipSection(snapshot.autoship) }
                locksSection(snapshot.locks)
                if !snapshot.activity.isEmpty { activitySection(snapshot.activity) }
                if !snapshot.commands.isEmpty { commandsSection(snapshot.commands) }
                if isQuiet(snapshot) { quietNote }
            }
            // No top inset — the header provides the standard gap below the divider.
            .padding(.bottom, 12).padding(.horizontal, 8)
        } else {
            SidebarPanePlaceholder { ProgressView() }
        }
    }

    /// True when nothing is happening beyond the container itself — no locks, ships, or activity.
    private func isQuiet(_ snapshot: ControlSnapshot) -> Bool {
        snapshot.autoship.isEmpty && snapshot.locks.isEmpty && snapshot.activity.isEmpty
            && snapshot.commands.isEmpty && store.deadlockedSessions.isEmpty
    }

    private var quietNote: some View {
        Text("No autoship, file locks, or command activity yet.")
            .font(.caption).foregroundStyle(.secondary)
            .fixedSize(horizontal: false, vertical: true)
            .padding(.top, 2)
    }

    // MARK: - Container

    @ViewBuilder
    private func containerSection(_ c: ControlContainerInfo) -> some View {
        section("Container") {
            VStack(alignment: .leading, spacing: 6) {
                // First-run acquisition: while the engine is pulling the kernel / runtime / sandbox
                // image (the usual cause of a slow first session), show a labeled progress bar above
                // the identity rows. Reads the shared observable the panel poll keeps fresh.
                if let download = store.containerDownload {
                    DownloadBar(progress: download)
                }
                // Identity: friendly type as the heading, the opaque (randomized) container name as
                // subtext. One row per live control container; a generic row when none exists yet.
                if c.entries.isEmpty {
                    HStack(spacing: 6) {
                        Circle().fill(containerColor(c)).frame(width: 8, height: 8)
                        Text("Control container").font(.callout).lineLimit(1)
                        Spacer(minLength: 8)
                        Text(containerStatusText(c)).font(.caption2).foregroundStyle(.secondary)
                    }
                } else {
                    ForEach(c.entries) { entry in
                        HStack(spacing: 6) {
                            Circle().fill(entry.running ? palette.success : palette.attention)
                                .frame(width: 8, height: 8)
                            VStack(alignment: .leading, spacing: 1) {
                                Text(entry.typeLabel).font(.callout).lineLimit(1)
                                Text(entry.name).font(.caption2).foregroundStyle(.secondary)
                                    .lineLimit(1).truncationMode(.middle)
                            }
                            Spacer(minLength: 8)
                            Text(statusText(running: entry.running, in: c))
                                .font(.caption2).foregroundStyle(.secondary)
                        }
                    }
                }
                if c.serviceEnabled {
                    // CPU count and RAM allocation are intentionally not shown: nucleic-control
                    // containers auto-manage resources (allocated at the host maximum), so those
                    // figures are constant and uninformative. The live active-session count rides
                    // the container row above instead (see `statusText`); the real-time load meter
                    // below remains, since it reflects actual usage rather than the fixed ceiling.
                    //
                    // Resource monitor: live CPU + RAM of the running container, color-coded by load
                    // (green < 70%, amber 70–89%, red 90%+). Present only while running and the probe
                    // succeeded — RAM nearing 100% is the lead-up to a status-137 OOM kill.
                    if let usage = c.usage {
                        VStack(spacing: 4) {
                            ResourceMeter(label: "CPU", percent: usage.cpuPercent)
                            ResourceMeter(
                                label: "RAM", percent: usage.memoryPercent,
                                detail: Self.memoryDetail(usage))
                        }
                        .padding(.top, 4)
                    }
                } else {
                    Text("Nucleic Control needs the container service. Enable it in Settings → Sandbox.")
                        .font(.caption2).foregroundStyle(.secondary)
                        .fixedSize(horizontal: false, vertical: true)
                }
            }
            .padding(.vertical, 4)
        }
    }

    private func containerColor(_ c: ControlContainerInfo) -> Color {
        if !c.serviceEnabled { return .secondary }
        if c.running { return palette.success }
        if c.exists { return palette.attention }
        return .secondary
    }

    private func containerStatusText(_ c: ControlContainerInfo) -> String {
        if !c.serviceEnabled { return "Service off" }
        if c.running { return statusText(running: true, in: c) }
        if c.exists { return "Stopped" }
        return "Not created"
    }

    /// Trailing status for a running container row. Once any Control session is active, the live
    /// session count replaces a bare "Running" — with the (maxed-out) CPU/RAM figures hidden, the
    /// active-session count is the at-a-glance signal worth surfacing. Idle running falls back to
    /// "Running"; not-running falls back to "Stopped".
    private func statusText(running: Bool, in c: ControlContainerInfo) -> String {
        guard running else { return "Stopped" }
        return c.activeSessions > 0 ? "\(c.activeSessions) active" : "Running"
    }

    /// "4.2 / 8.0 GB" — used vs. total for the RAM meter's tooltip.
    private static func memoryDetail(_ u: ContainerResourceSample) -> String {
        func gb(_ bytes: UInt64) -> String {
            String(format: "%.1f", Double(bytes) / 1_073_741_824)
        }
        return "\(gb(u.memoryUsedBytes)) / \(gb(u.memoryTotalBytes)) GB"
    }

    // MARK: - Autoship

    @ViewBuilder
    private func autoshipSection(_ entries: [AutoshipEntry]) -> some View {
        section("Autoship") {
            ForEach(entries) { e in
                let v = autoshipVisual(e)
                ControlInfoRow(
                    symbol: v.symbol, tint: v.tint, title: e.title, subtitle: v.line,
                    onOpen: { open(e.id) })
            }
        }
    }

    /// Glyph, tint, and status line for one autoship entry, mirroring `handleShipUpdate`'s notes.
    private func autoshipVisual(_ e: AutoshipEntry) -> (symbol: String, tint: Color, line: String) {
        switch e.status {
        case nil:
            return ("shippingbox", .secondary, "Armed → \(e.target)")
        case .queued(let position):
            let where_ = position == 0 ? "Next to ship" : "Queued #\(position + 1)"
            return ("clock", palette.attention, "\(where_) → \(e.target)")
        case .merging:
            return ("arrow.triangle.merge", palette.accent, "Merging → \(e.target)…")
        case .merged(let commit):
            // Commit is empty for an agent-performed merge observed in-container (no SHA on hand);
            // omit the "· <sha>" suffix in that case.
            let sha = commit.isEmpty ? "" : " · \(commit.prefix(7))"
            return ("checkmark.seal.fill", palette.success, "Shipped → \(e.target)\(sha)")
        case .conflicted(let paths):
            let list = paths.prefix(2).joined(separator: ", ")
            let more = paths.count > 2 ? "…" : ""
            return ("exclamationmark.triangle.fill", palette.attention,
                "Conflict in \(list)\(more) — needs attention (still armed)")
        case .failed(let reason):
            return ("exclamationmark.triangle.fill", palette.attention, "Failed — \(reason)")
        case .skipped:
            return ("minus.circle", .secondary, "Nothing to ship")
        }
    }

    // MARK: - File locks

    @ViewBuilder
    private func locksSection(_ locks: LockQueueSnapshot) -> some View {
        if !store.deadlockedSessions.isEmpty {
            deadlockBanner
        }
        if !locks.files.isEmpty {
            section("File locks") {
                ForEach(locks.files) { lock in
                    FileLockCard(lock: lock, onOpen: open, onRelease: release)
                }
            }
        }
        // Waiters with no predicted files won't appear under any file above; surface them here.
        let unfiled = locks.waiters.filter { $0.wantedFiles.isEmpty }
        if !unfiled.isEmpty {
            section("Queued (no specific file)") {
                ForEach(unfiled) { waiter in
                    WaiterCard(waiter: waiter, onOpen: open)
                }
            }
        }
    }

    /// Shown only when the lock manager finds a wait-for cycle the queue can never clear on its own.
    private var deadlockBanner: some View {
        HStack(spacing: 8) {
            Image(systemName: "exclamationmark.triangle.fill")
                .foregroundStyle(palette.attention)
            let n = store.deadlockedSessions.count
            Text("Deadlock: \(n) session\(n == 1 ? "" : "s") are waiting on each other's locks. "
                + "Release one holder below to break the cycle.")
                .font(.caption)
            Spacer(minLength: 0)
        }
        .padding(.horizontal, 8).padding(.vertical, 10)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(palette.attention.opacity(0.12))
    }

    // MARK: - Activity

    @ViewBuilder
    private func activitySection(_ events: [GitInterceptorEvent]) -> some View {
        section("Activity") {
            ForEach(events) { e in
                let failed = !e.succeeded
                ControlInfoRow(
                    symbol: failed ? "exclamationmark.triangle" : e.symbol,
                    tint: failed ? palette.attention : .secondary,
                    title: e.label,
                    subtitle: failed ? "\(e.sessionTitle) · failed" : e.sessionTitle,
                    trailing: Self.relative(e.at),
                    onOpen: { open(e.sessionID) })
            }
        }
    }

    // MARK: - Commands

    @ViewBuilder
    private func commandsSection(_ events: [CommandInterceptorEvent]) -> some View {
        section("Commands") {
            ForEach(events) { e in
                let failed = !e.succeeded
                ControlInfoRow(
                    symbol: failed ? "exclamationmark.triangle" : e.symbol,
                    tint: failed ? palette.attention : (e.isMutating ? palette.accent : .secondary),
                    title: e.label,
                    subtitle: Self.commandSubtitle(e),
                    trailing: Self.relative(e.at),
                    onOpen: { open(e.sessionID) })
            }
        }
    }

    /// The command row's single meta line: session · duration · output preview (or `failed`),
    /// kept to one line so every row in the section is the same fixed height.
    private static func commandSubtitle(_ e: CommandInterceptorEvent) -> String {
        var parts: [String] = [e.sessionTitle]
        if let ms = e.durationMs { parts.append(durationText(ms)) }
        if !e.succeeded { parts.append("exit \(e.exitCode)") }
        if !e.outputPreview.isEmpty { parts.append(e.outputPreview) }
        return parts.joined(separator: " · ")
    }

    private static func durationText(_ ms: Int) -> String {
        ms < 1000 ? "\(ms)ms" : String(format: "%.1fs", Double(ms) / 1000)
    }

    private static let relativeFormatter: RelativeDateTimeFormatter = {
        let f = RelativeDateTimeFormatter()
        f.unitsStyle = .abbreviated
        return f
    }()

    private static func relative(_ date: Date) -> String {
        relativeFormatter.localizedString(for: date, relativeTo: Date())
    }

    // MARK: - Shared chrome + actions

    @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 func open(_ sessionID: SessionID) {
        store.openSessionID = sessionID
    }

    private func release(_ sessionID: SessionID) {
        Task {
            await store.forceReleaseLocks(sessionID)
            snapshot = await store.controlSnapshot()
        }
    }

    private func releaseAll() {
        guard let snapshot else { return }
        let ids = holderIDs(snapshot)
        Task {
            for id in ids { await store.forceReleaseLocks(id) }
            self.snapshot = await store.controlSnapshot()
        }
    }
}

private extension AutoshipEntry {
    /// Queued or actively merging — the states that count toward the header's "shipping" tally.
    var isInFlight: Bool {
        switch status {
        case .queued, .merging: return true
        default: return false
        }
    }
}

/// A compact session-style row shared by the Autoship and Activity sections: a small leading
/// status glyph centered against the row, a title line, an optional dimmer subtitle, and an
/// optional trailing note (e.g. a relative timestamp). Tapping opens the responsible chat.
private struct ControlInfoRow: View {
    let symbol: String
    let tint: Color
    let title: String
    var subtitle: String?
    var trailing: String?
    let onOpen: () -> Void

    var body: some View {
        HStack(spacing: 6) {
            Image(systemName: symbol)
                .font(.system(size: 10, weight: .semibold))
                .foregroundStyle(tint)
                .frame(width: 12)
            VStack(alignment: .leading, spacing: 1) {
                Text(title).lineLimit(1).truncationMode(.middle)
                if let subtitle, !subtitle.isEmpty {
                    Text(subtitle).font(.caption2).foregroundStyle(.secondary).lineLimit(1)
                }
            }
            Spacer(minLength: 8)
            if let trailing {
                Text(trailing).font(.caption2).foregroundStyle(.tertiary).fixedSize()
            }
        }
        .padding(.vertical, 5)
        .frame(maxWidth: .infinity, alignment: .leading)
        .contentShape(Rectangle())
        .onTapGesture { onOpen() }
    }
}

/// The container section's first-run download indicator: a labeled progress bar shown while the
/// engine acquires the sandbox's on-disk artifacts (the Linux kernel, the container runtime, and —
/// the dominant cost — the sandbox image, then its unpack). Determinate when the registry reports
/// byte totals (a real fill + percentage + "MB" readout), otherwise an indeterminate sweeping bar.
/// Either way it tells the user the container is *downloading*, the usual reason a first session
/// stalls before it responds.
private struct DownloadBar: View {
    @Environment(\.appPalette) private var palette
    let progress: ContainerDownloadProgress

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack(spacing: 6) {
                Image(systemName: "arrow.down.circle")
                    .font(.system(size: 11, weight: .semibold))
                    .foregroundStyle(palette.accent)
                Text(progress.label).font(.caption).lineLimit(1)
                Spacer(minLength: 8)
                if let fraction = progress.fraction {
                    Text("\(Int((fraction * 100).rounded()))%")
                        .font(.caption2.monospacedDigit()).foregroundStyle(.secondary)
                }
            }
            // Determinate when bytes are known; an indeterminate sweep otherwise.
            if let fraction = progress.fraction {
                ProgressView(value: fraction).tint(palette.accent)
            } else {
                ProgressView().progressViewStyle(.linear).tint(palette.accent)
            }
            if let detail = progress.byteDetail {
                Text(detail).font(.caption2.monospacedDigit()).foregroundStyle(.tertiary)
            }
        }
        .padding(.vertical, 4)
        .frame(maxWidth: .infinity, alignment: .leading)
        .help("The sandbox container is downloading — the first session waits for this to finish.")
    }
}

/// A compact "progress-like" usage line for the container resource monitor: a fixed-width label
/// (`CPU` / `RAM`), a thin capsule bar filled to `percent`, and the percentage. The fill is
/// color-coded by load — green below 70%, amber 70–89%, red 90%+ — so a container creeping toward
/// its memory ceiling (the status-137 OOM threshold) reads at a glance. `detail` (e.g. used/total
/// GB) rides the tooltip so both lines stay the same shape.
private struct ResourceMeter: View {
    @Environment(\.appPalette) private var palette
    let label: String
    let percent: Double
    var detail: String? = nil

    /// Green / amber / red by the requested thresholds: ok < 70%, medium-high 70–89%, critical 90%+.
    private var tint: Color {
        if percent >= 90 { return palette.danger }
        if percent >= 70 { return palette.attention }
        return palette.success
    }

    /// Clamp to a 0…1 fill fraction so a noisy probe can't overflow the track.
    private var fraction: CGFloat { CGFloat(min(100, max(0, percent)) / 100) }

    var body: some View {
        HStack(spacing: 8) {
            Text(label)
                .font(.caption2).foregroundStyle(.secondary)
                .frame(width: 28, alignment: .leading)
            GeometryReader { geo in
                ZStack(alignment: .leading) {
                    Capsule().fill(Color.secondary.opacity(0.15))
                    // A visible sliver even at ~0% so the bar never looks empty/broken.
                    Capsule().fill(tint)
                        .frame(width: max(2, geo.size.width * fraction))
                }
            }
            .frame(height: 5)
            Text("\(Int(percent.rounded()))%")
                .font(.caption2.monospacedDigit()).foregroundStyle(.secondary)
                .frame(width: 34, alignment: .trailing)
        }
        .help(detail.map { "\(label): \($0)" } ?? "\(label): \(Int(percent.rounded()))%")
    }
}

/// One locked file, rendered as session-style rows — one per participant. Each row shows a
/// small lock symbol (green = holding, amber hourglass = waiting) centered against the row,
/// the file name as the title, and the chat that holds (or waits for) it beneath.
private struct FileLockCard: View {
    @Environment(\.appPalette) private var palette
    let lock: FileLock
    let onOpen: (SessionID) -> Void
    let onRelease: (SessionID) -> Void

    private var filename: String { (lock.path as NSString).lastPathComponent }

    var body: some View {
        VStack(alignment: .leading, spacing: 2) {
            ForEach(lock.holders) { holder in
                LockEntryRow(
                    title: filename, subtitle: holder.title, fullPath: lock.path,
                    icon: "lock.fill", tint: palette.success,
                    onOpen: { onOpen(holder.sessionID) },
                    onRelease: { onRelease(holder.sessionID) })
            }
            ForEach(lock.waiters) { waiter in
                LockEntryRow(
                    title: filename, subtitle: waiter.title, fullPath: lock.path,
                    icon: "hourglass", tint: palette.attention,
                    indent: 16,
                    onOpen: { onOpen(waiter.sessionID) },
                    onRelease: nil)
            }
        }
    }
}

/// A single lock entry styled to match `SessionRow`: a small leading status symbol centered
/// vertically against the whole row, a title line, and a dimmer subtitle line. Tapping opens
/// the chat; holders also get a trailing "Release" button. The symbol + color carry the
/// holding/waiting distinction, so there's no role text.
private struct LockEntryRow: View {
    let title: String
    /// `nil` hides the second line (e.g. a queued session with nothing to add).
    var subtitle: String?
    /// Full path for the tooltip, since the title shows only the file name.
    var fullPath: String?
    let icon: String
    let tint: Color
    /// Leading indent, used to nest waiting rows under the file their holder locked.
    var indent: CGFloat = 0
    let onOpen: () -> Void
    /// Holders only — drives the trailing "Release" button.
    var onRelease: (() -> Void)?

    var body: some View {
        HStack(spacing: 6) {
            // Sized and centered like SessionRow's status dot so the rows align.
            Image(systemName: icon)
                .font(.system(size: 10, weight: .semibold))
                .foregroundStyle(tint)
                .frame(width: 8)
            VStack(alignment: .leading, spacing: 1) {
                Text(title).lineLimit(1).truncationMode(.middle)
                if let subtitle, !subtitle.isEmpty {
                    Text(subtitle).font(.caption2).foregroundStyle(.secondary).lineLimit(1)
                }
            }
            Spacer(minLength: 8)
            if let onRelease {
                Button("Release") { onRelease() }
                    .buttonStyle(.borderless).font(.caption)
                    .help("Force-release this session's lock so queued agents can proceed. "
                        + "It re-arms once the session merges or edits again.")
            }
        }
        // No outer horizontal padding — the panel's card chunk already insets by 8, lining
        // these rows up with the section title and the session rows below. `indent` nudges
        // waiting rows to the right so they read as queued under their file.
        .padding(.leading, indent)
        .padding(.vertical, 5)
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(.vertical, 1)
        .contentShape(Rectangle())
        .onTapGesture { onOpen() }
        .help(fullPath ?? "")
        .contextMenu {
            Button("Open Chat") { onOpen() }
        }
    }
}

/// A queued session that predicted no specific file (it matched another agent's work by
/// intent). Same session-style row, with the chat name as the title and what's blocking it
/// (or its task) beneath.
private struct WaiterCard: View {
    @Environment(\.appPalette) private var palette
    let waiter: WaitingSession
    let onOpen: (SessionID) -> Void

    private var subtitle: String? {
        if !waiter.blockedBy.isEmpty {
            return "Blocked by " + waiter.blockedBy.map(\.title).joined(separator: ", ")
        }
        return waiter.task.isEmpty ? nil : waiter.task
    }

    var body: some View {
        LockEntryRow(
            title: waiter.title, subtitle: subtitle, fullPath: nil,
            icon: "hourglass", tint: palette.attention,
            onOpen: { onOpen(waiter.sessionID) }, onRelease: nil)
    }
}
