import AppKit
import SwiftUI

/// Approval dialog listing every pending pairing request as a card. The host
/// panel draws native window chrome; this view only lays out the content.
struct PairingApprovalPanelView: View {
    let center: PairingApprovalCenter

    var body: some View {
        self.content
            .frame(width: PairingApprovalPanelController.panelWidth)
    }

    private var content: some View {
        let cards = self.center.cards
        return VStack(alignment: .leading, spacing: 12) {
            self.header(cards: cards)
            // Every request renders and stays actionable; the scroll plus the
            // controller's screen-height clamp bound hostile queue sizes.
            ScrollView(.vertical) {
                VStack(alignment: .leading, spacing: 12) {
                    ForEach(cards) { card in
                        PairingRequestCardView(
                            card: card,
                            isOnlyRequest: cards.count == 1,
                            onDecision: { self.center.decide(card, $0) })
                    }
                }
            }
            .scrollBounceBehavior(.basedOnSize)
            self.footer(cards: cards)
        }
        .padding(18)
    }

    /// Single request keeps the minimal "Not Now" footer; multiple requests
    /// add one-click bulk actions so a queue never needs card-by-card clicks.
    @ViewBuilder
    private func footer(cards: [PairingApprovalCenter.Card]) -> some View {
        let notNow = Button("Not Now") { self.center.snooze() }
            .keyboardShortcut(.cancelAction)
            .buttonStyle(.plain)
            .foregroundStyle(.secondary)
        if cards.count > 1 {
            HStack(spacing: 8) {
                notNow
                Spacer()
                // No keyboard shortcuts here: bulk approval is a security
                // decision and must never be a stray Return press. Both
                // actions resolve the rendered `cards` snapshot, never the
                // live queue, so a request that arrives mid-click cannot be
                // approved before it was ever displayed.
                Button(role: .destructive) {
                    self.center.decideAll(cards, .reject)
                } label: {
                    Text("Reject All")
                        .padding(.horizontal, 6)
                }
                .pairingActionStyle(prominent: false)
                Button {
                    self.center.decideAll(cards, .approve)
                } label: {
                    Text("Approve All")
                        .padding(.horizontal, 6)
                }
                .pairingActionStyle(prominent: true)
            }
        } else {
            HStack {
                Spacer()
                notNow
            }
        }
    }

    private func header(cards: [PairingApprovalCenter.Card]) -> some View {
        HStack(spacing: 10) {
            Image(systemName: "checkmark.shield")
                .font(.system(size: 22, weight: .medium))
                .foregroundStyle(.tint)
            VStack(alignment: .leading, spacing: 1) {
                Text(cards.count == 1 ? "Pairing Request" : "Pairing Requests")
                    .font(.title3.weight(.semibold))
                Text(PairingCardPresentation.headerSummary(for: cards))
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            Spacer()
        }
    }
}

struct PairingRequestCardView: View {
    let card: PairingApprovalCenter.Card
    let isOnlyRequest: Bool
    let onDecision: (PairingApprovalCenter.Decision) -> Void

    @State private var isHoveringDetail = false

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack(alignment: .top, spacing: 12) {
                self.icon
                VStack(alignment: .leading, spacing: 2) {
                    Text(PairingCardPresentation.title(for: self.card))
                        .font(.headline)
                        .lineLimit(1)
                    if let subtitle = PairingCardPresentation.subtitle(for: self.card) {
                        Text(subtitle)
                            .font(.subheadline)
                            .foregroundStyle(.secondary)
                            .lineLimit(1)
                    }
                }
                Spacer(minLength: 0)
            }

            self.trustRow
            self.accessRows
            self.detailRow

            HStack(spacing: 8) {
                Spacer()
                self.rejectButton
                self.approveButton
            }
        }
        .padding(14)
        .background(RoundedRectangle(cornerRadius: 16).fill(.quinary))
    }

    private var icon: some View {
        Image(systemName: PairingCardPresentation.deviceSymbol(for: self.card))
            .font(.system(size: 20, weight: .medium))
            .frame(width: 42, height: 42)
            .background(Circle().fill(.quaternary))
    }

    private var trustRow: some View {
        let trust = PairingCardPresentation.trustLine(for: self.card)
        return Label(trust.text, systemImage: trust.symbol)
            .font(.caption)
            .foregroundStyle(self.trustStyle(trust.tone))
    }

    private func trustStyle(_ tone: PairingCardPresentation.TrustTone) -> AnyShapeStyle {
        switch tone {
        case .caution: AnyShapeStyle(.orange)
        case .neutral: AnyShapeStyle(.secondary)
        }
    }

    @ViewBuilder
    private var accessRows: some View {
        // Every requested scope/capability renders: hiding one behind a cap
        // could conceal what approval grants. The panel scrolls when long.
        let rows = PairingCardPresentation.accessRows(for: self.card)
        if !rows.isEmpty {
            VStack(alignment: .leading, spacing: 4) {
                ForEach(rows) { row in
                    Label(row.text, systemImage: row.symbol)
                        .font(.caption)
                        .foregroundStyle(
                            row.isElevated ? AnyShapeStyle(.orange) : AnyShapeStyle(.primary))
                }
            }
        }
    }

    private var detailRow: some View {
        HStack(spacing: 4) {
            Text(PairingCardPresentation.identityLine(for: self.card))
                .font(.caption.monospaced())
                .foregroundStyle(.tertiary)
                .lineLimit(1)
                .truncationMode(.middle)
            // Hover-only copy affordance; stays out of the keyboard focus loop.
            Button {
                NSPasteboard.general.clearContents()
                NSPasteboard.general.setString(self.card.subjectId, forType: .string)
            } label: {
                Image(systemName: "doc.on.doc")
                    .font(.caption2)
                    .foregroundStyle(.tertiary)
            }
            .buttonStyle(.plain)
            .focusable(false)
            .help("Copy full ID")
            .opacity(self.isHoveringDetail ? 1 : 0)
            Spacer()
            Text(PairingCardPresentation.metaLine(for: self.card))
                .font(.caption)
                .foregroundStyle(.tertiary)
                .help(PairingCardPresentation.versionTooltip(for: self.card))
        }
        .onHover { self.isHoveringDetail = $0 }
    }

    private var approveButton: some View {
        Button {
            self.onDecision(.approve)
        } label: {
            Text(self.card.kind == .node ? "Approve Node" : "Approve Device")
                .padding(.horizontal, 6)
        }
        .pairingActionStyle(prominent: true)
        .keyboardShortcut(self.isOnlyRequest ? .defaultAction : nil)
    }

    private var rejectButton: some View {
        Button(role: .destructive) {
            self.onDecision(.reject)
        } label: {
            Text("Reject")
                .padding(.horizontal, 6)
        }
        .pairingActionStyle(prominent: false)
    }
}

extension View {
    /// Shared capsule styling for pairing decision buttons: Liquid Glass on
    /// macOS 26+, bordered fallback on macOS 15.
    @ViewBuilder
    func pairingActionStyle(prominent: Bool) -> some View {
        if #available(macOS 26.0, *) {
            if prominent {
                self.buttonStyle(.glassProminent).buttonBorderShape(.capsule)
            } else {
                self.buttonStyle(.glass).buttonBorderShape(.capsule)
            }
        } else {
            if prominent {
                self.buttonStyle(.borderedProminent).buttonBorderShape(.capsule)
            } else {
                self.buttonStyle(.bordered).buttonBorderShape(.capsule)
            }
        }
    }
}

/// Pure display mapping for pairing cards; kept UI-framework-free so tests
/// can assert copy and symbols without instantiating views.
enum PairingCardPresentation {
    struct AccessRow: Identifiable, Equatable {
        /// Single grants use their raw value; intentional grouped grants use a
        /// dedicated group id so SwiftUI never drops a rendered access row.
        let id: String
        let symbol: String
        let text: String
        let isElevated: Bool
    }

    enum TrustTone: Equatable {
        case caution
        case neutral
    }

    struct TrustLine: Equatable {
        let symbol: String
        let text: String
        let tone: TrustTone
    }

    static func headerSummary(for cards: [PairingApprovalCenter.Card]) -> String {
        guard cards.count == 1, let card = cards.first else {
            return "\(cards.count) devices want to connect to OpenClaw."
        }
        return card.kind == .node
            ? "A node wants to connect to OpenClaw."
            : "A device wants to connect to OpenClaw."
    }

    static func title(for card: PairingApprovalCenter.Card) -> String {
        let name = card.displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
        if let name, !name.isEmpty, name != card.subjectId {
            return name
        }
        switch card.kind {
        case .node: return "Unnamed node"
        case .device: return self.isMac(card.platform) ? "OpenClaw Mac app" : "New device"
        }
    }

    static func subtitle(for card: PairingApprovalCenter.Card) -> String? {
        var parts: [String] = []
        if let platform = self.prettyPlatform(card.platform) {
            parts.append(platform)
        }
        if let model = self.prettyModel(card.modelIdentifier, deviceFamily: card.deviceFamily),
           parts.allSatisfy({ $0 != model })
        {
            parts.append(model)
        }
        if card.kind == .device, let role = card.role?.trimmingCharacters(in: .whitespacesAndNewlines),
           !role.isEmpty
        {
            parts.append(role == "operator" ? "Operator" : role)
        }
        return parts.isEmpty ? nil : parts.joined(separator: " · ")
    }

    static func trustLine(for card: PairingApprovalCenter.Card) -> TrustLine {
        if card.isRepair {
            return TrustLine(
                symbol: "arrow.triangle.2.circlepath",
                text: "Repair request — its access token will rotate",
                tone: .caution)
        }
        switch card.previouslyPaired {
        case true:
            // The requester merely CLAIMS this id (it is not authenticated),
            // so an already-paired id is a caution — approval replaces the
            // existing peer's token — never a positive trust signal.
            return TrustLine(
                symbol: "exclamationmark.triangle",
                text: "This ID is already paired — approving replaces its access token",
                tone: .caution)
        case false:
            return TrustLine(
                symbol: "sparkles",
                text: "First connection from this \(card.kind == .node ? "node" : "device")",
                tone: .neutral)
        case nil:
            return TrustLine(
                symbol: "clock",
                text: "Checking pairing history…",
                tone: .neutral)
        }
    }

    static func accessRows(for card: PairingApprovalCenter.Card) -> [AccessRow] {
        switch card.kind {
        case .device:
            // Admin access is what approval actually grants; surface it first
            // and highlighted so it can never hide among ordinary scopes.
            let rows = self.friendlyScopes(card.scopes).map { scope in
                AccessRow(
                    id: "scope:\(scope.raw)",
                    symbol: scope.raw == "operator.admin" ? "exclamationmark.shield" : "key",
                    text: scope.text,
                    isElevated: scope.raw == "operator.admin")
            }
            return rows.filter(\.isElevated) + rows.filter { !$0.isElevated }
        case .node:
            var rows: [AccessRow] = []
            // Commands in NODE_SYSTEM_RUN_COMMANDS (src/infra/node-commands.ts)
            // mean approving grants arbitrary command execution on the node.
            let isSystemRun = { (command: String) in
                command == "system.run" || command == "system.which" || command.hasPrefix("system.run.")
            }
            if card.commands.contains(where: isSystemRun) {
                rows.append(AccessRow(
                    id: "system-run",
                    symbol: "exclamationmark.shield",
                    text: "Can run system commands",
                    isElevated: true))
            }
            rows.append(contentsOf: self.friendlyCapNames(card.caps).map {
                AccessRow(id: $0.id, symbol: $0.symbol, text: $0.text, isElevated: false)
            })
            // Approval persists the whole declared command surface; list the
            // remaining commands so none of it is granted invisibly.
            var seen = Set<String>()
            let otherCommands = card.commands.filter {
                !$0.isEmpty && !isSystemRun($0) && seen.insert($0).inserted
            }
            if !otherCommands.isEmpty {
                rows.append(AccessRow(
                    id: "commands",
                    symbol: "terminal",
                    text: "Commands: \(otherCommands.joined(separator: ", "))",
                    isElevated: false))
            }
            return rows
        }
    }

    /// Left, monospaced: stable identity facts (short id, source address).
    static func identityLine(for card: PairingApprovalCenter.Card) -> String {
        var parts = ["ID \(self.shortIdentifier(card.subjectId))"]
        if let ip = self.prettyIP(card.remoteIp) {
            parts.append(ip)
        }
        return parts.joined(separator: " · ")
    }

    /// Right, trailing: app version and request age.
    static func metaLine(for card: PairingApprovalCenter.Card, now: Date = Date()) -> String {
        var parts: [String] = []
        if let version = card.version?.trimmingCharacters(in: .whitespacesAndNewlines),
           !version.isEmpty
        {
            parts.append("v\(version)")
        }
        parts.append(self.relativeRequestTime(for: card, now: now))
        return parts.joined(separator: " · ")
    }

    static func versionTooltip(for card: PairingApprovalCenter.Card) -> String {
        var parts: [String] = []
        if let version = card.version?.trimmingCharacters(in: .whitespacesAndNewlines),
           !version.isEmpty
        {
            parts.append("App \(version)")
        }
        if let core = card.coreVersion?.trimmingCharacters(in: .whitespacesAndNewlines),
           !core.isEmpty
        {
            parts.append("Core \(core)")
        }
        return parts.joined(separator: " · ")
    }

    static func relativeRequestTime(for card: PairingApprovalCenter.Card, now: Date = Date()) -> String {
        let elapsed = now.timeIntervalSince(card.requestedAt)
        if elapsed < 60 {
            return "just now"
        }
        let formatter = RelativeDateTimeFormatter()
        formatter.unitsStyle = .short
        return formatter.localizedString(for: card.requestedAt, relativeTo: now)
    }

    static func deviceSymbol(for card: PairingApprovalCenter.Card) -> String {
        let model = card.modelIdentifier?.lowercased() ?? ""
        if model.hasPrefix("macbook") {
            return "macbook"
        }
        if model.hasPrefix("macmini") {
            return "macmini"
        }
        if model.hasPrefix("macstudio") {
            return "macstudio"
        }
        if model.hasPrefix("macpro") {
            return "macpro.gen3"
        }
        if model.hasPrefix("imac") {
            return "desktopcomputer"
        }

        let family = (card.deviceFamily ?? "").lowercased()
        let platform = (card.platform ?? "").lowercased()
        let hints = "\(family) \(platform)"
        if hints.contains("iphone") || hints.contains("ios") {
            return "iphone"
        }
        if hints.contains("ipad") {
            return "ipad"
        }
        if hints.contains("android") {
            return "smartphone"
        }
        if hints.contains("mac") || hints.contains("darwin") {
            return "laptopcomputer"
        }
        if hints.contains("linux") || hints.contains("windows") {
            return "server.rack"
        }
        return "network"
    }

    static func shortIdentifier(_ id: String) -> String {
        let trimmed = id.trimmingCharacters(in: .whitespacesAndNewlines)
        guard trimmed.count > 20 else {
            return trimmed
        }
        return "\(trimmed.prefix(8))...\(trimmed.suffix(7))"
    }

    static func prettyIP(_ ip: String?) -> String? {
        let trimmed = ip?.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let trimmed, !trimmed.isEmpty else {
            return nil
        }
        return trimmed.replacingOccurrences(of: "::ffff:", with: "")
    }

    static func prettyPlatform(_ raw: String?) -> String? {
        let platform = raw?.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let platform, !platform.isEmpty else {
            return nil
        }
        // Device pairing sends browser-style tokens (MacIntel); map those
        // before the generic "os version" formatter capitalizes them.
        switch platform.lowercased() {
        case "macintel", "x86_64-apple-darwin":
            return "Mac (Intel)"
        case "macarm", "macarm64", "arm64-apple-darwin", "aarch64-apple-darwin":
            return "Mac (Apple silicon)"
        case "darwin":
            return "Mac"
        default:
            if let pretty = PlatformLabelFormatter.pretty(platform) {
                return pretty
            }
            return platform.lowercased().contains("mac") ? "Mac" : platform
        }
    }

    static func friendlyScopes(_ scopes: [String]) -> [(raw: String, text: String)] {
        var seen = Set<String>()
        return scopes.compactMap { scope in
            let normalized = scope.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !normalized.isEmpty, seen.insert(normalized).inserted else {
                return nil
            }
            switch normalized {
            case "operator.admin":
                return (normalized, "Admin access")
            case "operator.read":
                return (normalized, "Read OpenClaw data")
            case "operator.write":
                return (normalized, "Send messages and make changes")
            case "operator.approvals":
                return (normalized, "Manage approvals")
            case "operator.pairing":
                return (normalized, "Pair and repair devices")
            case "operator.talk.secrets":
                return (normalized, "Use Talk credentials")
            default:
                return (normalized, normalized)
            }
        }
    }

    private static let sessionProviderByCapability = [
        "codex-app-server-threads": "Codex",
        "codex-cli-sessions": "Codex",
        "claude-sessions": "Claude",
        "opencode-sessions": "OpenCode",
        "pi-sessions": "Pi",
    ]

    static func friendlyCapNames(_ caps: [String]) -> [(id: String, symbol: String, text: String)] {
        var seen = Set<String>()
        let normalizedCaps = caps.compactMap { cap -> String? in
            let normalized = cap.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
            guard !normalized.isEmpty, seen.insert(normalized).inserted else {
                return nil
            }
            return normalized
        }

        var seenSessionProviders = Set<String>()
        let sessionProviders = normalizedCaps.compactMap { capability -> String? in
            guard let provider = self.sessionProviderByCapability[capability],
                  seenSessionProviders.insert(provider).inserted
            else { return nil }
            return provider
        }
        var renderedSessionGroup = false

        return normalizedCaps.compactMap { normalized in
            if self.sessionProviderByCapability[normalized] != nil {
                guard !renderedSessionGroup else { return nil }
                renderedSessionGroup = true
                return (
                    "cap-group:sessions",
                    "rectangle.stack",
                    "Sessions: \(sessionProviders.joined(separator: ", "))")
            }
            switch normalized {
            case "screen":
                return ("cap:\(normalized)", "rectangle.inset.filled.badge.record", "Screen capture")
            case "camera":
                return ("cap:\(normalized)", "camera", "Camera")
            case "file":
                return ("cap:\(normalized)", "folder", "File transfer")
            case "location":
                return ("cap:\(normalized)", "location", "Location")
            case "voice", "audio":
                return ("cap:\(normalized)", "mic", "Microphone and voice")
            case "canvas":
                return ("cap:\(normalized)", "paintbrush", "Canvas display")
            default:
                return ("cap:\(normalized)", "puzzlepiece.extension", self.prettifyRawName(normalized))
            }
        }
    }

    private static func prettifyRawName(_ raw: String) -> String {
        let words = raw.split(whereSeparator: { $0 == "-" || $0 == "_" || $0 == "." }).map(String.init)
        guard let first = words.first else {
            return raw
        }
        let capitalized = first.prefix(1).uppercased() + first.dropFirst()
        return ([capitalized] + words.dropFirst()).joined(separator: " ")
    }

    private static func prettyModel(_ modelIdentifier: String?, deviceFamily: String?) -> String? {
        let model = modelIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines)
        if let model, !model.isEmpty {
            return model
        }
        let family = deviceFamily?.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let family, !family.isEmpty else {
            return nil
        }
        return family
    }

    private static func isMac(_ platform: String?) -> Bool {
        guard let platform else {
            return false
        }
        let lower = platform.lowercased()
        return lower.contains("mac") || lower.contains("darwin")
    }
}
