import Foundation

/// A **common package** the operator can opt into installing into the macOS VM base image (Settings →
/// "macOS virtual machines" → "Common packages"), or push into a running / base guest after the fact.
///
/// This is the sibling of "Included apps": *Included apps* bakes in local `.app` bundles the user
/// supplies, whereas a *common package* is a well-known tool the **guest fetches and installs itself**
/// during provisioning, over the guest's NAT network.
///
/// Note this catalog is for **optional** extras only. Software every base gets unconditionally is not
/// modelled here — it is a plain phase in `scripts/provision-macos-guest.sh` (the Homebrew toolchain,
/// and Chrome for Testing, which is the `chrome` browser target for computer-use).
///
/// The catalog is intentionally small and curated. Each entry carries the exact guest-side shell that
/// installs it, so ONE source of truth drives both paths:
///
///  * **Base build** — the engine writes ``installShell(forIDs:)`` into the provisioning share and
///    `scripts/provision-macos-guest.sh` runs it near the end of provisioning.
///  * **After the fact** — `MacVMEngine.installPackages…` runs the same shell over the native agent's
///    vsock `exec`, into a running clone or a briefly-booted base (no rebuild).
///
/// An entry can be marked `available == false` to appear in Settings **grayed out** ahead of real
/// support — e.g. full Xcode, which a macOS 27 guest can't yet run (it requires an Apple-ID-gated Xcode
/// beta that can't be fetched unattended). Such a placeholder can never be selected or installed.
public struct MacVMPackage: Identifiable, Sendable, Hashable {
    public let id: String
    /// User-facing name, e.g. "Google Chrome".
    public let name: String
    /// One-line description shown under the name in Settings.
    public let summary: String
    /// When `false`, the entry is shown disabled in Settings and can never be selected or installed —
    /// a placeholder for a package we intend to support but can't automate yet.
    public let available: Bool
    /// Why the package is unavailable (shown next to the disabled row); `nil` when ``available``.
    public let unavailableNote: String?
    /// The guest-side shell that installs the package. It runs as the admin `agent` account **without
    /// sudo or a tty** (`/Applications` is admin-writable), so the SAME body is valid during
    /// provisioning and over a live vsock `exec`. `nil` for unavailable placeholders.
    public let installScript: String?

    /// Whether this entry can actually be selected and installed (available AND has an install script).
    public var isInstallable: Bool { available && installScript != nil }
}

extension MacVMPackage {
    /// The curated catalog surfaced in Settings, in display order.
    ///
    /// Chrome is deliberately absent: every base now installs **Chrome for Testing** unconditionally
    /// (`scripts/provision-macos-guest.sh`), so it is no longer something to opt into.
    public static let catalog: [MacVMPackage] = [xcode]

    /// Look up a catalog entry by id.
    public static func named(_ id: String) -> MacVMPackage? { catalog.first { $0.id == id } }

    /// Marker a package's install shell prints on success so callers can tell which packages actually
    /// landed (the per-package subshell is best-effort — a failure is logged, not fatal — so the exit
    /// code alone can't report this).
    static let okMarker = "__NUCLEIC_PKG_OK__"

    /// Full **Xcode** + iOS simulators. Not a *package* — nothing to opt into and nothing to download:
    /// Xcode's own download is Apple-ID-gated, so instead the base build copies the HOST's
    /// `/Applications/Xcode[-beta].app` into every base automatically (`MacVMEngine.addingHostXcode`),
    /// which is also what gives the guest a `metal` compiler. Kept in the catalog, disabled, so the
    /// Settings list explains where Xcode comes from rather than leaving its absence a mystery.
    public static let xcode = MacVMPackage(
        id: "xcode",
        name: "Xcode",
        summary: "Full Xcode with the iOS simulators and the Metal toolchain.",
        available: false,
        unavailableNote:
            "Installed automatically — the base image copies the Mac's own Xcode from /Applications.",
        installScript: nil)

    /// Build the combined guest-side shell that installs every **installable** package whose id is in
    /// `ids` (unknown or unavailable ids are dropped, order preserved). Returns `nil` when nothing
    /// installable remains, so callers can skip the guest round-trip entirely.
    ///
    /// Each package runs in its own `set -e` subshell so one failure is isolated (logged, not fatal) and
    /// can't abort the others. On success the subshell prints ``okMarker`` + the id, which
    /// ``installedIDs(fromOutput:)`` parses to report exactly which packages landed.
    public static func installShell(forIDs ids: [String]) -> String? {
        let pkgs = ids.compactMap(named).filter { $0.isInstallable }
        guard !pkgs.isEmpty else { return nil }
        var body = "# Nucleic common-package installer (generated from Settings → Common packages)\n"
        for pkg in pkgs {
            body += """
                echo "▸ Installing \(pkg.name) …"
                (
                  set -e
                \(pkg.installScript!)
                  echo "\(okMarker) \(pkg.id)"
                ) || echo "  ⚠ \(pkg.name) install failed — skipping." >&2

                """
        }
        return body
    }

    /// Parse a run's combined stdout for the success markers ``installShell(forIDs:)`` emits, returning
    /// the ids that actually installed (in catalog order).
    public static func installedIDs(fromOutput output: String) -> [String] {
        catalog.filter { output.contains("\(okMarker) \($0.id)") }.map { $0.id }
    }
}
