import Foundation
import libgit2
import os

// MARK: - GitHeadSnapshot

/// **The last-committed half of the composer's diff** (06-history-undo.md ▸ Commit messages: "a
/// structural diff of two board snapshots — last-committed vs. current").
///
/// ### Why HEAD's tree, and not a snapshot carried forward
///
/// The engine could remember the board it committed last time and hand that back as "previous". It
/// deliberately does not, for four reasons, each of which is a case the carried value would get
/// wrong:
///
/// - **Launch catch-up has no previous to carry.** "Changes found pending at board open diff HEAD's
///   tree against the working tree through the same composer" (06) — at open the app's only snapshot
///   is the one it just loaded, which already *contains* the pending changes. The previous state
///   exists nowhere but in the repository.
/// - **The app is not the only writer.** An agent that commits its own work moves HEAD without the
///   app writing anything; a carried snapshot would diff against a state that is already history.
/// - **A failed or skipped commit does not advance history.** A carried value would advance anyway
///   and silently under-describe the next window.
/// - **It is checkable.** "Last committed" is a fact the repository answers; a carried value is a
///   claim the engine makes about itself, and nothing would ever catch it drifting.
///
/// The cost is this file: HEAD's tree is materialized into a temporary directory and read back
/// through the one `BoardLoader`, so the previous snapshot is produced by exactly the machinery that
/// produced the current one. Re-parsing rather than re-deriving is the point — two loaders would be
/// two definitions of what a board is.
///
/// ### What it writes
///
/// **`index.md` blobs in full; every other blob as a zero-byte placeholder.** The snapshot models
/// frontmatter, bodies and *attachment names* — never attachment bytes — so materializing a board's
/// images would copy megabytes per commit to answer a question about file names. Directories are
/// created so the shape the loader walks is the shape HEAD has.
///
/// ### Isolation
///
/// `GitCommitOperation`'s rule restated: `nonisolated`, opens its own `git_repository`, frees it in
/// the same synchronous scope, and no handle crosses an `await`. Called from inside the flush's
/// detached task, never from the main actor.
enum GitHeadSnapshot {

    private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "git")

    /// libgit2's global state — `GitCommitOperation.startUp`'s twin and for its reason (a flush can
    /// run when no `Repository` is alive, so this file cannot ride on SwiftGitX's refcount).
    private static let startUp: Bool = {
        git_libgit2_init() >= 0
    }()

    /// **The board as HEAD has it**, or `nil` when there is nothing to read one from: an unborn HEAD,
    /// an unopenable repository, a tree with no board `index.md` in it.
    ///
    /// `nil` is a *shrug*, not an error — the composer that receives it simply has no previous half
    /// and falls back to describing the commit by its paths. Nothing here can fail a commit.
    nonisolated static func load(at boardRoot: URL) -> BoardModel? {
        _ = startUp
        guard BoardGitMode.hasGitEntry(at: boardRoot) else { return nil }
        var repository: OpaquePointer?
        guard git_repository_open(&repository, boardRoot.path) == 0, let repository else { return nil }
        defer { git_repository_free(repository) }
        guard git_repository_head_unborn(repository) != 1 else { return nil }

        var reference: OpaquePointer?
        guard git_repository_head(&reference, repository) == 0, let reference else { return nil }
        defer { git_reference_free(reference) }
        var object: OpaquePointer?
        guard git_reference_peel(&object, reference, GIT_OBJECT_TREE) == 0, let tree = object else {
            return nil
        }
        defer { git_tree_free(tree) }

        let scratch = FileManager.default.temporaryDirectory
            .appendingPathComponent("LaneworkHeadSnapshot-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: scratch) }
        guard (try? FileManager.default.createDirectory(at: scratch, withIntermediateDirectories: true)) != nil
        else { return nil }

        materialize(tree: tree, in: repository, into: scratch, depth: 0)

        do {
            return try BoardLoader.load(boardRoot: scratch).model
        } catch {
            // A HEAD whose tree the loader refuses — a board committed before `index.md` existed, a
            // schema from the future — is simply not a previous snapshot. The window still commits;
            // its message is composed from the paths alone.
            logger.debug("HEAD's tree did not load as a board: \(error.description, privacy: .public)")
            return nil
        }
    }

    /// One tree level, recursively. Total and silent: a blob that cannot be read is skipped, because
    /// a partial previous snapshot degrades one event's wording while a thrown error would cost the
    /// commit its message entirely.
    ///
    /// The depth cap is a guard against a pathological repository, not a statement about boards — a
    /// board is three levels deep, four counting `attachments/`.
    private static func materialize(
        tree: OpaquePointer,
        in repository: OpaquePointer,
        into directory: URL,
        depth: Int
    ) {
        guard depth < 8 else { return }
        let manager = FileManager.default

        for position in 0..<git_tree_entrycount(tree) {
            guard let entry = git_tree_entry_byindex(tree, position),
                  let rawName = git_tree_entry_name(entry) else { continue }
            let name = String(cString: rawName)
            guard !name.isEmpty, name != ".", name != ".." else { continue }
            // A `/` in a tree entry name is impossible in a well-formed tree and would be a path
            // escape if it were not: refuse rather than interpret.
            guard !name.contains("/") else { continue }

            switch git_tree_entry_type(entry) {
            case GIT_OBJECT_TREE:
                var child: OpaquePointer?
                guard let id = git_tree_entry_id(entry),
                      git_tree_lookup(&child, repository, id) == 0,
                      let child else { continue }
                defer { git_tree_free(child) }
                let folder = directory.appendingPathComponent(name, isDirectory: true)
                guard (try? manager.createDirectory(at: folder, withIntermediateDirectories: true)) != nil
                else { continue }
                materialize(tree: child, in: repository, into: folder, depth: depth + 1)

            case GIT_OBJECT_BLOB:
                let file = directory.appendingPathComponent(name)
                // **Content for `index.md`, a placeholder for everything else.** The loader reads
                // frontmatter and bodies out of the first and only the *names* of the rest.
                guard name == IntegrityRules.indexFileName else {
                    try? Data().write(to: file)
                    continue
                }
                var blob: OpaquePointer?
                guard let id = git_tree_entry_id(entry),
                      git_blob_lookup(&blob, repository, id) == 0,
                      let blob else { continue }
                defer { git_blob_free(blob) }
                let size = Int(git_blob_rawsize(blob))
                let bytes = git_blob_rawcontent(blob)
                let data = (bytes != nil && size > 0)
                    ? Data(bytes: bytes!, count: size)
                    : Data()
                try? data.write(to: file)

            default:
                // Submodules and symlinks: neither is a board, and neither is followed anywhere else
                // in this app either (`BoardLoader.directoryCandidates` excludes links).
                continue
            }
        }
    }
}
