import Foundation
import Observation
import OpenClawKit
import OpenClawProtocol
import UserNotifications
import WatchKit

@MainActor @Observable
final class WatchDirectNode {
    private struct PersistedConfiguration: Codable {
        var link: GatewayConnectDeepLink
        var gatewayID: String
        var setupSentAtMs: Int64?
    }

    private struct ActiveSession: Equatable {
        let baseURL: URL
        let token: String
    }

    private struct ConnectResponse: Decodable {
        let sessionToken: String
        let deviceToken: String
    }

    private enum ConnectCredential {
        case bootstrap(String)
        case device(String)

        var token: String {
            switch self {
            case let .bootstrap(token), let .device(token): token
            }
        }
    }

    private struct ChallengeResponse: Decodable {
        let nonce: String
    }

    private struct PollResponse: Decodable {
        let event: NodeEvent?
    }

    private struct NodeEvent: Decodable {
        let event: String
        let payload: InvokeRequest?
    }

    private struct InvokeRequest: Decodable {
        let id: String
        let nodeId: String
        let command: String
        let paramsJSON: String?
    }

    private struct HTTPError: LocalizedError {
        let status: Int
        let detail: String

        var errorDescription: String? {
            self.detail.isEmpty
                ? String(
                    format: String(localized: "Gateway HTTP error (%@)"),
                    self.status.formatted())
                : self.detail
        }
    }

    private static let keychainService = "ai.openclaw.watch.direct-node"
    private static let keychainAccount = "gateway"
    private static let enabledDefaultsKey = "watch.directNode.enabled"
    private static let lastSetupSentAtDefaultsKey = "watch.directNode.lastSetupSentAtMs"
    private static let maximumSetupAgeMs: Int64 = 12 * 60 * 1000
    private static let maximumSetupClockSkewMs: Int64 = 2 * 60 * 1000
    private static let commands = [
        OpenClawDeviceCommand.info.rawValue,
        OpenClawDeviceCommand.status.rawValue,
        OpenClawSystemCommand.notify.rawValue,
    ]

    private let networkMetrics: WatchURLSessionMetrics
    private let urlSession: URLSession
    private var configuration: PersistedConfiguration?
    private var connectTask: Task<Void, Never>?
    private var activeSession: ActiveSession?
    private var isForeground = false
    private var connectionGeneration = 0

    private(set) var isEnabled: Bool
    private(set) var isConnected = false
    private(set) var statusText = String(
        localized: "Use iPhone Settings to enable direct connection.")
    private(set) var endpointText: String?

    var isConfigured: Bool {
        self.configuration != nil
    }

    init() {
        let sessionConfiguration = URLSessionConfiguration.ephemeral
        sessionConfiguration.waitsForConnectivity = true
        sessionConfiguration.timeoutIntervalForRequest = 30
        sessionConfiguration.timeoutIntervalForResource = 35
        let networkMetrics = WatchURLSessionMetrics()
        self.networkMetrics = networkMetrics
        self.urlSession = URLSession(
            configuration: sessionConfiguration,
            delegate: networkMetrics,
            delegateQueue: nil)
        self.isEnabled = UserDefaults.standard.bool(forKey: Self.enabledDefaultsKey)
        self.configuration = Self.loadConfiguration()
        if let setupSentAtMs = configuration?.setupSentAtMs,
           setupSentAtMs > Self.lastAcceptedSetupSentAtMs()
        {
            Self.saveLastAcceptedSetupSentAtMs(setupSentAtMs)
        }
        self.endpointText = self.configuration.map(Self.endpointText)
        if self.configuration != nil {
            self.statusText = self.isEnabled
                ? String(localized: "Ready to connect")
                : String(localized: "Direct connection is off")
        }
    }

    func configure(setupCode: String, sentAtMs: Int64) {
        let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
        let oldestAcceptedMs = nowMs - Self.maximumSetupAgeMs
        let newestAcceptedMs = nowMs + Self.maximumSetupClockSkewMs
        guard (oldestAcceptedMs...newestAcceptedMs).contains(sentAtMs) else {
            self.statusText = String(
                localized: "Ignored an expired direct connection setup. Send setup again from iPhone.")
            return
        }
        let newestInstalledSetupMs = configuration?.setupSentAtMs ?? 0
        guard sentAtMs > max(Self.lastAcceptedSetupSentAtMs(), newestInstalledSetupMs) else { return }
        guard let link = GatewayConnectDeepLink.fromSetupCode(setupCode),
              link.isValidEndpoint,
              link.bootstrapToken != nil,
              link.token == nil,
              link.password == nil,
              let secureEndpoint = link.connectionEndpoints.first(where: \.tls)
        else {
            self.statusText = String(
                localized: "Direct mode requires a trusted HTTPS Gateway endpoint.")
            return
        }
        let secureEndpoints = link.connectionEndpoints.filter(\.tls)
        let secureLink = GatewayConnectDeepLink(
            host: secureEndpoint.host,
            port: secureEndpoint.port,
            tls: true,
            bootstrapToken: link.bootstrapToken,
            token: nil,
            password: nil,
            fallbackEndpoints: Array(secureEndpoints.dropFirst()))
        let previousConfiguration = self.configuration
        let configuration = PersistedConfiguration(
            link: secureLink,
            gatewayID: Self.gatewayID(for: secureLink),
            setupSentAtMs: sentAtMs)
        guard Self.saveConfiguration(configuration) else {
            self.statusText = String(localized: "Could not save direct connection securely.")
            return
        }
        if let previousConfiguration,
           previousConfiguration.gatewayID != configuration.gatewayID,
           let identity = DeviceIdentityStore.loadOrCreatePersisted(profile: .primary)
        {
            DeviceAuthStore.clearToken(
                deviceId: identity.deviceId,
                role: "node",
                gatewayID: previousConfiguration.gatewayID,
                profile: .primary)
        }
        Self.saveLastAcceptedSetupSentAtMs(sentAtMs)
        self.disconnectActiveSession()
        self.configuration = configuration
        self.endpointText = Self.endpointText(configuration)
        self.statusText = String(localized: "Setup received. Connecting…")
        self.setEnabled(true)
    }

    func setEnabled(_ enabled: Bool) {
        self.isEnabled = enabled
        UserDefaults.standard.set(enabled, forKey: Self.enabledDefaultsKey)
        if enabled {
            self.connect()
        } else {
            self.disconnectActiveSession()
            self.connectionGeneration &+= 1
            self.connectTask?.cancel()
            self.connectTask = nil
            self.isConnected = false
            self.statusText = self.isConfigured
                ? String(localized: "Direct connection is off")
                : String(localized: "Use iPhone Settings to enable direct connection.")
        }
    }

    func connect() {
        guard self.isForeground, self.isEnabled, let configuration else { return }
        self.disconnectActiveSession()
        self.connectTask?.cancel()
        self.connectionGeneration &+= 1
        let generation = self.connectionGeneration
        self.connectTask = Task { [weak self] in
            await self?.run(configuration, generation: generation)
        }
    }

    func connectForForeground() {
        self.isForeground = true
        self.connect()
    }

    func disconnectForBackground() {
        self.isForeground = false
        self.disconnectActiveSession()
        self.connectionGeneration &+= 1
        self.connectTask?.cancel()
        self.connectTask = nil
        self.isConnected = false
        if self.isEnabled, self.isConfigured {
            self.statusText = String(localized: "Reconnects when OpenClaw is active")
        }
    }

    func forget() {
        self.disconnectActiveSession()
        self.connectionGeneration &+= 1
        self.connectTask?.cancel()
        self.connectTask = nil
        if let configuration {
            if let identity = DeviceIdentityStore.loadOrCreatePersisted(profile: .primary) {
                DeviceAuthStore.clearToken(
                    deviceId: identity.deviceId,
                    role: "node",
                    gatewayID: configuration.gatewayID,
                    profile: .primary)
            }
        }
        _ = GenericPasswordKeychainStore.delete(
            service: Self.keychainService,
            account: Self.keychainAccount)
        configuration = nil
        self.endpointText = nil
        self.isConnected = false
        self.setEnabled(false)
    }

    private func run(_ configuration: PersistedConfiguration, generation: Int) async {
        while self.isCurrentConnection(generation, configuration: configuration) {
            var lastError: Error?
            for endpoint in configuration.link.connectionEndpoints {
                guard self.isCurrentConnection(generation, configuration: configuration) else { return }
                let link = configuration.link.selectingEndpoint(endpoint)
                guard let baseURL = Self.httpBaseURL(for: link) else { continue }
                do {
                    try await self.connectAndPoll(
                        configuration: configuration,
                        link: link,
                        baseURL: baseURL,
                        generation: generation)
                    return
                } catch is CancellationError {
                    return
                } catch let error as URLError where error.code == .cancelled {
                    return
                } catch {
                    guard self.isCurrentConnection(generation, configuration: configuration) else { return }
                    lastError = error
                    self.isConnected = false
                }
            }
            guard self.isCurrentConnection(generation, configuration: configuration) else { return }
            self.statusText = lastError.map {
                String(
                    format: String(localized: "Direct connection failed: %@"),
                    $0.localizedDescription)
            } ?? String(localized: "No usable Gateway endpoint")
            do {
                try await Task.sleep(for: .seconds(3))
            } catch {
                return
            }
        }
    }

    private func connectAndPoll(
        configuration: PersistedConfiguration,
        link: GatewayConnectDeepLink,
        baseURL: URL,
        generation: Int) async throws
    {
        try self.requireCurrentConnection(generation, configuration: configuration)
        self.statusText = String(localized: "Connecting directly…")
        guard let identity = DeviceIdentityStore.loadOrCreatePersisted(profile: .primary) else {
            throw HTTPError(
                status: 0,
                detail: String(localized: "Could not save the watch device identity"))
        }
        let storedToken = DeviceAuthStore.loadToken(
            deviceId: identity.deviceId,
            role: "node",
            gatewayID: configuration.gatewayID,
            profile: .primary)?.token
        guard storedToken != nil || link.bootstrapToken != nil else {
            throw HTTPError(status: 401, detail: String(localized: "No watch device credential"))
        }
        let response: ConnectResponse
        if let bootstrapToken = link.bootstrapToken {
            do {
                response = try await self.establishSession(
                    identity: identity,
                    baseURL: baseURL,
                    credential: .bootstrap(bootstrapToken))
            } catch let error as HTTPError where error.status == 401 {
                guard let storedToken else { throw error }
                response = try await self.establishSession(
                    identity: identity,
                    baseURL: baseURL,
                    credential: .device(storedToken))
            }
        } else if let storedToken {
            response = try await self.establishSession(
                identity: identity,
                baseURL: baseURL,
                credential: .device(storedToken))
        } else {
            throw HTTPError(status: 401, detail: String(localized: "No watch device credential"))
        }
        // A successful bootstrap response has already consumed the one-time code.
        // Finish that durable handoff across background/toggle cancellation, but
        // never let an obsolete attempt overwrite a forgotten or newer setup.
        do {
            guard self.isInstalledConfiguration(configuration) else { throw CancellationError() }
            guard DeviceAuthStore.storeTokenPersisted(
                deviceId: identity.deviceId,
                role: "node",
                token: response.deviceToken,
                scopes: [],
                gatewayID: configuration.gatewayID,
                profile: .primary)
            else {
                throw HTTPError(
                    status: 0,
                    detail: String(localized: "Could not save the watch device credential"))
            }
            if link.bootstrapToken != nil {
                try self.finishCredentialHandoff(configuration: configuration)
            }
            try self.requireCurrentConnection(generation, configuration: configuration)
        } catch {
            self.sendDisconnect(ActiveSession(baseURL: baseURL, token: response.sessionToken))
            throw error
        }
        let session = ActiveSession(baseURL: baseURL, token: response.sessionToken)
        self.activeSession = session
        defer { releaseActiveSession(session) }
        self.isConnected = true
        self.statusText = String(localized: "Connected directly")
        while self.isCurrentConnection(generation, configuration: configuration) {
            let pollData = try await request(
                baseURL: baseURL,
                path: "poll",
                method: "POST",
                token: response.sessionToken)
            try self.requireCurrentConnection(generation, configuration: configuration)
            let poll = try JSONDecoder().decode(PollResponse.self, from: pollData)
            guard let event = poll.event else { continue }
            guard event.event == "node.invoke.request", let invoke = event.payload else { continue }
            let invokeRequest = BridgeInvokeRequest(
                id: invoke.id,
                command: invoke.command,
                paramsJSON: invoke.paramsJSON,
                nodeId: invoke.nodeId)
            let result = await handleInvoke(invokeRequest)
            try requireCurrentConnection(generation, configuration: configuration)
            _ = try await self.request(
                baseURL: baseURL,
                path: "result",
                method: "POST",
                token: response.sessionToken,
                body: result)
            try self.requireCurrentConnection(generation, configuration: configuration)
        }
    }

    private func isCurrentConnection(
        _ generation: Int,
        configuration: PersistedConfiguration) -> Bool
    {
        !Task.isCancelled
            && generation == self.connectionGeneration
            && self.isForeground
            && self.isEnabled
            && self.isInstalledConfiguration(configuration)
    }

    private func isInstalledConfiguration(_ configuration: PersistedConfiguration) -> Bool {
        configuration.gatewayID == self.configuration?.gatewayID
            && configuration.setupSentAtMs == self.configuration?.setupSentAtMs
    }

    private func requireCurrentConnection(
        _ generation: Int,
        configuration: PersistedConfiguration) throws
    {
        guard self.isCurrentConnection(generation, configuration: configuration) else {
            throw CancellationError()
        }
    }

    private func establishSession(
        identity: DeviceIdentity,
        baseURL: URL,
        credential: ConnectCredential) async throws -> ConnectResponse
    {
        let challengeData = try await request(
            baseURL: baseURL,
            path: "challenge",
            method: "GET",
            token: nil)
        let challenge = try JSONDecoder().decode(ChallengeResponse.self, from: challengeData)
        let notificationSettings = await UNUserNotificationCenter.current().notificationSettings()
        let params = try connectParams(
            identity: identity,
            nonce: challenge.nonce,
            credential: credential,
            notificationsAuthorized: notificationSettings.authorizationStatus == .authorized
                || notificationSettings.authorizationStatus == .provisional)
        let connectData = try await request(
            baseURL: baseURL,
            path: "connect",
            method: "POST",
            token: nil,
            body: params)
        return try JSONDecoder().decode(ConnectResponse.self, from: connectData)
    }

    private func connectParams(
        identity: DeviceIdentity,
        nonce: String,
        credential: ConnectCredential,
        notificationsAuthorized: Bool) throws -> ConnectParams
    {
        let signedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
        let payload = GatewayDeviceAuthPayload.buildV3(
            fields: .init(
                deviceId: identity.deviceId,
                client: .init(id: "openclaw-watchos", mode: "node"),
                role: "node",
                scopes: [],
                signedAtMs: signedAtMs,
                token: credential.token,
                nonce: nonce),
            platform: InstanceIdentity.platformString,
            deviceFamily: InstanceIdentity.deviceFamily)
        guard let device = GatewayDeviceAuthPayload.signedDeviceDictionary(
            payload: payload,
            identity: identity,
            signedAtMs: signedAtMs,
            nonce: nonce)
        else {
            throw HTTPError(status: 0, detail: String(localized: "Could not sign watch identity"))
        }
        var client: [String: AnyCodable] = [
            "id": AnyCodable("openclaw-watchos"),
            "displayName": AnyCodable(InstanceIdentity.displayName),
            "version": AnyCodable(
                Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"),
            "platform": AnyCodable(InstanceIdentity.platformString),
            "deviceFamily": AnyCodable(InstanceIdentity.deviceFamily),
            "mode": AnyCodable("node"),
            "instanceId": AnyCodable(InstanceIdentity.instanceId),
        ]
        if let modelIdentifier = InstanceIdentity.modelIdentifier {
            client["modelIdentifier"] = AnyCodable(modelIdentifier)
        }
        let auth: [String: AnyCodable] = switch credential {
        case let .device(token):
            ["deviceToken": AnyCodable(token)]
        case let .bootstrap(token):
            ["bootstrapToken": AnyCodable(token)]
        }
        return ConnectParams(
            // Direct Watch HTTP transport was added after v3; only current gateways expose it.
            minprotocol: GATEWAY_MIN_PROTOCOL_VERSION,
            maxprotocol: GATEWAY_PROTOCOL_VERSION,
            client: client,
            caps: [],
            commands: Self.commands,
            permissions: ["notifications": AnyCodable(notificationsAuthorized)],
            pathenv: nil,
            role: "node",
            scopes: [],
            device: device,
            auth: auth,
            locale: Locale.preferredLanguages.first ?? Locale.current.identifier,
            useragent: ProcessInfo.processInfo.operatingSystemVersionString)
    }

    private func request(
        baseURL: URL,
        path: String,
        method: String,
        token: String?) async throws -> Data
    {
        try await self.performRequest(
            baseURL: baseURL,
            path: path,
            method: method,
            token: token,
            body: nil)
    }

    private func request(
        baseURL: URL,
        path: String,
        method: String,
        token: String?,
        body: some Encodable) async throws -> Data
    {
        let encodedBody = try JSONEncoder().encode(body)
        return try await self.performRequest(
            baseURL: baseURL,
            path: path,
            method: method,
            token: token,
            body: encodedBody)
    }

    private func performRequest(
        baseURL: URL,
        path: String,
        method: String,
        token: String?,
        body: Data?) async throws -> Data
    {
        let url = baseURL
            .appendingPathComponent("api")
            .appendingPathComponent("nodes")
            .appendingPathComponent("watch")
            .appendingPathComponent(path)
        var request = URLRequest(url: url)
        request.httpMethod = method
        request.timeoutInterval = path == "poll" ? 25 : 8
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        if let token {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        if let body {
            request.httpBody = body
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }
        let (data, response) = try await urlSession.data(for: request)
        guard let http = response as? HTTPURLResponse else {
            throw HTTPError(status: 0, detail: String(localized: "Invalid Gateway response"))
        }
        guard (200..<300).contains(http.statusCode) else {
            let detail = String(data: data, encoding: .utf8) ?? ""
            throw HTTPError(status: http.statusCode, detail: detail)
        }
        return data
    }

    private func finishCredentialHandoff(configuration: PersistedConfiguration) throws {
        let link = configuration.link
        let sanitized = PersistedConfiguration(
            link: GatewayConnectDeepLink(
                host: link.host,
                port: link.port,
                tls: link.tls,
                bootstrapToken: nil,
                token: nil,
                password: nil,
                fallbackEndpoints: link.fallbackEndpoints),
            gatewayID: configuration.gatewayID,
            setupSentAtMs: configuration.setupSentAtMs)
        guard Self.saveConfiguration(sanitized) else {
            throw HTTPError(
                status: 0,
                detail: String(localized: "Paired, but could not finish secure setup"))
        }
        self.configuration = sanitized
        self.endpointText = Self.endpointText(sanitized)
    }

    private func handleInvoke(_ request: BridgeInvokeRequest) async -> BridgeInvokeResponse {
        do {
            switch request.command {
            case OpenClawDeviceCommand.info.rawValue:
                return try self.encodedResponse(id: request.id, payload: self.deviceInfo())
            case OpenClawDeviceCommand.status.rawValue:
                return try self.encodedResponse(id: request.id, payload: self.deviceStatus())
            case OpenClawSystemCommand.notify.rawValue:
                return try await self.handleNotification(request)
            default:
                return Self.errorResponse(
                    id: request.id,
                    code: .invalidRequest,
                    message: "INVALID_REQUEST: unsupported watchOS command")
            }
        } catch {
            return Self.errorResponse(
                id: request.id,
                code: .unavailable,
                message: error.localizedDescription)
        }
    }

    private func handleNotification(_ request: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
        let params = try Self.decode(OpenClawSystemNotifyParams.self, from: request.paramsJSON)
        let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
        let body = params.body.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !title.isEmpty || !body.isEmpty else {
            return Self.errorResponse(
                id: request.id,
                code: .invalidRequest,
                message: "INVALID_REQUEST: empty notification")
        }
        let center = UNUserNotificationCenter.current()
        let settings = await center.notificationSettings()
        guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else {
            return Self.errorResponse(
                id: request.id,
                code: .unavailable,
                message: "NOT_AUTHORIZED: notifications")
        }

        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        switch params.priority ?? .active {
        case .passive:
            content.interruptionLevel = .passive
        case .timeSensitive:
            content.interruptionLevel = .timeSensitive
        case .active:
            content.interruptionLevel = .active
        }
        let sound = params.sound?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        content.sound = sound.map { ["none", "silent", "off"].contains($0) } == true ? nil : .default
        try await center.add(UNNotificationRequest(
            identifier: UUID().uuidString,
            content: content,
            trigger: nil))
        return BridgeInvokeResponse(id: request.id, ok: true)
    }

    private func deviceInfo() -> OpenClawDeviceInfoPayload {
        let device = WKInterfaceDevice.current()
        let info = Bundle.main.infoDictionary ?? [:]
        let appVersion = (info["CFBundleShortVersionString"] as? String) ?? "0"
        let appBuild = (info["CFBundleVersion"] as? String) ?? "0"
        return OpenClawDeviceInfoPayload(
            deviceName: device.name,
            modelIdentifier: InstanceIdentity.modelIdentifier ?? "Apple Watch",
            systemName: "watchOS",
            systemVersion: device.systemVersion,
            appVersion: appVersion,
            appBuild: appBuild,
            locale: Locale.preferredLanguages.first ?? Locale.current.identifier)
    }

    private func deviceStatus() -> OpenClawDeviceStatusPayload {
        let device = WKInterfaceDevice.current()
        device.isBatteryMonitoringEnabled = true
        let batteryState: OpenClawBatteryState = switch device.batteryState {
        case .charging: .charging
        case .full: .full
        case .unplugged: .unplugged
        case .unknown: .unknown
        @unknown default: .unknown
        }
        let battery = OpenClawBatteryStatusPayload(
            level: device.batteryLevel >= 0 ? Double(device.batteryLevel) : nil,
            state: batteryState,
            lowPowerModeEnabled: ProcessInfo.processInfo.isLowPowerModeEnabled)
        let thermalState: OpenClawThermalState = switch ProcessInfo.processInfo.thermalState {
        case .nominal: .nominal
        case .fair: .fair
        case .serious: .serious
        case .critical: .critical
        @unknown default: .nominal
        }
        let attributes = (try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())) ?? [:]
        let total = (attributes[.systemSize] as? NSNumber)?.int64Value ?? 0
        let free = (attributes[.systemFreeSize] as? NSNumber)?.int64Value ?? 0
        let networkMetrics = self.networkMetrics.snapshot()
        return OpenClawDeviceStatusPayload(
            battery: battery,
            thermal: OpenClawThermalStatusPayload(state: thermalState),
            storage: OpenClawStorageStatusPayload(
                totalBytes: total,
                freeBytes: free,
                usedBytes: max(0, total - free)),
            network: OpenClawNetworkStatusPayload(
                status: self.isConnected ? .satisfied : .requiresConnection,
                isExpensive: networkMetrics?.isExpensive ?? false,
                isConstrained: networkMetrics?.isConstrained ?? false,
                interfaces: networkMetrics?.isCellular == true ? [.cellular] : [.other]),
            uptimeSeconds: ProcessInfo.processInfo.systemUptime)
    }

    private func encodedResponse(id: String, payload: some Encodable) throws -> BridgeInvokeResponse {
        let data = try JSONEncoder().encode(payload)
        guard let json = String(data: data, encoding: .utf8) else {
            throw CocoaError(.fileWriteInapplicableStringEncoding)
        }
        return BridgeInvokeResponse(id: id, ok: true, payloadJSON: json)
    }

    private static func decode<T: Decodable>(_ type: T.Type, from json: String?) throws -> T {
        try JSONDecoder().decode(type, from: Data((json ?? "{}").utf8))
    }

    private static func errorResponse(
        id: String,
        code: OpenClawNodeErrorCode,
        message: String) -> BridgeInvokeResponse
    {
        BridgeInvokeResponse(
            id: id,
            ok: false,
            error: OpenClawNodeError(code: code, message: message))
    }

    private static func httpBaseURL(for link: GatewayConnectDeepLink) -> URL? {
        guard link.tls else { return nil }
        var components = URLComponents()
        components.scheme = "https"
        components.host = link.host
        components.port = link.port
        return components.url
    }

    private static func gatewayID(for link: GatewayConnectDeepLink) -> String {
        "watch-direct:\(link.tls ? "https" : "http")://\(link.host.lowercased()):\(link.port)"
    }

    private static func endpointText(_ configuration: PersistedConfiguration) -> String {
        "\(configuration.link.tls ? "https" : "http")://\(configuration.link.host):\(configuration.link.port)"
    }

    private static func loadConfiguration() -> PersistedConfiguration? {
        guard let raw = GenericPasswordKeychainStore.loadString(
            service: keychainService,
            account: keychainAccount),
            let data = raw.data(using: .utf8)
        else { return nil }
        return try? JSONDecoder().decode(PersistedConfiguration.self, from: data)
    }

    private static func saveConfiguration(_ configuration: PersistedConfiguration) -> Bool {
        guard let data = try? JSONEncoder().encode(configuration),
              let raw = String(data: data, encoding: .utf8)
        else { return false }
        return GenericPasswordKeychainStore.saveString(
            raw,
            service: self.keychainService,
            account: self.keychainAccount)
    }

    private func disconnectActiveSession() {
        self.isConnected = false
        guard let session = activeSession else { return }
        self.activeSession = nil
        self.sendDisconnect(session)
    }

    private func releaseActiveSession(_ session: ActiveSession) {
        guard self.activeSession == session else { return }
        self.activeSession = nil
        self.sendDisconnect(session)
    }

    private func sendDisconnect(_ session: ActiveSession) {
        Task { [weak self] in
            _ = try? await self?.request(
                baseURL: session.baseURL,
                path: "disconnect",
                method: "POST",
                token: session.token)
        }
    }

    private static func lastAcceptedSetupSentAtMs() -> Int64 {
        (UserDefaults.standard.object(forKey: self.lastSetupSentAtDefaultsKey) as? NSNumber)?.int64Value ?? 0
    }

    private static func saveLastAcceptedSetupSentAtMs(_ sentAtMs: Int64) {
        UserDefaults.standard.set(NSNumber(value: sentAtMs), forKey: self.lastSetupSentAtDefaultsKey)
    }
}

private struct WatchNetworkMetricsSnapshot {
    let isCellular: Bool
    let isExpensive: Bool
    let isConstrained: Bool
}

private final class WatchURLSessionMetrics: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
    private let lock = NSLock()
    private var latest: WatchNetworkMetricsSnapshot?

    func snapshot() -> WatchNetworkMetricsSnapshot? {
        self.lock.lock()
        defer { lock.unlock() }
        return self.latest
    }

    func urlSession(
        _: URLSession,
        task _: URLSessionTask,
        didFinishCollecting metrics: URLSessionTaskMetrics)
    {
        guard let transaction = metrics.transactionMetrics.last else { return }
        let snapshot = WatchNetworkMetricsSnapshot(
            isCellular: transaction.isCellular,
            isExpensive: transaction.isExpensive,
            isConstrained: transaction.isConstrained)
        self.lock.lock()
        self.latest = snapshot
        self.lock.unlock()
    }

    func urlSession(
        _: URLSession,
        task _: URLSessionTask,
        willPerformHTTPRedirection _: HTTPURLResponse,
        newRequest _: URLRequest,
        completionHandler: @escaping (URLRequest?) -> Void)
    {
        completionHandler(nil)
    }
}
