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 — e.g. the latest Google Chrome, pulled from Google's public DMG over the
/// guest's NAT network.
///
/// 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.
    public static let catalog: [MacVMPackage] = [chrome, 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__"

    /// Latest stable **Google Chrome** — a public, unauthenticated *universal* DMG, so the guest fetches
    /// and installs it fully unattended. Mounts the DMG, copies `Google Chrome.app` into `/Applications`
    /// (replacing any existing copy), and strips quarantine so it launches without a Gatekeeper prompt.
    public static let chrome = MacVMPackage(
        id: "chrome",
        name: "Google Chrome",
        summary: "",
        available: true,
        unavailableNote: nil,
        installScript: """
        DEST=/Applications
        CHROME_URL='https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg'
        TMP="$(mktemp -d)"
        MNT="$TMP/mnt"
        cleanup() { /usr/bin/hdiutil detach -quiet "$MNT" >/dev/null 2>&1 || true; /bin/rm -rf "$TMP"; }
        trap cleanup EXIT
        echo "  ↓ downloading Google Chrome …"
        /usr/bin/curl -fL --retry 3 -o "$TMP/googlechrome.dmg" "$CHROME_URL"
        /bin/mkdir -p "$MNT"
        /usr/bin/hdiutil attach -nobrowse -quiet -noverify -mountpoint "$MNT" "$TMP/googlechrome.dmg"
        /bin/rm -rf "$DEST/Google Chrome.app"
        /bin/cp -R "$MNT/Google Chrome.app" "$DEST/"
        /usr/bin/xattr -dr com.apple.quarantine "$DEST/Google Chrome.app" 2>/dev/null || true
        echo "  ✓ Google Chrome installed."
        """)

    /// Full **Xcode** + iOS simulators. **Placeholder for now** (`available: false`): a macOS 27 guest
    /// requires an Xcode *beta*, whose download is Apple-ID-gated and so can't be fetched unattended.
    /// Shown disabled until a supported, automatable path exists — the scaffolding is here so wiring it
    /// up later is a one-line flip plus an install script.
    public static let xcode = MacVMPackage(
        id: "xcode",
        name: "Xcode",
        summary: "Full Xcode with the iOS simulators.",
        available: false,
        unavailableNote: "Coming soon — macOS 27 guests currently require an Apple-ID-gated Xcode beta.",
        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 }
    }
}
