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`.)
    // v2: Phase 4 bakes the major dev toolchains into the base — Rust (rustc/cargo/rustup), Go, plus
    //     cmake/make/pkg-config + more common CLI tools — and adds ~/.cargo/bin & ~/go/bin to the
    //     /etc/zshenv PATH, so every clone is born build-ready for JS/Python/Rust/Go/C++.
    // v3: disable window restoration (so clones don't reopen the base build's `nucleic-bootstrap.sh`
    //     Terminal window) and bake in a LaunchAgent that clears NotificationCenter at login + every 60s.
    // v4: if a FULL Xcode (Xcode.app OR Xcode-beta.app) is baked into the base, finish its one-time
    //     setup during provisioning — xcode-select it, accept the license, run `-runFirstLaunch`, and
    //     pre-download the Metal toolchain — so clones don't pay first-launch + Metal on every run.
    // v5: re-staged the rebuilt NucleicVMAgent (`ExecSession`: exit frame never blocks on pipe EOF,
    //     process-group signalling; the exec surface is nash-ONLY — a nash-less guest refuses with
    //     an actionable spawnError instead of falling back to zsh, NASH.md §7.5).
    // v6: the "mac_vm_exec never returns" hang fix — the guest's network-volume file-access consent
    //     (kTCCServiceSystemPolicyNetworkVolumes, prompted against the agent bundle because the
    //     virtiofs share counts as a network volume; nash was not implicated) is now pre-answered in
    //     the base. SIP off: provisioning writes the row into the agent user's TCC.db (Phase 7d).
    //     SIP on (the supported macOS 27 base — no db write possible): the engine boots the base and
    //     clicks the real consent's Allow via the host-side surface (MacVMEngine+ConsentGrant).
    //     v5-stamped bases take a LIGHT pass (one boot + click, no full re-provision) — see
    //     ``reprovisionStaleBaseIfNeeded(for:)``.
    // v7: v6's consent pass VERIFIED THE WRONG THING and stamped bases that still prompt. It probed
    //     the `/Volumes/My Shared Files` automount, which doesn't trip the network-volume gate, so
    //     the touch passed instantly, the click loop never ran, and no consent was ever answered.
    //     The probe now replicates a session clone exactly — `mount_virtiofs` of the custom-tagged
    //     share at its original absolute host path (the real `hostPathRecreationScript`), then a read
    //     of that path — so the pass can only succeed if real work would. The bump also invalidates
    //     every falsely-stamped v6 base.
    // v8: the lightweight MDM is now mandatory. Provisioning is split into minimal exec-agent,
    //     User-Approved MDM enrollment + exact policy verification, then the full toolchain pass.
    //     The final production-shaped Network Volumes probe is validation-only and cannot click a
    //     consent fallback. Force every v7 base through the new enrollment/readiness invariant.
    // v9: Lane B (docs/MACOS_VM_POLICY_ALTERNATIVE.md) replaces MDM as the DEFAULT policy lane. Phase 2
    //     now boots to Recovery to `csrutil disable`, writes the agent's FDA / Network Volumes / Screen
    //     Recording / Accessibility / PostEvent rows straight into the system TCC.db (keyed to the
    //     agent's compiled designated requirement), re-enables SIP, and confirms the grants survived —
    //     no MDM, no APNs, no OCR-for-privileges. The recipe gate now requires `policyGrantsVerified`.
    //     (Lane A/MDM remains available behind `nucleic.macvm.policyLane=mdm`.) Force every v8 base
    //     through the datastore lane.
    // v10: provisioner Phase 7d installed the in-guest agent with `cp -R` WITHOUT first stopping the
    //     running KeepAlive LaunchAgent, so launchd's respawn raced the copy and left a MALFORMED
    //     bundle (`codesign`: "bundle format unrecognized"; launchd: EX_CONFIG / spawn failed) — the
    //     agent silently never launched on later boots. Fixed: bootout the agent first, copy with
    //     `ditto` (bundle-aware), and verify with `codesign -v`. Every v9 base was built with the
    //     broken copy, so force them all to reprovision onto the valid-bundle install.
    // v11: every base now bakes in the HOST's full Xcode (`addingHostXcode`, staged with the operator's
    //      "Included apps"), so clones ship a working `metal`. A CLT-only guest cannot have one at all:
    //      the Command Line Tools ship no `xcodebuild`, so Apple's only supported Metal installer
    //      (`-downloadComponent MetalToolchain`) is unreachable, and `xcrun` won't resolve `metal`
    //      unless a full Xcode is the selected developer dir. Provisioner Phase 7⅞ now also VERIFIES
    //      the result by compiling a probe kernel. Every v10 base is Metal-less, so force them all.
    // v12: the baked-in Xcode is now made READY, not just present. Phase 7⅞ accepts its license and
    //      VERIFIES the acceptance functionally (`xcodebuild -version` as the agent — what a build
    //      actually hits), falling back from `-license accept` to driving the interactive
    //      `sudo xcodebuild -license` with its confirmations pre-fed; it also enables developer-tools
    //      access (`DevToolsSecurity`, `_developer` group) so the first debug/test run doesn't pop an
    //      auth panel no exec-only agent can answer. v11 bases can still stop at an unaccepted
    //      license — a prompt with no TTY behind it — so force them all.
    // v13: Xcode and the promised Homebrew toolchains are now publication invariants, not best-effort
    //      extras. Host-side Xcode staging errors propagate instead of being swallowed; the in-guest
    //      copy uses ditto and missing/incomplete Xcode, license/first-launch failures, an unusable
    //      Metal toolchain, or a failed SwiftUI typecheck abort provisioning. Core brew tools are also
    //      resolved before sealing. v12 could stamp a base usable after any of those failures, so
    //      force every such base through the verified recipe.
    public static let macOSProvisioningRecipe = 13

    /// 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.
    // v2: default rootfs flipped from Ubuntu to narOS and then to the narOS desktop flavor
    //     (naros-vm-desktop, N5) — force a rebuild so existing Ubuntu/headless bases pick up narOS.
    public static let linuxProvisioningRecipe = 2

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

    /// Whether a base's recorded status marks its last provisioning pass as having landed everything
    /// clones depend on. For macOS that is the toolchain pass **and** the in-guest agent (the vsock
    /// control plane rides it — without the agent no clone is ever reachable); a Linux base bakes its
    /// agent into the rootfs during the provision build, so `provisioned` alone is the signal. Pure,
    /// unit-tested; drives the launch staleness check's retry-on-failed-pass behavior.
    static func baseIsUsable(_ status: MacVMBaseStatus, os: GuestOS) -> Bool {
        switch os {
        case .macOS:
            // Either policy lane satisfies usability (docs/MACOS_VM_POLICY_ALTERNATIVE.md): a Lane B
            // base has verified datastore grants; a Lane A base has MDM enrollment + profiles. A base
            // built under either lane must not be treated as unusable just because the app's current
            // default lane differs from the one it was built with.
            return status.provisioned && status.agentInstalled
                && (status.policyGrantsVerified
                    || (status.mdmEnrolled && status.profilesInstalled))
        case .linux: return status.provisioned
        }
    }

    /// 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       // Linux base predates recipe tracking — stamped current
        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 on Linux — stamped with the current recipe **without** reprovisioning. macOS
    /// bases are never grandfathered now: recipe 13 made a verified Xcode/SwiftUI toolchain a
    /// publication invariant, and a missing stamp cannot prove that invariant.
    @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

        // A recipe stamp — matching or absent — proves nothing for an unusable base: a failed pass
        // must be retried regardless, or every clone boots unreachable and sits at "Booting…" until
        // the vsock deadline.
        // A Lane A (MDM) base additionally needs its persisted host MDM identity on disk; a Lane B
        // (datastore) base, identified by its verified grants, has no such artifact.
        let usable = Self.baseIsUsable(status, os: os)
            && (os != .macOS || status.policyGrantsVerified
                || FileManager.default.fileExists(atPath: bundle.mdmStateURL.path))

        // A missing macOS stamp cannot prove the recipe-13 Xcode/SwiftUI invariant, so run the same
        // verified provision path as any stale base. Linux retains the original grandfathering policy.
        guard let recorded = status.provisioningRecipe else {
            if os == .macOS {
                return await reprovision(os: os, bundle: bundle, status: status)
            }
            guard usable else {
                return await reprovision(os: os, bundle: bundle, status: status)
            }
            status.provisioningRecipe = current
            writeBaseStatus(bundle, status)
            return .grandfathered
        }
        if recorded == current, usable { return .upToDate }

        return await reprovision(os: os, bundle: bundle, status: status)
    }

    /// Run the actual re-provision for a base found stale (recipe behind) or unusable (last pass never
    /// landed the control plane). Split from ``reprovisionStaleBaseIfNeeded(for:)`` so both triggers
    /// share the busy guard and the per-OS build path.
    private func reprovision(
        os: GuestOS, bundle: MacVMBundle, status: MacVMBaseStatus
    ) async -> BaseRecipeCheck {
        var status = status
        // 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`). A base with no
                // provisioned agent account falls through to `buildBaseImage`'s clean-reinstall path
                // (the macOS-27 declarative first-boot slot was consumed; reinstall is the only
                // unattended fix, and the cached IPSW makes it a reinstall, not a re-download).
                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
    }
}
