import Foundation

/// Which guest operating system a VM boots. The VM service began macOS-only (`VZMac*` platform,
/// restore-image install, native AppKit agent) and now also boots **Linux** guests directly on the
/// Virtualization framework (`VZGenericPlatformConfiguration` + `VZLinuxBootLoader`, a downloaded
/// kernel + rootfs, and a small vsock agent). The two share the whole control plane (vsock exec,
/// host-side computer-use surface, NAT, clone-on-boot, idle/ceiling policy); only the platform +
/// bootloader + device set, the base acquisition/provisioning, and the exec shell fork on this kind.
///
/// NB: the surrounding types keep their `MacVM*` names for now to bound churn — a neutral `VMEngine`
/// rename is a deliberate follow-up. Treat these types as "the VM service," not "the macOS VM service."
public enum GuestOS: String, Sendable, Equatable, Codable {
    case macOS
    case linux
}

/// Request to bring up a **guest VM** (Apple's `Virtualization` framework) for a session, so
/// an agent can run Mac-only work — `xcodebuild`, `xcrun simctl`, `codesign`, simulator-driven
/// end-to-end tests — inside an isolated, per-session Mac instead of on the one shared host. The
/// macOS analogue of ``ContainerSpec``: `MacVMManager` owns its lifecycle and `MacVMEngine` clones
/// the golden base bundle, boots it, and `exec`s into it over SSH.
///
/// The guest is a **clone of a pre-provisioned golden base** (see `MacVMEngine+Base`): a real macOS
/// install seeded with the dev toolchain (Xcode, the iOS/other simulators, node/npm, python/pip),
/// an SSH-reachable `agent` account, and Nucleic's host public key. Everything expensive (the ~14 GB
/// restore-image install + the toolchain provisioning) happens once; each session gets a fast
/// copy-on-write clone of it, which is what lets several agents drive independent Macs at once.
public struct MacVMSpec: Sendable, Equatable {
    /// One shared host directory made visible inside the guest (virtiofs). Unlike the Linux
    /// container's identical-path bind mounts, a macOS guest mounts virtiofs shares under
    /// `/Volumes/My Shared Files/<name>/`, so the guest path is derived from `name`, not `host`.
    public struct Mount: Sendable, Equatable {
        /// Host directory to share (the session's repo / worktree).
        public let host: String
        /// The share name the guest sees it under (the leaf of the guest mount path). Must be a
        /// filesystem-safe single token; defaults are derived from the host path's last component.
        public let name: String
        public let readOnly: Bool
        public init(host: String, name: String, readOnly: Bool) {
            self.host = host
            self.name = name
            self.readOnly = readOnly
        }
    }

    /// Stable logical VM name for this session (e.g. `nucleic-mac-<sessionID.short>`). Doubles as
    /// the per-VM on-disk clone directory name and the network-lookup key.
    public let name: String
    /// Which guest OS this VM boots. Defaults to ``GuestOS/macOS`` so every existing call site keeps
    /// its behavior; a Linux session sets `.linux`, which selects the Linux platform/bootloader/device
    /// set, the Linux base bundle, and the `/bin/sh` exec shell.
    public let os: GuestOS
    /// Virtual CPU count for the guest. Clamped to the framework's allowed range at boot.
    public let cpus: Int
    /// Guest memory ceiling in GiB. Clamped to the framework's allowed range at boot.
    public let memoryGiB: Int
    /// Shared host directories surfaced to the guest over virtiofs (the repo/worktree).
    public let mounts: [Mount]
    /// Default working directory for `exec` **inside the guest**. Typically the guest mount path of
    /// the session's repo (`/Volumes/My Shared Files/<name>`); `nil` → the agent account's home.
    public let workdir: String?
    /// Extra environment exported into every `exec`'d command inside the guest.
    public let env: [String: String]
    /// Stop the guest after this much inactivity (freeing its several GB of host RAM); the clone is
    /// kept on disk so the next turn reboots it without re-installing.
    public let idleTimeout: TimeInterval
    /// The in-guest account Nucleic SSHes in as — the provisioned, key-authorized `agent` user.
    public let sshUser: String
    /// When true this VM is booted **computer-use-capable**: created on the main queue with a
    /// host-side `VZVirtualMachineView` bound to it, so Nucleic captures its framebuffer and injects
    /// keyboard/mouse HID entirely host-side (no in-guest agent, no TCC, no SIP). Set from
    /// `SessionController.allowsMacVMComputer`. Exec-only VMs leave this false and boot headless on a
    /// background queue.
    public let computerUse: Bool

    public init(
        name: String,
        os: GuestOS = .macOS,
        cpus: Int = MacVMSettings.defaultVMCPUs,
        memoryGiB: Int = MacVMSettings.defaultVMMemoryGiB,
        mounts: [Mount] = [],
        workdir: String? = nil,
        env: [String: String] = [:],
        idleTimeout: TimeInterval = TimeInterval(MacVMSettings.defaultIdleTimeoutSeconds),
        sshUser: String = MacVMSettings.defaultSSHUser,
        computerUse: Bool = false
    ) {
        self.name = name
        self.os = os
        self.cpus = max(1, cpus)
        self.memoryGiB = max(1, memoryGiB)
        self.mounts = mounts
        self.workdir = workdir
        self.env = env
        self.idleTimeout = idleTimeout
        self.sshUser = sshUser
        self.computerUse = computerUse
    }

    /// A copy of this spec under a different VM `name` — mirrors ``ContainerSpec/renamed(_:)`` so the
    /// policy layer can run a VM under a distinct physical clone name while keeping a stable key.
    public func renamed(_ newName: String) -> MacVMSpec {
        MacVMSpec(
            name: newName, os: os, cpus: cpus, memoryGiB: memoryGiB, mounts: mounts, workdir: workdir,
            env: env, idleTimeout: idleTimeout, sshUser: sshUser, computerUse: computerUse)
    }
}

/// Errors surfaced from the in-process macOS-VM engine.
public enum MacVMError: Error, Sendable, CustomStringConvertible {
    /// The Virtualization macOS-guest runtime isn't usable here (not Apple silicon, or — surfaced at
    /// VM start — no virtualization entitlement).
    case unavailable(String)
    /// An operation referenced a VM the engine isn't currently running.
    case notRunning(String)
    /// No golden base bundle exists and none could be produced (no restore image / no prebuilt path).
    case baseImageMissing(String)
    /// Installing macOS from the restore image failed. Carries the underlying message.
    case installFailed(String)
    /// Provisioning the golden base (dev toolchain, in-guest agent) failed.
    case provisionFailed(String)
    /// Cloning the base bundle for a per-session VM failed.
    case cloneFailed(String)
    /// The guest VM failed to create/start. Carries the underlying message.
    case startFailed(String)
    /// The in-guest vsock agent never became reachable within the boot deadline (the control plane is
    /// vsock-only now — there is no network fallback).
    case agentUnavailable(String)
    /// Too many macOS guests are already running (host-resource / configured ceiling).
    case concurrencyLimit(Int)
    /// The golden base is exclusively in use (a build/provisioning pass or a Recovery session has it
    /// booted writable), so a new session clone can't be cut from it right now. Transient — retry.
    case baseBusy(String)
    /// Copying user-provided apps into a running guest's `/Applications` over vsock failed.
    case appInstallFailed(String)
    /// A Linux base build could not resolve its boot kernel / initrd / rootfs artifacts (bad URL,
    /// download failure, or a configured prebuilt bundle missing its files).
    case kernelMissing(String)

    public var description: String {
        switch self {
        case let .unavailable(why): return "macOS VM runtime unavailable: \(why)"
        case let .notRunning(name): return "macOS VM \"\(name)\" isn't running."
        case let .baseImageMissing(why): return "No base macOS image available: \(why)"
        case let .installFailed(msg): return "Failed to install macOS into the base VM: \(msg)"
        case let .provisionFailed(msg): return "Failed to provision the base macOS VM: \(msg)"
        case let .cloneFailed(msg): return "Failed to clone the base macOS VM: \(msg)"
        case let .startFailed(msg): return "Failed to start the macOS VM: \(msg)"
        case let .agentUnavailable(msg): return "The macOS guest agent never became reachable: \(msg)"
        case let .concurrencyLimit(n): return "The macOS VM limit (\(n)) is already in use."
        case let .baseBusy(why): return "The base macOS image is busy: \(why)"
        case let .appInstallFailed(msg): return "Failed to copy apps into the macOS VM: \(msg)"
        case let .kernelMissing(why): return "No Linux boot artifacts available: \(why)"
        }
    }
}

/// A point-in-time reading for a running macOS guest, for the VM panel's resource monitor. Memory
/// is the configured ceiling and (best-effort) the guest's own reported free/used; CPU is a coarse
/// host-side estimate. Absent when the VM isn't running.
public struct MacVMResourceSample: Sendable, Equatable {
    public let cpuPercent: Double
    public let memoryUsedBytes: UInt64
    public let memoryTotalBytes: UInt64

    public init(cpuPercent: Double, memoryUsedBytes: UInt64, memoryTotalBytes: UInt64) {
        self.cpuPercent = min(100, max(0, cpuPercent))
        self.memoryUsedBytes = memoryUsedBytes
        self.memoryTotalBytes = memoryTotalBytes
    }

    public var memoryPercent: Double {
        memoryTotalBytes == 0 ? 0 : min(100, Double(memoryUsedBytes) / Double(memoryTotalBytes) * 100)
    }
}

/// Progress of the one-time base-image acquisition (restore-image download, macOS install, toolchain
/// provisioning), surfaced to the VM settings panel. `nil` once the golden base exists — the steady
/// state. Mirrors ``ContainerDownloadProgress`` in spirit but tracks the much longer install phases.
public struct MacVMBaseProgress: Sendable, Equatable {
    public enum Phase: String, Sendable, Equatable {
        case downloadingRestoreImage
        case installing
        /// macOS-27 declarative first boot: the guest account + auto-login + Remote Login are
        /// created unattended (`VZMacGuestProvisioningOptions`), then Nucleic's key is authorized.
        case firstBootSetup
        case provisioning
        /// Baking the native `NucleicVMAgent.app` (+ LaunchAgent + TCC grants) into the base.
        case installingAgent
        /// Reading back readiness + shutting the guest down cleanly so the base can be cloned.
        case finalizing
        case ready
    }
    public let phase: Phase
    /// 0…1 within the current phase when known (install/download report a fraction); `nil` for the
    /// indeterminate provisioning phase.
    public let fraction: Double?
    /// A live sub-step for the indeterminate provisioning phases — the tail of the guest's
    /// `provision.log` (e.g. "▸ [4/8] Installing Homebrew + dev tools …") so the (long, fraction-less)
    /// toolchain install shows real progress instead of a frozen spinner. `nil` when there's nothing
    /// finer to report.
    public let detail: String?

    public init(phase: Phase, fraction: Double? = nil, detail: String? = nil) {
        self.phase = phase
        self.fraction = fraction
        self.detail = detail
    }

    public var label: String {
        switch phase {
        case .downloadingRestoreImage: return "Downloading macOS restore image"
        case .installing: return "Installing macOS into the base VM"
        case .firstBootSetup: return "Setting up the guest account (first boot, unattended)"
        case .provisioning: return "Installing the dev toolchain (Xcode, simulators, node, python)"
        case .installingAgent: return "Installing the in-guest agent + computer-use permissions"
        case .finalizing: return "Finalizing the base image"
        case .ready: return "Base macOS image ready"
        }
    }
}

/// Provisioning state of the golden base bundle, read from its `bundle.json`. Drives the VM settings
/// panel. Computer use runs **host-side** (the guest's virtual display + HID), so it needs no
/// provisioning at all — `computerUseReady` is just "installed". The optional in-guest **semantic AX
/// agent** (`axAgentReady`) is the one thing that still needs SIP (docs/MACOS_VM.md §12.5). Tolerant
/// of older bases missing the newer keys.
public struct MacVMBaseStatus: Sendable, Equatable {
    /// A clean macOS was installed into the bundle.
    public var installed: Bool
    /// The key-authorized `agent` account exists (macOS-27 declarative first boot, or the manual
    /// Setup-Assistant path). Nucleic can SSH in — the precondition for the toolchain/agent pass.
    public var accountProvisioned: Bool
    /// The dev toolchain provisioning ran to completion (Xcode CLT, Homebrew, node/python, the
    /// `/etc/zshenv` PATH) — the base is exec-ready.
    public var provisioned: Bool
    /// `NucleicVMAgent.app` + its LaunchAgent are installed in the guest (the optional AX add-on).
    public var agentInstalled: Bool
    /// SIP is disabled in the guest (required to write the AX agent's TCC grants).
    public var sipDisabled: Bool
    /// The AX agent is installed AND its TCC grants are in place — semantic `ax_*` control will work.
    public var axAgentReady: Bool
    /// Installed guest OS version / build, for display + host-compatibility checks (a macOS version
    /// like `26.0`, or an Ubuntu release like `24.04`).
    public var osVersion: String?
    public var buildVersion: String?
    /// Which guest OS this base holds. Defaults to macOS so older `bundle.json` files (which predate
    /// the key) read back correctly.
    public var os: GuestOS

    /// Computer use (screenshot + click/type/scroll) works host-side on ANY installed base — no
    /// provisioning, no SIP. So "computer-use-ready" is simply "installed".
    public var computerUseReady: Bool { installed }

    public init(
        installed: Bool = false, accountProvisioned: Bool = false, provisioned: Bool = false,
        agentInstalled: Bool = false, sipDisabled: Bool = false, axAgentReady: Bool = false,
        osVersion: String? = nil, buildVersion: String? = nil, os: GuestOS = .macOS
    ) {
        self.installed = installed
        self.accountProvisioned = accountProvisioned
        self.provisioned = provisioned
        self.agentInstalled = agentInstalled
        self.sipDisabled = sipDisabled
        self.axAgentReady = axAgentReady
        self.osVersion = osVersion
        self.buildVersion = buildVersion
        self.os = os
    }

    /// Parse from a decoded `bundle.json` dictionary, defaulting any keys an older base predates.
    public init(json: [String: Any]) {
        self.init(
            installed: json["installed"] as? Bool ?? false,
            accountProvisioned: json["accountProvisioned"] as? Bool ?? false,
            provisioned: json["provisioned"] as? Bool ?? false,
            agentInstalled: json["agentInstalled"] as? Bool ?? false,
            sipDisabled: json["sipDisabled"] as? Bool ?? false,
            // Accept the legacy `computerUseReady` key (its old meaning was the agent path).
            axAgentReady: (json["axAgentReady"] as? Bool) ?? (json["computerUseReady"] as? Bool) ?? false,
            osVersion: json["osVersion"] as? String,
            buildVersion: json["buildVersion"] as? String,
            os: (json["os"] as? String).flatMap(GuestOS.init(rawValue:)) ?? .macOS)
    }

    /// Serialize back to the `bundle.json` dictionary shape (JSON-safe values only).
    public var dictionary: [String: Any] {
        var d: [String: Any] = [
            "installed": installed, "accountProvisioned": accountProvisioned,
            "provisioned": provisioned, "agentInstalled": agentInstalled,
            "sipDisabled": sipDisabled, "axAgentReady": axAgentReady,
            "os": os.rawValue,
        ]
        if let osVersion { d["osVersion"] = osVersion }
        if let buildVersion { d["buildVersion"] = buildVersion }
        return d
    }

    /// A one-line human summary for the settings panel.
    public var summary: String {
        if !installed {
            return os == .linux ? "No Linux base image built yet." : "No base image built yet."
        }
        if os == .linux {
            // Linux computer-use + exec are host-side + vsock — no account/SIP/AX concepts apply.
            return provisioned
                ? "Ready — Linux desktop + builds + computer use."
                : "Installed — provisioning the Linux desktop + toolchain is incomplete."
        }
        if !accountProvisioned { return "Installed — account setup incomplete." }
        if axAgentReady { return "Ready — builds + computer use + semantic AX." }
        if provisioned { return "Ready — builds + computer use." }
        return "Installed — computer use ready; toolchain provisioning incomplete."
    }
}
