import Foundation
import NucleicProtocol

/// Materialize a session worktree — branch, working files, staged/unstaged split, native
/// transcript — from Carbon shards (docs/CARBON_SHARDING.md §9.1). Ownership-agnostic: the
/// resuscitation fence, the manual restore browser, and device replacement all call this the
/// same way; the caller decides *whether* restoring is legitimate, this decides only *how*.
///
/// Phase 2 scope: restore from the **local** store (a manifest/shard missing locally throws —
/// the phase 3 coordinator is what back-fills before calling).
public struct CarbonRestore: Sendable {
    private let git: GitRunner
    private let store: CarbonShardStore
    private let keyProvider: CarbonShardStore.KeyProvider

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

    /// What a completed worktree restore hands back to session reconstruction.
    public struct RestoredSession: Sendable, Equatable {
        public let branch: String
        public let branchTipSHA: String
        public let baseSHA: String?
        public let worktreePath: String
        /// The snapshot was captured with an unmerged index (spec §6.1 step 2) — everything
        /// restored unstaged; surface the honest note.
        public let indexUnavailable: Bool
    }

    // MARK: - §9.1 steps 2–6

    /// Restore a session's worktree at `worktreePath` from the newest local snapshot (or an
    /// explicit older `manifestID`, for the restore browser). The project repo must already
    /// exist (§9.1 step 1 — ensure-project — is the caller's, phase 4).
    public func materializeWorktree(
        projectRepoPath: String, projectUUID: UUID, sessionID: String,
        at worktreePath: String, manifestID: String? = nil
    ) async throws -> RestoredSession {
        let crypto = try crypto(projectUUID)

        // Step 2: select the snapshot manifest + verify chain integrity by walking to a start.
        let heads = await store.heads(projectUUID: projectUUID)
        let headKey = CarbonStreamKey(
            projectUUID: projectUUID, stream: .worktreeSnapshot, sessionID: sessionID)
        guard let snapshotHeadID = manifestID ?? heads[headKey]?.manifestID else {
            throw CarbonRestoreError.noSnapshotForSession(sessionID)
        }
        let snapshotChain = try await chain(from: snapshotHeadID, projectUUID: projectUUID)
        guard let target = snapshotChain.last else {
            throw CarbonRestoreError.missingManifest(snapshotHeadID)
        }

        // Step 3: apply packs oldest→newest — per-project base chain first (localOnly's whole
        // graph / base deltas), then the session history chain (thin bases for the snapshots),
        // then the snapshot chain itself.
        for chainSession in [nil, sessionID] {
            let key = CarbonStreamKey(
                projectUUID: projectUUID, stream: .history, sessionID: chainSession)
            if let head = heads[key] {
                for entry in try await chain(from: head.manifestID, projectUUID: projectUUID) {
                    try await apply(entry.manifest, crypto: crypto, repoPath: projectRepoPath)
                }
            }
        }
        for entry in snapshotChain {
            try await apply(entry.manifest, crypto: crypto, repoPath: projectRepoPath)
        }

        guard let tip = target.manifest.branchTipSHA, let branch = target.manifest.branch,
            let wtTree = target.manifest.worktreeTreeSHA
        else {
            throw CarbonRestoreError.missingManifest(target.id)
        }

        // Step 4: branch + worktree. An existing branch pointing elsewhere means another live
        // copy may exist — abort and surface, never force (§9.1).
        let existing = try await git.run(
            ["rev-parse", "--verify", "--quiet", "refs/heads/\(branch)"], in: projectRepoPath)
        if existing.ok {
            let sha = existing.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
            guard sha == tip else {
                throw CarbonRestoreError.branchCollision(branch: branch, existing: sha)
            }
        } else {
            try await git.checked(["branch", branch, tip], in: projectRepoPath)
        }
        guard !FileManager.default.fileExists(atPath: worktreePath) else {
            throw CarbonRestoreError.worktreeExists(worktreePath)
        }
        try await git.checked(["worktree", "add", worktreePath, branch], in: projectRepoPath)

        // Step 5: re-dirty the tree — two-tree merge updates working files incl. deletions;
        // untracked-at-snapshot files materialize (they are tracked in wtTree).
        try await git.checked(["read-tree", "-u", "-m", tip, wtTree], in: worktreePath)

        // Step 6: replay the staged/unstaged split (D10).
        if let idxTree = target.manifest.indexTreeSHA {
            try await git.checked(["read-tree", idxTree], in: worktreePath)
            // Best effort: non-zero just reports files whose stat differs.
            _ = try? await git.run(["update-index", "--refresh"], in: worktreePath)
        }

        return RestoredSession(
            branch: branch, branchTipSHA: tip, baseSHA: target.manifest.baseSHA,
            worktreePath: worktreePath, indexUnavailable: target.manifest.indexUnavailable)
    }

    // MARK: - §9.1 step 7: native memory

    /// Reassemble the native resume file from its segment chain (honoring rebase restarts).
    /// Returns the `backendSessionID` to resume with, or nil when no chain exists — the
    /// caller falls back to fresh context.
    @discardableResult
    public func restoreNativeTranscript(
        projectUUID: UUID, sessionID: String, to fileURL: URL
    ) async throws -> String? {
        let crypto = try crypto(projectUUID)
        let key = CarbonStreamKey(
            projectUUID: projectUUID, stream: .nativeTranscript, sessionID: sessionID)
        guard let head = await store.heads(projectUUID: projectUUID)[key] else { return nil }
        // A rebase manifest restarts the chain (`parentManifest = nil`, offset 0), so walking
        // to the chain start naturally lands on the newest rebase.
        let segments = try await chain(from: head.manifestID, projectUUID: projectUUID)
        var file = Data()
        for entry in segments {
            let payload = try await CarbonPayload.assemble(
                entry.manifest.shards, projectUUID: projectUUID, crypto: crypto, store: store)
            let offset = Int(entry.manifest.segmentOffset ?? 0)
            // Segments are contiguous by construction; tolerate overlap by truncating back.
            if offset < file.count { file = file.prefix(offset) }
            file.append(payload)
        }
        try FileManager.default.createDirectory(
            at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
        try file.write(to: fileURL)
        return segments.last?.manifest.backendSessionID
    }

    // MARK: - Plumbing

    private func crypto(_ projectUUID: UUID) throws -> CarbonCrypto {
        guard let projectKey = keyProvider(projectUUID) else {
            throw CarbonRestoreError.keyMissing(projectUUID)
        }
        return CarbonCrypto(projectKey: projectKey)
    }

    /// Walk `parentManifest` links from a head to the chain start; returns oldest → newest.
    /// A missing link throws — chain integrity is a precondition (§9.1 step 2). Internal so
    /// the release fence (`CarbonLockFence`) reuses the exact same chain/apply plumbing.
    func chain(
        from headID: String, projectUUID: UUID
    ) async throws -> [(id: String, manifest: CarbonManifest)] {
        var entries: [(id: String, manifest: CarbonManifest)] = []
        var cursor: String? = headID
        while let id = cursor {
            guard let manifest = try await store.manifest(id: id, projectUUID: projectUUID)
            else { throw CarbonRestoreError.missingManifest(id) }
            entries.append((id, manifest))
            cursor = manifest.parentManifest
        }
        return entries.reversed()
    }

    /// Apply one manifest's git pack into the repo's object store. `--fix-thin` completes
    /// thin packs against the objects earlier chain applications (or origin) provided.
    func apply(
        _ manifest: CarbonManifest, crypto: CarbonCrypto, repoPath: String
    ) async throws {
        guard manifest.payloadKind == .gitPack, !manifest.shards.isEmpty else { return }
        let payload = try await CarbonPayload.assemble(
            manifest.shards, projectUUID: manifest.project.uuid, crypto: crypto, store: store)
        guard !payload.isEmpty else { return }
        let packFile = FileManager.default.temporaryDirectory
            .appendingPathComponent("carbon-restore-\(UUID().uuidString).pack")
        defer { try? FileManager.default.removeItem(at: packFile) }
        try payload.write(to: packFile)
        let result = try await git.runRedirected(
            ["index-pack", "--fix-thin", "--stdin"], in: repoPath, stdinFile: packFile)
        guard result.ok else {
            throw CarbonRestoreError.packApplyFailed(result.stderr)
        }
    }
}
