import Foundation

/// The JSON view of the wire message set (docs/WINDOWS_PORT.md §6).
///
/// The wire is CBOR. The Windows renderer talks to `NucleicProtocolC.dll` over a flat C ABI where
/// every payload is UTF-8 JSON, because C# marshals JSON cheaply and CBOR not at all. This type is
/// that translation — and it is deliberately **not** a second encoder.
///
/// `ClientMsg` / `HostMsg` are `Codable` (tagged `{"t": …}` — see `MessageEnvelope.swift`), and the
/// CBOR wire codec is itself a `Codable` implementation. So the JSON view rides the *same*
/// conformance the wire does: there is no hand-written per-case mapping, and therefore no per-case
/// drift to guard against. What can still drift is the handful of coder-level decisions below, and
/// the tag strings a C# discriminator keys on — which is what ``tag(of:)-4t5ms`` and
/// `fixtures/protocol-abi/` pin.
///
/// **Pinned decisions**, all of them load-bearing for the C# DTOs (`windows/NucleicProtocol.Interop`):
///
/// - *Keys*: `.useDefaultKeys`. The JSON field names are the Swift property names, verbatim; a C#
///   DTO must match them exactly (`JsonPropertyName`, not a naming policy).
/// - *Dates*: `.deferredToDate` — a `Double` of seconds since the 2001 reference date. This is
///   what the CBOR codec already emits (`CBOREncoder`'s doc comment), so a date reads the same on
///   both sides of the bridge. **Not** ISO8601 and **not** Unix epoch: matching the wire matters
///   more than matching C# convention, because the fixtures pair the two encodings.
/// - *Data*: `.base64`. Here the JSON view intentionally *diverges* from CBOR, which (having no
///   `Data` special case) emits an array of byte-valued integers. A 4 KB blob as 4,096 JSON
///   numbers would be both enormous and hostile to `byte[]`; base64 is neither. Each codec is
///   self-consistent, which is all round-tripping requires.
/// - *Non-conforming floats*: encoded as the strings `Infinity` / `-Infinity` / `NaN` rather than
///   throwing. CBOR can carry them and JSON cannot, so without this a single stray value would
///   fail the whole message instead of one field.
/// - *Output*: `.sortedKeys` + `.withoutEscapingSlashes`. Sorted keys make the encoding
///   deterministic, which is what lets `fixtures/protocol-abi/` be compared byte-for-byte;
///   unescaped slashes keep file paths readable in a fixture diff.
public enum ProtocolJSON {
    /// The pinned encoder. A fresh instance per call: `JSONEncoder` is a class, and handing the
    /// same one to concurrent DLL threads is not worth the microseconds it would save.
    public static func makeEncoder() -> JSONEncoder {
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .useDefaultKeys
        encoder.dateEncodingStrategy = .deferredToDate
        encoder.dataEncodingStrategy = .base64
        encoder.nonConformingFloatEncodingStrategy = .convertToString(
            positiveInfinity: "Infinity", negativeInfinity: "-Infinity", nan: "NaN")
        encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
        return encoder
    }

    /// The pinned decoder — the exact inverse of ``makeEncoder()``.
    public static func makeDecoder() -> JSONDecoder {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .useDefaultKeys
        decoder.dateDecodingStrategy = .deferredToDate
        decoder.dataDecodingStrategy = .base64
        decoder.nonConformingFloatDecodingStrategy = .convertFromString(
            positiveInfinity: "Infinity", negativeInfinity: "-Infinity", nan: "NaN")
        return decoder
    }

    public static func encode<T: Encodable>(_ value: T) throws -> Data {
        try makeEncoder().encode(value)
    }

    public static func decode<T: Decodable>(_ type: T.Type, from json: Data) throws -> T {
        try makeDecoder().decode(type, from: json)
    }

    /// Encode to a UTF-8 `String`, the form the C ABI hands to C#.
    public static func encodeToString<T: Encodable>(_ value: T) throws -> String {
        String(decoding: try encode(value), as: UTF8.self)
    }

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

// MARK: - Message tags

/// The `"t"` discriminator of every wire message, as a total function over the enum.
///
/// This exists to be **exhaustive**: adding a case to `ClientMsg` or `HostMsg` without naming it
/// here is a compile error, and `ProtocolABITests` then requires that name to appear in the golden
/// corpus with a fixture. That chain — compiler, then test — is what keeps a new message kind from
/// reaching the renderer with no pinned JSON shape.
///
/// The switches below are the single source of truth *for the name*; the test cross-checks each
/// one against the `"t"` field the encoder actually emitted, so a typo here cannot silently
/// disagree with the wire.
extension ProtocolJSON {
    public static func tag(of msg: ClientMsg) -> String {
        switch msg {
        case .hello: "hello"
        case .listSessions: "listSessions"
        case .listDashboard: "listDashboard"
        case .subscribe: "subscribe"
        case .unsubscribe: "unsubscribe"
        case .approvalRespond: "approvalRespond"
        case .sendInput: "sendInput"
        case .interrupt: "interrupt"
        case .ping: "ping"
        case .startChat: "startChat"
        case .captureTodo: "captureTodo"
        case .dispatchTodo: "dispatchTodo"
        case .setTodoStatus: "setTodoStatus"
        case .deleteTodo: "deleteTodo"
        case .renameSession: "renameSession"
        case .setFavorite: "setFavorite"
        case .setArchived: "setArchived"
        case .markSessionDone: "markSessionDone"
        case .deleteSession: "deleteSession"
        case .integrate: "integrate"
        case .discard: "discard"
        case .setSessionModel: "setSessionModel"
        case .setSessionEffort: "setSessionEffort"
        case .setSessionAuto: "setSessionAuto"
        case .setSessionAutoShip: "setSessionAutoShip"
        case .setSessionShipBranch: "setSessionShipBranch"
        case .cancelQueuedMessage: "cancelQueuedMessage"
        case .resolveProcessStall: "resolveProcessStall"
        case .fetchDiff: "fetchDiff"
        case .listPeers: "listPeers"
        case .addressUpdate: "addressUpdate"
        case .meshRoster: "meshRoster"
        case .transferOffer: "transferOffer"
        case .transferChunk: "transferChunk"
        case .transferCommit: "transferCommit"
        case .transferCancel: "transferCancel"
        case .requestSessionTransfer: "requestSessionTransfer"
        case .fetchTranscript: "fetchTranscript"
        case .fetchCarbonHeads: "fetchCarbonHeads"
        case .fetchCarbonManifests: "fetchCarbonManifests"
        case .fetchCarbonShards: "fetchCarbonShards"
        case .carbonShardAck: "carbonShardAck"
        case .carbonLockAcquire: "carbonLockAcquire"
        case .carbonLockRelease: "carbonLockRelease"
        case .registerLiveActivity: "registerLiveActivity"
        case .endLiveActivity: "endLiveActivity"
        case .registerPushToStartToken: "registerPushToStartToken"
        case .setForeground: "setForeground"
        case .requestPairingCode: "requestPairingCode"
        case .cancelPairingCode: "cancelPairingCode"
        case .respondMacPair: "respondMacPair"
        case .intelligenceResult: "intelligenceResult"
        case .credentialManifest: "credentialManifest"
        case .credentialProvision: "credentialProvision"
        case .credentialRevoke: "credentialRevoke"
        case .createProject: "createProject"
        case .castSubscribe: "castSubscribe"
        case .composerTyping: "composerTyping"
        case .updateSettings: "updateSettings"
        case .directOffer: "directOffer"
        case .directSelect: "directSelect"
        case .agentLoginBegin: "agentLoginBegin"
        case .agentLoginCallback: "agentLoginCallback"
        case .agentLoginCancel: "agentLoginCancel"
        }
    }

    public static func tag(of msg: HostMsg) -> String {
        switch msg {
        case .welcome: "welcome"
        case .sessionList: "sessionList"
        case .snapshot: "snapshot"
        case .events: "events"
        case .approvalRequested: "approvalRequested"
        case .approvalResolved: "approvalResolved"
        case .sessionUpdated: "sessionUpdated"
        case .dashboard: "dashboard"
        case .sessionDiff: "sessionDiff"
        case .peerList: "peerList"
        case .meshRoster: "meshRoster"
        case .transferAccept: "transferAccept"
        case .transferReject: "transferReject"
        case .transferReady: "transferReady"
        case .transferCommitted: "transferCommitted"
        case .transferChunkAck: "transferChunkAck"
        case .transcriptChunk: "transcriptChunk"
        // The enum case has the longer Swift name, but the frozen wire discriminator is the
        // original `transcriptComplete`. Keep this helper aligned with MessageEnvelope's codec:
        // the protocol-ABI corpus keys fixtures by the actual discriminator C# receives.
        case .transcriptFetchComplete: "transcriptComplete"
        case .transcriptUnavailable: "transcriptUnavailable"
        case .carbonHeads: "carbonHeads"
        case .carbonHeadAdvanced: "carbonHeadAdvanced"
        case .carbonManifestData: "carbonManifestData"
        case .carbonManifestUnavailable: "carbonManifestUnavailable"
        case .carbonShardData: "carbonShardData"
        case .carbonShardUnavailable: "carbonShardUnavailable"
        case .carbonLockDecision: "carbonLockDecision"
        case .carbonLockLedgerAdvanced: "carbonLockLedgerAdvanced"
        case .toolSummaries: "toolSummaries"
        case .transcriptReverted: "transcriptReverted"
        case .relayMembership: "relayMembership"
        case .pairingCode: "pairingCode"
        case .macPairRequested: "macPairRequested"
        case .macPairResolved: "macPairResolved"
        case .intelligenceRequest: "intelligenceRequest"
        case .credentialNeeded: "credentialNeeded"
        case .credentialUpdate: "credentialUpdate"
        case .credentialRevoked: "credentialRevoked"
        case .projectCreated: "projectCreated"
        case .runnerPoolCredential: "runnerPoolCredential"
        case .chatStarted: "chatStarted"
        case .casts: "casts"
        case .castCatchUp: "castCatchUp"
        case .settings: "settings"
        case .directAnswer: "directAnswer"
        case .directGo: "directGo"
        case .directDecline: "directDecline"
        case .agentLoginChallenge: "agentLoginChallenge"
        case .agentLoginResult: "agentLoginResult"
        case .agentAuthStatus: "agentAuthStatus"
        case .error: "error"
        case .pong: "pong"
        case .unknown: "unknown"
        }
    }
}
