import Foundation

public struct GatewayConnectionProblem: Equatable, Sendable {
    public enum Kind: String, Equatable, Sendable {
        case gatewayAuthTokenMissing
        case gatewayAuthTokenMismatch
        case gatewayAuthTokenNotConfigured
        case gatewayAuthPasswordMissing
        case gatewayAuthPasswordMismatch
        case gatewayAuthPasswordNotConfigured
        case bootstrapTokenInvalid
        case deviceTokenMismatch
        case deviceTokenScopeMismatch
        case pairingRequired
        case pairingRoleUpgradeRequired
        case pairingScopeUpgradeRequired
        case pairingMetadataUpgradeRequired
        case protocolMismatch
        case deviceIdentityRequired
        case deviceSignatureExpired
        case deviceNonceRequired
        case deviceNonceMismatch
        case deviceSignatureInvalid
        case devicePublicKeyInvalid
        case deviceIdMismatch
        case tailscaleIdentityMissing
        case tailscaleProxyMissing
        case tailscaleWhoisFailed
        case tailscaleIdentityMismatch
        case authRateLimited
        case timeout
        case connectionRefused
        case reachabilityFailed
        case websocketCancelled
        case tlsPinMismatch
        case tlsCertificateUntrusted
        case tlsCertificateUnavailable
        case unknown
    }

    public enum Owner: String, Equatable, Sendable {
        case gateway
        case iphone
        case both
        case network
        case unknown
    }

    public let kind: Kind
    public let owner: Owner
    public let title: String
    public let message: String
    public let actionLabel: String?
    public let actionCommand: String?
    public let docsURL: URL?
    public let requestId: String?
    public let retryable: Bool
    public let pauseReconnect: Bool
    public let technicalDetails: String?
    public let tlsStoreKey: String?
    public let tlsExpectedFingerprint: String?
    public let tlsObservedFingerprint: String?
    public let tlsSystemTrustOk: Bool

    public init(
        kind: Kind,
        owner: Owner,
        title: String,
        message: String,
        actionLabel: String? = nil,
        actionCommand: String? = nil,
        docsURL: URL? = nil,
        requestId: String? = nil,
        retryable: Bool,
        pauseReconnect: Bool,
        technicalDetails: String? = nil,
        tlsStoreKey: String? = nil,
        tlsExpectedFingerprint: String? = nil,
        tlsObservedFingerprint: String? = nil,
        tlsSystemTrustOk: Bool = false)
    {
        self.kind = kind
        self.owner = owner
        self.title = title
        self.message = message
        self.actionLabel = Self.trimmedOrNil(actionLabel)
        self.actionCommand = Self.trimmedOrNil(actionCommand)
        self.docsURL = docsURL
        self.requestId = Self.trimmedOrNil(requestId)
        self.retryable = retryable
        self.pauseReconnect = pauseReconnect
        self.technicalDetails = Self.trimmedOrNil(technicalDetails)
        self.tlsStoreKey = Self.trimmedOrNil(tlsStoreKey)
        self.tlsExpectedFingerprint = Self.trimmedOrNil(tlsExpectedFingerprint)
        self.tlsObservedFingerprint = Self.trimmedOrNil(tlsObservedFingerprint)
        self.tlsSystemTrustOk = tlsSystemTrustOk
    }

    public var needsPairingApproval: Bool {
        switch self.kind {
        case .pairingRequired, .pairingRoleUpgradeRequired, .pairingScopeUpgradeRequired,
             .pairingMetadataUpgradeRequired, .deviceTokenScopeMismatch:
            true
        default:
            false
        }
    }

    public var needsCredentialUpdate: Bool {
        switch self.kind {
        case .gatewayAuthTokenMissing,
             .gatewayAuthTokenMismatch,
             .gatewayAuthTokenNotConfigured,
             .gatewayAuthPasswordMissing,
             .gatewayAuthPasswordMismatch,
             .gatewayAuthPasswordNotConfigured,
             .bootstrapTokenInvalid,
             .deviceTokenMismatch:
            true
        default:
            false
        }
    }

    public var suggestsOnboardingReset: Bool {
        self.kind == .gatewayAuthTokenMismatch
    }

    public var statusText: String {
        switch self.kind {
        case .pairingRequired, .pairingRoleUpgradeRequired, .pairingScopeUpgradeRequired,
             .pairingMetadataUpgradeRequired, .protocolMismatch:
            if let requestId {
                return "\(self.title) (request ID: \(requestId))"
            }
            return self.title
        default:
            return self.title
        }
    }

    public var canTrustRotatedCertificate: Bool {
        self.kind == .tlsPinMismatch
            && self.tlsSystemTrustOk
            && self.tlsStoreKey != nil
            && self.tlsObservedFingerprint != nil
    }

    private static func trimmedOrNil(_ value: String?) -> String? {
        let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        return trimmed.isEmpty ? nil : trimmed
    }
}

public enum GatewayConnectionProblemMapper {
    public static func map(
        error: Error,
        preserving previousProblem: GatewayConnectionProblem? = nil) -> GatewayConnectionProblem?
    {
        guard let nextProblem = self.rawMap(error) else {
            return nil
        }
        guard let previousProblem else {
            return nextProblem
        }
        if self.shouldPreserve(previousProblem: previousProblem, over: nextProblem) {
            return previousProblem
        }
        return nextProblem
    }

    public static func shouldPreserve(
        previousProblem: GatewayConnectionProblem,
        over nextProblem: GatewayConnectionProblem) -> Bool
    {
        if nextProblem.kind == .websocketCancelled {
            return previousProblem.pauseReconnect || previousProblem.requestId != nil
        }
        return false
    }

    public static func shouldPreserve(
        previousProblem: GatewayConnectionProblem,
        overDisconnectReason reason: String) -> Bool
    {
        let normalized = reason.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        guard !normalized.isEmpty else { return false }
        if normalized.contains("cancelled") || normalized.contains("canceled") {
            return previousProblem.pauseReconnect || previousProblem.requestId != nil
        }
        return false
    }

    private static func rawMap(_ error: Error) -> GatewayConnectionProblem? {
        if let authError = error as? GatewayConnectAuthError {
            return self.map(authError)
        }
        if let responseError = error as? GatewayResponseError {
            return self.map(responseError)
        }
        if let tlsError = error as? GatewayTLSValidationError {
            return self.map(tlsError)
        }
        return self.mapTransportError(error)
    }

    private static func map(_ authError: GatewayConnectAuthError) -> GatewayConnectionProblem {
        let pairingCommand = self.approvalCommand(requestId: authError.requestId)

        switch authError.detail {
        case .authTokenMissing:
            return self.problem(
                kind: .gatewayAuthTokenMissing,
                owner: .both,
                title: authError.titleOverride ?? "Gateway token required",
                message: authError.userMessageOverride
                    ?? "This gateway requires an auth token, but this device did not send one.",
                actionLabel: authError.actionLabel ?? "Open Settings",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/authentication"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authTokenMismatch:
            return self.problem(
                kind: .gatewayAuthTokenMismatch,
                owner: .both,
                title: authError.titleOverride ?? "Gateway token is out of date",
                message: authError.userMessageOverride
                    ?? "The token on this device does not match the gateway token.",
                actionLabel: authError
                    .actionLabel ?? (authError.canRetryWithDeviceToken ? "Retry once" : "Update gateway token"),
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/authentication"),
                requestId: authError.requestId,
                retryable: authError.retryableOverride ?? authError.canRetryWithDeviceToken,
                pauseReconnect: authError.pauseReconnectOverride ?? !authError.canRetryWithDeviceToken,
                authError: authError)
        case .authTokenNotConfigured:
            return self.problem(
                kind: .gatewayAuthTokenNotConfigured,
                owner: .gateway,
                title: authError.titleOverride ?? "Gateway token is not configured",
                message: authError.userMessageOverride
                    ?? "This gateway is set to token auth, but no gateway token is configured on the gateway.",
                actionLabel: authError.actionLabel ?? "Fix on gateway",
                actionCommand: authError.actionCommand ?? "openclaw config set gateway.auth.token <new-token>",
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/authentication"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authPasswordMissing:
            return self.problem(
                kind: .gatewayAuthPasswordMissing,
                owner: .both,
                title: authError.titleOverride ?? "Gateway password required",
                message: authError.userMessageOverride
                    ?? "This gateway requires a password, but this device did not send one.",
                actionLabel: authError.actionLabel ?? "Open Settings",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/authentication"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authPasswordMismatch:
            return self.problem(
                kind: .gatewayAuthPasswordMismatch,
                owner: .both,
                title: authError.titleOverride ?? "Gateway password is out of date",
                message: authError.userMessageOverride
                    ?? "The saved password on this device does not match the gateway password.",
                actionLabel: authError.actionLabel ?? "Update password",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/authentication"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authPasswordNotConfigured:
            return self.problem(
                kind: .gatewayAuthPasswordNotConfigured,
                owner: .gateway,
                title: authError.titleOverride ?? "Gateway password is not configured",
                message: authError.userMessageOverride
                    ?? "This gateway is set to password auth, but no gateway password is configured on the gateway.",
                actionLabel: authError.actionLabel ?? "Fix on gateway",
                actionCommand: authError.actionCommand ?? "openclaw config set gateway.auth.password <new-password>",
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/authentication"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authBootstrapTokenInvalid:
            return self.problem(
                kind: .bootstrapTokenInvalid,
                owner: .iphone,
                title: authError.titleOverride ?? "Setup code expired",
                message: authError.userMessageOverride
                    ?? "The setup QR or bootstrap token is no longer valid.",
                actionLabel: authError.actionLabel ?? "Scan QR again",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/platforms/ios"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authDeviceTokenMismatch:
            return self.problem(
                kind: .deviceTokenMismatch,
                owner: .both,
                title: authError.titleOverride ?? "This device's saved device token is no longer valid",
                message: authError.userMessageOverride
                    ?? "The gateway rejected the stored device token for this role.",
                actionLabel: authError.actionLabel ?? "Repair pairing",
                actionCommand: authError.actionCommand ?? pairingCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authScopeMismatch:
            return self.problem(
                kind: .deviceTokenScopeMismatch,
                owner: .both,
                title: authError.titleOverride ?? "Device permissions need approval",
                message: authError.userMessageOverride
                    ?? "The gateway accepted this device token but rejected the requested operator scopes.",
                actionLabel: authError.actionLabel ?? "Review pairing",
                actionCommand: authError.actionCommand ?? pairingCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .pairingRequired:
            return self.pairingProblem(for: authError)
        case .protocolMismatch:
            return self.protocolMismatchProblem(for: authError)
        case .controlUiDeviceIdentityRequired, .deviceIdentityRequired:
            return self.problem(
                kind: .deviceIdentityRequired,
                owner: .iphone,
                title: authError.titleOverride ?? "Secure device identity is required",
                message: authError.userMessageOverride
                    ??
                    "This connection must include a signed device identity before the gateway can bind permissions to this device.",
                actionLabel: authError.actionLabel ?? "Retry from the app",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/platforms/ios"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .deviceAuthSignatureExpired:
            return self.problem(
                kind: .deviceSignatureExpired,
                owner: .iphone,
                title: authError.titleOverride ?? "Secure handshake expired",
                message: authError.userMessageOverride ?? "The device signature is too old to use.",
                actionLabel: authError.actionLabel ?? "Check device time",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/troubleshooting"),
                requestId: authError.requestId,
                retryable: true,
                pauseReconnect: true,
                authError: authError)
        case .deviceAuthNonceRequired:
            return self.problem(
                kind: .deviceNonceRequired,
                owner: .iphone,
                title: authError.titleOverride ?? "Secure handshake is incomplete",
                message: authError.userMessageOverride
                    ?? "The gateway expected a one-time challenge response, but the nonce was missing.",
                actionLabel: authError.actionLabel ?? "Retry",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/troubleshooting"),
                requestId: authError.requestId,
                retryable: true,
                pauseReconnect: true,
                authError: authError)
        case .deviceAuthNonceMismatch:
            return self.problem(
                kind: .deviceNonceMismatch,
                owner: .iphone,
                title: authError.titleOverride ?? "Secure handshake did not match",
                message: authError.userMessageOverride ?? "The challenge response was stale or mismatched.",
                actionLabel: authError.actionLabel ?? "Retry",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/troubleshooting"),
                requestId: authError.requestId,
                retryable: true,
                pauseReconnect: true,
                authError: authError)
        case .deviceAuthSignatureInvalid, .deviceAuthInvalid:
            return self.problem(
                kind: .deviceSignatureInvalid,
                owner: .iphone,
                title: authError.titleOverride ?? "This device identity could not be verified",
                message: authError.userMessageOverride
                    ?? "The gateway could not verify the identity this device presented.",
                actionLabel: authError.actionLabel ?? "Re-pair this device",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .deviceAuthPublicKeyInvalid:
            return self.problem(
                kind: .devicePublicKeyInvalid,
                owner: .iphone,
                title: authError.titleOverride ?? "This device identity could not be verified",
                message: authError.userMessageOverride
                    ?? "The gateway could not verify the public key this device presented.",
                actionLabel: authError.actionLabel ?? "Re-pair this device",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .deviceAuthDeviceIdMismatch:
            return self.problem(
                kind: .deviceIdMismatch,
                owner: .iphone,
                title: authError.titleOverride ?? "This device identity could not be verified",
                message: authError.userMessageOverride
                    ?? "The gateway rejected the device identity because the device ID did not match.",
                actionLabel: authError.actionLabel ?? "Re-pair this device",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authTailscaleIdentityMissing:
            return self.problem(
                kind: .tailscaleIdentityMissing,
                owner: .network,
                title: authError.titleOverride ?? "Tailscale identity check failed",
                message: authError.userMessageOverride
                    ?? "This connection expected Tailscale identity headers, but they were not available.",
                actionLabel: authError.actionLabel ?? "Turn on Tailscale",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/tailscale"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authTailscaleProxyMissing:
            return self.problem(
                kind: .tailscaleProxyMissing,
                owner: .network,
                title: authError.titleOverride ?? "Tailscale identity check failed",
                message: authError.userMessageOverride
                    ?? "The gateway expected a Tailscale auth proxy, but it was not configured.",
                actionLabel: authError.actionLabel ?? "Review Tailscale setup",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/tailscale"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authTailscaleWhoisFailed:
            return self.problem(
                kind: .tailscaleWhoisFailed,
                owner: .network,
                title: authError.titleOverride ?? "Tailscale identity check failed",
                message: authError.userMessageOverride
                    ?? "The gateway could not verify this Tailscale client identity.",
                actionLabel: authError.actionLabel ?? "Review Tailscale setup",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/tailscale"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authTailscaleIdentityMismatch:
            return self.problem(
                kind: .tailscaleIdentityMismatch,
                owner: .network,
                title: authError.titleOverride ?? "Tailscale identity check failed",
                message: authError.userMessageOverride
                    ?? "The forwarded Tailscale identity did not match the verified identity.",
                actionLabel: authError.actionLabel ?? "Review Tailscale setup",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/tailscale"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authRateLimited:
            return self.problem(
                kind: .authRateLimited,
                owner: .gateway,
                title: authError.titleOverride ?? "Too many failed attempts",
                message: authError.userMessageOverride
                    ?? "The gateway is temporarily refusing new auth attempts after repeated failures.",
                actionLabel: authError.actionLabel ?? "Wait and retry",
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(
                    authError.docsURLString,
                    fallback: "https://docs.openclaw.ai/gateway/troubleshooting"),
                requestId: authError.requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case .authRequired, .authUnauthorized, .none:
            return self.problem(
                kind: .unknown,
                owner: authError.ownerRaw.flatMap { self.owner(from: $0) } ?? .unknown,
                title: authError.titleOverride ?? "Gateway rejected the connection",
                message: authError.userMessageOverride ?? authError.message,
                actionLabel: authError.actionLabel,
                actionCommand: authError.actionCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: nil),
                requestId: authError.requestId,
                retryable: authError.retryableOverride ?? false,
                pauseReconnect: authError.pauseReconnectOverride ?? authError.isNonRecoverable,
                authError: authError)
        }
    }

    private static func map(_ responseError: GatewayResponseError) -> GatewayConnectionProblem? {
        let code = responseError.code.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
        if code == "NOT_PAIRED" || responseError.detailsReason == "not-paired" {
            let authError = GatewayConnectAuthError(
                message: responseError.message,
                detailCodeRaw: GatewayConnectAuthDetailCode.pairingRequired.rawValue,
                canRetryWithDeviceToken: false,
                recommendedNextStepRaw: nil,
                requestId: self.stringValue(responseError.details["requestId"]?.value),
                detailsReason: responseError.detailsReason,
                ownerRaw: nil,
                titleOverride: nil,
                userMessageOverride: nil,
                actionLabel: nil,
                actionCommand: nil,
                docsURLString: nil,
                retryableOverride: nil,
                pauseReconnectOverride: nil)
            return self.map(authError)
        }
        return nil
    }

    private static func map(_ tlsError: GatewayTLSValidationError) -> GatewayConnectionProblem {
        let failure = tlsError.failure
        switch failure.kind {
        case .pinMismatch:
            let trustedSuffix = failure.systemTrustOk
                ? " The new certificate is trusted by this device; this is commonly caused by certificate rotation."
                : " This device could not verify the new certificate."
            return GatewayConnectionProblem(
                kind: .tlsPinMismatch,
                owner: failure.systemTrustOk ? .network : .unknown,
                title: "Gateway certificate changed",
                message: "The saved TLS certificate pin for \(failure.host) no longer matches the gateway certificate.\(trustedSuffix)",
                actionLabel: "Review certificate",
                actionCommand: nil,
                docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                retryable: false,
                pauseReconnect: true,
                technicalDetails: tlsError.localizedDescription,
                tlsStoreKey: failure.storeKey,
                tlsExpectedFingerprint: failure.expectedFingerprint,
                tlsObservedFingerprint: failure.observedFingerprint,
                tlsSystemTrustOk: failure.systemTrustOk)
        case .certificateUnavailable:
            return GatewayConnectionProblem(
                kind: .tlsCertificateUnavailable,
                owner: .network,
                title: "Gateway certificate unavailable",
                message: "OpenClaw could not read the gateway certificate for \(failure.host).",
                actionLabel: "Retry",
                actionCommand: nil,
                docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                retryable: true,
                pauseReconnect: false,
                technicalDetails: tlsError.localizedDescription)
        case .untrustedCertificate:
            return GatewayConnectionProblem(
                kind: .tlsCertificateUntrusted,
                owner: .network,
                title: "Gateway certificate is not trusted",
                message: "This device does not trust the TLS certificate presented by \(failure.host).",
                actionLabel: "Check certificate",
                actionCommand: nil,
                docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                retryable: false,
                pauseReconnect: true,
                technicalDetails: tlsError.localizedDescription)
        }
    }

    private static func mapTransportError(_ error: Error) -> GatewayConnectionProblem? {
        let nsError = error as NSError
        let rawMessage = nsError.userInfo[NSLocalizedDescriptionKey] as? String ?? nsError.localizedDescription
        let lower = rawMessage.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        if lower.isEmpty {
            return nil
        }

        let urlErrorCode = URLError.Code(rawValue: nsError.code)
        if nsError.domain == URLError.errorDomain {
            switch urlErrorCode {
            case .timedOut:
                return GatewayConnectionProblem(
                    kind: .timeout,
                    owner: .network,
                    title: "Connection timed out",
                    message: "The gateway did not respond before the connection timed out.",
                    actionLabel: "Retry",
                    actionCommand: nil,
                    docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                    retryable: true,
                    pauseReconnect: false,
                    technicalDetails: rawMessage)
            case .cannotConnectToHost:
                return GatewayConnectionProblem(
                    kind: .connectionRefused,
                    owner: .network,
                    title: "Gateway refused the connection",
                    message: "The gateway host was reachable, but it refused the connection.",
                    actionLabel: "Retry",
                    actionCommand: nil,
                    docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                    retryable: true,
                    pauseReconnect: false,
                    technicalDetails: rawMessage)
            case .cannotFindHost, .dnsLookupFailed, .notConnectedToInternet, .networkConnectionLost,
                 .internationalRoamingOff, .callIsActive, .dataNotAllowed:
                return GatewayConnectionProblem(
                    kind: .reachabilityFailed,
                    owner: .network,
                    title: "Gateway is not reachable",
                    message: "OpenClaw could not reach the gateway over the current network.",
                    actionLabel: "Check network",
                    actionCommand: nil,
                    docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                    retryable: true,
                    pauseReconnect: false,
                    technicalDetails: rawMessage)
            case .cancelled:
                return GatewayConnectionProblem(
                    kind: .websocketCancelled,
                    owner: .network,
                    title: "Connection interrupted",
                    message: "The connection to the gateway was interrupted before setup completed.",
                    actionLabel: "Retry",
                    actionCommand: nil,
                    docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                    retryable: true,
                    pauseReconnect: false,
                    technicalDetails: rawMessage)
            default:
                break
            }
        }

        if lower.contains("timed out") {
            return GatewayConnectionProblem(
                kind: .timeout,
                owner: .network,
                title: "Connection timed out",
                message: "The gateway did not respond before the connection timed out.",
                actionLabel: "Retry",
                actionCommand: nil,
                docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                retryable: true,
                pauseReconnect: false,
                technicalDetails: rawMessage)
        }
        if lower.contains("connection refused") || lower.contains("refused") {
            return GatewayConnectionProblem(
                kind: .connectionRefused,
                owner: .network,
                title: "Gateway refused the connection",
                message: "The gateway host was reachable, but it refused the connection.",
                actionLabel: "Retry",
                actionCommand: nil,
                docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                retryable: true,
                pauseReconnect: false,
                technicalDetails: rawMessage)
        }
        if lower.contains("cannot find host") || lower.contains("could not connect") || lower
            .contains("network is unreachable")
        {
            return GatewayConnectionProblem(
                kind: .reachabilityFailed,
                owner: .network,
                title: "Gateway is not reachable",
                message: "OpenClaw could not reach the gateway over the current network.",
                actionLabel: "Check network",
                actionCommand: nil,
                docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                retryable: true,
                pauseReconnect: false,
                technicalDetails: rawMessage)
        }
        if lower.contains("cancelled") || lower.contains("canceled") {
            return GatewayConnectionProblem(
                kind: .websocketCancelled,
                owner: .network,
                title: "Connection interrupted",
                message: "The connection to the gateway was interrupted before setup completed.",
                actionLabel: "Retry",
                actionCommand: nil,
                docsURL: URL(string: "https://docs.openclaw.ai/gateway/troubleshooting"),
                retryable: true,
                pauseReconnect: false,
                technicalDetails: rawMessage)
        }
        return nil
    }

    private static func pairingProblem(for authError: GatewayConnectAuthError) -> GatewayConnectionProblem {
        let requestId = authError.requestId
        let pairingCommand = self.approvalCommand(requestId: requestId)

        switch authError.detailsReason {
        case "role-upgrade":
            return self.problem(
                kind: .pairingRoleUpgradeRequired,
                owner: .gateway,
                title: authError.titleOverride ?? "Additional approval required",
                message: authError.userMessageOverride
                    ??
                    "This device is already paired, but it is requesting a new role that was not previously approved.",
                actionLabel: authError.actionLabel ?? "Approve on gateway",
                actionCommand: authError.actionCommand ?? pairingCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
                requestId: requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case "scope-upgrade":
            return self.problem(
                kind: .pairingScopeUpgradeRequired,
                owner: .gateway,
                title: authError.titleOverride ?? "Additional permissions required",
                message: authError.userMessageOverride
                    ?? "This device is already paired, but it is requesting new permissions that require approval.",
                actionLabel: authError.actionLabel ?? "Approve on gateway",
                actionCommand: authError.actionCommand ?? pairingCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
                requestId: requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        case "metadata-upgrade":
            return self.problem(
                kind: .pairingMetadataUpgradeRequired,
                owner: .gateway,
                title: authError.titleOverride ?? "Device approval needs refresh",
                message: authError.userMessageOverride
                    ??
                    "The gateway detected a change in this device's approved identity metadata and requires re-approval.",
                actionLabel: authError.actionLabel ?? "Approve on gateway",
                actionCommand: authError.actionCommand ?? pairingCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
                requestId: requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        default:
            return self.problem(
                kind: .pairingRequired,
                owner: .gateway,
                title: authError.titleOverride ?? "This device is not approved yet",
                message: authError.userMessageOverride
                    ?? "The gateway received the connection request, but this device must be approved first.",
                actionLabel: authError.actionLabel ?? "Approve on gateway",
                actionCommand: authError.actionCommand ?? pairingCommand,
                docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
                requestId: requestId,
                retryable: false,
                pauseReconnect: true,
                authError: authError)
        }
    }

    private static func protocolMismatchProblem(for authError: GatewayConnectAuthError) -> GatewayConnectionProblem {
        let title: String
        let message: String
        let owner: GatewayConnectionProblem.Owner
        let actionLabel: String
        if let clientMax = authError.clientMaxProtocol,
           let expected = authError.expectedProtocol,
           clientMax < expected
        {
            title = authError.titleOverride ?? "App update required"
            message = authError.userMessageOverride
                ?? "This app is older than the gateway. Update OpenClaw on this device, then retry."
            owner = .iphone
            actionLabel = authError.actionLabel ?? "Update app"
        } else if let clientMin = authError.clientMinProtocol,
                  let expected = authError.expectedProtocol,
                  clientMin > expected
        {
            title = authError.titleOverride ?? "Gateway update required"
            message = authError.userMessageOverride
                ?? "The gateway is older than this app. Update OpenClaw on the gateway host, then retry."
            owner = .gateway
            actionLabel = authError.actionLabel ?? "Update gateway"
        } else {
            title = authError.titleOverride ?? "OpenClaw update required"
            message = authError.userMessageOverride
                ?? "The app and gateway use incompatible protocol versions. Update OpenClaw on both, then retry."
            owner = .both
            actionLabel = authError.actionLabel ?? "Update OpenClaw"
        }
        return self.problem(
            kind: .protocolMismatch,
            owner: owner,
            title: title,
            message: message,
            actionLabel: actionLabel,
            actionCommand: authError.actionCommand,
            docsURL: self.docsURL(
                authError.docsURLString,
                fallback: "https://docs.openclaw.ai/gateway/troubleshooting"),
            requestId: authError.requestId,
            retryable: false,
            pauseReconnect: true,
            authError: authError)
    }

    private static func problem(
        kind: GatewayConnectionProblem.Kind,
        owner: GatewayConnectionProblem.Owner,
        title: String,
        message: String,
        actionLabel: String?,
        actionCommand: String?,
        docsURL: URL?,
        requestId: String?,
        retryable: Bool,
        pauseReconnect: Bool,
        authError: GatewayConnectAuthError)
        -> GatewayConnectionProblem
    {
        GatewayConnectionProblem(
            kind: kind,
            owner: authError.ownerRaw.flatMap(self.owner(from:)) ?? owner,
            title: title,
            message: message,
            actionLabel: actionLabel,
            actionCommand: actionCommand,
            docsURL: docsURL,
            requestId: requestId,
            retryable: authError.retryableOverride ?? retryable,
            pauseReconnect: authError.pauseReconnectOverride ?? pauseReconnect,
            technicalDetails: self.technicalDetails(for: authError))
    }

    private static func approvalCommand(requestId: String?) -> String {
        if let requestId = self.nonEmpty(requestId) {
            return "openclaw devices approve \(requestId)"
        }
        return "openclaw devices list"
    }

    private static func technicalDetails(for authError: GatewayConnectAuthError) -> String? {
        var parts: [String] = []
        if let detail = self.nonEmpty(authError.detailCodeRaw) {
            parts.append(detail)
        }
        if let reason = self.nonEmpty(authError.detailsReason) {
            parts.append("reason=\(reason)")
        }
        if let requestId = self.nonEmpty(authError.requestId) {
            parts.append("requestId=\(requestId)")
        }
        if let nextStep = self.nonEmpty(authError.recommendedNextStepRaw) {
            parts.append("next=\(nextStep)")
        }
        if authError.canRetryWithDeviceToken {
            parts.append("deviceTokenRetry=true")
        }
        if let clientRange = self.protocolRange(min: authError.clientMinProtocol, max: authError.clientMaxProtocol) {
            parts.append("clientProtocol=\(clientRange)")
        }
        if let expected = authError.expectedProtocol {
            parts.append("gatewayProtocol=\(expected)")
        }
        if let minimumProbe = authError.minimumProbeProtocol {
            parts.append("probeMin=\(minimumProbe)")
        }
        return parts.isEmpty ? nil : parts.joined(separator: " · ")
    }

    private static func protocolRange(min: Int?, max: Int?) -> String? {
        switch (min, max) {
        case (nil, nil):
            nil
        case let (min?, max?) where min == max:
            "\(min)"
        case let (min?, max?):
            "\(min)-\(max)"
        case let (min?, nil):
            "min \(min)"
        case let (nil, max?):
            "max \(max)"
        }
    }

    private static func docsURL(_ preferred: String?, fallback: String?) -> URL? {
        if let preferred = self.nonEmpty(preferred), let url = URL(string: preferred) {
            return url
        }
        if let fallback = self.nonEmpty(fallback), let url = URL(string: fallback) {
            return url
        }
        return nil
    }

    private static func owner(from raw: String) -> GatewayConnectionProblem.Owner? {
        switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
        case "gateway":
            .gateway
        case "iphone", "ios", "device":
            .iphone
        case "both":
            .both
        case "network":
            .network
        case "unknown", "":
            .unknown
        default:
            nil
        }
    }

    private static func stringValue(_ value: Any?) -> String? {
        self.nonEmpty(value as? String)
    }

    private static func nonEmpty(_ value: String?) -> String? {
        let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        return trimmed.isEmpty ? nil : trimmed
    }
}
