import Foundation
import NucleicProtocol

/// One staged transfer item on disk: the file to stream plus its manifest descriptor.
struct StagedTransferItem: Sendable {
    let kind: TransferItemKind
    let url: URL
    let descriptor: TransferItemDescriptor
}

/// The **Carbon Copy** of a session — the source-side replica the Covalence mesh streams to a
/// destination host (mesh P5). This is the Carbon (data) layer's unit of replication: it gathers
/// the session's data (transcript, optional native transcript, and — after the destination reports
/// `haveSHAs` — the git bundle), hashes each item, and builds the `TransferOffer`. Everything is
/// staged under a per-transfer temp directory so a chunk stream can be re-read on resume and
/// cleaned up on completion. (The *transport* that carries it is Covalence; the copied *data* is
/// Carbon.)
public final class SessionCarbonCopy: @unchecked Sendable {
    public let transferID: String
    public let offer: TransferOffer
    let stagingDir: URL
    /// Items known at offer time (transcript, optional native). The bundle is added post-accept.
    /// Mutated only within the owning `SessionTransferCoordinator` actor.
    private var stagedItems: [TransferItemKind: StagedTransferItem]

    private let worktree: Worktree
    private let project: Project
    private let worktrees: any WorktreeManaging

    private init(
        transferID: String, offer: TransferOffer, stagingDir: URL,
        stagedItems: [TransferItemKind: StagedTransferItem],
        worktree: Worktree, project: Project, worktrees: any WorktreeManaging
    ) {
        self.transferID = transferID
        self.offer = offer
        self.stagingDir = stagingDir
        self.stagedItems = stagedItems
        self.worktree = worktree
        self.project = project
        self.worktrees = worktrees
    }

    /// Prepare the transcript + native items and the offer. The git bundle is deferred to
    /// `buildBundle(haveSHAs:)` because it depends on what the destination already has.
    ///
    /// - Parameters:
    ///   - transcriptURL: our JSONL transcript file for the session.
    ///   - nativeTranscriptURL: the native agent transcript, if present (best-effort memory carry).
    ///     When nil, `record.backendSessionID` is forced nil so the destination starts fresh.
    ///   - stagingRoot: a scratch dir (e.g. `…/Nucleic/transfers`); a `<transferID>` subdir is made.
    public static func prepare(
        transferID: String,
        session: Session,
        project: Project,
        projectDescriptor: ProjectDescriptor,
        worktree: Worktree,
        transcriptURL: URL,
        nativeTranscriptURL: URL?,
        branchTipSHA: String,
        transcriptHeaderVersion: Int,
        stagingRoot: URL,
        worktrees: any WorktreeManaging
    ) throws -> SessionCarbonCopy {
        let stagingDir = stagingRoot.appendingPathComponent(transferID, isDirectory: true)
        try FileManager.default.createDirectory(at: stagingDir, withIntermediateDirectories: true)

        var items: [TransferItemKind: StagedTransferItem] = [:]
        var known: [TransferItemDescriptor] = []

        // Transcript (always present, the UI truth).
        let transcriptStaged = stagingDir.appendingPathComponent("transcript.jsonl")
        try copyItem(from: transcriptURL, to: transcriptStaged)
        let tItem = try makeItem(kind: .transcript, url: transcriptStaged)
        items[.transcript] = tItem
        known.append(tItem.descriptor)

        // Native transcript — best effort. Only claim memory-carry (a non-nil backendSessionID in
        // the record) if we actually have the native file to send.
        var carryBackendSessionID: String? = nil
        if let nativeTranscriptURL, FileManager.default.fileExists(atPath: nativeTranscriptURL.path),
           let backendSessionID = session.backendSessionID {
            let nativeStaged = stagingDir.appendingPathComponent("native.jsonl")
            try copyItem(from: nativeTranscriptURL, to: nativeStaged)
            let nItem = try makeItem(kind: .nativeTranscript, url: nativeStaged)
            items[.nativeTranscript] = nItem
            known.append(nItem.descriptor)
            carryBackendSessionID = backendSessionID
        }

        let record = SessionTransferRecord(
            sessionID: session.id, backend: session.backend,
            backendSessionID: carryBackendSessionID, title: session.title,
            branch: worktree.branch, baseSHA: worktree.baseSHA,
            model: session.model, effort: session.effort, lastSeq: session.lastSeq,
            createdAt: session.createdAt,
            covalenceOriginDeviceID: session.covalenceOriginDeviceID)

        let offer = TransferOffer(
            transferID: transferID, record: record, project: projectDescriptor,
            transcriptHeaderVersion: transcriptHeaderVersion,
            availableBaseSHAs: [worktree.baseSHA, branchTipSHA].filter { !$0.isEmpty },
            knownItems: known)

        return SessionCarbonCopy(
            transferID: transferID, offer: offer, stagingDir: stagingDir,
            stagedItems: items, worktree: worktree, project: project, worktrees: worktrees)
    }

    // (initializer above)

    /// Build the git bundle against the destination's `haveSHAs`, stage it, and return its
    /// descriptor — added to the item set so it can be streamed. Idempotent (re-buildable on resume).
    @discardableResult
    public func buildBundle(haveSHAs: [String]) async throws -> TransferItemDescriptor {
        let bundleURL = stagingDir.appendingPathComponent("session.bundle")
        try await worktrees.createTransferBundle(worktree, in: project, haveSHAs: haveSHAs, to: bundleURL)
        let item = try Self.makeItem(kind: .bundle, url: bundleURL)
        stagedItems[.bundle] = item
        return item.descriptor
    }

    /// The staged item for a kind (for streaming), if prepared.
    func item(_ kind: TransferItemKind) -> StagedTransferItem? { stagedItems[kind] }

    /// The order items are streamed in: bundle first (largest, verified first), then transcript,
    /// then native.
    var streamOrder: [TransferItemKind] {
        [.bundle, .transcript, .nativeTranscript].filter { stagedItems[$0] != nil }
    }

    /// Remove the staging directory once the transfer completes (or is abandoned).
    public func cleanup() {
        try? FileManager.default.removeItem(at: stagingDir)
    }

    // MARK: - Helpers

    private static func copyItem(from source: URL, to dest: URL) throws {
        try? FileManager.default.removeItem(at: dest)
        try FileManager.default.copyItem(at: source, to: dest)
    }

    private static func makeItem(kind: TransferItemKind, url: URL) throws -> StagedTransferItem {
        let attrs = try FileManager.default.attributesOfItem(atPath: url.path)
        let byteCount = (attrs[.size] as? Int) ?? 0
        let sha = try TransferHash.sha256Hex(ofFileAt: url)
        let chunkCount = max(1, Int((Double(byteCount) / Double(TransferChunk.maxDataBytes)).rounded(.up)))
        return StagedTransferItem(
            kind: kind, url: url,
            descriptor: TransferItemDescriptor(
                kind: kind, byteCount: byteCount, sha256: sha, chunkCount: chunkCount))
    }
}
