import Foundation
#if arch(arm64)
import Virtualization
#endif

/// In-process **macOS-guest VM runtime** built directly on Apple's `Virtualization` framework — the
/// Mac-native counterpart to ``ContainerEngine``. Where the container engine boots Linux guests via
/// the `containerization` framework, this actor boots real macOS guests: it owns a golden **base
/// bundle** (a provisioned macOS install with the dev toolchain + the native in-guest `agent`),
/// clones it copy-on-write per session, boots each clone on its own serial `VZVirtualMachine` queue,
/// and `exec`s commands inside **over vsock** (the native agent's streaming exec) so an agent can
/// build/test Mac targets in isolation — no SSH, no NAT, no network in the host↔guest control plane.
///
/// **Daemonless ⇒ ephemeral**, exactly like the container engine: no VM survives the app process, so
/// the `live` registry starts empty each launch and `reconcile` is on-disk GC. The per-session clone
/// (the CoW `Disk.img` + its aux storage) persists on disk, so an idle-stopped or app-restarted VM
/// reboots fast without re-installing macOS.
///
/// Lifecycle *policy* (busy ref-counting, idle timers, the concurrent-VM ceiling, per-session naming)
/// lives one layer up in ``MacVMManager``; this engine is the mechanism it drives. The only
/// entitlement required is `com.apple.security.virtualization` (already carried by the app and the
/// spike) — macOS guests use it too, and NAT networking needs no extra, restricted entitlement.
public actor MacVMEngine {
    /// One live macOS guest the engine is currently running.
    struct LiveVM {
        #if arch(arm64)
        let instance: MacVMInstance
        /// The native in-guest agent channel for this VM (docs/MACOS_VM_NATIVE_AGENT.md), probed
        /// lazily by ``MacVMEngine/agentClient(name:)`` and cached here for the VM's lifetime.
        var agent: MacVMAgentState = .unprobed
        /// The Linux **a11y (AT-SPI) semantic agent** channel (docs/LINUX_VM_SEMANTIC_AGENT.md) — a
        /// separate vsock port from the C `ping`/`exec` agent above, probed lazily by
        /// ``MacVMEngine/linuxA11yClient(name:)``. Only meaningful for Linux guests.
        var a11yAgent: MacVMAgentState = .unprobed
        #endif
        /// Which guest OS this VM is running — selects the exec shell and gates macOS-only computer-use
        /// paths (the semantic AX ops have no Linux equivalent).
        let os: GuestOS
        /// The clone bundle this VM booted from (its writable disk + aux storage live here).
        let bundle: MacVMBundle
        /// Locally-administered MAC assigned to this clone, used only to find its NAT DHCP lease for
        /// the settings-panel IP display (the control plane is vsock).
        let macAddress: String
        /// The guest account the native agent runs as (created by declarative provisioning).
        let sshUser: String
        /// The guest's NAT IP for display, once discovered — best-effort and not on any control path.
        var ipAddress: String?
        /// Configured memory ceiling in bytes, for the resource sample.
        let memoryCeilingBytes: UInt64
        /// When the VM was started (for a coarse CPU estimate / uptime).
        let startedAt: Date
        /// True when this VM was booted computer-use-capable (main queue + a host-side surface bound),
        /// so the computer-use dispatch prefers the SIP-free host-IO path over SSH/agent.
        var computerUse: Bool = false
        /// True while the guest is paused (frozen in RAM) via ``MacVMEngine/suspend(name:)`` — a
        /// `mac_vm_control`/`linux_vm_control` `suspend`. Reuse (``ensureRunning``) transparently thaws
        /// it, so a suspended VM is invisible to the exec/computer-use paths.
        var paused: Bool = false
    }

    /// On-disk root for engine artifacts: the cached restore image, the agent-account credential, the
    /// golden base bundle, and per-session clone bundles.
    let storageRoot: URL
    /// Live VMs keyed by logical name. Empty at launch (nothing survives the app process).
    var live: [String: LiveVM] = [:]
    /// In-flight boots keyed by name, so concurrent `ensureRunning` for the SAME name join the one
    /// boot instead of racing into a second `VZVirtualMachine` on the same writable disk. Also counted
    /// toward the concurrency ceiling so a burst of distinct-name boots can't overshoot it.
    private var boots: [String: Task<(name: String, ipAddress: String), Error>] = [:]
    /// Last spec seen per name, so `restart` (which only has a name) can recreate from disk.
    var lastSpec: [String: MacVMSpec] = [:]

    /// Admission/eviction policy for the concurrent-VM ceiling, injected by the app layer (`AppStore` —
    /// the only layer that knows chat state). Given the names of the VMs currently occupying slots, it
    /// returns those whose owning chat is *done* (no turn in flight — the brief idle-hold window a chat
    /// keeps its VM after completing), ordered stalest-interaction first. When a boot would exceed the
    /// ceiling the engine reclaims the first returned VM to grant the slot; an empty result means
    /// "nothing safe to evict — queue instead". `nil` in headless/spike contexts, where the old
    /// throw-on-full behavior stands (no eviction, no queueing). See ``ensureRunning(_:)``.
    private var evictionRanker: (@Sendable ([String]) async -> [String])?

    /// Boots parked on the admission queue because the ceiling is full and no done chat's VM could be
    /// reclaimed. Each is resumed `true` the moment a slot frees (a guest stopped, was reclaimed, or a
    /// boot failed), or `false` when its own wait deadline elapses. Keyed so a timeout resumes exactly
    /// its own waiter and a normal wake can drain them all. See ``awaitSlotOrTimeout(_:)``.
    private var slotWaiters: [UUID: CheckedContinuation<Bool, Never>] = [:]

    #if arch(arm64)
    /// Probe state of a live VM's native in-guest agent (the vsock channel). `unavailable` is
    /// sticky for the VM's lifetime — a base image without the agent will never grow one mid-boot,
    /// so re-probing every action would just pay the connect timeout repeatedly. A transport
    /// failure on a previously `available` channel resets to `unprobed` instead (the LaunchAgent's
    /// `KeepAlive` restarts a crashed agent, so one reconnect attempt is worth it).
    enum MacVMAgentState {
        case unprobed
        case unavailable
        case available(MacVMAgentClient)
    }
    #endif

    /// Held while at least one VM is live, to keep the host app from being App-Napped / auto-terminated
    /// out from under a running guest (mirrors ``ContainerEngine``'s assertion; the runtime is
    /// daemonless so the app process bounds every VM's life). `nil` when nothing is live.
    private var backgroundActivity: (any NSObjectProtocol)?

    /// Progress of the one-time base-image build (download / install / provision), surfaced to the VM
    /// settings panel. `nil` in the steady state (base already built, nothing installing). Mutated only
    /// on the actor; read via ``currentBaseProgress()``.
    var baseProgress: MacVMBaseProgress?
    /// True while a base-image build is in flight, so a second ``buildBaseImage`` call can't remove the
    /// shared `base.building` temp bundle out from under the running installer. Actor-guarded.
    var baseBuilding = false
    /// True while the base is booted writable for a **Recovery** session (`csrutil disable`), set via
    /// ``beginBaseRecovery()``/``endBaseRecovery()``. Both flags block cutting a fresh session clone
    /// from the base, since cloning reads the base disk that would then be mid-write.
    var baseRecoveryActive = false
    /// True while the base is booted writable to **inject user apps** into its `/Applications`
    /// (``installAppsIntoBase(appPaths:)`` — the "add an app without a full rebuild" path). Like the
    /// build/Recovery flags it must block cutting a fresh clone while the base disk is mid-write.
    var baseAppInstalling = false
    /// Whether the operator asked to watch the base-build VM in a diagnostic monitor window. Applied
    /// when the provisioning surface attaches, and toggled live via ``setBaseProvisionObserver(visible:)``.
    var baseObserverRequested = false

    /// The system-managed base-image VM currently booted for maintenance (injecting user apps without
    /// a full rebuild), surfaced to the Control panel's VM section so the running base is visible
    /// alongside the per-session guests. `nil` in the steady state. Actor-guarded; read via
    /// ``baseMaintenanceVM()``.
    var baseMaintenance: MacVMMaintenanceInfo?

    /// The base disk is being written (a build/provisioning pass, a Recovery session, or an app
    /// injection), so it must not be read for a copy-on-write clone right now (a torn read would
    /// corrupt the clone).
    var baseIsBusy: Bool { baseBuilding || baseRecoveryActive || baseAppInstalling }

    /// Host-side computer-use surface (VZVirtualMachineView framebuffer capture + HID injection),
    /// injected by the app layer at startup. `nil` in headless/spike contexts. Bound to computer-use
    /// VMs (main-queue) for the agent, and to the base-build VM to drive the HID provisioning bootstrap.
    var surfaceHost: (any MacVMSurfaceHost)?

    /// Wire the app-layer host-IO surface (called once at startup, like the manager injection).
    public func setSurfaceHost(_ host: any MacVMSurfaceHost) { surfaceHost = host }

    /// Wire the admission/eviction policy (called once at startup, like ``setSurfaceHost(_:)``). Without
    /// it the ceiling stays a hard throw; with it, a boot at the ceiling reclaims the stalest *done*
    /// chat's VM, or queues when every slot is a busy chat. See ``evictionRanker`` and ``ensureRunning(_:)``.
    public func setEvictionRanker(_ ranker: @escaping @Sendable ([String]) async -> [String]) {
        evictionRanker = ranker
    }

    public init(storageRoot: URL? = nil) {
        self.storageRoot = storageRoot ?? Self.defaultStorageRoot()
    }

    // MARK: - Capability

    /// Whether the in-process macOS-guest runtime can run on this machine. `true` iff
    /// ``unsupportedReason`` is `nil`.
    public static var isSupported: Bool { unsupportedReason == nil }

    /// A specific, actionable reason the macOS-VM runtime is unavailable, or `nil` when supported.
    /// The hard gate is the CPU architecture: Apple's Mac-guest APIs (`VZMac*`) exist only on Apple
    /// silicon — an Intel host cannot create a macOS guest at all. The virtualization *entitlement* is
    /// undetectable here (a missing one surfaces as a `startFailed` when the first VM boots); the
    /// deployment floor (macOS 26) is enforced by the build itself.
    public static var unsupportedReason: String? {
        #if arch(arm64)
        return nil
        #else
        return "Running macOS guests requires Apple silicon."
        #endif
    }

    static func defaultStorageRoot() -> URL {
        let base = (try? FileManager.default.url(
            for: .applicationSupportDirectory, in: .userDomainMask,
            appropriateFor: nil, create: true))
            ?? URL(fileURLWithPath: NSTemporaryDirectory())
        return base.appendingPathComponent("Nucleic/macvms", isDirectory: true)
    }

    // MARK: - On-disk layout

    /// Directory holding per-session clone bundles.
    var instancesDir: URL { storageRoot.appendingPathComponent("instances", isDirectory: true) }
    /// The golden base bundle the engine builds when the user hasn't supplied a prebuilt one.
    var builtBaseDir: URL { storageRoot.appendingPathComponent("base", isDirectory: true) }
    /// The golden **Linux** base bundle (kernel + initrd + provisioned ext4 rootfs).
    var builtLinuxBaseDir: URL { storageRoot.appendingPathComponent("linux-base", isDirectory: true) }
    /// Cached Linux boot artifacts (downloaded kernel `Image`, initrd, Ubuntu Base rootfs tarball),
    /// version-keyed by source filename like the restore-image cache.
    var linuxArtifactsDir: URL { storageRoot.appendingPathComponent("linux-artifacts", isDirectory: true) }
    /// Cached restore images (`.ipsw`).
    var restoreDir: URL { storageRoot.appendingPathComponent("restore", isDirectory: true) }
    /// Small persisted credentials the base build needs (currently just the generated `agent`-account
    /// password used to prime sudo during provisioning). No SSH keys — the control plane is vsock.
    var credentialsDir: URL { storageRoot.appendingPathComponent("credentials", isDirectory: true) }

    /// Per-session clone bundle for `name`.
    func instanceBundle(for name: String) -> MacVMBundle {
        MacVMBundle(root: instancesDir.appendingPathComponent(name, isDirectory: true))
    }

    // MARK: - Base-image progress

    /// The one-time base-image build currently in flight, for the VM settings panel. `nil` once the
    /// golden base exists. A trivial property read (no I/O).
    public func currentBaseProgress() -> MacVMBaseProgress? { baseProgress }

    // MARK: - Lifecycle

    /// Ensure the named macOS VM is running and return its name plus the guest's SSH-reachable IP.
    /// Idempotent: a VM already live is returned as-is; a boot already in flight for this name is
    /// *joined* (not duplicated); otherwise the golden base is cloned copy-on-write, booted on its own
    /// queue, and awaited until SSH answers.
    ///
    /// The whole dedup + concurrency-ceiling check runs synchronously on this actor before the first
    /// `await`, so it is atomic: two concurrent calls for the same name can't double-boot the same
    /// writable disk, and a burst of distinct-name calls can't blow past ``MacVMSettings/maxConcurrentVMs``.
    @discardableResult
    public func ensureRunning(_ spec: MacVMSpec) async throws -> (name: String, ipAddress: String) {
        lastSpec[spec.name] = spec
        if let existing = live[spec.name], let ip = existing.ipAddress {
            // Transparently thaw a VM the agent suspended (`*_vm_control suspend`) before handing it
            // back, so exec/computer-use never has to know it was frozen.
            if existing.paused { _ = await resume(name: spec.name) }
            return (spec.name, ip)
        }
        // Join an in-flight boot for the same name rather than starting a second one.
        if let inFlight = boots[spec.name] {
            return try await inFlight.value
        }
        guard Self.isSupported else {
            throw MacVMError.unavailable(Self.unsupportedReason ?? "unsupported")
        }
        // Admit the boot past the concurrent-guest ceiling. macOS caps simultaneous guests; overshooting
        // would fail an opaque `VZVirtualMachine.start`, so a boot at the ceiling must free a slot first.
        // Policy (injected via `evictionRanker`): reclaim the stalest *done* chat's VM — it's in its
        // post-completion idle-hold window anyway — and only if none can be reclaimed park in the
        // admission queue until a slot frees or the wait deadline elapses. The ceiling test below and the
        // slot reservation that follows it straddle no `await`, so they stay atomic per call: two callers
        // can't both read a free slot and both reserve it, and a parked waiter re-tests before reserving.
        //
        // With no admission policy wired (headless/spike embedders), keep the original behavior: a boot at
        // the ceiling throws immediately rather than evicting or queueing — there is no chat state to rank
        // by, and nothing to reclaim.
        let limit = MacVMSettings.maxConcurrentVMs
        if evictionRanker == nil, live.count + boots.count >= limit {
            throw MacVMError.concurrencyLimit(limit)
        }
        let deadline = Date().addingTimeInterval(TimeInterval(MacVMSettings.vmQueueTimeoutSeconds))
        while live.count + boots.count >= limit {
            // Another call may have booted or joined this exact VM while we were parked — re-honor the
            // dedup/reuse fast paths before consuming a slot for a duplicate of a VM that now exists.
            if let existing = live[spec.name], let ip = existing.ipAddress {
                if existing.paused { _ = await resume(name: spec.name) }
                return (spec.name, ip)
            }
            if let inFlight = boots[spec.name] { return try await inFlight.value }
            // Reclaim the stalest done chat's VM if the policy offers one; its clone stays on disk, so the
            // evicted chat reboots transparently on its next call (exactly like an idle-timer stop) — and
            // so, like that stop, this one is graceful. The victim is a *done* chat, hence typically
            // suspended and quick to halt; the grace period is bounded well inside `vmQueueTimeoutSeconds`,
            // so a wedged victim can't stall the boot we're queued behind past its own ceiling error.
            if let victim = await pickEvictable(excluding: spec.name) {
                await shutdown(
                    name: victim, timeout: TimeInterval(MacVMSettings.gracefulShutdownTimeoutSeconds))
                continue
            }
            // Nothing reclaimable — every occupied slot is an actively-working chat. Queue until one
            // frees, giving up with the ceiling error once the bounded wait is exhausted so a tool call
            // can't hang forever behind busy peers.
            if Date() >= deadline { throw MacVMError.concurrencyLimit(limit) }
            _ = await awaitSlotOrTimeout(deadline.timeIntervalSinceNow)
        }
        // Reserve the slot by publishing the boot task synchronously, then await it. The task inherits
        // this actor's isolation, so `performBoot` runs serialized on the actor.
        let task = Task { try await self.performBoot(spec) }
        boots[spec.name] = task
        defer { boots[spec.name] = nil }
        return try await task.value
    }

    /// The actual boot: clone the golden base, build the VZ config, start the guest on its own queue,
    /// wire the guest-stop hook, and wait until SSH answers. Only reached via ``ensureRunning``'s
    /// dedup/ceiling gate, so it never double-boots a name or overshoots the ceiling.
    private func performBoot(_ spec: MacVMSpec) async throws -> (name: String, ipAddress: String) {
        #if arch(arm64)
        // Resolve (building if necessary) the golden base for this guest OS, then clone it.
        let base = try await ensureBaseBundle(for: spec.os)
        let bundle = instanceBundle(for: spec.name)
        let mac: String
        if bundle.isComplete(for: spec.os) {
            // Reuse an existing clone (idle-restart / relaunch): its identity + MAC persist on disk.
            // This touches only the clone's own disk, so it's safe even while the base is busy.
            mac = (try? String(contentsOf: bundle.macAddressURL, encoding: .utf8))?
                .trimmingCharacters(in: .whitespacesAndNewlines) ?? Self.randomMAC()
        } else {
            // Cutting a fresh clone READS the base disk — refuse while it's booted writable (a build/
            // provisioning pass or a Recovery session), so the clone can't capture a torn state.
            guard !baseIsBusy else {
                throw MacVMError.baseBusy(
                    "the base image is being provisioned or edited — retry once it's done")
            }
            mac = try cloneBase(base, to: bundle, os: spec.os)
        }

        let config: VZVirtualMachineConfiguration
        do {
            config = try Self.makeConfiguration(
                bundle: bundle, os: spec.os, cpus: spec.cpus, memoryGiB: spec.memoryGiB, mac: mac,
                mounts: spec.mounts)
        } catch {
            throw MacVMError.startFailed("configuration: \(error)")
        }

        let instance = MacVMInstance(
            configuration: config, label: spec.name, mainQueue: spec.computerUse)
        do {
            try await instance.start()
        } catch {
            throw MacVMError.startFailed(String(describing: error))
        }

        let memoryBytes = UInt64(spec.memoryGiB) * 1024 * 1024 * 1024
        live[spec.name] = LiveVM(
            instance: instance, os: spec.os, bundle: bundle, macAddress: mac, sshUser: spec.sshUser,
            ipAddress: nil, memoryCeilingBytes: memoryBytes, startedAt: Date(),
            computerUse: spec.computerUse)
        syncBackgroundActivity()

        // Bind the host-side computer-use surface (framebuffer capture + HID) to this VM, so the
        // agent can see/drive it with no in-guest software, no TCC, no SIP. Attaching right after
        // start (before SSH is even up) lets the surface render the boot/login screen too.
        if spec.computerUse, let host = surfaceHost {
            await host.attach(
                name: spec.name, virtualMachine: UncheckedSendableBox(value: instance.vm as AnyObject))
        }
        // A guest that powers itself off (in-guest `shutdown`, panic, OOM) must not linger in `live`
        // — that would leak a concurrency slot and pin the app-activity assertion. Clear it when the
        // VZ delegate reports the stop. The hook hops back onto this actor.
        let name = spec.name
        instance.setOnStop { [weak self] in
            Task { await self?.handleGuestStopped(name) }
        }

        // Wait until the in-guest agent answers over vsock — the readiness gate. First boot of a fresh
        // clone can take a couple of minutes (login window + launchd + the agent's LaunchAgent); reuse
        // is quicker.
        do {
            try await awaitAgentReady(name: spec.name)
            // Re-create the repo's original host path inside the guest (bind-mount on Linux, symlink on
            // macOS) so build artifacts that hardcode an absolute path still resolve — see
            // MacVMEngine+HostPaths. Idempotent + best-effort: never fails the boot.
            await recreateHostPaths(spec)
            // Best-effort NAT IP for the settings-panel display only; never gates readiness.
            let ip = Self.leaseIP(forMAC: mac) ?? ""
            live[spec.name]?.ipAddress = ip.isEmpty ? nil : ip
            return (spec.name, ip)
        } catch {
            // The VM came up but the agent never answered — tear it back down so a retry starts clean.
            await instance.stop()
            live[spec.name] = nil
            syncBackgroundActivity()
            signalSlotFreed()  // the reserved slot is freed by this failure — wake a queued boot
            throw error
        }
        #else
        throw MacVMError.unavailable(Self.unsupportedReason ?? "requires Apple silicon")
        #endif
    }

    /// Drop a VM the guest stopped on its own (VZ delegate → ``MacVMInstance/setOnStop(_:)``), so a
    /// crashed/powered-off guest doesn't keep consuming a concurrency slot or hold the app-activity
    /// assertion. Idempotent: a no-op if the engine already tore it down.
    private func handleGuestStopped(_ name: String) {
        guard let entry = live[name] else { return }
        live[name] = nil
        syncBackgroundActivity()
        signalSlotFreed()
        if entry.computerUse { Task { await surfaceHost?.detach(name: name) } }
        #if arch(arm64)
        if case .available(let client) = entry.agent {
            Task { await client.close() }
        }
        #endif
    }

    // MARK: - Admission queue (concurrent-VM ceiling)

    /// Resume every boot parked on the admission queue: a slot just freed (a guest stopped, was
    /// reclaimed, or a boot failed), so each parked caller re-tests the ceiling and either reserves the
    /// slot or re-parks. Waking all rather than one is harmless at the small VM ceiling and avoids a
    /// lost wakeup when the freed slot is immediately re-taken — a needlessly-woken waiter just re-parks.
    private func signalSlotFreed() {
        guard !slotWaiters.isEmpty else { return }
        let waiters = slotWaiters
        slotWaiters.removeAll()
        for cont in waiters.values { cont.resume(returning: true) }
    }

    /// Park the caller on the admission queue until a slot frees (resumed `true` by ``signalSlotFreed()``)
    /// or `timeout` seconds elapse (resumed `false`). Actor-isolated: the continuation is stored and
    /// later resumed on this actor. A detached timer resumes the still-parked waiter at the deadline; a
    /// waiter already woken by a freed slot is a no-op for that timer (it was removed on wake).
    private func awaitSlotOrTimeout(_ timeout: TimeInterval) async -> Bool {
        let id = UUID()
        return await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
            slotWaiters[id] = cont
            Task { [weak self] in
                try? await Task.sleep(nanoseconds: UInt64(max(0, timeout) * 1_000_000_000))
                await self?.expireSlotWaiter(id)
            }
        }
    }

    /// Deadline hook for ``awaitSlotOrTimeout(_:)``: resume the still-parked waiter `id` with `false`.
    /// A no-op if it was already woken (and removed) by a freed slot.
    private func expireSlotWaiter(_ id: UUID) {
        slotWaiters.removeValue(forKey: id)?.resume(returning: false)
    }

    /// Pick the stalest *done* chat's VM to reclaim for a boot that would otherwise exceed the ceiling,
    /// excluding the requesting session's own VM. Delegates the chat-state ranking to the injected
    /// ``evictionRanker`` (only the app layer knows which chats are done and how recently each was used);
    /// returns `nil` when no policy is wired or nothing is safe to evict (so the caller queues instead).
    private func pickEvictable(excluding requester: String) async -> String? {
        guard let evictionRanker else { return nil }
        let occupying = live.keys.filter { $0 != requester }
        guard !occupying.isEmpty else { return nil }
        return await evictionRanker(Array(occupying)).first
    }

    /// Run `argv` inside the named guest **over vsock** (the in-guest agent's streaming `exec` op),
    /// returning a ``ProcessHandle`` whose stdio is the remote command's stdio. The agent's mac-VM
    /// tool treats it identically to a host or containerized process — see ``MacVMExecChannel``.
    public func exec(
        name: String, workdir: String?, env: [String: String], argv: [String]
    ) async throws -> any ProcessHandle {
        let body = argv.map(Self.shQuote).joined(separator: " ")
        return try await execHandle(name: name, workdir: workdir, env: env, remoteBody: body)
    }

    /// Convenience one-shot used by the `mac_vm_exec` tool: run a shell `command` inside the guest and
    /// return its exit code plus captured stdout/stderr — the macOS analogue of `runOnHost`.
    public func run(
        name: String, command: String, workdir: String?, env: [String: String] = [:]
    ) async throws -> (exitCode: Int32, stdout: String, stderr: String) {
        let handle = try await execHandle(name: name, workdir: workdir, env: env, remoteBody: command)
        // Drain both streams concurrently, then await exit — a big build must not deadlock on a full
        // stderr pipe while we read stdout (or vice versa).
        async let out = Self.collect(handle.stdoutLines)
        async let err = Self.collect(handle.stderrLines)
        let stdout = await out
        let stderr = await err
        let code = await handle.wait()
        return (code, stdout, stderr)
    }

    /// Run a command in the guest over SSH and return its stdout up to `maxBytes`, plus whether it was
    /// truncated — for a base64 payload (a screenshot) that ``run``'s 64 KB cap would mangle but that
    /// still needs a ceiling so a pathological capture can't blow up host memory / the model context.
    /// A truncated base64 is unusable, so the caller discards it. Ignores exit code; stderr discarded.
    func rawStdout(
        name: String, command: String, maxBytes: Int = 8 * 1024 * 1024
    ) async throws -> (text: String, truncated: Bool) {
        let handle = try await execHandle(name: name, workdir: nil, env: [:], remoteBody: command)
        let result = await Self.collectBounded(handle.stdoutLines, cap: maxBytes)
        _ = await handle.wait()
        return result
    }

    /// Drain a line stream into one string up to `cap` bytes, reporting whether the cap was hit. Unlike
    /// ``collect`` this appends NO truncation marker (which would corrupt a base64 payload) — the caller
    /// decides what a truncated result means.
    private static func collectBounded(
        _ stream: AsyncThrowingStream<Data, Error>, cap: Int
    ) async -> (text: String, truncated: Bool) {
        var lines: [String] = []
        var bytes = 0
        var truncated = false
        do {
            for try await line in stream {
                if bytes < cap {
                    lines.append(String(decoding: line, as: UTF8.self))
                    bytes += line.count + 1
                } else {
                    truncated = true
                }
            }
        } catch { /* stream ended on error — return what we have */ }
        return (lines.joined(separator: "\n"), truncated)
    }

    /// Force-stop the guest (freeing its host RAM) but keep the on-disk clone bundle so the next
    /// `ensureRunning` reboots it without re-installing. Drops the registry entry; best-effort.
    ///
    /// Cuts virtual power without letting the guest quiesce, so call it directly only when the clone is
    /// about to be deleted (``remove(name:)``) or the boot already failed. When the clone will be booted
    /// again, go through ``shutdown(name:timeout:)``, which asks the guest to halt first and falls back
    /// to this.
    public func stop(name: String) async {
        guard let entry = live[name] else { return }
        if entry.computerUse { await surfaceHost?.detach(name: name) }
        #if arch(arm64)
        if case .available(let client) = entry.agent { await client.close() }
        await entry.instance.stop()
        #endif
        live[name] = nil
        syncBackgroundActivity()
        signalSlotFreed()  // a slot just freed — wake a boot parked on the admission queue
    }

    /// Shut the guest down the way a real machine powers off: ask it to halt itself, give it a bounded
    /// grace period, and cut power only if it refuses or overruns. Use this — not ``stop(name:)`` —
    /// wherever the on-disk clone **survives and is rebooted later**, because ``stop(name:)`` yanks
    /// virtual power from a mounted, writable root filesystem, and the next `ensureRunning` then boots
    /// that unclean filesystem. (``remove(name:)``/teardown may still stop forcefully: the clone is
    /// about to be deleted, so there is nothing left to corrupt and no reason to wait.)
    ///
    /// A *suspended* guest is thawed first: VZ reports `canRequestStop == false` while paused, so the
    /// request would otherwise be refused and we'd silently fall back to cutting power — precisely the
    /// state a Done chat's VM sits in when its idle timer fires.
    public func shutdown(name: String, timeout: TimeInterval) async {
        #if arch(arm64)
        if let entry = live[name] {
            if entry.paused { _ = await resume(name: name) }
            if await entry.instance.requestStop() {
                _ = await entry.instance.waitUntilStopped(timeout: timeout)
            }
        }
        #endif
        // Either the guest halted itself (VZ then reports `canStop == false`, so the forceful stop
        // inside `stop(name:)` no-ops) or it refused/overran and we cut power here. Either way this
        // performs the registry cleanup — idempotently with the `handleGuestStopped` hook a clean
        // power-off fires, since both guard on `live[name]` and only one can win.
        await stop(name: name)
    }

    /// Suspend (pause/freeze) the named running guest in place: it keeps its host RAM but stops
    /// consuming CPU, and ``resume(name:)`` thaws it instantly. Cheaper to resume than a ``stop``→boot,
    /// but (unlike stop) does NOT free the guest's RAM. Returns `true` once paused; `false` when the VM
    /// isn't running or the guest couldn't be paused.
    @discardableResult
    public func suspend(name: String) async -> Bool {
        #if arch(arm64)
        guard let entry = live[name] else { return false }
        let ok = await entry.instance.pause()
        if ok { live[name]?.paused = true }
        return ok
        #else
        return false
        #endif
    }

    /// Resume (thaw) a suspended guest. Returns `true` once resumed; `false` when the VM isn't running
    /// or wasn't paused.
    @discardableResult
    public func resume(name: String) async -> Bool {
        #if arch(arm64)
        guard let entry = live[name] else { return false }
        let ok = await entry.instance.resume()
        if ok { live[name]?.paused = false }
        return ok
        #else
        return false
        #endif
    }

    /// Whether the named VM is currently paused (suspended). `false` when it isn't running.
    public func isPaused(_ name: String) async -> Bool { live[name]?.paused ?? false }

    /// Stop then re-boot the guest in place (reuses the clone; reclaims the host RAM a long-running VM
    /// holds). No-op if the engine never saw this VM. Shuts down gracefully — the clone this reboots is
    /// the very filesystem a forceful stop would leave unclean.
    public func restart(name: String) async {
        let grace = TimeInterval(MacVMSettings.gracefulShutdownTimeoutSeconds)
        guard let spec = lastSpec[name] else { await shutdown(name: name, timeout: grace); return }
        await shutdown(name: name, timeout: grace)
        _ = try? await ensureRunning(spec)
    }

    /// Stop the guest and delete its on-disk clone bundle. Returns `true` once it's gone (including
    /// when it never existed). Best-effort.
    @discardableResult
    public func remove(name: String) async -> Bool {
        await stop(name: name)
        lastSpec[name] = nil
        let root = instanceBundle(for: name).root
        try? FileManager.default.removeItem(at: root)
        return !FileManager.default.fileExists(atPath: root.path)
    }

    /// True if the named VM is currently running in this process.
    public func isRunning(_ name: String) async -> Bool { live[name] != nil }

    /// True if the VM is running, or has a persisted clone bundle on disk (stopped but re-bootable).
    /// OS-agnostic: a clone directory holds either a macOS or a Linux guest, so accept either shape.
    public func vmExists(_ name: String) async -> Bool {
        if live[name] != nil { return true }
        let bundle = instanceBundle(for: name)
        return bundle.isComplete(for: .macOS) || bundle.isComplete(for: .linux)
    }

    /// Names of all macOS VMs currently running in this process.
    public func list() async -> [String] { Array(live.keys) }

    /// Running VMs with their discovered IPs, for the settings panel and the Control panel. Carries
    /// each guest's OS so the Control panel's Virtual Machines section can label it macOS vs. Linux.
    public func runningVMs() async -> [MacVMEntry] {
        live.map { name, vm in
            MacVMEntry(name: name, ipAddress: vm.ipAddress, os: vm.os)
        }
    }

    /// Number of macOS guests currently running — the manager consults this against the configured
    /// ceiling before booting another (macOS caps simultaneous macOS guests).
    public func runningCount() async -> Int { live.count }

    /// The system-managed base-image VM booted for maintenance right now (app injection), or `nil`
    /// when the base isn't running for that. Kept separate from ``runningVMs()`` — that list drives
    /// the settings "Running now" / "Copy to running VMs" actions, which target per-session guests,
    /// not this transient base boot.
    public func baseMaintenanceVM() async -> MacVMMaintenanceInfo? { baseMaintenance }

    /// Best-effort resource sample for the VM panel. The Virtualization framework exposes no CPU or
    /// memory figures for a macOS guest, so we probe from *inside* it over SSH: `vm_stat` for memory
    /// (instantaneous and reliable) and a two-sample `top` for CPU (the first `top` sample is
    /// meaningless, hence `-l 2`). Both ride one round-trip. Memory total is the configured ceiling
    /// (= `hw.memsize` in the guest), so it needs no probe. Returns a ceiling-only sample (used/CPU
    /// zero) when the guest isn't reachable yet, and `nil` when the VM isn't running at all.
    ///
    /// This is called off the Control panel's critical path (a background refresh that caches the
    /// result), never inline — the `top -l 2` read alone sleeps ~1 s in the guest.
    public func sampleResourceUsage(name: String) async -> MacVMResourceSample? {
        guard let entry = live[name] else { return nil }
        let total = entry.memoryCeilingBytes
        // No IP yet → still booting; skip the SSH probe and report the ceiling only.
        guard entry.ipAddress != nil else {
            return MacVMResourceSample(cpuPercent: 0, memoryUsedBytes: 0, memoryTotalBytes: total)
        }
        let probe = "vm_stat; echo __CPU__; top -l 2 -n 0 | awk '/CPU usage/{c=$0} END{print c}'"
        let stdout = (try? await run(name: name, command: probe, workdir: nil))?.stdout ?? ""
        return Self.parseResourceProbe(stdout, memoryTotal: total)
    }

    /// Parse the guest resource probe (`vm_stat` + a `top` "CPU usage" line) into a sample. Used
    /// memory is `(active + wired + compressed) × page-size`; CPU is `100 − idle%` from `top`. Pure
    /// and total-driven so it's unit-testable without a live guest — a blank/garbled probe yields a
    /// ceiling-only sample (zeros) rather than throwing.
    static func parseResourceProbe(_ output: String, memoryTotal: UInt64) -> MacVMResourceSample {
        var pageSize: UInt64 = 4096
        var active: UInt64 = 0, wired: UInt64 = 0, compressed: UInt64 = 0
        var cpuPercent = 0.0

        for raw in output.split(separator: "\n") {
            let line = raw.trimmingCharacters(in: .whitespaces)
            // `vm_stat` header carries the page size, e.g. "… (page size of 16384 bytes)".
            if line.contains("page size of"), let n = Self.trailingUInt(in: line) { pageSize = n }
            else if line.hasPrefix("Pages active:"), let n = Self.trailingUInt(in: line) { active = n }
            else if line.hasPrefix("Pages wired down:"), let n = Self.trailingUInt(in: line) { wired = n }
            else if line.hasPrefix("Pages occupied by compressor:"),
                let n = Self.trailingUInt(in: line) { compressed = n }
            else if line.contains("CPU usage"), let idle = Self.idlePercent(in: line) {
                cpuPercent = max(0, 100 - idle)
            }
        }

        let used = (active &+ wired &+ compressed) &* pageSize
        return MacVMResourceSample(
            cpuPercent: cpuPercent, memoryUsedBytes: used, memoryTotalBytes: memoryTotal)
    }

    /// The integer formed by the digits at the end of a `vm_stat` line (e.g. "Pages active: 123456."
    /// → 123456, "…page size of 16384 bytes)" picks up 16384 since it's the only run of digits).
    private static func trailingUInt(in line: String) -> UInt64? {
        let digits = String(line.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0) })
        return UInt64(digits)
    }

    /// The idle percentage from a `top` "CPU usage: 4.76% user, 9.52% sys, 85.71% idle" line.
    private static func idlePercent(in line: String) -> Double? {
        guard let range = line.range(of: "% idle") else { return nil }
        let number = line[..<range.lowerBound]
            .reversed().prefix { $0.isNumber || $0 == "." }.reversed()
        return Double(String(number))
    }

    // MARK: - Host-side computer-use surface (SIP-free framebuffer capture + HID injection)

    /// Whether `name` has a bound host-side surface — a computer-use VM booted with the app's surface
    /// injected. When true, the computer-use dispatch drives it host-side (no in-guest agent/TCC/SIP).
    func hasComputerSurface(_ name: String) -> Bool {
        surfaceHost != nil && (live[name]?.computerUse ?? false)
    }

    /// Host-side framebuffer capture (JPEG base64, 1920×1200) for a computer-use VM, or `nil`.
    func captureSurfaceBase64(name: String) async -> String? {
        guard hasComputerSurface(name), let data = await surfaceHost?.capture(name: name) else {
            return nil
        }
        return data.base64EncodedString()
    }

    /// Inject one host-side input action into a computer-use VM (no-op without a surface).
    func sendSurfaceInput(name: String, _ input: MacVMSurfaceInput) async {
        guard hasComputerSurface(name) else { return }
        await surfaceHost?.send(name: name, input)
    }

    /// Host-tracked pointer position for a computer-use VM (we synthesize the moves).
    func surfaceCursor(name: String) async -> Point? {
        guard hasComputerSurface(name) else { return nil }
        return await surfaceHost?.cursorPosition(name: name)
    }

    /// Show/hide the operator-assist viewer for a computer-use VM: bring its live, interactive
    /// `VZVirtualMachineView` on-screen so the user can drive the guest by hand (`mac_vm_request_operator`),
    /// or send it back off-screen. No-op without a computer-use surface for `name`.
    func presentOperatorAssist(name: String, visible: Bool) async {
        guard hasComputerSurface(name) else { return }
        await surfaceHost?.presentOperatorAssist(name: name, visible: visible)
    }

    /// Current GUI framebuffer of the named guest as a base64 JPEG, for the live **VM monitor** panel.
    /// Reuses the computer-use `screenshot` path — the native in-guest agent's ScreenCaptureKit capture
    /// when present, else SSH `screencapture` in the console user's Aqua session — but with no lifecycle
    /// effect, and returns `nil` (rather than throwing) when the VM isn't running or the capture is
    /// empty/unavailable, so a passive monitor can poll it cheaply. Like computer-use, it needs a base
    /// image provisioned for screen capture (Screen Recording TCC + auto-login); an unprovisioned guest
    /// yields `nil` and the panel shows a "no signal" placeholder.
    public func captureScreen(name: String) async -> String? {
        guard live[name] != nil else { return nil }
        return try? await performComputerAction(
            name: name, action: "screenshot", x: nil, y: nil, text: nil,
            scrollDirection: nil, scrollAmount: nil, durationMs: nil
        ).imageBase64
    }

    /// On app launch, reconcile on-disk clone bundles: keep only the active sessions' clones and drop
    /// the rest (daemonless — no live VM survives the process, so this is pure on-disk GC).
    public func reconcileDisk(keepNames: Set<String>) async {
        let fm = FileManager.default
        guard let entries = try? fm.contentsOfDirectory(
            at: instancesDir, includingPropertiesForKeys: nil) else { return }
        for dir in entries where !keepNames.contains(dir.lastPathComponent) {
            try? fm.removeItem(at: dir)
        }
    }

    // MARK: - Background activity

    private func syncBackgroundActivity() {
        if !live.isEmpty, backgroundActivity == nil {
            backgroundActivity = ProcessInfo.processInfo.beginActivity(
                options: [
                    .userInitiatedAllowingIdleSystemSleep, .suddenTerminationDisabled,
                    .automaticTerminationDisabled,
                ],
                reason: "Nucleic macOS VM running")
        } else if live.isEmpty, let activity = backgroundActivity {
            ProcessInfo.processInfo.endActivity(activity)
            backgroundActivity = nil
        }
    }

    // MARK: - vsock exec plumbing

    /// Build a `ProcessHandle` for a command run inside the guest **over vsock** — a dedicated
    /// connection to the in-guest agent's streaming `exec` op (docs/MACOS_VM_NATIVE_AGENT.md §4.4). This
    /// replaced the `ssh`-over-NAT handle: no IP discovery, no host-key churn, no Remote Login, no
    /// network permission surface. The composed script is identical to what sshd used to run, so the
    /// guest's toolchain PATH (via `/etc/zshenv`) resolves the same way.
    ///
    /// Each exec gets its OWN vsock connection (the agent listener is one thread per connection), so a
    /// multi-minute build never blocks the control channel and concurrent execs don't interfere.
    private func execHandle(
        name: String, workdir: String?, env: [String: String], remoteBody: String
    ) async throws -> any ProcessHandle {
        #if arch(arm64)
        return try await openExecChannel(name: name, workdir: workdir, env: env, remoteBody: remoteBody)
        #else
        throw MacVMError.unavailable(Self.unsupportedReason ?? "requires Apple silicon")
        #endif
    }

    #if arch(arm64)
    /// Open a dedicated vsock exec channel to the named guest and dispatch `remoteBody` (wrapped with
    /// env/workdir by ``remoteScript``). Returns the concrete ``MacVMExecChannel`` so bulk callers can
    /// stream binary stdin (``MacVMExecChannel/writeStdinRaw(_:deadline:)``); ``execHandle`` erases it
    /// to `any ProcessHandle` for the ordinary exec path.
    func openExecChannel(
        name: String, workdir: String?, env: [String: String], remoteBody: String
    ) async throws -> MacVMExecChannel {
        guard let entry = live[name] else { throw MacVMError.notRunning(name) }
        return try await openExecChannel(
            instance: entry.instance, label: name, workdir: workdir, env: env, remoteBody: remoteBody)
    }

    /// Open an exec channel directly on a ``MacVMInstance`` — for VMs the engine boots OUTSIDE the
    /// `live` registry (the base app-injection boot), which have no name to look up. `label` is used
    /// only in the error message.
    func openExecChannel(
        instance: MacVMInstance, label: String, workdir: String?, env: [String: String],
        remoteBody: String
    ) async throws -> MacVMExecChannel {
        let script = Self.remoteScript(workdir: workdir, env: env, body: remoteBody)
        let box: UncheckedSendableBox<VZVirtioSocketConnection>
        do {
            box = try await withAgentTimeout(seconds: 10) {
                try await instance.connectAgent(port: MacVMAgentWire.port)
            }
        } catch {
            throw MacVMError.agentUnavailable(
                "could not open an exec channel to \"\(label)\": \(error)")
        }
        return MacVMExecChannel(connection: box, command: script)
    }
    #endif

    /// Compose the remote shell script: export the extra env, `cd` into the working directory, then
    /// run the body. The guest's toolchain PATH is made available to non-login shells by provisioning
    /// (it appends to `/etc/zshenv`), so `xcodebuild`/`brew`/`node` resolve without a login shell.
    static func remoteScript(workdir: String?, env: [String: String], body: String) -> String {
        var s = ""
        for (k, v) in env.sorted(by: { $0.key < $1.key }) {
            s += "export \(k)=\(shQuote(v)); "
        }
        if let workdir, !workdir.isEmpty {
            s += "cd \(shQuote(workdir)) || exit 1; "
        }
        s += body
        return s
    }

    /// POSIX single-quote a string for safe interpolation into a shell command.
    static func shQuote(_ s: String) -> String {
        "'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
    }

    /// Drain an async line stream into one newline-joined string, capped so a runaway build log
    /// (`xcodebuild` output is routinely multi-MB) can't blow up the tool result the agent reads back
    /// or balloon host memory — mirrors the host path's `runOnHost` capping. Keeps consuming past the
    /// cap (so the ssh pipe never blocks) but drops the overflow, appending a truncation marker.
    private static func collect(
        _ stream: AsyncThrowingStream<Data, Error>, cap: Int = 64 * 1024
    ) async -> String {
        var lines: [String] = []
        var bytes = 0
        var truncated = false
        do {
            for try await line in stream {
                if bytes < cap {
                    lines.append(String(decoding: line, as: UTF8.self))
                    bytes += line.count + 1
                } else {
                    truncated = true
                }
            }
        } catch { /* stream ended on error — return what we have */ }
        var out = lines.joined(separator: "\n")
        if truncated { out += "\n…[output truncated at \(cap / 1024) KB]" }
        return out
    }

    // MARK: - Guest readiness

    /// Wait until the in-guest agent answers a vsock `ping`, bounded. This IS the boot-readiness gate:
    /// the host↔guest control plane is vsock-only, so "the agent answers" is exactly "the guest is
    /// usable for exec + computer-use". No DHCP lease, no sshd, no IP on the critical path.
    ///
    /// On success the live `ping` channel is cached as the VM's control client, so the first
    /// computer-use op doesn't pay a second probe. Non-sticky: a failed probe just retries (unlike the
    /// lazy ``agentClient(name:)``, whose `.unavailable` is sticky for the VM's lifetime).
    func awaitAgentReady(name: String) async throws {
        #if arch(arm64)
        guard let instance = live[name]?.instance else { throw MacVMError.notRunning(name) }
        let deadline = Date().addingTimeInterval(300)  // first boot: login window + launchd + agent
        while Date() < deadline {
            if let box = try? await withAgentTimeout(seconds: 5, {
                try await instance.connectAgent(port: MacVMAgentWire.port)
            }) {
                let client = MacVMAgentClient(connection: box)
                if let pong = try? await client.request(op: "ping", timeout: 5),
                    pong["version"]?.intValue == MacVMAgentWire.version
                {
                    if live[name] != nil { live[name]?.agent = .available(client) }
                    return
                }
                await client.close()
            }
            try? await Task.sleep(nanoseconds: 2_000_000_000)
        }
        throw MacVMError.agentUnavailable(
            "the in-guest agent never answered a vsock ping for \"\(name)\" within the boot deadline")
        #else
        throw MacVMError.unavailable(Self.unsupportedReason ?? "requires Apple silicon")
        #endif
    }

    /// Look up the IPv4 address currently leased to `mac` in `/var/db/dhcpd_leases` (written by
    /// Apple's vmnet NAT DHCP server) — **display only** now (the NAT device exists purely for the
    /// guest's own outbound internet; the control plane is vsock). Reads the file, then delegates to
    /// the pure ``parseLeases``.
    static func leaseIP(forMAC mac: String) -> String? {
        guard let text = try? String(contentsOfFile: "/var/db/dhcpd_leases", encoding: .utf8)
        else { return nil }
        return parseLeases(text, forMAC: mac)
    }

    /// Pure parser for `dhcpd_leases` content: return the IPv4 leased to `mac`. MACs are compared
    /// octet-by-octet as integers, since the lease file drops leading zeros (`2:34:…`) while
    /// `VZMACAddress.string` keeps them (`02:34:…`). The last (newest) matching lease wins.
    static func parseLeases(_ text: String, forMAC mac: String) -> String? {
        let want = normalizeMAC(mac)
        var currentIP: String?
        var lastMatchIP: String?
        for rawLine in text.split(whereSeparator: \.isNewline) {
            let line = rawLine.trimmingCharacters(in: .whitespaces)
            if line == "{" { currentIP = nil }
            else if line.hasPrefix("ip_address=") {
                currentIP = String(line.dropFirst("ip_address=".count))
            } else if line.hasPrefix("hw_address=") {
                // Format: `hw_address=1,d6:a7:58:8e:78:d4` — strip the leading `<type>,`.
                var hw = String(line.dropFirst("hw_address=".count))
                if let comma = hw.firstIndex(of: ",") { hw = String(hw[hw.index(after: comma)...]) }
                if normalizeMAC(hw) == want, let ip = currentIP { lastMatchIP = ip }
            }
        }
        return lastMatchIP
    }

    /// Normalize a MAC to a colon-joined list of two-digit lowercase hex octets for comparison.
    static func normalizeMAC(_ mac: String) -> String {
        mac.split(separator: ":").compactMap { UInt8($0, radix: 16) }
            .map { String(format: "%02x", $0) }.joined(separator: ":")
    }

    /// A fresh locally-administered MAC (the U/L bit set, multicast bit clear) as a colon string.
    static func randomMAC() -> String {
        #if arch(arm64)
        return VZMACAddress.randomLocallyAdministered().string
        #else
        var octets = (0..<6).map { _ in UInt8.random(in: 0...255) }
        octets[0] = (octets[0] & 0xFE) | 0x02
        return octets.map { String(format: "%02x", $0) }.joined(separator: ":")
        #endif
    }
}

#if arch(arm64)
/// Owns one live `VZVirtualMachine` and the serial dispatch queue every VZ call must run on. VZ
/// objects are not `Sendable` and are only safe to touch on their own queue, so this box confines all
/// of that: it is `@unchecked Sendable`, never hands a VZ-typed value across its boundary, and bridges
/// the queue's completion-handler API to `async` via continuations. The engine actor holds these
/// boxes but performs every VM operation through this narrow, queue-correct surface.
final class MacVMInstance: NSObject, VZVirtualMachineDelegate, @unchecked Sendable {
    let queue: DispatchQueue
    /// The wrapped VM. Internal (not private) so the install extension in `MacVMEngine+Base` can drive
    /// `VZMacOSInstaller` against it — but ONLY ever touched on `queue`.
    let vm: VZVirtualMachine
    /// Fired (once) when the guest powers itself off or stops with an error.
    private var onStop: (@Sendable () -> Void)? = nil

    init(configuration: VZVirtualMachineConfiguration, label: String, mainQueue: Bool = false) {
        // Computer-use VMs run on the MAIN queue so a `VZVirtualMachineView` can bind to them (the
        // view + its VM must share the main thread). Safe: `init` is called from the engine actor,
        // which never runs on main, so `main.sync` below can't deadlock. Exec-only VMs keep a
        // dedicated background queue. macOS caps concurrent macOS guests at 2, so main-queue VMs are
        // not a scalability concern.
        let q = mainQueue ? DispatchQueue.main : DispatchQueue(label: "xyz.blakeslee.nucleic.macvm.\(label)")
        self.queue = q
        self.vm = q.sync { VZVirtualMachine(configuration: configuration, queue: q) }
        super.init()
        q.sync { self.vm.delegate = self }
    }

    /// Start the guest; resumes once `VZVirtualMachine.start` reports success or throws its error.
    func start() async throws {
        try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
            queue.async { [self] in
                vm.start { result in
                    switch result {
                    case .success: cont.resume()
                    case .failure(let error): cont.resume(throwing: error)
                    }
                }
            }
        }
    }

    /// Force-stop (power-off) the guest. Best-effort and idempotent — a clone is disposable, so this
    /// doesn't bother with a graceful guest shutdown request.
    func stop() async {
        await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
            queue.async { [self] in
                guard vm.canStop else { cont.resume(); return }
                vm.stop { _ in cont.resume() }
            }
        }
    }

    /// Ask the guest to shut *itself* down (VZ delivers a power-button / ACPI request, which systemd on
    /// Linux and macOS both honor) instead of cutting its virtual power. Returns `false` when the guest
    /// won't accept the request — notably **while paused**, where VZ reports `canRequestStop == false`,
    /// so thaw a suspended guest before calling this. Pair with ``waitUntilStopped(timeout:)``: the
    /// request only *initiates* the shutdown, it doesn't wait for it.
    @discardableResult
    func requestStop() async -> Bool {
        await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
            queue.async { [self] in
                guard vm.canRequestStop else { cont.resume(returning: false); return }
                do { try vm.requestStop(); cont.resume(returning: true) }
                catch { cont.resume(returning: false) }
            }
        }
    }

    /// Poll (bounded) until the guest reaches `.stopped`, returning whether it got there in time. Polls
    /// rather than hooking `setOnStop`, because a *live* VM's one-shot stop hook already belongs to the
    /// engine's ``MacVMEngine/handleGuestStopped(_:)`` registry cleanup and must not be stolen.
    func waitUntilStopped(timeout: TimeInterval) async -> Bool {
        let deadline = Date().addingTimeInterval(timeout)
        while Date() < deadline {
            let stopped = await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
                queue.async { [self] in cont.resume(returning: vm.state == .stopped) }
            }
            if stopped { return true }
            try? await Task.sleep(nanoseconds: 250_000_000)
        }
        return false
    }

    /// Pause (freeze) the running guest in place: it stays resident in host RAM but consumes no CPU,
    /// and ``resume()`` thaws it instantly (much cheaper than a stop→reboot, but — unlike stop — it does
    /// NOT give the RAM back). Best-effort/idempotent: a no-op returning `false` when the guest can't
    /// pause (already paused, or not in a pausable state). Runs on `queue`, like every other VZ call.
    @discardableResult
    func pause() async -> Bool {
        await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
            queue.async { [self] in
                guard vm.canPause else { cont.resume(returning: false); return }
                vm.pause { result in
                    if case .success = result { cont.resume(returning: true) }
                    else { cont.resume(returning: false) }
                }
            }
        }
    }

    /// Resume (thaw) a paused guest. Best-effort/idempotent: a no-op returning `false` when the guest
    /// isn't in a resumable (paused) state.
    @discardableResult
    func resume() async -> Bool {
        await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
            queue.async { [self] in
                guard vm.canResume else { cont.resume(returning: false); return }
                vm.resume { result in
                    if case .success = result { cont.resume(returning: true) }
                    else { cont.resume(returning: false) }
                }
            }
        }
    }

    /// Register a one-shot hook fired when the guest stops on its own (power-off / panic / error).
    /// Assigned on `queue` so `onStop` stays confined to the VM queue — the same queue the delegate
    /// callbacks (hence `fireStop`) run on, so there's no cross-thread access to the reference.
    func setOnStop(_ hook: @escaping @Sendable () -> Void) {
        queue.async { [self] in onStop = hook }
    }

    // VZVirtualMachineDelegate — the guest stopped on its own or errored. Fire the hook (once).
    // Delivered by VZ on `queue`, so `onStop` is read/cleared here on its owning queue.
    func guestDidStop(_ virtualMachine: VZVirtualMachine) { fireStop() }
    func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
        fireStop()
    }

    private func fireStop() {
        let hook = onStop
        onStop = nil
        hook?()
    }
}
#endif
