import AppKit
import Foundation
import Observation
import OpenClawKit
import OpenClawProtocol
import OSLog

@MainActor
@Observable
final class DevicePairingApprovalPrompter {
    static let shared = DevicePairingApprovalPrompter()

    private let logger = Logger(subsystem: "ai.openclaw", category: "device-pairing")
    private var task: Task<Void, Never>?
    private var isStopping = false
    private var isPresenting = false
    private var queue: [PendingRequest] = []
    var pendingCount: Int = 0
    var pendingRepairCount: Int = 0
    private var activeAlert: NSAlert?
    private var activeRequestId: String?
    private var alertHostWindow: NSWindow?
    private var resolvedByRequestId: Set<String> = []

    private struct PairingList: Codable {
        let pending: [PendingRequest]
        let paired: [PairedDevice]?
    }

    private struct PairedDevice: Codable, Equatable {
        let deviceId: String
        let approvedAtMs: Double?
        let displayName: String?
        let platform: String?
        let remoteIp: String?
    }

    private struct PendingRequest: Codable, Equatable, Identifiable {
        let requestId: String
        let deviceId: String
        let publicKey: String
        let displayName: String?
        let platform: String?
        let clientId: String?
        let clientMode: String?
        let role: String?
        let scopes: [String]?
        let remoteIp: String?
        let silent: Bool?
        let isRepair: Bool?
        let ts: Double

        var id: String {
            self.requestId
        }
    }

    private struct PairingResolvedEvent: Codable {
        let requestId: String
        let deviceId: String
        let decision: String
        let ts: Double
    }

    private enum PairingResolution: String {
        case approved
        case rejected
    }

    func start() {
        guard self.task == nil else { return }
        self.isStopping = false
        self.task = Task { [weak self] in
            guard let self else { return }
            _ = try? await GatewayConnection.shared.refresh()
            await self.loadPendingRequestsFromGateway()
            let stream = await GatewayConnection.shared.subscribe(bufferingNewest: 200)
            for await push in stream {
                if Task.isCancelled { return }
                await MainActor.run { [weak self] in self?.handle(push: push) }
            }
        }
    }

    func stop() {
        self.isStopping = true
        self.endActiveAlert()
        self.task?.cancel()
        self.task = nil
        self.queue.removeAll(keepingCapacity: false)
        self.updatePendingCounts()
        self.isPresenting = false
        self.activeRequestId = nil
        self.alertHostWindow?.orderOut(nil)
        self.alertHostWindow?.close()
        self.alertHostWindow = nil
        self.resolvedByRequestId.removeAll(keepingCapacity: false)
    }

    private func loadPendingRequestsFromGateway() async {
        do {
            let list: PairingList = try await GatewayConnection.shared.requestDecoded(method: .devicePairList)
            await self.apply(list: list)
        } catch {
            self.logger.error("failed to load device pairing requests: \(error.localizedDescription, privacy: .public)")
        }
    }

    private func apply(list: PairingList) async {
        self.queue = list.pending.sorted(by: { $0.ts > $1.ts })
        self.updatePendingCounts()
        self.presentNextIfNeeded()
    }

    private func updatePendingCounts() {
        self.pendingCount = self.queue.count
        self.pendingRepairCount = self.queue.count(where: { $0.isRepair == true })
    }

    private func presentNextIfNeeded() {
        guard !self.isStopping else { return }
        guard !self.isPresenting else { return }
        guard let next = self.queue.first else { return }
        self.isPresenting = true
        self.presentAlert(for: next)
    }

    private func presentAlert(for req: PendingRequest) {
        self.logger.info("presenting device pairing alert requestId=\(req.requestId, privacy: .public)")
        NSApp.activate(ignoringOtherApps: true)

        let alert = NSAlert()
        alert.alertStyle = .warning
        alert.messageText = "Allow device to connect?"
        alert.informativeText = Self.describe(req)
        alert.addButton(withTitle: "Later")
        alert.addButton(withTitle: "Approve")
        alert.addButton(withTitle: "Reject")
        if #available(macOS 11.0, *), alert.buttons.indices.contains(2) {
            alert.buttons[2].hasDestructiveAction = true
        }

        self.activeAlert = alert
        self.activeRequestId = req.requestId
        let hostWindow = self.requireAlertHostWindow()

        let sheetSize = alert.window.frame.size
        if let screen = hostWindow.screen ?? NSScreen.main {
            let bounds = screen.visibleFrame
            let x = bounds.midX - (sheetSize.width / 2)
            let sheetOriginY = bounds.midY - (sheetSize.height / 2)
            let hostY = sheetOriginY + sheetSize.height - hostWindow.frame.height
            hostWindow.setFrameOrigin(NSPoint(x: x, y: hostY))
        } else {
            hostWindow.center()
        }

        hostWindow.makeKeyAndOrderFront(nil)
        alert.beginSheetModal(for: hostWindow) { [weak self] response in
            Task { @MainActor [weak self] in
                guard let self else { return }
                self.activeRequestId = nil
                self.activeAlert = nil
                await self.handleAlertResponse(response, request: req)
                hostWindow.orderOut(nil)
            }
        }
    }

    private func handleAlertResponse(_ response: NSApplication.ModalResponse, request: PendingRequest) async {
        var shouldRemove = response != .alertFirstButtonReturn
        defer {
            if shouldRemove {
                if self.queue.first == request {
                    self.queue.removeFirst()
                } else {
                    self.queue.removeAll { $0 == request }
                }
            }
            self.updatePendingCounts()
            self.isPresenting = false
            self.presentNextIfNeeded()
        }

        guard !self.isStopping else { return }

        if self.resolvedByRequestId.remove(request.requestId) != nil {
            return
        }

        switch response {
        case .alertFirstButtonReturn:
            shouldRemove = false
            if let idx = self.queue.firstIndex(of: request) {
                self.queue.remove(at: idx)
            }
            self.queue.append(request)
            return
        case .alertSecondButtonReturn:
            _ = await self.approve(requestId: request.requestId)
        case .alertThirdButtonReturn:
            await self.reject(requestId: request.requestId)
        default:
            return
        }
    }

    private func approve(requestId: String) async -> Bool {
        do {
            try await GatewayConnection.shared.devicePairApprove(requestId: requestId)
            self.logger.info("approved device pairing requestId=\(requestId, privacy: .public)")
            return true
        } catch {
            self.logger.error("approve failed requestId=\(requestId, privacy: .public)")
            self.logger.error("approve failed: \(error.localizedDescription, privacy: .public)")
            return false
        }
    }

    private func reject(requestId: String) async {
        do {
            try await GatewayConnection.shared.devicePairReject(requestId: requestId)
            self.logger.info("rejected device pairing requestId=\(requestId, privacy: .public)")
        } catch {
            self.logger.error("reject failed requestId=\(requestId, privacy: .public)")
            self.logger.error("reject failed: \(error.localizedDescription, privacy: .public)")
        }
    }

    private func endActiveAlert() {
        PairingAlertSupport.endActiveAlert(activeAlert: &self.activeAlert, activeRequestId: &self.activeRequestId)
    }

    private func requireAlertHostWindow() -> NSWindow {
        PairingAlertSupport.requireAlertHostWindow(alertHostWindow: &self.alertHostWindow)
    }

    private func handle(push: GatewayPush) {
        switch push {
        case let .event(evt) where evt.event == "device.pair.requested":
            guard let payload = evt.payload else { return }
            do {
                let req = try GatewayPayloadDecoding.decode(payload, as: PendingRequest.self)
                self.enqueue(req)
            } catch {
                self.logger
                    .error("failed to decode device pairing request: \(error.localizedDescription, privacy: .public)")
            }
        case let .event(evt) where evt.event == "device.pair.resolved":
            guard let payload = evt.payload else { return }
            do {
                let resolved = try GatewayPayloadDecoding.decode(payload, as: PairingResolvedEvent.self)
                self.handleResolved(resolved)
            } catch {
                self.logger
                    .error(
                        "failed to decode device pairing resolution: \(error.localizedDescription, privacy: .public)")
            }
        default:
            break
        }
    }

    private func enqueue(_ req: PendingRequest) {
        guard !self.queue.contains(req) else { return }
        self.queue.append(req)
        self.updatePendingCounts()
        self.presentNextIfNeeded()
    }

    private func handleResolved(_ resolved: PairingResolvedEvent) {
        let resolution = resolved.decision == PairingResolution.approved.rawValue ? PairingResolution
            .approved : .rejected
        if let activeRequestId, activeRequestId == resolved.requestId {
            self.resolvedByRequestId.insert(resolved.requestId)
            self.endActiveAlert()
            let decision = resolution.rawValue
            self.logger.info(
                "device pairing resolved while active requestId=\(resolved.requestId, privacy: .public) " +
                    "decision=\(decision, privacy: .public)")
            return
        }
        self.queue.removeAll { $0.requestId == resolved.requestId }
        self.updatePendingCounts()
    }

    private static func describe(_ req: PendingRequest) -> String {
        var lines: [String] = []
        lines.append("Device: \(req.displayName ?? req.deviceId)")
        if let platform = req.platform {
            lines.append("Platform: \(platform)")
        }
        if let role = req.role {
            lines.append("Role: \(role)")
        }
        if let scopes = req.scopes, !scopes.isEmpty {
            lines.append("Scopes: \(scopes.joined(separator: ", "))")
        }
        if let remoteIp = req.remoteIp {
            lines.append("IP: \(remoteIp)")
        }
        if req.isRepair == true {
            lines.append("Repair: yes")
        }
        return lines.joined(separator: "\n")
    }
}
