import Foundation
import NucleicProtocol

/// One event in a project's **lock ledger** (docs/CARBON_SHARDING.md §17.2) — the manifest
/// payload of the `lock` stream (`payloadKind = .lockEvent`, usually one shard). The ledger is
/// the append-only, single-writer record of lock state; a member's current lock table is a
/// pure fold of it (`CarbonLockLedger.fold`).
///
/// One chain per project (`CarbonStreamKey(project, .lock, sessionID: nil)`): a project's lock
/// authority — the host running its control container — arbitrates *all* of its domains, so
/// its events interleave in one single-writer chain. Each event names its own `domainRef`
/// (also mirrored into the manifest's `branch` field per §3.4); the fold partitions by it.
///
/// Encoded as canonical CBOR (the manifest-payload discipline): deterministic bytes, `nil`
/// optionals omitted, so every member folds identical state from identical chains.
public struct CarbonLockEvent: Sendable, Codable, Equatable {
    public enum Kind: String, Sendable, Codable {
        case acquired
        case queued
        /// The release **publication** (§17.4 stage 1). `requiredHeads` names the Carbon heads
        /// carrying the landed content; a device *observes* the release only once it holds
        /// them. Empty `requiredHeads` = unfenced (lifecycle/manual release — no landing).
        case released
        case retargeted
        /// The authority (generation) moved — waiters died with the old authority's host and
        /// re-acquire; held locks persist.
        case authorityChanged
        /// Compaction: `table` carries the full fold at this point; replay starts here.
        case snapshot
    }

    public var kind: Kind
    /// The lock domain (LOCKING §2 `rootRef`, or the nvrsion trunk branch). nil only for
    /// chain-wide events (`authorityChanged`, `snapshot`).
    public var domainRef: String?
    public var sessionID: String?
    /// The host running the session (for `authorityChanged`: the new authority).
    public var deviceID: String
    public var paths: [String]
    /// `released` only — the release fence (§17.4).
    public var requiredHeads: [CarbonHead]?
    /// Per-project, monotonic within one authority generation (== the manifest's
    /// `snapshotSeq`; the generation rides the manifest's `ownerGeneration`).
    public var ledgerSeq: UInt64
    /// `retargeted` only — the session's new `parentRef` (informational; locks never move,
    /// LOCKING §5.3).
    public var newParentRef: String?
    /// `snapshot` only — the folded state replay resumes from.
    public var table: CarbonLockFold?

    public init(
        kind: Kind, domainRef: String? = nil, sessionID: String? = nil, deviceID: String,
        paths: [String] = [], requiredHeads: [CarbonHead]? = nil, ledgerSeq: UInt64 = 0,
        newParentRef: String? = nil, table: CarbonLockFold? = nil
    ) {
        self.kind = kind
        self.domainRef = domainRef
        self.sessionID = sessionID
        self.deviceID = deviceID
        self.paths = paths
        self.requiredHeads = requiredHeads
        self.ledgerSeq = ledgerSeq
        self.newParentRef = newParentRef
        self.table = table
    }
}

/// The pure fold of a lock ledger: per-domain holders, pending (fenced) releases, and
/// waiters. Deterministic — the same chain folds to the same table on every member (§17.9),
/// pinned by encoding it canonically in compaction snapshots.
///
/// The fold is the **publication** view. A device's *observed* view additionally applies the
/// release fence: a path in `releasing` stays blocked until that device holds the fence's
/// `requiredHeads` (`observedBlockedPaths`).
public struct CarbonLockFold: Sendable, Codable, Equatable {
    public struct Holder: Sendable, Codable, Equatable {
        public var sessionID: String
        public var deviceID: String

        public init(sessionID: String, deviceID: String) {
            self.sessionID = sessionID
            self.deviceID = deviceID
        }
    }

    /// A published-but-fenced release covering one path (§17.4 stage 2). Kept until the path
    /// is next acquired — the authority checks the *acquirer's* device against these heads, so
    /// a grant is sound; newer heads are cumulative (chain order), so the newest release per
    /// path is all that's retained.
    public struct Fence: Sendable, Codable, Equatable {
        public var requiredHeads: [CarbonHead]
        public var sessionID: String?
        public var deviceID: String
        public var ledgerSeq: UInt64

        public init(
            requiredHeads: [CarbonHead], sessionID: String?, deviceID: String, ledgerSeq: UInt64
        ) {
            self.requiredHeads = requiredHeads
            self.sessionID = sessionID
            self.deviceID = deviceID
            self.ledgerSeq = ledgerSeq
        }
    }

    public struct Waiter: Sendable, Codable, Equatable {
        public var sessionID: String
        public var deviceID: String
        public var paths: [String]

        public init(sessionID: String, deviceID: String, paths: [String]) {
            self.sessionID = sessionID
            self.deviceID = deviceID
            self.paths = paths
        }
    }

    public struct Domain: Sendable, Codable, Equatable {
        /// path → current holder.
        public var held: [String: Holder]
        /// path → the newest published release fence covering it (`releasing — syncing
        /// files` until the observer holds the heads).
        public var releasing: [String: Fence]
        /// Queue order as published (informational; arbitration is the authority's).
        public var waiters: [Waiter]

        public init(
            held: [String: Holder] = [:], releasing: [String: Fence] = [:],
            waiters: [Waiter] = []
        ) {
            self.held = held
            self.releasing = releasing
            self.waiters = waiters
        }

        public var isEmpty: Bool { held.isEmpty && releasing.isEmpty && waiters.isEmpty }
    }

    public var domains: [String: Domain]
    /// Highest authority generation applied.
    public var generation: UInt64
    /// Last applied event's `ledgerSeq`.
    public var ledgerSeq: UInt64

    public init(domains: [String: Domain] = [:], generation: UInt64 = 0, ledgerSeq: UInt64 = 0) {
        self.domains = domains
        self.generation = generation
        self.ledgerSeq = ledgerSeq
    }

    /// The observed view of one domain (§17.4 stage 2): every path a local acquire must treat
    /// as blocked — held paths plus releasing paths whose fence this device hasn't satisfied.
    public func observedBlockedPaths(
        domainRef: String, fenceSatisfied: ([CarbonHead]) -> Bool
    ) -> Set<String> {
        guard let domain = domains[domainRef] else { return [] }
        var blocked = Set(domain.held.keys)
        for (path, fence) in domain.releasing where !fenceSatisfied(fence.requiredHeads) {
            blocked.insert(path)
        }
        return blocked
    }
}

/// One fold input: an event plus the authority generation its manifest was fenced with
/// (`ownerGeneration`, §3.4). Split out so the fold itself is a pure function testable
/// without a store.
public struct CarbonLockLedgerEntry: Sendable, Equatable {
    public let generation: UInt64
    public let event: CarbonLockEvent

    public init(generation: UInt64, event: CarbonLockEvent) {
        self.generation = generation
        self.event = event
    }
}

/// The per-project lock ledger against the Carbon store (§17.2): appends events as `lock`-
/// stream manifests (single writer — the project's lock authority) and folds the chain back
/// into a table. An actor so appends serialize: `ledgerSeq`/`parentManifest` are read-
/// modify-write on the chain head.
public actor CarbonLockLedger {
    private let store: CarbonShardStore
    private let keyProvider: CarbonShardStore.KeyProvider

    public init(store: CarbonShardStore, keyProvider: @escaping CarbonShardStore.KeyProvider) {
        self.store = store
        self.keyProvider = keyProvider
    }

    // MARK: - Append (authority-side only)

    /// Append one event to `project`'s ledger chain, assigning its `ledgerSeq`, and advance
    /// the head. `generation` is this authority's fence stamp (§17.3); appending below the
    /// chain's current generation throws (`staleHead`) — a stale authority never writes.
    @discardableResult
    public func append(
        _ event: CarbonLockEvent, project: CarbonManifest.ProjectRef,
        ownerDeviceID: String, generation: UInt64
    ) async throws -> CarbonHead {
        let projectUUID = project.uuid
        guard let key = keyProvider(projectUUID) else {
            throw CarbonStoreError.keyMissing(projectUUID)
        }
        let crypto = CarbonCrypto(projectKey: key)
        let chainKey = CarbonStreamKey(projectUUID: projectUUID, stream: .lock, sessionID: nil)
        let head = await store.heads(projectUUID: projectUUID)[chainKey]
        if let head, head.ownerGeneration > generation {
            throw CarbonStoreError.staleHead(
                current: head,
                attempted: CarbonHead(
                    key: chainKey, manifestID: "", ownerGeneration: generation, snapshotSeq: 0))
        }
        let seq = (head?.ownerGeneration == generation ? head?.snapshotSeq ?? 0 : 0) + 1

        var stamped = event
        stamped.ledgerSeq = seq
        let payload = try CBOREncoder().encode(stamped)
        let shards = try await CarbonPayload.shard(
            payload, projectUUID: projectUUID, crypto: crypto, store: store)
        let manifest = CarbonManifest(
            project: project, stream: .lock, sessionID: nil, branch: stamped.domainRef,
            ownerDeviceID: ownerDeviceID, ownerGeneration: generation, snapshotSeq: seq,
            parentManifest: head?.manifestID, payloadKind: .lockEvent, shards: shards,
            createdAtMs: UInt64(Date().timeIntervalSince1970 * 1000))
        let sealed = try manifest.sealed(with: crypto)
        try await store.putManifest(
            ciphertext: sealed.ciphertext, expectedID: sealed.id, projectUUID: projectUUID)
        let newHead = manifest.head(manifestID: sealed.id)
        try await store.advanceHead(newHead)
        return newHead
    }

    /// Append a compaction `snapshot` (§17.2) carrying the current fold, so replay stays
    /// bounded. No-op (returns nil) on an empty chain.
    @discardableResult
    public func compact(
        project: CarbonManifest.ProjectRef, ownerDeviceID: String, generation: UInt64
    ) async throws -> CarbonHead? {
        let chainKey = CarbonStreamKey(projectUUID: project.uuid, stream: .lock, sessionID: nil)
        guard await store.heads(projectUUID: project.uuid)[chainKey] != nil else { return nil }
        let table = try await fold(projectUUID: project.uuid)
        return try await append(
            CarbonLockEvent(kind: .snapshot, deviceID: ownerDeviceID, table: table),
            project: project, ownerDeviceID: ownerDeviceID, generation: generation)
    }

    // MARK: - Fold

    /// Fold the locally-held chain into the current lock table. Walks `parentManifest` links
    /// from the head back to the newest `snapshot` event (or the chain start) — the §17.2
    /// compaction bound — then replays forward. A missing manifest throws: fold integrity is
    /// a precondition, the §8.2 coordinator back-fills first.
    public func fold(projectUUID: UUID) async throws -> CarbonLockFold {
        let chainKey = CarbonStreamKey(projectUUID: projectUUID, stream: .lock, sessionID: nil)
        guard let head = await store.heads(projectUUID: projectUUID)[chainKey] else {
            return CarbonLockFold()
        }
        guard let key = keyProvider(projectUUID) else {
            throw CarbonStoreError.keyMissing(projectUUID)
        }
        let crypto = CarbonCrypto(projectKey: key)

        var entries: [CarbonLockLedgerEntry] = []
        var cursor: String? = head.manifestID
        while let id = cursor {
            guard let manifest = try await store.manifest(id: id, projectUUID: projectUUID)
            else { throw CarbonRestoreError.missingManifest(id) }
            let payload = try await CarbonPayload.assemble(
                manifest.shards, projectUUID: projectUUID, crypto: crypto, store: store)
            let event = try CBORDecoder().decode(CarbonLockEvent.self, from: payload)
            entries.append(.init(generation: manifest.ownerGeneration, event: event))
            if event.kind == .snapshot { break }  // compaction bound — replay starts here
            cursor = manifest.parentManifest
        }
        return Self.fold(entries.reversed())
    }

    /// The pure fold (§17.2): oldest → newest, deterministic, stale-generation events ignored
    /// (§10 — an event fenced below the highest generation already applied never mutates the
    /// table). Same entries ⇒ same table, on every member.
    public static func fold(_ entries: [CarbonLockLedgerEntry]) -> CarbonLockFold {
        var state = CarbonLockFold()
        for entry in entries {
            // §10 fencing: never fold a stale authority's events.
            guard entry.generation >= state.generation else { continue }
            if entry.generation > state.generation {
                state.generation = entry.generation
                state.ledgerSeq = 0
            }
            // Single-writer chains are strictly ordered; a replayed duplicate is a no-op.
            // (`snapshot`/`authorityChanged` events legitimately restart at seq 1 after a
            // generation bump — handled above.)
            guard entry.event.ledgerSeq > state.ledgerSeq else { continue }
            state.ledgerSeq = entry.event.ledgerSeq
            apply(entry.event, to: &state)
        }
        return state
    }

    private static func apply(_ event: CarbonLockEvent, to state: inout CarbonLockFold) {
        switch event.kind {
        case .snapshot:
            if var table = event.table {
                // The chain's fencing (generation/seq) is authoritative over the embedded
                // copy's counters.
                table.generation = state.generation
                table.ledgerSeq = state.ledgerSeq
                state = table
            }

        case .authorityChanged:
            // The old authority's waiter continuations died with its host (§17.3); their
            // sessions re-acquire. Held locks and pending fences persist.
            for ref in state.domains.keys {
                state.domains[ref]?.waiters = []
            }

        case .acquired:
            guard let ref = event.domainRef, let session = event.sessionID else { return }
            var domain = state.domains[ref] ?? .init()
            for path in event.paths {
                domain.held[path] = .init(sessionID: session, deviceID: event.deviceID)
                // A new holder supersedes any pending fence on the path: the authority
                // granted it only to a device that satisfied the fence (§17.4 soundness).
                domain.releasing[path] = nil
            }
            domain.waiters.removeAll { $0.sessionID == session }
            state.domains[ref] = domain

        case .queued:
            guard let ref = event.domainRef, let session = event.sessionID else { return }
            var domain = state.domains[ref] ?? .init()
            domain.waiters.removeAll { $0.sessionID == session }
            domain.waiters.append(
                .init(sessionID: session, deviceID: event.deviceID, paths: event.paths))
            state.domains[ref] = domain

        case .released:
            guard let ref = event.domainRef else { return }
            var domain = state.domains[ref] ?? .init()
            let heads = event.requiredHeads ?? []
            for path in event.paths {
                if let holder = domain.held[path], let session = event.sessionID,
                    holder.sessionID != session
                {
                    continue  // another session's hold — the authority never publishes this
                }
                domain.held[path] = nil
                if heads.isEmpty {
                    domain.releasing[path] = nil  // unfenced — observed everywhere at once
                } else {
                    domain.releasing[path] = .init(
                        requiredHeads: heads, sessionID: event.sessionID,
                        deviceID: event.deviceID, ledgerSeq: event.ledgerSeq)
                }
            }
            // A released session's queued wait (if any) is gone too — its acquire was
            // cancelled or satisfied; either way the authority stopped tracking it.
            if let session = event.sessionID {
                domain.waiters.removeAll { $0.sessionID == session }
            }
            if domain.isEmpty {
                state.domains[ref] = nil
            } else {
                state.domains[ref] = domain
            }

        case .retargeted:
            break  // informational: locks never move on cascade (LOCKING §5.3)
        }
    }
}
