import Foundation

/// Carbon — the data storage/integrity layer's shard replication vocabulary
/// (docs/CARBON_SHARDING.md §8.1). Value types (stream keys, heads) plus the fetch/reply
/// payloads behind the envelope verbs. Every client verb is sent only to peers advertising
/// `WireCapabilities.canServeCarbon` (an older peer throws on the unknown `ClientMsg` tag);
/// the host pushes ride the `.unknown` fallback but still need `SyncClient.Event` cases or a
/// requester silently drops them.
///
/// Like the transcript-sync fetch protocol, Carbon has its own format version, independent of
/// `SyncProtocol.version` (which stays 1).
public enum CarbonFormat {
    public static let version = 1
    /// §8.3 flow control: a server keeps at most this many un-acked `carbonShardData` frames
    /// in flight per request, then waits for `carbonShardAck` (the transfer window, NOT the
    /// transcript fire-and-stream).
    public static let shardAckWindow = 4
}

/// The stream kinds a project's Carbon data is decomposed into (spec §3). Raw values are the
/// frozen wire/manifest strings — never rename.
public enum CarbonStreamKind: String, Sendable, Codable, CaseIterable {
    /// A live session worktree's uncommitted state: two trees (working files + index),
    /// shipped as thin incremental packs (spec §3.1).
    case worktreeSnapshot
    /// Git objects the origin doesn't have: per-session branch deltas, plus the per-project
    /// base chain (`sessionID == nil`) — the whole graph for `localOnly` projects, and the
    /// local-ahead-of-origin base/trunk deltas for cloned ones (spec §3.2).
    case history
    /// The agent CLI's own append-only resume file, shipped as byte-range segments (spec §3.3).
    case nativeTranscript
    /// The per-domain lock ledger (spec §3.4, §17) — carries lock events, not project content.
    /// Defined from day one so the manifest schema is frozen; produced only from phase 7.
    case lock
}

/// What a manifest's shard payload concatenates to (spec §6.3).
public enum CarbonPayloadKind: String, Sendable, Codable {
    case gitPack
    case jsonlSegment
    case lockEvent
}

/// Identifies one manifest chain: a project's stream, optionally scoped to a session
/// (spec §2, §8.1). `sessionID == nil` is the per-project chain of that stream (the base
/// `history` chain; a lock ledger keys its domain ref via the manifest's `branch` field).
public struct CarbonStreamKey: Sendable, Codable, Hashable {
    public let projectUUID: UUID
    public let stream: CarbonStreamKind
    public let sessionID: String?

    public init(projectUUID: UUID, stream: CarbonStreamKind, sessionID: String? = nil) {
        self.projectUUID = projectUUID
        self.stream = stream
        self.sessionID = sessionID
    }
}

/// The newest manifest of a stream a member holds — gossiped as *state*, not an event, so a
/// missed push is irrelevant (spec §2, §8.2). Advancing a head is fenced by the §10 rule
/// (`CarbonShardStore.advanceHead`): `(generation, seq)` may only increase.
public struct CarbonHead: Sendable, Codable, Hashable {
    public let key: CarbonStreamKey
    public let manifestID: String
    public let ownerGeneration: UInt64
    public let snapshotSeq: UInt64
    /// The holder stores this chain's blobs but lacks the project key (spec §4.3) — it can
    /// mirror, not restore; the owner surfaces it. Decode-defaults to `false`.
    public let keyMissing: Bool

    public init(
        key: CarbonStreamKey, manifestID: String, ownerGeneration: UInt64,
        snapshotSeq: UInt64, keyMissing: Bool = false
    ) {
        self.key = key
        self.manifestID = manifestID
        self.ownerGeneration = ownerGeneration
        self.snapshotSeq = snapshotSeq
        self.keyMissing = keyMissing
    }

    private enum CodingKeys: String, CodingKey {
        case key, manifestID, ownerGeneration, snapshotSeq, keyMissing
    }

    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        key = try c.decode(CarbonStreamKey.self, forKey: .key)
        manifestID = try c.decode(String.self, forKey: .manifestID)
        ownerGeneration = try c.decode(UInt64.self, forKey: .ownerGeneration)
        snapshotSeq = try c.decode(UInt64.self, forKey: .snapshotSeq)
        keyMissing = try c.decodeIfPresent(Bool.self, forKey: .keyMissing) ?? false
    }
}

// MARK: - Fetch/reply payloads (§8.1)

/// Requester→holder: "send me your Carbon heads" (`nil` project = all enrolled). Heads are
/// *state*, not events — the reply is one idempotent `carbonHeads` snapshot, and a missed
/// `carbonHeadAdvanced` push is irrelevant once these are exchanged (§8.2 step 4).
public struct CarbonHeadsFetch: Sendable, Codable, Equatable {
    public let requestID: String
    public let projectUUID: UUID?
    public let formatVersion: Int

    public init(requestID: String, projectUUID: UUID?, formatVersion: Int = CarbonFormat.version) {
        self.requestID = requestID
        self.projectUUID = projectUUID
        self.formatVersion = formatVersion
    }
}

/// Holder→requester: the full head state answering one `fetchCarbonHeads`.
public struct CarbonHeadsReply: Sendable, Codable, Equatable {
    public let requestID: String
    public let heads: [CarbonHead]

    public init(requestID: String, heads: [CarbonHead]) {
        self.requestID = requestID
        self.heads = heads
    }
}

/// Requester→holder: fetch manifest or shard ciphertexts by content address (the verb carries
/// which). Each id is answered individually — `carbon{Manifest,Shard}Data` or
/// `carbon{Manifest,Shard}Unavailable` — so the requester knows the request is settled when
/// every id has an answer, and a partial holder still serves what it has.
public struct CarbonBlobsFetch: Sendable, Codable, Equatable {
    /// §8.1 batching caps: a requester splits larger wants across requests.
    public static let maxManifestIDs = 64
    public static let maxShardIDs = 256

    public let requestID: String
    public let projectUUID: UUID
    public let ids: [String]
    public let formatVersion: Int

    public init(
        requestID: String, projectUUID: UUID, ids: [String],
        formatVersion: Int = CarbonFormat.version
    ) {
        self.requestID = requestID
        self.projectUUID = projectUUID
        self.ids = ids
        self.formatVersion = formatVersion
    }
}

/// Holder→requester: one verified ciphertext blob (a manifest or a shard — the tag says
/// which). A shard blob is ≤ 2 MiB + AEAD overhead, one frame, far under the 16 MiB cap.
public struct CarbonBlobData: Sendable, Codable, Equatable {
    public let requestID: String
    public let projectUUID: UUID
    public let id: String
    public let blob: Data

    public init(requestID: String, projectUUID: UUID, id: String, blob: Data) {
        self.requestID = requestID
        self.projectUUID = projectUUID
        self.id = id
        self.blob = blob
    }
}

/// Holder→requester: one id this holder can't serve. `notHeld` is the ordinary probing miss
/// (fall through to another holder); a quarantined-on-read blob also answers `notHeld` (§7.2).
public struct CarbonBlobUnavailable: Sendable, Codable, Equatable {
    public enum Reason: String, Sendable, Codable {
        case notHeld
        case internalError
        case unsupportedFormat
    }

    public let requestID: String
    /// nil ⇒ the whole request was refused (e.g. `unsupportedFormat`), not one id.
    public let id: String?
    public let reason: Reason

    public init(requestID: String, id: String?, reason: Reason) {
        self.requestID = requestID
        self.id = id
        self.reason = reason
    }
}

/// Requester→holder: window ack for received `carbonShardData` frames (§8.3). The client acks
/// every frame (batched ids allowed); the server sends more only as acks arrive.
public struct CarbonShardAck: Sendable, Codable, Equatable {
    public let requestID: String
    public let ids: [String]

    public init(requestID: String, ids: [String]) {
        self.requestID = requestID
        self.ids = ids
    }
}

// MARK: - Lock verbs (§17.5)

/// Remote session → lock authority: acquire (`paths` non-nil) or release (`paths == nil` = all,
/// else partial) in one lock domain. Routed to the host arbitrating the domain (§17.3); the
/// agent-side contract is unchanged — its suspension just spans the wire, answered by
/// `carbonLockDecision` frames correlated on `requestID`.
public struct CarbonLockRequest: Sendable, Codable, Equatable {
    public let requestID: String
    public let projectUUID: UUID
    /// The LOCKING §2 `rootRef` (worktree projects) or the trunk branch (nvrsion).
    public let domainRef: String
    public let sessionID: String
    public let paths: [String]?
    /// Release only — the §17.4 fence heads the *landing host* produced (its Carbon producer
    /// sharded the landed content before sending this). The authority publishes
    /// `released(paths, requiredHeads)` with them. nil/absent = unfenced (lifecycle/manual);
    /// decode-defaults nil for pre-lock peers' frames.
    public let requiredHeads: [CarbonHead]?
    public let formatVersion: Int

    public init(
        requestID: String, projectUUID: UUID, domainRef: String, sessionID: String,
        paths: [String]?, requiredHeads: [CarbonHead]? = nil,
        formatVersion: Int = CarbonFormat.version
    ) {
        self.requestID = requestID
        self.projectUUID = projectUUID
        self.domainRef = domainRef
        self.sessionID = sessionID
        self.paths = paths
        self.requiredHeads = requiredHeads
        self.formatVersion = formatVersion
    }
}

/// Lock authority → remote session: the arbitration outcome for one `carbonLockAcquire`.
/// `queued` is an interim notice (the waiter parked); a final `granted`/`cancelled` follows
/// on the same `requestID`.
public struct CarbonLockDecisionMsg: Sendable, Codable, Equatable {
    public enum Outcome: String, Sendable, Codable {
        case granted
        case queued
        case cancelled
    }

    public let requestID: String
    public let projectUUID: UUID
    public let domainRef: String
    public let sessionID: String
    public let outcome: Outcome

    public init(
        requestID: String, projectUUID: UUID, domainRef: String, sessionID: String,
        outcome: Outcome
    ) {
        self.requestID = requestID
        self.projectUUID = projectUUID
        self.domainRef = domainRef
        self.sessionID = sessionID
        self.outcome = outcome
    }
}
