import Foundation
import OpenClawKit

// Canonical transport leaf models live in OpenClawKit. The local envelope types
// below retain the existing Watch inbox persistence shape without `type`.
typealias WatchPayloadType = OpenClawWatchPayloadType
typealias WatchRiskLevel = OpenClawWatchRisk
typealias WatchExecApprovalDecision = OpenClawWatchExecApprovalDecision
typealias WatchExecApprovalTransportOutcome = OpenClawWatchExecApprovalOutcome
typealias WatchExecApprovalCloseReason = OpenClawWatchExecApprovalCloseReason
typealias WatchOpaqueUTF8Key = ExactOpaqueIdentifierKey
typealias WatchApprovalID = ExecApprovalIdentifier
typealias WatchGatewayID = GatewayStableIdentifier

struct WatchExecApprovalIdentityKey: Hashable, Sendable {
    var gatewayID: WatchGatewayID.Key
    var approvalID: WatchApprovalID.Key
}

typealias WatchExecApprovalItem = OpenClawWatchExecApprovalItem

struct WatchExecApprovalPromptMessage: Codable, Equatable {
    var approval: WatchExecApprovalItem
    var sentAtMs: Int64?
    var resetResolutionAttemptId: String?
}

struct WatchExecApprovalResolvedMessage: Codable, Equatable {
    var approvalId: String
    var gatewayStableID: String?
    var decision: WatchExecApprovalDecision?
    var outcome: WatchExecApprovalTransportOutcome?
    var resolvedAtMs: Int64?
    var source: String?
    var outcomeText: String?

    static func parseTransportOutcome(_ value: Any?) -> WatchExecApprovalTransportOutcome? {
        guard let rawValue = value as? String else { return nil }
        return WatchExecApprovalTransportOutcome(rawValue: rawValue)
    }
}

struct WatchExecApprovalExpiredMessage: Codable, Equatable {
    var approvalId: String
    var gatewayStableID: String?
    var reason: WatchExecApprovalCloseReason
    var expiredAtMs: Int64?
}

struct WatchExecApprovalSnapshotMessage: Codable, Equatable {
    var approvals: [WatchExecApprovalItem]
    var gatewayStableID: String?
    var sentAtMs: Int64?
    var snapshotId: String?
    var requestId: String?
    var requestGatewayStableID: String?

    init(
        approvals: [WatchExecApprovalItem],
        gatewayStableID: String? = nil,
        sentAtMs: Int64? = nil,
        snapshotId: String? = nil,
        requestId: String? = nil,
        requestGatewayStableID: String? = nil)
    {
        self.approvals = approvals
        self.gatewayStableID = gatewayStableID
        self.sentAtMs = sentAtMs
        self.snapshotId = snapshotId
        self.requestId = requestId
        self.requestGatewayStableID = requestGatewayStableID
    }
}

typealias WatchExecApprovalSnapshotRequestMessage = OpenClawWatchExecApprovalSnapshotRequestMessage
typealias WatchExecApprovalSnapshotRequestItem = OpenClawWatchExecApprovalSnapshotRequestItem
typealias WatchExecApprovalResolveMessage = OpenClawWatchExecApprovalResolveMessage
typealias WatchAppCommand = OpenClawWatchAppCommand

enum WatchStatusLocalizationKey: String {
    case connected
    case reconnecting
    case offline
    case gatewayProblemRequestIDFormat
    case ready
    case connecting
    case listening
    case thinking
    case speaking
    case off
    case missingFormat
    case requestingApproval
    case approvalRequested
    case apiKeyMissing
    case unavailable
    case connectIPhoneChat
    case noChatMessages
    case chatUnavailable
    case noMessagesSynced
    case waitingForIPhone
    case refresh
    case openChat
    case chat
    case startTalk
    case stopTalk
    case sendingFormat
    case failedFormat
    case sentFormat
    case queuedFormat
    case refreshingFromIPhone
    case allowOnce
    case deny
    case retryApproval
    case allowedOnce
    case approvalSetToAlwaysAllow
    case denied
    case approvalResolved
    case approvalExpired
    case approvalNoLongerAvailable
    case approvalResolvedElsewhere
    case approvalReplaced
    case approvalUnavailable

    var localized: String {
        switch self {
        case .connected:
            String(localized: "Connected")
        case .reconnecting:
            String(localized: "Reconnecting…")
        case .offline:
            String(localized: "Offline")
        case .gatewayProblemRequestIDFormat:
            String(localized: "%@ (request ID: %@)")
        case .ready:
            String(localized: "Ready")
        case .connecting:
            String(localized: "Connecting")
        case .listening:
            String(localized: "Listening")
        case .thinking:
            String(localized: "Thinking")
        case .speaking:
            String(localized: "Speaking")
        case .off:
            String(localized: "Off")
        case .missingFormat:
            String(localized: "Missing %@")
        case .requestingApproval:
            String(localized: "Requesting approval")
        case .approvalRequested:
            String(localized: "Approval requested")
        case .apiKeyMissing:
            String(localized: "API key missing")
        case .unavailable:
            String(localized: "Unavailable")
        case .connectIPhoneChat:
            String(localized: "Connect iPhone chat to read messages")
        case .noChatMessages:
            String(localized: "No chat messages yet")
        case .chatUnavailable:
            String(localized: "Chat unavailable")
        case .noMessagesSynced:
            String(localized: "No messages synced")
        case .waitingForIPhone:
            String(localized: "Waiting for iPhone")
        case .refresh:
            String(localized: "Refresh")
        case .openChat:
            String(localized: "Open Chat")
        case .chat:
            String(localized: "Chat")
        case .startTalk:
            String(localized: "Start Talk")
        case .stopTalk:
            String(localized: "Stop Talk")
        case .sendingFormat:
            String(localized: "Sending %@…")
        case .failedFormat:
            String(localized: "%@ failed: %@")
        case .sentFormat:
            String(localized: "%@: sent")
        case .queuedFormat:
            String(localized: "%@: queued")
        case .refreshingFromIPhone:
            String(localized: "Refreshing from iPhone…")
        case .allowOnce:
            String(localized: "Allow Once")
        case .deny:
            String(localized: "Deny")
        case .retryApproval:
            String(localized: "Couldn't reach iPhone. Tap to retry.")
        case .allowedOnce:
            String(localized: "Allowed once")
        case .approvalSetToAlwaysAllow:
            String(localized: "Approval set to Always Allow.")
        case .denied:
            String(localized: "Denied")
        case .approvalResolved:
            String(localized: "Approval resolved")
        case .approvalExpired:
            String(localized: "Approval expired")
        case .approvalNoLongerAvailable:
            String(localized: "Approval no longer available")
        case .approvalResolvedElsewhere:
            String(localized: "Approval resolved elsewhere")
        case .approvalReplaced:
            String(localized: "Approval replaced")
        case .approvalUnavailable:
            String(localized: "Approval unavailable")
        }
    }
}

enum WatchDeliveryStatusCode: String, Codable, Equatable {
    case sending
    case sent
    case queued
    case failed
    case blocked
}

struct WatchAppCommandStatus: Codable, Equatable {
    var command: WatchAppCommand
    var code: WatchDeliveryStatusCode
    var detail: String?
    var legacyVerbatim: String?

    func localizedText(
        localize: (WatchStatusLocalizationKey) -> String = { $0.localized }) -> String
    {
        if let legacyVerbatim {
            return legacyVerbatim
        }
        let label = self.command.localizedLabel(localize: localize)
        return switch self.code {
        case .sending:
            String(format: localize(.sendingFormat), label)
        case .sent:
            String(format: localize(.sentFormat), label)
        case .queued:
            String(format: localize(.queuedFormat), label)
        case .failed:
            String(format: localize(.failedFormat), label, self.detail ?? localize(.unavailable))
        case .blocked:
            self.detail ?? localize(.refreshingFromIPhone)
        }
    }

    static func decodeLegacyLocalizedText(_ text: String) -> Self? {
        guard !text.isEmpty else { return nil }
        return Self(
            command: .refresh,
            code: .failed,
            detail: nil,
            legacyVerbatim: text)
    }
}

struct WatchReplyStatus: Codable, Equatable {
    var code: WatchDeliveryStatusCode
    var actionLabel: String
    var detail: String?
    var legacyVerbatim: String?

    func localizedText(
        localize: (WatchStatusLocalizationKey) -> String = { $0.localized }) -> String
    {
        if let legacyVerbatim {
            return legacyVerbatim
        }
        return switch self.code {
        case .sending:
            String(format: localize(.sendingFormat), self.actionLabel)
        case .sent:
            String(format: localize(.sentFormat), self.actionLabel)
        case .queued:
            String(format: localize(.queuedFormat), self.actionLabel)
        case .failed:
            String(
                format: localize(.failedFormat),
                self.actionLabel,
                self.detail ?? localize(.unavailable))
        case .blocked:
            localize(.refreshingFromIPhone)
        }
    }

    static func decodeLegacyLocalizedText(_ text: String) -> Self? {
        guard !text.isEmpty else { return nil }
        return Self(
            code: .failed,
            actionLabel: "",
            detail: nil,
            legacyVerbatim: text)
    }
}

enum WatchExecApprovalStatusCode: String, Codable, Equatable {
    case sending
    case sent
    case queued
    case retry
    case legacy
}

struct WatchExecApprovalStatus: Codable, Equatable {
    var code: WatchExecApprovalStatusCode
    var decision: WatchExecApprovalDecision?
    var legacyVerbatim: String?

    func localizedText(
        localize: (WatchStatusLocalizationKey) -> String = { $0.localized }) -> String
    {
        if let legacyVerbatim {
            return legacyVerbatim
        }
        guard self.code != .retry else {
            return localize(.retryApproval)
        }
        let label = (self.decision ?? .allowOnce).localizedLabel(localize: localize)
        return switch self.code {
        case .sending:
            String(format: localize(.sendingFormat), label)
        case .sent:
            String(format: localize(.sentFormat), label)
        case .queued:
            String(format: localize(.queuedFormat), label)
        case .retry:
            localize(.retryApproval)
        case .legacy:
            legacyVerbatim ?? localize(.unavailable)
        }
    }

    static func decodeLegacyLocalizedText(_ text: String) -> Self? {
        guard !text.isEmpty else { return nil }
        return Self(code: .legacy, legacyVerbatim: text)
    }
}

enum WatchExecApprovalOutcomeCode: String, Codable, Equatable {
    case allowedOnce
    case allowedAlways
    case denied
    case resolved
    case expired
    case notFound
    case resolvedElsewhere
    case replaced
    case unavailable
    case verbatim
}

struct WatchExecApprovalOutcome: Codable, Equatable {
    var code: WatchExecApprovalOutcomeCode
    var verbatim: String?

    func localizedText(
        localize: (WatchStatusLocalizationKey) -> String = { $0.localized }) -> String
    {
        if let verbatim {
            return verbatim
        }
        return switch self.code {
        case .allowedOnce:
            localize(.allowedOnce)
        case .allowedAlways:
            localize(.approvalSetToAlwaysAllow)
        case .denied:
            localize(.denied)
        case .resolved:
            localize(.approvalResolved)
        case .expired:
            localize(.approvalExpired)
        case .notFound:
            localize(.approvalNoLongerAvailable)
        case .resolvedElsewhere:
            localize(.approvalResolvedElsewhere)
        case .replaced:
            localize(.approvalReplaced)
        case .unavailable:
            localize(.approvalUnavailable)
        case .verbatim:
            localize(.unavailable)
        }
    }

    static func decodeLegacyLocalizedText(_ text: String) -> Self? {
        guard !text.isEmpty else { return nil }
        return switch text {
        case "Allowed once", "Approval allowed once.", "This approval was already allowed once.":
            Self(code: .allowedOnce)
        case "Approval set to Always Allow.", "This approval was already set to Always Allow.":
            Self(code: .allowedAlways)
        case "Denied", "Approval denied.", "This approval was already denied.":
            Self(code: .denied)
        case "Approval resolved":
            Self(code: .resolved)
        case "Approval expired":
            Self(code: .expired)
        case "Approval no longer available":
            Self(code: .notFound)
        case "Approval resolved elsewhere", "This approval was already resolved elsewhere.":
            Self(code: .resolvedElsewhere)
        case "Approval replaced":
            Self(code: .replaced)
        case "Approval unavailable":
            Self(code: .unavailable)
        default:
            Self(code: .verbatim, verbatim: text)
        }
    }

    static func resolved(
        outcome: WatchExecApprovalTransportOutcome?,
        legacyText: String?,
        decision: WatchExecApprovalDecision?,
        source: String?) -> Self
    {
        if let outcome {
            return switch outcome {
            case .allowedOnce:
                Self(code: .allowedOnce)
            case .allowedAlways:
                Self(code: .allowedAlways)
            case .denied:
                Self(code: .denied)
            }
        }
        let normalizedLegacyText = legacyText?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .prefix(160)
        if let normalizedLegacyText, !normalizedLegacyText.isEmpty {
            return Self.decodeLegacyLocalizedText(String(normalizedLegacyText))
                ?? Self(code: .unavailable)
        }
        if let decision {
            return switch decision {
            case .allowOnce:
                Self(code: .allowedOnce)
            case .deny:
                Self(code: .denied)
            }
        }
        return Self(code: source == "another-reviewer" ? .resolvedElsewhere : .resolved)
    }
}

extension WatchAppCommand {
    fileprivate func localizedLabel(
        localize: (WatchStatusLocalizationKey) -> String) -> String
    {
        switch self {
        case .refresh:
            localize(.refresh)
        case .openChat:
            localize(.openChat)
        case .sendChat:
            localize(.chat)
        case .startTalk:
            localize(.startTalk)
        case .stopTalk:
            localize(.stopTalk)
        }
    }
}

extension WatchExecApprovalDecision {
    fileprivate func localizedLabel(
        localize: (WatchStatusLocalizationKey) -> String) -> String
    {
        switch self {
        case .allowOnce:
            localize(.allowOnce)
        case .deny:
            localize(.deny)
        }
    }
}

struct WatchAppSnapshotMessage: Codable, Equatable {
    var gatewayStatus: OpenClawWatchAppStatus
    var gatewayConnected: Bool
    var agentName: String
    var agentAvatarURL: String?
    var agentAvatarText: String?
    var sessionKey: String
    var gatewayStableID: String?
    var talkStatus: OpenClawWatchAppStatus
    var talkEnabled: Bool
    var talkListening: Bool
    var talkSpeaking: Bool
    var pendingApprovalCount: Int
    var chatItems: [WatchChatItem]?
    var chatStatus: OpenClawWatchAppStatus?
    var sentAtMs: Int64?
    var snapshotId: String?

    init(
        gatewayStatus: OpenClawWatchAppStatus,
        gatewayConnected: Bool,
        agentName: String,
        agentAvatarURL: String?,
        agentAvatarText: String?,
        sessionKey: String,
        gatewayStableID: String?,
        talkStatus: OpenClawWatchAppStatus,
        talkEnabled: Bool,
        talkListening: Bool,
        talkSpeaking: Bool,
        pendingApprovalCount: Int,
        chatItems: [WatchChatItem]?,
        chatStatus: OpenClawWatchAppStatus?,
        sentAtMs: Int64?,
        snapshotId: String?)
    {
        self.gatewayStatus = gatewayStatus
        self.gatewayConnected = gatewayConnected
        self.agentName = agentName
        self.agentAvatarURL = agentAvatarURL
        self.agentAvatarText = agentAvatarText
        self.sessionKey = sessionKey
        self.gatewayStableID = gatewayStableID
        self.talkStatus = talkStatus
        self.talkEnabled = talkEnabled
        self.talkListening = talkListening
        self.talkSpeaking = talkSpeaking
        self.pendingApprovalCount = pendingApprovalCount
        self.chatItems = chatItems
        self.chatStatus = chatStatus
        self.sentAtMs = sentAtMs
        self.snapshotId = snapshotId
    }

    static func parsePayload(_ payload: [String: Any]) -> Self? {
        guard let type = payload["type"] as? String,
              type == WatchPayloadType.appSnapshot.rawValue
        else {
            return nil
        }
        let gatewayConnected = Self.boolValue(payload["gatewayConnected"])
        let agentName = (payload["agentName"] as? String)?
            .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        let agentAvatarURL = (payload["agentAvatarUrl"] as? String)?
            .trimmingCharacters(in: .whitespacesAndNewlines)
        let agentAvatarText = (payload["agentAvatarText"] as? String)?
            .trimmingCharacters(in: .whitespacesAndNewlines)
        let sessionKey = (payload["sessionKey"] as? String)?
            .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String)
        let talkEnabled = Self.boolValue(payload["talkEnabled"])
        let talkListening = Self.boolValue(payload["talkListening"])
        let talkSpeaking = Self.boolValue(payload["talkSpeaking"])
        let pendingApprovalCount = (payload["pendingApprovalCount"] as? Int)
            ?? (payload["pendingApprovalCount"] as? NSNumber)?.intValue
            ?? 0
        let sentAtMs = (payload["sentAtMs"] as? NSNumber)?.int64Value
        let snapshotId = (payload["snapshotId"] as? String)?
            .trimmingCharacters(in: .whitespacesAndNewlines)
        let chatItems = (payload["chatItems"] as? [Any])?.compactMap(Self.parseChatItem)
        return Self(
            gatewayStatus: Self.parseStatus(
                payload["gatewayStatus"],
                fallbackText: payload["gatewayStatusText"] as? String)
                ?? Self.decodeLegacyGatewayStatus(
                    text: payload["gatewayStatusText"] as? String,
                    connected: gatewayConnected),
            gatewayConnected: gatewayConnected,
            agentName: agentName.isEmpty ? "Main" : agentName,
            agentAvatarURL: agentAvatarURL?.isEmpty == false ? agentAvatarURL : nil,
            agentAvatarText: agentAvatarText?.isEmpty == false ? agentAvatarText : nil,
            sessionKey: sessionKey.isEmpty ? "main" : sessionKey,
            gatewayStableID: gatewayStableID,
            talkStatus: Self.parseStatus(
                payload["talkStatus"],
                fallbackText: payload["talkStatusText"] as? String)
                ?? Self.decodeLegacyTalkStatus(
                    text: payload["talkStatusText"] as? String,
                    enabled: talkEnabled,
                    listening: talkListening,
                    speaking: talkSpeaking),
            talkEnabled: talkEnabled,
            talkListening: talkListening,
            talkSpeaking: talkSpeaking,
            pendingApprovalCount: max(0, pendingApprovalCount),
            chatItems: chatItems,
            chatStatus: Self.parseStatus(
                payload["chatStatus"],
                fallbackText: payload["chatStatusText"] as? String)
                ?? Self.decodeLegacyChatStatus(
                    code: payload["chatStatusCode"] as? String,
                    text: payload["chatStatusText"] as? String),
            sentAtMs: sentAtMs,
            snapshotId: snapshotId)
    }

    static func localizedChatStatusText(
        status: OpenClawWatchAppStatus?,
        chatCount: Int,
        hasAppSnapshot: Bool,
        localize: (WatchStatusLocalizationKey) -> String = { $0.localized }) -> String
    {
        if let status {
            return status.localizedText(localize: localize)
        }
        if chatCount > 0 {
            return String(
                AttributedString(localized: "^[\(chatCount) recent message](inflect: true)").characters)
        }
        return hasAppSnapshot
            ? localize(.noMessagesSynced)
            : localize(.waitingForIPhone)
    }

    private enum CodingKeys: String, CodingKey {
        case gatewayStatus
        case gatewayStatusText
        case gatewayConnected
        case agentName
        case agentAvatarURL
        case agentAvatarText
        case sessionKey
        case gatewayStableID
        case talkStatus
        case talkStatusText
        case talkEnabled
        case talkListening
        case talkSpeaking
        case pendingApprovalCount
        case chatItems
        case chatStatus
        case chatStatusCode
        case chatStatusText
        case sentAtMs
        case snapshotId
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.gatewayConnected = try container.decode(Bool.self, forKey: .gatewayConnected)
        self.agentName = try container.decode(String.self, forKey: .agentName)
        self.agentAvatarURL = try container.decodeIfPresent(String.self, forKey: .agentAvatarURL)
        self.agentAvatarText = try container.decodeIfPresent(String.self, forKey: .agentAvatarText)
        self.sessionKey = try container.decode(String.self, forKey: .sessionKey)
        self.gatewayStableID = try container.decodeIfPresent(String.self, forKey: .gatewayStableID)
        self.talkEnabled = try container.decode(Bool.self, forKey: .talkEnabled)
        self.talkListening = try container.decode(Bool.self, forKey: .talkListening)
        self.talkSpeaking = try container.decode(Bool.self, forKey: .talkSpeaking)
        self.pendingApprovalCount = try container.decode(Int.self, forKey: .pendingApprovalCount)
        self.chatItems = try container.decodeIfPresent([WatchChatItem].self, forKey: .chatItems)
        self.sentAtMs = try container.decodeIfPresent(Int64.self, forKey: .sentAtMs)
        self.snapshotId = try container.decodeIfPresent(String.self, forKey: .snapshotId)
        let gatewayStatusText = try container.decodeIfPresent(String.self, forKey: .gatewayStatusText)
        let talkStatusText = try container.decodeIfPresent(String.self, forKey: .talkStatusText)
        let chatStatusCode = try container.decodeIfPresent(String.self, forKey: .chatStatusCode)
        let chatStatusText = try container.decodeIfPresent(String.self, forKey: .chatStatusText)
        if let gatewayStatus = try? container.decode(
            OpenClawWatchAppStatus.self,
            forKey: .gatewayStatus)
        {
            self.gatewayStatus = gatewayStatus
        } else if container.contains(.gatewayStatus),
                  let gatewayStatusText,
                  !gatewayStatusText.isEmpty
        {
            self.gatewayStatus = OpenClawWatchAppStatus(
                code: .legacy,
                verbatim: gatewayStatusText)
        } else {
            self.gatewayStatus = Self.decodeLegacyGatewayStatus(
                text: gatewayStatusText,
                connected: self.gatewayConnected)
        }
        if let talkStatus = try? container.decode(
            OpenClawWatchAppStatus.self,
            forKey: .talkStatus)
        {
            self.talkStatus = talkStatus
        } else if container.contains(.talkStatus),
                  let talkStatusText,
                  !talkStatusText.isEmpty
        {
            self.talkStatus = OpenClawWatchAppStatus(
                code: .legacy,
                verbatim: talkStatusText)
        } else {
            self.talkStatus = Self.decodeLegacyTalkStatus(
                text: talkStatusText,
                enabled: self.talkEnabled,
                listening: self.talkListening,
                speaking: self.talkSpeaking)
        }
        self.chatStatus = (try? container.decode(
            OpenClawWatchAppStatus.self,
            forKey: .chatStatus)) ?? Self.decodeLegacyChatStatus(
            code: chatStatusCode,
            text: chatStatusText)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.gatewayStatus, forKey: .gatewayStatus)
        try container.encode(self.gatewayConnected, forKey: .gatewayConnected)
        try container.encode(self.agentName, forKey: .agentName)
        try container.encodeIfPresent(self.agentAvatarURL, forKey: .agentAvatarURL)
        try container.encodeIfPresent(self.agentAvatarText, forKey: .agentAvatarText)
        try container.encode(self.sessionKey, forKey: .sessionKey)
        try container.encodeIfPresent(self.gatewayStableID, forKey: .gatewayStableID)
        try container.encode(self.talkStatus, forKey: .talkStatus)
        try container.encode(self.talkEnabled, forKey: .talkEnabled)
        try container.encode(self.talkListening, forKey: .talkListening)
        try container.encode(self.talkSpeaking, forKey: .talkSpeaking)
        try container.encode(self.pendingApprovalCount, forKey: .pendingApprovalCount)
        try container.encodeIfPresent(self.chatItems, forKey: .chatItems)
        try container.encodeIfPresent(self.chatStatus, forKey: .chatStatus)
        try container.encodeIfPresent(self.sentAtMs, forKey: .sentAtMs)
        try container.encodeIfPresent(self.snapshotId, forKey: .snapshotId)
    }

    private static func parseStatus(
        _ value: Any?,
        fallbackText: String? = nil) -> OpenClawWatchAppStatus?
    {
        guard let payload = value as? [String: Any],
              let rawCode = payload["code"] as? String
        else {
            return nil
        }
        let verbatim = payload["verbatim"] as? String
        guard let code = OpenClawWatchAppStatusCode(rawValue: rawCode) else {
            let legacyText = verbatim?.isEmpty == false ? verbatim : fallbackText
            guard let legacyText, !legacyText.isEmpty else { return nil }
            return OpenClawWatchAppStatus(code: .legacy, verbatim: legacyText)
        }
        return OpenClawWatchAppStatus(
            code: code,
            localizationKey: payload["localizationKey"] as? String,
            arguments: payload["arguments"] as? [String] ?? [],
            verbatim: verbatim)
    }

    private static func decodeLegacyGatewayStatus(
        text: String?,
        connected: Bool) -> OpenClawWatchAppStatus
    {
        if connected {
            return OpenClawWatchAppStatus(code: .gatewayConnected)
        }
        guard let text, !text.isEmpty else {
            return OpenClawWatchAppStatus(code: .gatewayOffline)
        }
        return OpenClawWatchAppStatus(code: .legacy, verbatim: text)
    }

    private static func decodeLegacyTalkStatus(
        text: String?,
        enabled: Bool,
        listening: Bool,
        speaking: Bool) -> OpenClawWatchAppStatus
    {
        if speaking {
            return OpenClawWatchAppStatus(code: .talkSpeaking)
        }
        if listening {
            return OpenClawWatchAppStatus(code: .talkListening)
        }
        if !enabled {
            return OpenClawWatchAppStatus(code: .talkOff)
        }
        guard let text, !text.isEmpty else {
            return OpenClawWatchAppStatus(code: .talkReady)
        }
        return OpenClawWatchAppStatus(code: .legacy, verbatim: text)
    }

    private static func decodeLegacyChatStatus(
        code: String?,
        text: String?) -> OpenClawWatchAppStatus?
    {
        let statusCode: OpenClawWatchAppStatusCode? = switch code {
        case "connectIPhone":
            OpenClawWatchAppStatusCode.chatConnectIPhone
        case "noMessages":
            OpenClawWatchAppStatusCode.chatNoMessages
        case "unavailable":
            OpenClawWatchAppStatusCode.chatUnavailable
        default:
            nil
        }
        if let statusCode {
            return OpenClawWatchAppStatus(code: statusCode)
        }
        guard let text, !text.isEmpty else { return nil }
        return OpenClawWatchAppStatus(code: .legacy, verbatim: text)
    }

    private static func parseChatItem(_ item: Any) -> WatchChatItem? {
        guard let dict = item as? [String: Any] else { return nil }
        guard let id = (dict["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
              !id.isEmpty
        else {
            return nil
        }
        let trimmedRole = (dict["role"] as? String)?
            .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        let text = (dict["text"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let text, !text.isEmpty else { return nil }
        let timestampMs = (dict["timestampMs"] as? NSNumber)?.int64Value
        return WatchChatItem(
            id: id,
            role: trimmedRole.isEmpty ? "assistant" : trimmedRole,
            text: text,
            timestampMs: timestampMs)
    }

    private static func boolValue(_ value: Any?) -> Bool {
        if let bool = value as? Bool {
            return bool
        }
        if let number = value as? NSNumber {
            return number.boolValue
        }
        return false
    }
}

typealias WatchChatCompletionMessage = OpenClawWatchChatCompletionMessage
typealias WatchChatItem = OpenClawWatchChatItem
typealias WatchAppSnapshotRequestMessage = OpenClawWatchAppSnapshotRequestMessage
typealias WatchAppCommandMessage = OpenClawWatchAppCommandMessage
typealias WatchPromptAction = OpenClawWatchAction

struct WatchNotifyMessage: Codable {
    var id: String?
    var title: String
    var body: String
    var sentAtMs: Int64?
    var promptId: String?
    var sessionKey: String?
    var gatewayStableID: String?
    var kind: String?
    var details: String?
    var expiresAtMs: Int64?
    var risk: String?
    var actions: [WatchPromptAction]
}

struct WatchExecApprovalRecord: Codable, Equatable, Identifiable {
    var approval: WatchExecApprovalItem
    var transport: String
    var sourceSentAtMs: Int64?
    var updatedAt: Date
    var isResolving: Bool
    var pendingDecision: WatchExecApprovalDecision?
    var activeResolutionAttemptID: String?
    var status: WatchExecApprovalStatus?
    var statusAt: Date?

    var id: WatchExecApprovalIdentityKey {
        WatchExecApprovalIdentityKey(
            gatewayID: WatchOpaqueUTF8Key(self.approval.gatewayStableID ?? ""),
            approvalID: WatchOpaqueUTF8Key(self.approval.id))
    }

    var approvalID: String {
        self.approval.id
    }

    private enum CodingKeys: String, CodingKey {
        case approval
        case transport
        case sourceSentAtMs
        case updatedAt
        case isResolving
        case pendingDecision
        case activeResolutionAttemptID
        case status
        case statusText
        case statusAt
    }

    init(
        approval: WatchExecApprovalItem,
        transport: String,
        sourceSentAtMs: Int64?,
        updatedAt: Date,
        isResolving: Bool,
        pendingDecision: WatchExecApprovalDecision?,
        activeResolutionAttemptID: String?,
        status: WatchExecApprovalStatus?,
        statusAt: Date?)
    {
        self.approval = approval
        self.transport = transport
        self.sourceSentAtMs = sourceSentAtMs
        self.updatedAt = updatedAt
        self.isResolving = isResolving
        self.pendingDecision = pendingDecision
        self.activeResolutionAttemptID = activeResolutionAttemptID
        self.status = status
        self.statusAt = statusAt
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.approval = try container.decode(WatchExecApprovalItem.self, forKey: .approval)
        self.transport = try container.decode(String.self, forKey: .transport)
        self.sourceSentAtMs = try container.decodeIfPresent(Int64.self, forKey: .sourceSentAtMs)
        self.updatedAt = try container.decode(Date.self, forKey: .updatedAt)
        self.isResolving = try container.decode(Bool.self, forKey: .isResolving)
        self.pendingDecision = try container.decodeIfPresent(
            WatchExecApprovalDecision.self,
            forKey: .pendingDecision)
        self.activeResolutionAttemptID = try container.decodeIfPresent(
            String.self,
            forKey: .activeResolutionAttemptID)
        self.status = try container.decodeIfPresent(
            WatchExecApprovalStatus.self,
            forKey: .status) ?? Self.decodeLegacyStatus(
            container.decodeIfPresent(String.self, forKey: .statusText))
        self.statusAt = try container.decodeIfPresent(Date.self, forKey: .statusAt)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.approval, forKey: .approval)
        try container.encode(self.transport, forKey: .transport)
        try container.encodeIfPresent(self.sourceSentAtMs, forKey: .sourceSentAtMs)
        try container.encode(self.updatedAt, forKey: .updatedAt)
        try container.encode(self.isResolving, forKey: .isResolving)
        try container.encodeIfPresent(self.pendingDecision, forKey: .pendingDecision)
        try container.encodeIfPresent(
            self.activeResolutionAttemptID,
            forKey: .activeResolutionAttemptID)
        try container.encodeIfPresent(self.status, forKey: .status)
        try container.encodeIfPresent(self.statusAt, forKey: .statusAt)
    }

    private static func decodeLegacyStatus(_ text: String?) -> WatchExecApprovalStatus? {
        text.flatMap(WatchExecApprovalStatus.decodeLegacyLocalizedText)
    }
}

extension OpenClawWatchAppStatus {
    func localizedText(
        localize: (WatchStatusLocalizationKey) -> String = { $0.localized },
        localizePresentation: (String, [String]) -> String = { key, arguments in
            String(
                format: String(localized: String.LocalizationValue(key)),
                locale: .current,
                arguments: arguments.map { $0 as CVarArg })
        }) -> String
    {
        switch self.code {
        case .gatewayConnected,
             .gatewayConnecting,
             .gatewayReconnecting,
             .gatewayOffline,
             .gatewayProblem,
             .gatewayProblemWithRequestID:
            self.localizedGatewayText(
                localize: localize,
                localizePresentation: localizePresentation)
        case .talkOff,
             .talkReady,
             .talkConnecting,
             .talkListening,
             .talkThinking,
             .talkSpeaking,
             .talkOffline,
             .talkPermissionRequired,
             .talkRequestingApproval,
             .talkApprovalRequested,
             .talkAPIKeyMissing,
             .talkFailure:
            self.localizedTalkText(
                localize: localize,
                localizePresentation: localizePresentation)
        case .chatConnectIPhone, .chatNoMessages, .chatUnavailable:
            self.localizedChatText(localize: localize)
        case .legacy:
            self.verbatim ?? localize(.unavailable)
        }
    }

    private func localizedGatewayText(
        localize: (WatchStatusLocalizationKey) -> String,
        localizePresentation: (String, [String]) -> String) -> String
    {
        switch self.code {
        case .gatewayConnected:
            localize(.connected)
        case .gatewayConnecting:
            localize(.connecting)
        case .gatewayReconnecting:
            localize(.reconnecting)
        case .gatewayOffline:
            localize(.offline)
        case .gatewayProblem:
            self.localizedPresentation(localize: localizePresentation)
        case .gatewayProblemWithRequestID:
            self.localizedGatewayProblemWithRequestID(
                localize: localize,
                localizePresentation: localizePresentation)
        default:
            localize(.unavailable)
        }
    }

    private func localizedTalkText(
        localize: (WatchStatusLocalizationKey) -> String,
        localizePresentation: (String, [String]) -> String) -> String
    {
        switch self.code {
        case .talkOff:
            localize(.off)
        case .talkReady:
            localize(.ready)
        case .talkConnecting:
            localize(.connecting)
        case .talkListening:
            localize(.listening)
        case .talkThinking:
            localize(.thinking)
        case .talkSpeaking:
            localize(.speaking)
        case .talkOffline:
            localize(.offline)
        case .talkPermissionRequired:
            self.arguments.first.map { String(format: localize(.missingFormat), $0) }
                ?? localize(.unavailable)
        case .talkRequestingApproval:
            localize(.requestingApproval)
        case .talkApprovalRequested:
            localize(.approvalRequested)
        case .talkAPIKeyMissing:
            localize(.apiKeyMissing)
        case .talkFailure:
            self.localizedPresentation(localize: localizePresentation)
        default:
            localize(.unavailable)
        }
    }

    private func localizedChatText(
        localize: (WatchStatusLocalizationKey) -> String) -> String
    {
        switch self.code {
        case .chatConnectIPhone:
            localize(.connectIPhoneChat)
        case .chatNoMessages:
            localize(.noChatMessages)
        case .chatUnavailable:
            localize(.chatUnavailable)
        default:
            localize(.unavailable)
        }
    }

    private func localizedPresentation(
        localize: (String, [String]) -> String) -> String
    {
        if let verbatim {
            return verbatim
        }
        guard let localizationKey else {
            return WatchStatusLocalizationKey.unavailable.localized
        }
        return localize(localizationKey, self.arguments)
    }

    private func localizedGatewayProblemWithRequestID(
        localize: (WatchStatusLocalizationKey) -> String,
        localizePresentation: (String, [String]) -> String) -> String
    {
        guard let requestID = self.arguments.last else {
            return self.localizedPresentation(localize: localizePresentation)
        }
        let title = if let verbatim {
            verbatim
        } else if let localizationKey {
            localizePresentation(localizationKey, Array(self.arguments.dropLast()))
        } else {
            localize(.unavailable)
        }
        return String(
            format: localize(.gatewayProblemRequestIDFormat),
            title,
            requestID)
    }
}
