import ClawdisKit
import Foundation
import Network
import OSLog

@MainActor
final class MacNodeModeCoordinator {
    static let shared = MacNodeModeCoordinator()

    private let logger = Logger(subsystem: "com.steipete.clawdis", category: "mac-node")
    private var task: Task<Void, Never>?
    private let runtime = MacNodeRuntime()
    private let session = MacNodeBridgeSession()
    private var tunnel: RemotePortTunnel?

    func start() {
        guard self.task == nil else { return }
        self.task = Task { [weak self] in
            await self?.run()
        }
    }

    func stop() {
        self.task?.cancel()
        self.task = nil
        Task { await self.session.disconnect() }
        self.tunnel?.terminate()
        self.tunnel = nil
    }

    func setPreferredBridgeStableID(_ stableID: String?) {
        BridgeDiscoveryPreferences.setPreferredStableID(stableID)
        Task { await self.session.disconnect() }
    }

    private func run() async {
        var retryDelay: UInt64 = 1_000_000_000
        var lastCameraEnabled: Bool?
        let defaults = UserDefaults.standard
        while !Task.isCancelled {
            if await MainActor.run(body: { AppStateStore.shared.isPaused }) {
                try? await Task.sleep(nanoseconds: 1_000_000_000)
                continue
            }

            let cameraEnabled = defaults.object(forKey: cameraEnabledKey) as? Bool ?? false
            if lastCameraEnabled == nil {
                lastCameraEnabled = cameraEnabled
            } else if lastCameraEnabled != cameraEnabled {
                lastCameraEnabled = cameraEnabled
                await self.session.disconnect()
                try? await Task.sleep(nanoseconds: 200_000_000)
            }

            guard let endpoint = await self.resolveBridgeEndpoint(timeoutSeconds: 5) else {
                try? await Task.sleep(nanoseconds: min(retryDelay, 5_000_000_000))
                retryDelay = min(retryDelay * 2, 10_000_000_000)
                continue
            }

            retryDelay = 1_000_000_000
            do {
                let hello = await self.makeHello()
                try await self.session.connect(
                    endpoint: endpoint,
                    hello: hello,
                    onConnected: { [weak self] serverName in
                        self?.logger.info("mac node connected to \(serverName, privacy: .public)")
                    },
                    onInvoke: { [weak self] req in
                        guard let self else {
                            return BridgeInvokeResponse(
                                id: req.id,
                                ok: false,
                                error: ClawdisNodeError(code: .unavailable, message: "UNAVAILABLE: node not ready"))
                        }
                        return await self.runtime.handleInvoke(req)
                    })
            } catch {
                if await self.tryPair(endpoint: endpoint, error: error) {
                    continue
                }
                self.logger.error("mac node bridge connect failed: \(error.localizedDescription, privacy: .public)")
                try? await Task.sleep(nanoseconds: min(retryDelay, 5_000_000_000))
                retryDelay = min(retryDelay * 2, 10_000_000_000)
            }
        }
    }

    private func makeHello() async -> BridgeHello {
        let token = MacNodeTokenStore.loadToken()
        let caps = self.currentCaps()
        let commands = self.currentCommands(caps: caps)
        let permissions = await self.currentPermissions()
        return BridgeHello(
            nodeId: Self.nodeId(),
            displayName: InstanceIdentity.displayName,
            token: token,
            platform: "macos",
            version: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String,
            deviceFamily: "Mac",
            modelIdentifier: InstanceIdentity.modelIdentifier,
            caps: caps,
            commands: commands,
            permissions: permissions)
    }

    private func currentCaps() -> [String] {
        var caps: [String] = [ClawdisCapability.canvas.rawValue, ClawdisCapability.screen.rawValue]
        if UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false {
            caps.append(ClawdisCapability.camera.rawValue)
        }
        return caps
    }

    private func currentPermissions() async -> [String: Bool] {
        let statuses = await PermissionManager.status()
        return Dictionary(uniqueKeysWithValues: statuses.map { ($0.key.rawValue, $0.value) })
    }

    private func currentCommands(caps: [String]) -> [String] {
        var commands: [String] = [
            ClawdisCanvasCommand.present.rawValue,
            ClawdisCanvasCommand.hide.rawValue,
            ClawdisCanvasCommand.navigate.rawValue,
            ClawdisCanvasCommand.evalJS.rawValue,
            ClawdisCanvasCommand.snapshot.rawValue,
            ClawdisCanvasA2UICommand.push.rawValue,
            ClawdisCanvasA2UICommand.pushJSONL.rawValue,
            ClawdisCanvasA2UICommand.reset.rawValue,
            MacNodeScreenCommand.record.rawValue,
            ClawdisSystemCommand.run.rawValue,
            ClawdisSystemCommand.notify.rawValue,
        ]

        let capsSet = Set(caps)
        if capsSet.contains(ClawdisCapability.camera.rawValue) {
            commands.append(ClawdisCameraCommand.snap.rawValue)
            commands.append(ClawdisCameraCommand.clip.rawValue)
        }

        return commands
    }

    private func tryPair(endpoint: NWEndpoint, error: Error) async -> Bool {
        let text = error.localizedDescription.uppercased()
        guard text.contains("NOT_PAIRED") || text.contains("UNAUTHORIZED") else { return false }

        do {
            let shouldSilent = await MainActor.run {
                AppStateStore.shared.connectionMode == .remote
            }
            let hello = await self.makeHello()
            let token = try await MacNodeBridgePairingClient().pairAndHello(
                endpoint: endpoint,
                hello: hello,
                silent: shouldSilent,
                onStatus: { [weak self] status in
                    self?.logger.info("mac node pairing: \(status, privacy: .public)")
                })
            if !token.isEmpty {
                MacNodeTokenStore.saveToken(token)
            }
            return true
        } catch {
            self.logger.error("mac node pairing failed: \(error.localizedDescription, privacy: .public)")
            return false
        }
    }

    private static func nodeId() -> String {
        "mac-\(InstanceIdentity.instanceId)"
    }

    private func resolveLoopbackBridgeEndpoint(timeoutSeconds: Double) async -> NWEndpoint? {
        guard let port = Self.loopbackBridgePort(),
              let endpointPort = NWEndpoint.Port(rawValue: port)
        else {
            return nil
        }
        let endpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: endpointPort)
        let reachable = await Self.probeEndpoint(endpoint, timeoutSeconds: timeoutSeconds)
        return reachable ? endpoint : nil
    }

    static func loopbackBridgePort() -> UInt16? {
        if let raw = ProcessInfo.processInfo.environment["CLAWDIS_BRIDGE_PORT"],
           let parsed = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)),
           parsed > 0,
           parsed <= Int(UInt16.max)
        {
            return UInt16(parsed)
        }
        return 18790
    }

    static func probeEndpoint(_ endpoint: NWEndpoint, timeoutSeconds: Double) async -> Bool {
        let connection = NWConnection(to: endpoint, using: .tcp)
        let stream = Self.makeStateStream(for: connection)
        connection.start(queue: DispatchQueue(label: "com.steipete.clawdis.macos.bridge-loopback-probe"))
        do {
            try await Self.waitForReady(stream, timeoutSeconds: timeoutSeconds)
            connection.cancel()
            return true
        } catch {
            connection.cancel()
            return false
        }
    }

    private static func makeStateStream(
        for connection: NWConnection) -> AsyncStream<NWConnection.State>
    {
        AsyncStream { continuation in
            connection.stateUpdateHandler = { state in
                continuation.yield(state)
                switch state {
                case .ready, .failed, .cancelled:
                    continuation.finish()
                default:
                    break
                }
            }
        }
    }

    private static func waitForReady(
        _ stream: AsyncStream<NWConnection.State>,
        timeoutSeconds: Double) async throws
    {
        try await self.withTimeout(seconds: timeoutSeconds) {
            for await state in stream {
                switch state {
                case .ready:
                    return
                case let .failed(err):
                    throw err
                case .cancelled:
                    throw NSError(domain: "Bridge", code: 20, userInfo: [
                        NSLocalizedDescriptionKey: "Connection cancelled",
                    ])
                default:
                    continue
                }
            }
            throw NSError(domain: "Bridge", code: 21, userInfo: [
                NSLocalizedDescriptionKey: "Connection closed",
            ])
        }
    }

    private static func withTimeout<T: Sendable>(
        seconds: Double,
        operation: @escaping @Sendable () async throws -> T) async throws -> T
    {
        let task = Task { try await operation() }
        let timeout = Task {
            try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
            throw NSError(domain: "Bridge", code: 22, userInfo: [
                NSLocalizedDescriptionKey: "operation timed out",
            ])
        }
        defer { timeout.cancel() }
        return try await withTaskCancellationHandler(operation: {
            try await task.value
        }, onCancel: {
            timeout.cancel()
        })
    }

    private func resolveBridgeEndpoint(timeoutSeconds: Double) async -> NWEndpoint? {
        let mode = await MainActor.run(body: { AppStateStore.shared.connectionMode })
        if mode == .remote {
            do {
                if self.tunnel == nil || self.tunnel?.process.isRunning == false {
                    self.tunnel = try await RemotePortTunnel.create(remotePort: 18790)
                }
                if let localPort = self.tunnel?.localPort,
                   let port = NWEndpoint.Port(rawValue: localPort)
                {
                    return .hostPort(host: "127.0.0.1", port: port)
                }
            } catch {
                self.logger.error("mac node bridge tunnel failed: \(error.localizedDescription, privacy: .public)")
                self.tunnel?.terminate()
                self.tunnel = nil
            }
        } else if let tunnel = self.tunnel {
            tunnel.terminate()
            self.tunnel = nil
        }
        if mode == .local, let endpoint = await self.resolveLoopbackBridgeEndpoint(timeoutSeconds: 0.4) {
            return endpoint
        }
        return await Self.discoverBridgeEndpoint(timeoutSeconds: timeoutSeconds)
    }

    private static func discoverBridgeEndpoint(timeoutSeconds: Double) async -> NWEndpoint? {
        final class DiscoveryState: @unchecked Sendable {
            let lock = NSLock()
            var resolved = false
            var browsers: [NWBrowser] = []
            var continuation: CheckedContinuation<NWEndpoint?, Never>?

            func finish(_ endpoint: NWEndpoint?) {
                self.lock.lock()
                defer { lock.unlock() }
                if self.resolved { return }
                self.resolved = true
                for browser in self.browsers {
                    browser.cancel()
                }
                self.continuation?.resume(returning: endpoint)
                self.continuation = nil
            }
        }

        return await withCheckedContinuation { cont in
            let state = DiscoveryState()
            state.continuation = cont

            let params = NWParameters.tcp
            params.includePeerToPeer = true

            for domain in ClawdisBonjour.bridgeServiceDomains {
                let browser = NWBrowser(
                    for: .bonjour(type: ClawdisBonjour.bridgeServiceType, domain: domain),
                    using: params)
                browser.browseResultsChangedHandler = { results, _ in
                    let preferred = BridgeDiscoveryPreferences.preferredStableID()
                    if let preferred,
                       let match = results.first(where: {
                           if case .service = $0.endpoint {
                               return BridgeEndpointID.stableID($0.endpoint) == preferred
                           }
                           return false
                       })
                    {
                        state.finish(match.endpoint)
                        return
                    }

                    if let result = results.first(where: { if case .service = $0.endpoint { true } else { false } }) {
                        state.finish(result.endpoint)
                    }
                }
                browser.stateUpdateHandler = { browserState in
                    if case .failed = browserState {
                        state.finish(nil)
                    }
                }
                state.browsers.append(browser)
                browser.start(queue: DispatchQueue(label: "com.steipete.clawdis.macos.bridge-discovery.\(domain)"))
            }

            Task {
                try? await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000))
                state.finish(nil)
            }
        }
    }
}

enum MacNodeTokenStore {
    private static let suiteName = "com.steipete.clawdis.shared"
    private static let tokenKey = "mac.node.bridge.token"

    private static var defaults: UserDefaults {
        UserDefaults(suiteName: suiteName) ?? .standard
    }

    static func loadToken() -> String? {
        let raw = self.defaults.string(forKey: self.tokenKey)?.trimmingCharacters(in: .whitespacesAndNewlines)
        return raw?.isEmpty == false ? raw : nil
    }

    static func saveToken(_ token: String) {
        self.defaults.set(token, forKey: self.tokenKey)
    }
}
