import Foundation
import NucleicProtocol

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif

/// SHA-256 hex of some bytes — the transfer manifest's per-item integrity check.
enum TransferHash {
    static func sha256Hex(_ data: Data) -> String {
        SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
    }

    /// Stream a file through SHA-256 without loading it whole (transcripts/bundles can be large).
    static func sha256Hex(ofFileAt url: URL) throws -> String {
        let handle = try FileHandle(forReadingFrom: url)
        defer { try? handle.close() }
        var hasher = SHA256()
        while let chunk = try handle.read(upToCount: 1 << 20), !chunk.isEmpty {
            hasher.update(data: chunk)
        }
        return hasher.finalize().map { String(format: "%02x", $0) }.joined()
    }
}

/// A reply the source coordinator observes from the destination during a transfer. The
/// production adapter maps `SyncClient.Event` transfer cases (and connection loss) onto these;
/// the loopback test maps the importer's `HostMsg` replies onto them.
public enum TransferReply: Sendable, Equatable {
    case accept(TransferAccept)
    case reject(TransferReject)
    case ready(String)
    case committed(String)
    case chunkAck(TransferChunkAck)
    /// The channel dropped mid-transfer (peer offline / revoked). Carries a human reason.
    case disconnected(String)
}

/// The source's channel to the destination: send transfer verbs (as `ClientMsg`, since the
/// source is a client of the destination) and observe replies. In production this wraps the
/// `SyncClient` `PeerClient` holds for the destination Mac; in tests it's an in-memory loopback
/// to a `SessionTransferImporter`.
public protocol TransferChannel: Sendable {
    func send(_ msg: ClientMsg) async
    func replies() -> AsyncStream<TransferReply>
}

/// What the destination importer needs from its host app to resolve, place, and activate an
/// incoming session — kept behind a protocol so the importer is testable without `AppStore`.
public protocol TransferImportEnvironment: Sendable {
    /// Resolve an incoming project descriptor to a local project (by UUID, root commit, or
    /// normalized remote). Nil ⇒ the destination doesn't have the project (`projectNotFound`).
    func resolveProject(_ descriptor: ProjectDescriptor) async -> Project?
    /// The directory session transcripts live under on this Mac (`…/Nucleic/sessions`).
    var transcriptsDir: URL { get }
    /// Where the native agent transcript should be placed for a staged session (claude-home +
    /// cwd-derived dir), or nil if this destination can't host it (⇒ fresh-context fallback).
    func nativeTranscriptDestination(
        session: Session, project: Project, worktreePath: String, backendSessionID: String
    ) -> URL?
    /// Activate a freshly-imported session: build its controller and broadcast its arrival.
    func didActivateTransferredSession(_ session: Session) async
    /// The local mesh-synced mirror of `sessionID`'s transcript, when this device holds one that
    /// is **complete through `minSeq`** (full-transcript mesh sync keeps active sessions
    /// mirrored on every Mac). A non-nil URL lets the importer adopt the mirror instead of
    /// having the source stream the transcript — the "the mesh already synced it" fast path for
    /// between-turn Covalence moves. Return nil when unmirrored or behind; the transcript then
    /// rides the ordinary chunk stream. Default: nil (hosts without a mirror store).
    func mirroredTranscript(sessionID: SessionID, from deviceID: String, minSeq: UInt64) async -> URL?
}

extension TransferImportEnvironment {
    public func mirroredTranscript(
        sessionID: SessionID, from deviceID: String, minSeq: UInt64
    ) async -> URL? { nil }
}

/// A concrete `TransferImportEnvironment` the app builds by passing its transcripts dir plus two
/// closures (project resolution + activation). The native-transcript destination is computed
/// purely from the session/project/worktree, so the app doesn't have to reimplement Claude's
/// cwd-dirname convention — keeping the `@MainActor` `AppStore` out of a sync protocol method.
public struct StandardTransferImportEnvironment: TransferImportEnvironment {
    public let transcriptsDir: URL
    private let resolve: @Sendable (ProjectDescriptor) async -> Project?
    private let activate: @Sendable (Session) async -> Void
    private let mirror: (@Sendable (SessionID, String, UInt64) async -> URL?)?

    public init(
        transcriptsDir: URL,
        resolveProject: @escaping @Sendable (ProjectDescriptor) async -> Project?,
        didActivate: @escaping @Sendable (Session) async -> Void,
        mirroredTranscript: (@Sendable (SessionID, String, UInt64) async -> URL?)? = nil
    ) {
        self.transcriptsDir = transcriptsDir
        self.resolve = resolveProject
        self.activate = didActivate
        self.mirror = mirroredTranscript
    }

    public func resolveProject(_ descriptor: ProjectDescriptor) async -> Project? {
        await resolve(descriptor)
    }

    public func didActivateTransferredSession(_ session: Session) async {
        await activate(session)
    }

    public func mirroredTranscript(
        sessionID: SessionID, from deviceID: String, minSeq: UInt64
    ) async -> URL? {
        await mirror?(sessionID, deviceID, minSeq)
    }

    public func nativeTranscriptDestination(
        session: Session, project: Project, worktreePath: String, backendSessionID: String
    ) -> URL? {
        // A sandboxed session writes its native transcript into the per-session claude-home
        // (sibling of its transcript); a host run uses ~/.claude. This mirrors
        // `SessionController.locateNativeTranscript`'s home choice, and the dir name is the
        // canonicalized cwd (identical host vs in-container, so it round-trips).
        let claudeHome: String
        if ContainerServiceSettings.serviceEnabled, project.effectiveSandbox?.enabled == true {
            claudeHome = GitWorktreeManager.canonical(
                transcriptsDir.appendingPathComponent(session.id.rawValue, isDirectory: true)
                    .appendingPathComponent("claude-home", isDirectory: true).path)
        } else {
            claudeHome = (NSHomeDirectory() as NSString).appendingPathComponent(".claude")
        }
        let dir = SessionController.claudeProjectDirName(GitWorktreeManager.canonical(worktreePath))
        return URL(fileURLWithPath: "\(claudeHome)/projects/\(dir)/\(backendSessionID).jsonl")
    }
}
