import Foundation
import NucleicProtocol

/// One snapshot event in a Carbon stream chain (docs/CARBON_SHARDING.md §6.3): what was
/// captured, which generation/seq fences it, which manifest precedes it, and the ordered
/// shards whose decrypted concatenation is the payload.
///
/// Encoding is **canonical CBOR v1** — RFC 8949 §4.2 core deterministic (definite lengths,
/// map keys sorted by encoded bytes), which `CBOREncoder` produces by construction. Field
/// names are the frozen wire strings (`CarbonManifestTests` pins the bytes); `nil` optionals
/// are omitted, so two encoders can never disagree on a null representation. The manifest ID
/// is the SHA-256 of the *encrypted* manifest (`sealed(with:)`), mirroring shard IDs.
public struct CarbonManifest: Sendable, Codable, Equatable {
    /// Project identity (spec: UUID / root commit / normalized remote — the transfer triple)
    /// plus what a restorer needs to re-create the project when absent (§9.1 step 1).
    public struct ProjectRef: Sendable, Codable, Equatable {
        public var uuid: UUID
        public var rootCommitSHA: String?
        public var normalizedRemote: String?
        public var cloneURL: String?
        public var localOnly: Bool

        public init(
            uuid: UUID, rootCommitSHA: String? = nil, normalizedRemote: String? = nil,
            cloneURL: String? = nil, localOnly: Bool = false
        ) {
            self.uuid = uuid
            self.rootCommitSHA = rootCommitSHA
            self.normalizedRemote = normalizedRemote
            self.cloneURL = cloneURL
            self.localOnly = localOnly
        }
    }

    /// An ordered reference to one payload shard. Plaintext concatenation order is the array
    /// order; `byteCount` is the **ciphertext** size (what disk/wire accounting sums).
    public struct ShardRef: Sendable, Codable, Equatable {
        public var id: String
        public var byteCount: UInt64

        public init(id: String, byteCount: UInt64) {
            self.id = id
            self.byteCount = byteCount
        }
    }

    public var v: Int
    public var project: ProjectRef
    public var stream: CarbonStreamKind
    /// nil for per-project chains (the base `history` chain; the lock ledger).
    public var sessionID: String?
    /// Session streams: the session branch. Lock ledger: the domain ref (spec §3.4).
    public var branch: String?
    /// Session streams: the fork anchor, preserved verbatim.
    public var baseSHA: String?
    public var ownerDeviceID: String
    /// The resuscitation-fence stamp (spec §10); the lock ledger rides its
    /// `authorityGeneration` here (§3.4).
    public var ownerGeneration: UInt64
    /// Monotonic per (stream, sessionID, ownerGeneration), starting at 1.
    public var snapshotSeq: UInt64
    /// The previous manifest in this chain, or nil at a chain start/restart.
    public var parentManifest: String?
    /// worktreeSnapshot: tip at capture. history: the new tip the pack reaches.
    public var branchTipSHA: String?
    public var wtCommitSHA: String?
    public var worktreeTreeSHA: String?
    public var idxCommitSHA: String?
    public var indexTreeSHA: String?
    /// The index had unmerged entries at capture (spec §6.1 step 2); restore leaves
    /// everything unstaged and the UI says so honestly.
    public var indexUnavailable: Bool
    public var payloadKind: CarbonPayloadKind
    /// jsonlSegment: byte offset of this segment in the native file (0 restarts the file).
    public var segmentOffset: UInt64?
    public var backendSessionID: String?
    /// Ordered; decrypted concatenation = the payload.
    public var shards: [ShardRef]
    /// Producer wall clock, informational only — **never** used for ordering (spec §10).
    public var createdAtMs: UInt64

    public init(
        v: Int = 1, project: ProjectRef, stream: CarbonStreamKind, sessionID: String? = nil,
        branch: String? = nil, baseSHA: String? = nil, ownerDeviceID: String,
        ownerGeneration: UInt64, snapshotSeq: UInt64, parentManifest: String? = nil,
        branchTipSHA: String? = nil, wtCommitSHA: String? = nil, worktreeTreeSHA: String? = nil,
        idxCommitSHA: String? = nil, indexTreeSHA: String? = nil, indexUnavailable: Bool = false,
        payloadKind: CarbonPayloadKind, segmentOffset: UInt64? = nil,
        backendSessionID: String? = nil, shards: [ShardRef], createdAtMs: UInt64
    ) {
        self.v = v
        self.project = project
        self.stream = stream
        self.sessionID = sessionID
        self.branch = branch
        self.baseSHA = baseSHA
        self.ownerDeviceID = ownerDeviceID
        self.ownerGeneration = ownerGeneration
        self.snapshotSeq = snapshotSeq
        self.parentManifest = parentManifest
        self.branchTipSHA = branchTipSHA
        self.wtCommitSHA = wtCommitSHA
        self.worktreeTreeSHA = worktreeTreeSHA
        self.idxCommitSHA = idxCommitSHA
        self.indexTreeSHA = indexTreeSHA
        self.indexUnavailable = indexUnavailable
        self.payloadKind = payloadKind
        self.segmentOffset = segmentOffset
        self.backendSessionID = backendSessionID
        self.shards = shards
        self.createdAtMs = createdAtMs
    }

    // MARK: - Canonical codec

    /// The frozen plaintext encoding (deterministic CBOR). Byte-stable across platforms and
    /// releases — `CarbonManifestTests.canonicalBytesAreFrozen` pins it.
    public func canonicalCBOR() throws -> Data {
        try CBOREncoder().encode(self)
    }

    public static func decode(canonical: Data) throws -> CarbonManifest {
        try CBORDecoder().decode(CarbonManifest.self, from: canonical)
    }

    /// Encrypt with the project's crypto; the sealed blob's `id` is the **manifest ID**.
    public func sealed(with crypto: CarbonCrypto) throws -> CarbonCrypto.Sealed {
        try crypto.seal(try canonicalCBOR())
    }

    /// Decrypt + decode a sealed manifest blob.
    public static func open(_ blob: Data, with crypto: CarbonCrypto) throws -> CarbonManifest {
        try decode(canonical: try crypto.open(blob))
    }

    /// The head this manifest advances its chain to (once its shards are held — the honest-
    /// head rule, spec §8.2 step 3).
    public func head(manifestID: String) -> CarbonHead {
        CarbonHead(
            key: CarbonStreamKey(
                projectUUID: project.uuid, stream: stream, sessionID: sessionID),
            manifestID: manifestID, ownerGeneration: ownerGeneration, snapshotSeq: snapshotSeq)
    }
}
