import Foundation

/// **Provisioning-recipe staleness → forced re-provisioning of an already-built base.**
///
/// A golden base bundle (macOS or Linux) is built once and reused across sessions. But the *recipe*
/// that provisions it — the `provision-macos-guest.sh` / `provision-linux-guest.sh` scripts, the
/// in-guest `NucleicVMAgent` / `nucleic-linux-agent`, the default packages — all ship **inside the
/// app**. When an app update changes that recipe, the on-disk base is silently out of date: its disk
/// still holds the *old* toolchain/agent, yet `buildBaseImage` treats a complete base as done.
///
/// This is the VM analogue of bumping the container image tag (`ProjectSandbox.defaultImage`): each OS
/// carries a monotonic **recipe version** the developer bumps in the same commit that changes the
/// recipe. The version last provisioned into a base is persisted in its `bundle.json`
/// (``MacVMBaseStatus/provisioningRecipe``). On launch the engine compares the on-disk recipe against
/// the app's current one and, on a mismatch, forces a re-provision — automatically, so an update that
/// updated the base recipe converges without the user re-clicking "Build base image".
extension MacVMEngine {
    /// The current macOS base **provisioning-recipe version**. Bump this whenever a change to the
    /// bundled macOS provisioning assets must re-provision every already-built base — e.g. a new step
    /// in `scripts/provision-macos-guest.sh`, a new `NucleicVMAgent` capability, or a change to the
    /// default packages/apps baked in. (Kept in lockstep with those assets exactly like the container
    /// image tag is kept in lockstep with `containers/nucleic-sandbox/Dockerfile`.)
    public static let macOSProvisioningRecipe = 1

    /// The current Linux base **provisioning-recipe version**. Bump when the bundled Linux provisioning
    /// assets change in a way that must re-provision an already-built base — a new step in
    /// `guest/linux-base/provision/provision-linux-guest.sh`, a new `nucleic-linux-agent` build, the
    /// a11y agent / desktop helper, etc.
    public static let linuxProvisioningRecipe = 1

    /// The current recipe version for `os`.
    public static func provisioningRecipe(for os: GuestOS) -> Int {
        switch os {
        case .macOS: return macOSProvisioningRecipe
        case .linux: return linuxProvisioningRecipe
        }
    }

    /// The outcome of a staleness check, for logging/telemetry.
    public enum BaseRecipeCheck: Sendable, Equatable {
        case noBase              // nothing built to check
        case prebuilt            // a user-supplied prebuilt base — not ours to re-provision
        case upToDate            // recipe matches; nothing to do
        case grandfathered       // base predates recipe tracking — stamped current, not reprovisioned
        case busy                // a build/Recovery is already in flight — skip this launch
        case reprovisioned       // was stale; re-provisioned to the current recipe
        case reprovisionFailed(String)  // was stale; the re-provision attempt threw
    }

    /// On launch, bring the **engine-built** base for `os` up to the current provisioning recipe.
    /// A configured *prebuilt* base is never touched (the user owns it). A base with no recorded recipe
    /// is grandfathered — stamped with the current recipe **without** reprovisioning, so the release
    /// that introduces recipe tracking doesn't trigger a reprovision storm on everyone's existing base;
    /// only a *later* bump forces the work.
    @discardableResult
    public func reprovisionStaleBaseIfNeeded(for os: GuestOS) async -> BaseRecipeCheck {
        // A user-supplied prebuilt base isn't ours to reprovision (we didn't build it, and its path may
        // be read-only). The engine only manages its own `builtBaseDir` / `builtLinuxBaseDir`.
        let prebuilt = os == .linux
            ? MacVMSettings.linuxBasePrebuiltPath : MacVMSettings.basePrebuiltPath
        if prebuilt != nil { return .prebuilt }

        let dir = os == .linux ? builtLinuxBaseDir : builtBaseDir
        let bundle = MacVMBundle(root: dir)
        guard bundle.isComplete(for: os) else { return .noBase }

        let current = Self.provisioningRecipe(for: os)
        var status = readBaseStatus(bundle)
        status.os = os

        // Grandfather a pre-tracking base: record the current recipe, don't reprovision.
        guard let recorded = status.provisioningRecipe else {
            status.provisioningRecipe = current
            writeBaseStatus(bundle, status)
            return .grandfathered
        }
        if recorded == current { return .upToDate }

        // Stale. Don't stomp an in-flight build/Recovery; the next launch will catch it.
        guard !baseIsBusy else { return .busy }

        #if arch(arm64)
        do {
            switch os {
            case .macOS:
                // The reentrant `buildBaseImage` path skips the multi-GB reinstall on a complete base
                // and re-runs only the provisioning pass, which re-stages the *current* bundled script
                // + agent and re-stamps the recipe (see `provisionAndFinalize`). It needs the account
                // already provisioned; a base that never got that far can't be reprovisioned unattended.
                guard status.accountProvisioned else {
                    return .reprovisionFailed("the base has no provisioned agent account yet")
                }
                try await buildBaseImage()
            case .linux:
                // A Linux base bakes the agent + toolchain into the rootfs at build time, so a genuine
                // recipe refresh means re-running the build. Clear `provisioned` so `buildLinuxBaseImage`
                // doesn't short-circuit as already-done; it reuses the cached kernel/rootfs artifacts
                // (no re-download) and re-runs the in-guest assemble + provision boots, re-stamping the
                // recipe on success.
                status.provisioned = false
                writeBaseStatus(bundle, status)
                try await buildLinuxBaseImage()
            }
            return .reprovisioned
        } catch {
            return .reprovisionFailed(String(describing: error))
        }
        #else
        // No Mac-guest runtime on this architecture, so there's nothing to (re)provision.
        return .reprovisionFailed("the macOS VM runtime requires Apple silicon")
        #endif
    }
}
