import Foundation

/// **The one place this app creates a `.zip`** — `/usr/bin/ditto -c -k`, spawned as a child
/// process, for File ▸ Share… (`BoardShareStager`; design ruling 2026-08-09, card 72691b11).
///
/// ### Why a spawned system tool rather than a library or a framework
///
/// Foundation has no public zip-writing API, and the two frameworks that could stand in for one
/// both miss: `AppleArchive` (macOS 11+) writes `.aar`/LZFSE streams, not the `.zip` format every
/// share destination this card targets (Mail, Messages, AirDrop, Finder) actually expects to open;
/// pulling in a third-party archiver (libarchive, minizip, …) is real dependency weight for one
/// button. `ditto -c -k` is Apple's own documented recipe for "make a zip archive" outside of
/// Finder's Compress menu, and it is what Finder's own Compress item shells out to — so a `.zip`
/// this produces is the same bytes a user's own Finder compress would have written.
///
/// ### The sandbox question, answered rather than assumed
///
/// A sandboxed app's `Process` spawns a **child that inherits the parent's sandbox** rather than
/// running unconfined — the concern the card's design ruling asks to "VERIFY in a sandboxed
/// build" rather than take on faith. `/usr/bin/ditto` is a platform binary reading and writing
/// only paths this app's own process already owns (`BoardShareStager`'s staging directory, inside
/// the app's temporary directory — no security-scoped bookmark, no path outside the sandbox the
/// process wasn't already free to touch), so there is no privilege the child needs that the parent
/// does not already have. **Verified empirically, not just argued**: `DittoZipArchiverTests` runs
/// this exact call inside `KanbanTests`, which is host-application-hosted
/// (`TEST_HOST = .../Lanework.app/...` in `project.yml`) and therefore runs *as* the sandboxed app
/// process, entitlements included — the closest this suite can get to the real Share button
/// without driving AppKit. A green run there is the sandboxed build the ruling asked for.
///
/// If a future OS or entitlement change ever makes that test go red, the ruling's own fallback is
/// recorded at the call site that would need it (`BoardShareStager`'s doc comment): share the
/// board's **folder URL** directly rather than a zip, which needs no subprocess at all.
enum DittoZipArchiver {

    /// Why the archive did not get made — `BoardShareStager` folds both into its own `.failed`.
    enum Failure: Error, Sendable, Equatable {
        /// `Process.run()` itself threw — the executable could not be launched at all (missing,
        /// unreadable, or a sandbox denial of the spawn itself rather than of what runs inside it).
        case launchFailed(String)

        /// `ditto` ran and exited non-zero — its own stderr, which names the actual reason (a
        /// source that vanished mid-copy, a destination it could not create) far better than this
        /// type could reword it.
        case nonZeroExit(status: Int32, message: String)
    }

    /// Zips the **contents** of `source` into `destination` — `--keepParent` so the archive's one
    /// top-level entry is `source`'s own folder name (a recipient decompressing the `.zip` gets
    /// back a folder named after the board, not its contents spilled loose), and
    /// `--sequesterRsrc` so any resource forks or Finder metadata land in `__MACOSX/` alongside
    /// the payload rather than corrupting it for a non-Apple unzip on the receiving end — the same
    /// two flags Finder's own Compress uses.
    ///
    /// `destination`'s parent must already exist; `ditto` does not create intermediate
    /// directories, and this call does not either — `BoardShareStager` owns the staging
    /// directory's lifecycle, this only ever writes one file into a folder that is already there.
    ///
    /// Synchronous and blocking — callers running this off the main actor (`BoardShareStager`,
    /// via `Task.detached`) is what keeps a multi-hundred-megabyte board's zip from freezing a
    /// spinner, the same posture `BoardTreeCopy`'s own doc comment states for the plain-copy half
    /// of this same flow.
    static func zip(contentsOf source: URL, to destination: URL) throws(Failure) {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
        process.arguments = ["-c", "-k", "--sequesterRsrc", "--keepParent", source.path, destination.path]

        // Piped rather than inherited: a child's stdout would otherwise interleave with this
        // process's own on the same terminal/log, and stderr is read back on a non-zero exit,
        // which is the only time `ditto`'s own words are worth surfacing.
        process.standardOutput = Pipe()
        let stderrPipe = Pipe()
        process.standardError = stderrPipe

        do {
            try process.run()
        } catch {
            throw .launchFailed(error.localizedDescription)
        }
        process.waitUntilExit()

        guard process.terminationStatus == 0 else {
            let data = stderrPipe.fileHandleForReading.readDataToEndOfFile()
            let message = String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
            throw .nonZeroExit(status: process.terminationStatus, message: message)
        }
    }
}
