import SwiftUI
import AppKit
import NucleicCore
import NucleicProtocol

/// The **Covalence Mesh Viewer** — a standalone diagnostics window over the mesh (docs/COVALENCE_QUEUE.md,
/// OBSERVABILITY §A.4's "in-app Diagnostics panel", scoped to Covalence). Top pane: live mesh state —
/// every known host's presence card + connection truth (as a list or as a node/link graph of the
/// mesh), the Covalence-managed sessions on this Mac,
/// the parked dispatch queue, and in-flight transfers. Bottom pane: the structural mesh event log
/// (`AppStore.meshDiagnostics`), filterable by category, for diagnosing why a dispatch parked, a
/// rebalance declined, or a peer flapped. Read-only by design (the "Reset Covalence…" escape hatch
/// stays in Settings); structural metadata only — never prompt/draft content.
@MainActor
final class CovalenceMeshViewerWindowController: NSObject, NSWindowDelegate {
    static let shared = CovalenceMeshViewerWindowController()

    private var window: NSWindow?

    /// Default height for the list reading of the hosts section — enough for the state pane and a
    /// useful slice of the event log.
    private static let listHeight: CGFloat = 700
    /// What the graph reading needs before the canvas starts losing its outer ring: the graph
    /// pane's own minimum (`CovalenceMeshViewerView.graphPaneMinHeight`) plus the event log's, the
    /// divider, and the title bar.
    private static let graphHeight: CGFloat = 900
    /// The height the window had before the graph grew it, so switching back to the list hands it
    /// back — unless the user resized in the meantime, in which case their size wins.
    private var heightBeforeGraph: CGFloat?

    private override init() { super.init() }

    /// Open (or refocus) the viewer. One window per app — reopening brings it forward. Opens at
    /// whichever height the remembered hosts reading needs, so a viewer last left on the graph
    /// doesn't come back clipped.
    func open(store: AppStore) {
        if let window {
            window.makeKeyAndOrderFront(nil)
            return
        }
        let content = CovalenceMeshViewerView()
            .environment(store)
            .frame(minWidth: 560, minHeight: 480)
        let height = Self.fits(
            MeshHostsMode.remembered == .graph ? Self.graphHeight : Self.listHeight)
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 760, height: height),
            styleMask: [.titled, .closable, .resizable, .miniaturizable],
            backing: .buffered, defer: false)
        window.title = "Covalence Mesh Viewer"
        window.contentViewController = NSHostingController(rootView: content)
        window.isReleasedWhenClosed = false
        window.delegate = self
        window.center()
        window.makeKeyAndOrderFront(nil)
        self.window = window
    }

    /// Grow the window when the graph opens — the canvas needs roughly twice the vertical room a
    /// list row does, and at the list's height most of it falls outside the pane. Switching back
    /// returns the height the user had, so the graph borrows the space rather than keeping it.
    func matchHeight(toGraphMode isGraph: Bool) {
        guard let window else { return }
        if isGraph {
            let target = Self.fits(Self.graphHeight)
            guard window.frame.height < target else { return }
            heightBeforeGraph = window.frame.height
            setHeight(target, of: window)
        } else {
            guard let previous = heightBeforeGraph else { return }
            heightBeforeGraph = nil
            // Only give it back if the window is still the size we made it — once the user has
            // sized the window themselves, that's the size they want.
            guard abs(window.frame.height - Self.fits(Self.graphHeight)) < 1 else { return }
            setHeight(previous, of: window)
        }
    }

    /// Resize about the title bar (the top edge stays put, as dragging the bottom edge would),
    /// then nudge back onto the screen if that pushed the window off it.
    private func setHeight(_ height: CGFloat, of window: NSWindow) {
        var frame = window.frame
        frame.origin.y -= height - frame.height
        frame.size.height = height
        if let visible = (window.screen ?? NSScreen.main)?.visibleFrame {
            frame.origin.y = max(visible.minY, min(frame.origin.y, visible.maxY - height))
        }
        window.setFrame(frame, display: true, animate: true)
    }

    /// A window taller than the screen is worse than a slightly cramped one.
    private static func fits(_ height: CGFloat) -> CGFloat {
        min(height, NSScreen.main?.visibleFrame.height ?? height)
    }

    func windowWillClose(_ notification: Notification) {
        guard let closing = notification.object as? NSWindow, closing === window else { return }
        window = nil
        heightBeforeGraph = nil
    }
}

/// The viewer's content: mesh state on top, the event log below, split resizably.
struct CovalenceMeshViewerView: View {
    @Environment(AppStore.self) private var store

    /// Polled (non-observable) snapshot bits: the durable queue, transfer locks, and the
    /// Covalence-session bookkeeping live behind async store APIs, refreshed on a short cadence
    /// (the AFMActivityMonitor pattern). Observable state (peers, presence, counts) renders
    /// reactively and needs no polling.
    @State private var queued: [MeshDispatchRecord] = []
    @State private var transfers: [SessionTransferLock] = []
    @State private var covalenceSessions: [CovalenceSessionDiagnostic] = []

    /// Read here as well as in the hosts section itself: the state pane's minimum height and the
    /// window's own height both depend on which reading is showing.
    @AppStorage(MeshHostsMode.storageKey) private var hostsModeRaw = MeshHostsMode.list.rawValue
    private var hostsMode: MeshHostsMode { MeshHostsMode(rawValue: hostsModeRaw) ?? .list }

    /// What the state pane needs with the graph in it: the canvas, the legend and the summary
    /// strip above it, plus room for a selected node's detail card. Below this the split view
    /// starts eating the canvas, which is why it's a floor and not a preference.
    static let graphPaneMinHeight: CGFloat = 560

    var body: some View {
        VSplitView {
            ScrollView {
                VStack(alignment: .leading, spacing: 18) {
                    MeshHealthSummaryRow(
                        queuedCount: queued.count, transferCount: transfers.count,
                        covalenceSessionCount: covalenceSessions.count)
                    MeshHostsSection()
                    if !covalenceSessions.isEmpty {
                        CovalenceSessionsSection(sessions: covalenceSessions)
                    }
                    if !queued.isEmpty {
                        ParkedDispatchSection(records: queued) { id in
                            await store.removeQueuedMeshDispatch(id: id)
                            queued = await store.queuedMeshDispatchRecords()
                        }
                    }
                    if !transfers.isEmpty {
                        ActiveTransfersSection(transfers: transfers)
                    }
                }
                .padding(16)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            .frame(minHeight: hostsMode == .graph ? Self.graphPaneMinHeight : 220)
            MeshEventLogPane()
                .frame(minHeight: 160)
        }
        .onChange(of: hostsMode) { _, mode in
            CovalenceMeshViewerWindowController.shared.matchHeight(toGraphMode: mode == .graph)
        }
        .task {
            while !Task.isCancelled {
                queued = await store.queuedMeshDispatchRecords()
                transfers = await store.activeTransferDiagnostics()
                covalenceSessions = await store.covalenceSessionDiagnostics()
                try? await Task.sleep(for: .seconds(2))
            }
        }
    }
}

// MARK: - Health summary

/// The one-glance health strip: mesh service state, peer connectivity, and the queue/transfer
/// counters that most often explain "why isn't my Covalence chat running?".
private struct MeshHealthSummaryRow: View {
    @Environment(AppStore.self) private var store
    let queuedCount: Int
    let transferCount: Int
    let covalenceSessionCount: Int

    private var connectedPeers: Int { store.meshPeers.filter(\.connected).count }

    /// Inbound phones/iPads connected right now — the mesh members the peer count misses.
    private var connectedPhones: Int {
        let phoneIDs = Set(store.pairedDevices.filter { !$0.kind.canOwnSessions }.map(\.deviceID))
        return store.inboundConnectedDeviceIDs.intersection(phoneIDs).count
    }

    /// Flowed, not an `HStack`: this strip runs to seven chips once phones, transports, the queue
    /// and transfers are all live, and the transport chip's label grows with every active method.
    /// An HStack answers that by compressing them until their labels hyphenate mid-word, so the
    /// overflow wraps to the next line instead (the Carbon Viewer's summary row does the same).
    var body: some View {
        FlowLayout(spacing: 8) {
            chip(
                store.syncRunning ? "Mesh on" : "Mesh off",
                systemImage: "antenna.radiowaves.left.and.right",
                tint: store.syncRunning ? .green : .secondary)
            chip(
                "Peers \(connectedPeers)/\(store.meshPeers.count)",
                systemImage: "macbook.and.iphone",
                tint: connectedPeers > 0 ? .green : .secondary)
            if connectedPhones > 0 {
                chip(
                    "\(connectedPhones) device(s)", systemImage: "iphone",
                    tint: .green)
            }
            if !store.syncActiveTransports.isEmpty {
                chip(
                    store.syncActiveTransports.map(\.label).sorted().joined(separator: " · "),
                    systemImage: "network", tint: .blue)
            }
            if queuedCount > 0 {
                chip("\(queuedCount) parked", systemImage: "tray.full", tint: .orange)
            }
            if transferCount > 0 {
                chip("\(transferCount) moving", systemImage: "arrow.left.arrow.right", tint: .blue)
            }
            if covalenceSessionCount > 0 {
                chip("\(covalenceSessionCount) Covalence chat(s)", systemImage: "point.3.connected.trianglepath.dotted", tint: .purple)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    /// One capsule. `lineLimit(1)` + `fixedSize` keep the label on a single line at its natural
    /// width — a chip that has to wrap or truncate to fit belongs on the next row instead.
    private func chip(_ text: String, systemImage: String, tint: Color) -> some View {
        Label(text, systemImage: systemImage)
            .font(.caption)
            .lineLimit(1)
            .foregroundStyle(tint)
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
            .background(tint.opacity(0.12), in: Capsule())
            .fixedSize()
    }
}

// MARK: - Hosts

/// Which reading of the hosts section is showing. Remembered across window opens — a diagnostics
/// surface someone keeps returning to shouldn't reset the view they chose — and read by the window
/// controller too, since the graph needs a taller window than the list.
enum MeshHostsMode: String, CaseIterable {
    case list, graph

    static let storageKey = "nucleic.meshViewer.hostsMode"

    /// The stored choice, for the window controller's initial sizing (it has no view to read
    /// `@AppStorage` from, and the window is sized before the content ever lays out).
    @MainActor static var remembered: MeshHostsMode {
        UserDefaults.standard.string(forKey: storageKey)
            .flatMap(MeshHostsMode.init(rawValue:)) ?? .list
    }

    var label: String {
        switch self {
        case .list: "List"
        case .graph: "Graph"
        }
    }

    var symbol: String {
        switch self {
        case .list: "list.bullet"
        case .graph: "point.3.filled.connected.trianglepath.dotted"
        }
    }
}

/// The mesh's hosts, in either of two readings of the same `AppStore.meshGraph` snapshot: a list
/// (one row per host — this Mac, every dialed peer, every host known only through a mirrored
/// presence card, and every paired device, notably the iPhones/iPads that only ever connect
/// *inbound* and never cast a card), or a graph (the hosts as nodes with the links between them).
/// The list answers "what does each host look like"; the graph answers "what is connected to what".
private struct MeshHostsSection: View {
    @Environment(AppStore.self) private var store

    @AppStorage(MeshHostsMode.storageKey) private var modeRaw = MeshHostsMode.list.rawValue
    /// The node the graph has selected, for its detail card. Not persisted: a selection is about
    /// the question being asked right now.
    @State private var graphSelection: String?

    private var mode: MeshHostsMode { MeshHostsMode(rawValue: modeRaw) ?? .list }

    /// The graph, minus the hosts we only know by hearsay — a peer's peer isn't a row this Mac
    /// can say anything useful about, but it *is* a node worth drawing.
    private var graph: MeshGraph { store.meshGraph }
    private var rows: [MeshGraph.Node] { graph.nodes.filter { !$0.isIndirect } }

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            HStack {
                Text("Hosts").font(.headline)
                Spacer()
                Picker("View", selection: Binding(
                    get: { mode }, set: { modeRaw = $0.rawValue })) {
                    ForEach(MeshHostsMode.allCases, id: \.self) { mode in
                        Label(mode.label, systemImage: mode.symbol).tag(mode)
                    }
                }
                .pickerStyle(.segmented)
                .labelsHidden()
                .fixedSize()
            }
            if rows.isEmpty {
                Text("No mesh hosts known yet — is Covalence on and a device paired?")
                    .font(.caption).foregroundStyle(.secondary)
            }
            switch mode {
            case .list:
                ForEach(rows) { node in
                    MeshHostRow(node: node)
                    Divider()
                }
            case .graph:
                if !rows.isEmpty {
                    MeshGraphView(graph: graph, selection: $graphSelection)
                }
            }
        }
    }
}

private struct MeshHostRow: View {
    let node: MeshGraph.Node

    private var name: String {
        node.isSelf ? "\(node.label) (this Mac)" : node.label
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 2) {
            HStack(alignment: .firstTextBaseline, spacing: 8) {
                Circle()
                    .fill(node.online ? Color.green : Color.secondary.opacity(0.4))
                    .frame(width: 7, height: 7)
                Label {
                    Text(name).fontWeight(node.isSelf ? .semibold : .regular)
                } icon: {
                    Image(systemName: MeshGraphStyle.symbol(for: node))
                }
                if let transport = node.transport?.label {
                    Text("via \(transport)").font(.caption).foregroundStyle(.secondary)
                } else if node.inbound, !node.isSelf {
                    Text("inbound").font(.caption).foregroundStyle(.secondary)
                }
                Spacer()
                if node.hasPresenceCard {
                    capabilityBadges
                    loadGauge
                } else if node.kind.canOwnSessions {
                    // A session-owning host (Mac/cloud) with no card can't be a dispatch candidate.
                    Text("no presence card").font(.caption).foregroundStyle(.secondary)
                        .help("This host hasn't cast a runner-presence card yet — "
                            + "it can't be a Covalence dispatch candidate until it does.")
                } else {
                    // A phone/iPad is a mesh member and a controller, never a dispatch candidate —
                    // so "no presence card" would be misleading. Show its role instead.
                    Text(node.online ? "connected" : "offline")
                        .font(.caption).foregroundStyle(.secondary)
                }
            }
            .font(.callout)
            // A second line for what needs more room: presence age, projects, and any dial error.
            HStack(spacing: 10) {
                if node.hasPresenceCard {
                    Text("\(node.projectCount) project(s)")
                    if let updated = node.presenceUpdatedAt, updated > .distantPast {
                        Text("card updated \(updated, style: .relative) ago")
                    }
                }
                if let error = node.lastError {
                    Label(error, systemImage: "exclamationmark.triangle")
                        .foregroundStyle(.orange)
                        .lineLimit(1)
                }
                Spacer()
            }
            .font(.caption2)
            .foregroundStyle(.secondary)
            .padding(.leading, 15)
        }
    }

    @ViewBuilder
    private var capabilityBadges: some View {
        HStack(spacing: 5) {
            if node.acceptsMeshDispatch {
                Image(systemName: "paperplane")
                    .help("Accepts Covalence-dispatched chats")
            }
            if node.canReceiveSessionTransfer {
                Image(systemName: "arrow.down.doc")
                    .help("Can receive session transfers (a rebalance destination)")
            }
            if node.canCreateProjects {
                Image(systemName: "plus.square.on.square")
                    .help("Can clone a repo at dispatch time")
            }
        }
        .font(.caption)
        .foregroundStyle(.secondary)
    }

    @ViewBuilder
    private var loadGauge: some View {
        let ratio = node.loadRatio ?? 0
        let cap = node.maxConcurrent.map(String.init) ?? "∞"
        HStack(spacing: 6) {
            ProgressView(value: min(ratio, 1))
                .progressViewStyle(.linear)
                .tint(MeshGraphStyle.loadColor(ratio))
                .frame(width: 56)
            Text("\(node.activeSessionCount)/\(cap)")
                .font(.caption.monospacedDigit())
                .foregroundStyle(.secondary)
        }
        .help("Active sessions / capacity — the load ratio the dispatch scorer ranks by "
            + "(unbounded capacity is scored as 8)")
    }
}

// MARK: - Covalence sessions

/// The Covalence-managed sessions this Mac currently owns, with their rebalance posture.
private struct CovalenceSessionsSection: View {
    @Environment(AppStore.self) private var store
    let sessions: [CovalenceSessionDiagnostic]

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("Covalence chats on this Mac").font(.headline)
            ForEach(sessions) { session in
                HStack(spacing: 8) {
                    Image(systemName: "point.3.connected.trianglepath.dotted")
                        .foregroundStyle(.purple)
                    VStack(alignment: .leading, spacing: 1) {
                        Text(session.title.isEmpty ? "Untitled chat" : session.title)
                        Text("\(session.projectName) · issued from "
                            + store.meshHostLabel(session.originDeviceID))
                            .font(.caption2).foregroundStyle(.secondary)
                    }
                    Spacer()
                    if session.isRebalancing {
                        Label("moving…", systemImage: "arrow.left.arrow.right")
                            .font(.caption).foregroundStyle(.blue)
                    } else if session.cooldownRemaining > 0 {
                        Text("cooldown \(Int(session.cooldownRemaining))s")
                            .font(.caption.monospacedDigit()).foregroundStyle(.secondary)
                            .help("Recently evaluated for a move — not re-considered until the "
                                + "per-session cooldown lapses")
                    } else {
                        Text(session.status.rawValue)
                            .font(.caption).foregroundStyle(.secondary)
                    }
                }
                .font(.callout)
            }
        }
    }
}

// MARK: - Parked dispatch queue

/// Chats waiting in the durable `mesh_dispatch` queue. Deliberately shows the *shape* of each
/// parked job (project, age, model) and not the message body — prompts are sensitive content
/// (OBSERVABILITY §A.1) and a diagnostics surface doesn't need them.
private struct ParkedDispatchSection: View {
    @Environment(AppStore.self) private var store
    let records: [MeshDispatchRecord]
    /// Remove a parked job from the durable queue, then refresh the section's snapshot.
    let onRemove: (String) async -> Void

    /// The row awaiting a confirmed removal (guards against a double-tap re-firing the async work).
    @State private var removing: Set<String> = []

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("Parked dispatch queue").font(.headline)
            Text("Waiting for an eligible Covalence host; retried automatically on presence "
                + "changes and peer connects.")
                .font(.caption).foregroundStyle(.secondary)
            ForEach(records, id: \.id) { record in
                HStack(spacing: 8) {
                    Image(systemName: "tray.full").foregroundStyle(.orange)
                    Text(store.projectSummary(record.projectID)?.name ?? "Unknown project")
                    if let model = record.model {
                        Text(model).font(.caption).foregroundStyle(.secondary)
                    }
                    Spacer()
                    Text("parked \(record.createdAt, style: .relative) ago")
                        .font(.caption).foregroundStyle(.secondary)
                    Button {
                        guard removing.insert(record.id).inserted else { return }
                        Task {
                            await onRemove(record.id)
                            removing.remove(record.id)
                        }
                    } label: {
                        Image(systemName: "xmark.circle.fill")
                    }
                    .buttonStyle(.borderless)
                    .foregroundStyle(.secondary)
                    .disabled(removing.contains(record.id))
                    .help("Remove this parked chat from the queue — it won't be dispatched.")
                }
                .font(.callout)
            }
        }
    }
}

// MARK: - Active transfers

/// In-flight session transfers (both directions), straight from the durable transfer locks.
private struct ActiveTransfersSection: View {
    @Environment(AppStore.self) private var store
    let transfers: [SessionTransferLock]

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("Session transfers").font(.headline)
            ForEach(transfers, id: \.transferID) { lock in
                HStack(spacing: 8) {
                    Image(systemName: lock.direction == .outbound
                        ? "arrow.up.right.circle" : "arrow.down.left.circle")
                        .foregroundStyle(lock.state.isActive ? .blue : .green)
                    Text(lock.direction == .outbound
                        ? "→ \(store.meshHostLabel(lock.peerDeviceID))"
                        : "← \(store.meshHostLabel(lock.peerDeviceID))")
                    Text("session \(lock.sessionID.short)")
                        .font(.caption.monospaced()).foregroundStyle(.secondary)
                    Spacer()
                    Text(lock.state.rawValue)
                        .font(.caption)
                        .padding(.horizontal, 6).padding(.vertical, 2)
                        .background(Color.blue.opacity(0.12), in: Capsule())
                    Text("\(lock.updatedAt, style: .relative) ago")
                        .font(.caption).foregroundStyle(.secondary)
                }
                .font(.callout)
            }
        }
    }
}

// MARK: - Event log

/// The live mesh event log: newest at the bottom, auto-following unless paused, filterable by
/// category and by problems-only. Backed by the `MeshDiagnosticsLog` ring buffer — cleared rows
/// are still in unified logging (category "mesh") if history past the cap is needed.
private struct MeshEventLogPane: View {
    @Environment(AppStore.self) private var store
    @State private var filter: MeshDiagnosticsEvent.Category?
    @State private var problemsOnly = false
    @State private var follow = true

    private var events: [MeshDiagnosticsEvent] {
        store.meshDiagnostics.events.filter { event in
            (filter == nil || event.category == filter)
                && (!problemsOnly || event.level != .info)
        }
    }

    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 10) {
                Text("Events").font(.headline)
                Picker("Category", selection: $filter) {
                    Text("All").tag(MeshDiagnosticsEvent.Category?.none)
                    ForEach(MeshDiagnosticsEvent.Category.allCases, id: \.self) { category in
                        Text(category.label).tag(Optional(category))
                    }
                }
                .pickerStyle(.menu)
                .labelsHidden()
                .fixedSize()
                Toggle("Problems only", isOn: $problemsOnly)
                    .toggleStyle(.checkbox)
                    .font(.caption)
                Spacer()
                Text("\(events.count) shown")
                    .font(.caption).foregroundStyle(.secondary)
                Button {
                    follow.toggle()
                } label: {
                    Image(systemName: follow ? "pause.circle" : "play.circle")
                }
                .buttonStyle(.borderless)
                .help(follow ? "Pause auto-scroll" : "Resume auto-scroll")
                Button {
                    store.meshDiagnostics.clear()
                } label: {
                    Image(systemName: "trash")
                }
                .buttonStyle(.borderless)
                .help("Clear the log (unified logging keeps the full trail)")
            }
            .padding(.horizontal, 12)
            .padding(.vertical, 8)
            Divider()
            ScrollViewReader { proxy in
                ScrollView {
                    LazyVStack(alignment: .leading, spacing: 2) {
                        if events.isEmpty {
                            Text("No mesh events yet.")
                                .font(.caption).foregroundStyle(.secondary)
                                .padding(8)
                        }
                        ForEach(events) { event in
                            MeshEventRow(event: event).id(event.id)
                        }
                    }
                    .padding(.horizontal, 12)
                    .padding(.vertical, 6)
                }
                .onChange(of: events.last?.id) {
                    guard follow, let last = events.last?.id else { return }
                    proxy.scrollTo(last, anchor: .bottom)
                }
            }
        }
    }
}

private struct MeshEventRow: View {
    let event: MeshDiagnosticsEvent

    private var levelColor: Color {
        switch event.level {
        case .info: .secondary
        case .warning: .orange
        case .error: .red
        }
    }

    private var categoryColor: Color {
        switch event.category {
        case .peer: .green
        case .presence: .teal
        case .cast: .gray
        case .approval: .orange
        case .dispatch: .blue
        case .rebalance: .purple
        case .transfer: .indigo
        }
    }

    var body: some View {
        HStack(alignment: .firstTextBaseline, spacing: 8) {
            Text(event.at, format: .dateTime.hour().minute().second())
                .font(.caption.monospacedDigit())
                .foregroundStyle(.tertiary)
            Text(event.category.label)
                .font(.caption2.weight(.medium))
                .foregroundStyle(categoryColor)
                .frame(width: 62, alignment: .leading)
            VStack(alignment: .leading, spacing: 0) {
                Text(event.message)
                    .font(.caption)
                    .foregroundStyle(event.level == .info ? Color.primary : levelColor)
                if let detail = event.detail {
                    Text(detail)
                        .font(.caption2)
                        .foregroundStyle(.secondary)
                }
            }
            Spacer(minLength: 0)
        }
        .textSelection(.enabled)
    }
}
