import Foundation
import NucleicProtocol

/// The observation half of the release fence (docs/CARBON_SHARDING.md §17.4 stage 2): does
/// *this device* hold a release's `requiredHeads`?
///
/// "Hold" is three-layered, each checked in order:
///
/// 1. **Chain held** — the local store's head for the stream is at or past the required
///    `(generation, seq)`. Heads only advance once every reachable manifest + shard is
///    persisted (the honest-head rule, §8.2 step 3), so this alone proves the *data* is here.
/// 2. **Packs applied** — for a device with a local clone, the history manifests' git packs
///    are in the repo's object store. Applied on demand (idempotent) when missing.
/// 3. **Ref fast-forwarded** — the manifest's named branch ref points at (or past) its
///    `branchTipSHA`. Fast-forward only: a diverged local branch (the §17.8 non-FF row) fails
///    the fence — the lock holds at `releasing`, surfaced like an update-on-grant conflict,
///    never forced.
///
/// The landing host passes trivially (it produced the heads and its refs already contain the
/// landing), so single-host release keeps zero added latency — the degenerate case, not a
/// special case.
public struct CarbonLockFence: Sendable {
    private let git: GitRunner
    private let store: CarbonShardStore
    private let keyProvider: CarbonShardStore.KeyProvider
    /// Resolve a project's local clone root. nil ⇒ no clone on this device — the store-level
    /// check (layer 1) is the whole fence.
    private let repoPath: @Sendable (UUID) -> String?

    public init(
        git: GitRunner = GitRunner(), store: CarbonShardStore,
        keyProvider: @escaping CarbonShardStore.KeyProvider,
        repoPath: @escaping @Sendable (UUID) -> String?
    ) {
        self.git = git
        self.store = store
        self.keyProvider = keyProvider
        self.repoPath = repoPath
    }

    /// Whether this device satisfies the fence for `heads` — the `CarbonLockManager`
    /// `fenceCheck` collaborator. Fails closed: anything unresolved (missing chain, apply
    /// failure, diverged ref) keeps the lock at `releasing`.
    public func satisfied(_ heads: [CarbonHead]) async -> Bool {
        for head in heads {
            guard await chainHeld(head) else { return false }
            if let path = repoPath(head.key.projectUUID) {
                guard await appliedAndFastForwarded(head, repoPath: path) else { return false }
            }
        }
        return true
    }

    // MARK: - Layer 1: chain held

    private func chainHeld(_ head: CarbonHead) async -> Bool {
        guard let local = await store.heads(projectUUID: head.key.projectUUID)[head.key]
        else { return false }
        // At-or-past suffices: heads are cumulative (a later head's chain contains the
        // required manifest), and a stale-generation local head never counts (§10).
        return local.ownerGeneration > head.ownerGeneration
            || (local.ownerGeneration == head.ownerGeneration
                && local.snapshotSeq >= head.snapshotSeq)
    }

    // MARK: - Layers 2–3: packs applied + ref fast-forwarded

    private func appliedAndFastForwarded(_ head: CarbonHead, repoPath: String) async -> Bool {
        let projectUUID = head.key.projectUUID
        guard let manifest = try? await store.manifest(id: head.manifestID, projectUUID: projectUUID)
        else { return false }
        // Only git-pack manifests carry repo content; anything else (a lock-ledger head in a
        // future caller) is satisfied by layer 1 alone.
        guard manifest.payloadKind == .gitPack, let tip = manifest.branchTipSHA else { return true }

        if !(await commitPresent(tip, in: repoPath)) {
            guard await applyChain(upTo: head.manifestID, projectUUID: projectUUID, repoPath: repoPath),
                await commitPresent(tip, in: repoPath)
            else { return false }
        }
        guard let ref = manifest.branch else { return true }
        return await fastForward(ref: ref, to: tip, in: repoPath)
    }

    private func commitPresent(_ sha: String, in repoPath: String) async -> Bool {
        let result = try? await git.run(["cat-file", "-e", "\(sha)^{commit}"], in: repoPath)
        return result?.ok ?? false
    }

    /// Apply the chain's packs oldest → newest, walking back only to the newest manifest
    /// whose tip is already present (a present commit implies its closure is present in a
    /// full clone — everything older was applied or produced here).
    private func applyChain(upTo headID: String, projectUUID: UUID, repoPath: String) async -> Bool {
        guard let key = keyProvider(projectUUID) else { return false }
        let crypto = CarbonCrypto(projectKey: key)
        let restore = CarbonRestore(git: git, store: store, keyProvider: keyProvider)

        var pending: [CarbonManifest] = []
        var cursor: String? = headID
        while let id = cursor {
            guard let manifest = try? await store.manifest(id: id, projectUUID: projectUUID)
            else { return false }
            if let tip = manifest.branchTipSHA, await commitPresent(tip, in: repoPath) { break }
            pending.append(manifest)
            cursor = manifest.parentManifest
        }
        for manifest in pending.reversed() {
            guard (try? await restore.apply(manifest, crypto: crypto, repoPath: repoPath)) != nil
            else { return false }
        }
        return true
    }

    /// Fast-forward `ref` to `tip`. Never force: `tip` already an ancestor ⇒ done (the local
    /// branch moved past the landing); diverged ⇒ false (§17.8 — hold at `releasing`,
    /// surface, let a human resolve).
    private func fastForward(ref: String, to tip: String, in repoPath: String) async -> Bool {
        let current = try? await git.run(["rev-parse", "--verify", "--quiet", ref], in: repoPath)
        guard let current, current.ok else {
            // Ref absent locally (e.g. a nested child's session branch) — create it.
            return (try? await git.checked(["update-ref", ref, tip], in: repoPath)) != nil
        }
        let sha = current.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
        if sha == tip { return true }
        if let past = try? await git.run(["merge-base", "--is-ancestor", tip, sha], in: repoPath),
            past.status == 0
        {
            return true  // local branch already contains the landing
        }
        if let behind = try? await git.run(["merge-base", "--is-ancestor", sha, tip], in: repoPath),
            behind.status == 0
        {
            return (try? await git.checked(["update-ref", ref, tip, sha], in: repoPath)) != nil
        }
        return false  // diverged — never force
    }
}
