import CoreGraphics
import Foundation
import os

#if arch(arm64)
import Virtualization
#endif

private let policyLog = Logger(subsystem: "com.nucleic", category: "macvm.policygrants")

// MARK: - Lane B — deterministic datastore policy grants under transient SIP-off
//
// docs/MACOS_VM_POLICY_ALTERNATIVE.md. Instead of enrolling the base in a local MDM to deliver a PPPC
// profile (Lane A — broken on macOS 27 by the apsd/APNs regression, and dependent on OCR-driven
// enrollment approval), Lane B grants everything by writing the system's own datastores directly:
//
//   1. Recovery boot → `csrutil disable`     (host drives recoveryOS Terminal via the display surface)
//   2. Normal boot   → write the agent's TCC rows into the system TCC.db + disable notification sources,
//                      then read them back    (deterministic, over the vsock exec channel; no OCR)
//   3. Recovery boot → `csrutil enable`
//   4. Normal boot   → confirm the rows SURVIVED re-enabling SIP  (Lane B's load-bearing assumption)
//
// Every step's success is asked of the GUEST (a `csrutil status` line, a `TCC.db` read-back), never of
// the screen — the recovery HID is only how the one fixed `csrutil` command is typed. The sealed base
// ships SIP-ON with the grants baked in. Strictly more capable than Lane A: a TCC row can grant Screen
// Recording, which no MDM PPPC payload can ever grant silently.
extension MacVMEngine {
    /// Surface/VM label for the SIP-off datastore write + read-back boots.
    public static let basePolicyGrantsSurfaceName = "base-policy-grants"
    /// Surface/VM label for the recoveryOS `csrutil` toggle boots.
    public static let baseRecoverySurfaceName = "base-recovery"

    /// The exact TCC services Lane B writes for the agent bundle id, and the read-back gate asserts.
    /// FDA + Network Volumes remove the "Data Access Blocked" banner and the virtioFS custom-mount
    /// prompt (the "mac_vm_exec never returns" hang); Screen Recording is the one grant no MDM PPPC
    /// payload can silently deliver; Accessibility + PostEvent are the semantic AX agent's control +
    /// input. Kept in lockstep with `scripts/macvm-policy-grants.sh`'s `SERVICES`.
    static let policyGrantServices = [
        "kTCCServiceSystemPolicyAllFiles",
        "kTCCServiceSystemPolicyNetworkVolumes",
        "kTCCServiceScreenCapture",
        "kTCCServiceAccessibility",
        "kTCCServicePostEvent",
    ]

    /// Vocabulary for the recoveryOS OCR pass — the labels the loop must find to reach Terminal, plus
    /// the ones it must recognize to advance a language / user-selection screen. Single words (Vision
    /// matches `customWords` at the word stage), lowercased like ``approvalLexicon``.
    static let recoveryLexicon: Set<String> = [
        "recovery", "utilities", "terminal", "options", "continue", "reinstall", "macos",
        "disk", "utility", "restore", "safari", "language", "select", "user", "password",
        "administrator", "admin", "agent", "next", "unlock",
        // The Apple-silicon startup picker `startUpFromMacOSRecovery` lands on.
        "macintosh", "hd", "shut", "down", "restart", "startup",
    ]

    /// The bundled `scripts/macvm-policy-grants.sh` (packaged under `Resources/macvm/`), or, for a
    /// dev/SwiftPM run, the copy in the source tree. Mirrors ``resolveProvisionScript()``.
    func resolvePolicyGrantsScript() -> URL? {
        if let bundled = Bundle.main.resourceURL?
            .appendingPathComponent("macvm/macvm-policy-grants.sh"),
            FileManager.default.fileExists(atPath: bundled.path)
        {
            return bundled
        }
        return Self.devRepoRoot()?.appendingPathComponent("scripts/macvm-policy-grants.sh")
    }

    // MARK: - Pure helpers (unit-tested)

    /// Compose the vsock `remoteBody` that runs `macvm-policy-grants.sh` in the guest without staging a
    /// file or mounting a share (which would itself trip the Network Volumes prompt this is granting):
    /// the whole script arrives base64'd on stdin and is piped into `sudo bash`, with parameters passed
    /// as `sudo`-set environment variables — so nothing sensitive rides a command line, and the script
    /// body is never word-split.
    ///
    /// `csreqHex` is the agent's compiled designated-requirement blob as hex (from
    /// ``designatedRequirementData(forAgentAt:)``), or `nil` for an ad-hoc build with no requirement —
    /// then the rows are written with `csreq` NULL and `requireCsreq` must be false.
    static func policyGrantsRemoteBody(
        scriptBase64: String, csreqHex: String?, requireCsreq: Bool, user: String, verifyOnly: Bool
    ) -> String {
        let hex = csreqHex ?? ""
        let env =
            "NUCLEIC_VMAGENT_CSREQ_HEX=\(shQuote(hex)) "
            + "NUCLEIC_REQUIRE_CSREQ=\(requireCsreq ? "1" : "0") "
            + "NUCLEIC_POLICY_GRANTS_VERIFY_ONLY=\(verifyOnly ? "1" : "0") "
            + "NUCLEIC_AGENT_USER=\(shQuote(user))"
        return "printf %s \(shQuote(scriptBase64)) | /usr/bin/base64 --decode "
            + "| /usr/bin/sudo \(env) /bin/bash"
    }

    /// What the guest reported back from `macvm-policy-grants.sh` — parsed from its stdout markers,
    /// never its exit code (a partial failure must name the row that broke).
    struct PolicyGrantsReadback: Sendable, Equatable {
        /// From the `SIP=` marker: `true` = SIP disabled this boot, `false` = enabled, `nil` = not reported.
        var sipDisabled: Bool?
        /// service → (auth_value string, whether its `csreq` blob is non-null).
        var grants: [String: GrantRow] = [:]
        /// The script's own verdict (`POLICY_GRANTS_OK`).
        var ok = false
        /// The reason string after `POLICY_GRANTS_FAIL:` when not OK.
        var failReason: String?
        /// notification-source label → state (`disabled`/`present`/…) — logged, never gated.
        var notif: [String: String] = [:]

        struct GrantRow: Sendable, Equatable {
            var auth: String
            var hasCsreq: Bool
        }

        /// A compact one-line summary for the log/error.
        var summary: String {
            let rows = grants.keys.sorted().map { key -> String in
                let row = grants[key]!
                return "\(key)=\(row.auth)\(row.hasCsreq ? "+req" : "")"
            }
            return "sip=\(sipDisabled.map(String.init) ?? "?") ok=\(ok) [\(rows.joined(separator: " "))]"
        }
    }

    /// Parse the machine-readable markers `macvm-policy-grants.sh` writes to stdout.
    static func parsePolicyGrantsMarkers(_ output: String) -> PolicyGrantsReadback {
        var result = PolicyGrantsReadback()
        for rawLine in output.split(whereSeparator: \.isNewline) {
            let line = rawLine.trimmingCharacters(in: .whitespaces)
            if line.hasPrefix("SIP=") {
                result.sipDisabled = (String(line.dropFirst(4)) == "1")
            } else if line.hasPrefix("GRANT ") {
                // GRANT <service> auth=<n> csreq=<0|1>
                let parts = line.split(separator: " ").map(String.init)
                guard parts.count >= 4 else { continue }
                let service = parts[1]
                var auth = "none"
                var hasCsreq = false
                for token in parts.dropFirst(2) {
                    let kv = token.split(separator: "=", maxSplits: 1).map(String.init)
                    guard kv.count == 2 else { continue }
                    switch kv[0] {
                    case "auth": auth = kv[1]
                    case "csreq": hasCsreq = (kv[1] == "1")
                    default: break
                    }
                }
                result.grants[service] = .init(auth: auth, hasCsreq: hasCsreq)
            } else if line.hasPrefix("NOTIF ") {
                let kv = String(line.dropFirst(6)).split(separator: "=", maxSplits: 1).map(String.init)
                if kv.count == 2 { result.notif[kv[0]] = kv[1] }
            } else if line == "POLICY_GRANTS_OK" {
                result.ok = true
            } else if line.hasPrefix("POLICY_GRANTS_FAIL") {
                result.ok = false
                if let colon = line.firstIndex(of: ":") {
                    result.failReason = String(line[line.index(after: colon)...])
                        .trimmingCharacters(in: .whitespaces)
                }
            }
        }
        return result
    }
}

#if arch(arm64)
extension MacVMEngine {
    /// Lane B end-to-end (docs/MACOS_VM_POLICY_ALTERNATIVE.md): disable SIP in Recovery, write + verify
    /// the datastore grants SIP-off, re-enable SIP, and confirm the grants survived. Every leg is
    /// fail-closed: a `csrutil` toggle that didn't take, a row that didn't land, or a grant that didn't
    /// survive SIP re-enable throws, so the recipe stamp is withheld and the base is left retry-stale.
    /// Returns the final (SIP-on) read-back for the caller's log.
    @discardableResult
    func policyGrantsPass(bundle: MacVMBundle) async throws -> PolicyGrantsReadback {
        guard surfaceHost != nil else {
            throw MacVMError.provisionFailed(
                "the Lane B policy-grants pass needs the app's display surface to drive recoveryOS "
                    + "(not available in this headless context)")
        }
        guard #available(macOS 13.0, *) else {
            throw MacVMError.provisionFailed(
                "Lane B needs the macOS 13+ Recovery boot option (VZMacOSVirtualMachineStartOptions."
                    + "startUpFromMacOSRecovery); use the MDM lane on older hosts")
        }
        baseMaintenance = MacVMMaintenanceInfo(
            name: Self.basePolicyGrantsSurfaceName, os: .macOS,
            activity: "Applying policy grants…", booting: true)
        defer { baseMaintenance = nil }

        // 1. Recovery → csrutil disable.
        hostLog("Lane B step 1/4: booting Recovery to disable SIP")
        baseProgress = MacVMBaseProgress(phase: .disablingSIP, fraction: nil)
        try await toggleSIPViaRecovery(bundle: bundle, disable: true)

        // 2. Normal boot → write the grants SIP-off, then read them back.
        hostLog("Lane B step 2/4: normal boot, writing TCC grants over vsock")
        baseProgress = MacVMBaseProgress(phase: .writingGrants, fraction: nil)
        let wrote = try await runPolicyGrants(bundle: bundle, verifyOnly: false)
        hostLog("Lane B step 2/4 read-back: \(wrote.summary)")
        guard wrote.sipDisabled == true else {
            throw MacVMError.provisionFailed(
                "the guest booted with SIP still ENABLED after the Recovery `csrutil disable` step, so "
                    + "the system TCC.db write was refused. The recoveryOS toggle did not take (a spike "
                    + "risk on macOS 27 VMs — docs/MACOS_VM_POLICY_ALTERNATIVE.md, Risk 2). Read-back: "
                    + wrote.summary)
        }
        guard wrote.ok else {
            throw MacVMError.provisionFailed(
                "the datastore policy grants were not all written under SIP-off: "
                    + (wrote.failReason ?? wrote.summary))
        }
        policyLog.error(
            "Lane B grants written SIP-off: \(wrote.summary, privacy: .public)")

        // 3. Recovery → csrutil enable (the sealed base must ship SIP-on).
        hostLog("Lane B step 3/4: booting Recovery to re-enable SIP")
        baseProgress = MacVMBaseProgress(phase: .reenablingSIP, fraction: nil)
        try await toggleSIPViaRecovery(bundle: bundle, disable: false)

        // 4. Normal boot → confirm the grants SURVIVED re-enabling SIP. This is Lane B's one
        //    load-bearing assumption (Risk 1); it is verified here, not assumed.
        hostLog("Lane B step 4/4: normal boot, verifying grants survived SIP re-enable")
        baseProgress = MacVMBaseProgress(phase: .verifyingPolicy, fraction: nil)
        let verified = try await runPolicyGrants(bundle: bundle, verifyOnly: true)
        hostLog("Lane B step 4/4 read-back: \(verified.summary)")
        guard verified.sipDisabled == false else {
            throw MacVMError.provisionFailed(
                "the guest booted with SIP still DISABLED after the Recovery `csrutil enable` step — "
                    + "the sealed base would ship SIP-off. Refusing to stamp. Read-back: "
                    + verified.summary)
        }
        guard verified.ok else {
            throw MacVMError.provisionFailed(
                "the datastore policy grants did NOT survive re-enabling SIP — Lane B's load-bearing "
                    + "assumption failed on this guest (docs/MACOS_VM_POLICY_ALTERNATIVE.md, Risk 1): "
                    + (verified.failReason ?? verified.summary))
        }
        policyLog.error(
            "Lane B grants verified under SIP-on: \(verified.summary, privacy: .public)")
        return verified
    }

    /// Boot the writable base into recoveryOS and run one fixed `csrutil` command through Recovery's
    /// Terminal via the display surface, then halt so the host controls the next (normal) boot. There is
    /// no vsock agent in recoveryOS, so this leg is necessarily HID/OCR-driven; its success is confirmed
    /// on the following normal boot (the `SIP=` read-back), not from the screen.
    @available(macOS 13.0, *)
    private func toggleSIPViaRecovery(bundle: MacVMBundle, disable: Bool) async throws {
        guard let surface = surfaceHost else {
            throw MacVMError.provisionFailed("the Recovery SIP toggle needs the display surface")
        }
        let command = disable ? "csrutil disable" : "csrutil enable"
        baseMaintenance = MacVMMaintenanceInfo(
            name: Self.baseRecoverySurfaceName, os: .macOS,
            activity: disable
                ? "Preparing to grant base permissions…" : "Securing the base image…",
            booting: true)
        let config = try Self.makeConfiguration(
            bundle: bundle, cpus: MacVMSettings.vmCPUs, memoryGiB: MacVMSettings.vmMemoryGiB,
            mac: Self.randomMAC(), mounts: [])
        let instance = MacVMInstance(configuration: config, label: Self.baseRecoverySurfaceName)
        do {
            try await instance.startInRecovery()
        } catch {
            throw MacVMError.provisionFailed(
                "could not boot the base into macOS Recovery for the `\(command)` step: \(error)")
        }
        await surface.attach(
            name: Self.baseRecoverySurfaceName,
            virtualMachine: UncheckedSendableBox(value: instance.vm as AnyObject))
        // recoveryOS has no vsock/agent, so the only navigation channel is reading the framebuffer — and
        // on this guest a truly OFF-SCREEN VZ view returns blank grabs, so OCR reads nothing and every
        // selector misses. The view must be composited on a display to render, but that does NOT require
        // a visible window: render it INVISIBLY (fully transparent + click-through). If the operator
        // explicitly opened the diagnostic monitor, honor that and show it for real instead.
        if baseObserverRequested {
            await surface.setObserverVisible(name: Self.baseRecoverySurfaceName, visible: true)
        } else {
            await surface.setHeadlessRendering(name: Self.baseRecoverySurfaceName, enabled: true)
        }
        do {
            hostLog("Recovery boot: navigating to Terminal to run `\(command)`")
            try await openRecoveryTerminalAndRun(
                surface: surface, command: command, bundle: bundle)
            hostLog("Recovery boot: `\(command)` typed; waiting for the guest to power off")
            await surface.detach(name: Self.baseRecoverySurfaceName)
            // The typed `shutdown -h now` powers recoveryOS off; wait for it, force-stopping on overrun.
            await instance.awaitStopped(timeout: 240)
        } catch {
            await surface.detach(name: Self.baseRecoverySurfaceName)
            await instance.stop()
            throw error
        }
    }

    /// Drive recoveryOS's GUI to open Terminal and run `command`, then `shutdown -h now`. Structured as
    /// a bounded OCR loop that advances whatever recovery screen is up (language / user-auth / the main
    /// Recovery window) until Terminal is open, then types the fixed command. Throws if Terminal never
    /// opens before the deadline — the caller treats that as a failed toggle (fail-closed).
    @available(macOS 13.0, *)
    private func openRecoveryTerminalAndRun(
        surface: any MacVMSurfaceHost, command: String, bundle: MacVMBundle,
        timeout: TimeInterval = 300
    ) async throws {
        // A per-toggle diagnostics dir on the HOST: recovery has no vsock agent, so a stuck navigation
        // gives no other evidence. We log what OCR reads each time the screen changes, and save
        // framebuffer frames periodically + on timeout — so one failed run shows exactly which recovery
        // screen it got stuck on and what text it could read there.
        let label = command.contains("disable") ? "disable" : "enable"
        let diagDir = bundle.provisioningDiagnosticsURL.appendingPathComponent(
            "recovery-\(label)-\(UUID().uuidString.lowercased())", isDirectory: true)
        try? FileManager.default.createDirectory(at: diagDir, withIntermediateDirectories: true)
        func pause(_ seconds: Double) async {
            try? await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
        }
        func lines() async -> [(text: String, box: CGRect)] {
            guard let capture = await surface.captureForRecognition(name: Self.baseRecoverySurfaceName)
            else { return [] }
            return await Self.recognizeTextLines(
                inCapture: capture, lexicon: Self.recoveryLexicon).lines
        }
        // Move-settle-click: the pointer move is its own event with a settle before the button-down, the
        // click-race fix the MDM approval loop learned. Coordinates are the fixed 1920×1200 input space.
        func click(_ box: CGRect, count: Int = 1) async {
            let point = Self.clickPoint(forNormalizedBox: box)
            await surface.send(name: Self.baseRecoverySurfaceName, .move(x: point.x, y: point.y))
            await pause(0.2)
            await surface.send(
                name: Self.baseRecoverySurfaceName,
                .click(x: point.x, y: point.y, button: .left, count: count))
        }
        func firstBox(_ recognized: [(text: String, box: CGRect)], _ labels: [String]) -> CGRect? {
            recognized.first(where: { row in labels.contains { Self.fuzzyEquals(row.text, $0) } })?.box
        }
        // A click POINT for a menu label — robust to Vision merging a whole menu bar into ONE line
        // ("Recovery File Edit Utilities Window"). First try a line that IS the label (its box center);
        // else find a line that CONTAINS the label and estimate the label's x by its character offset
        // within that line's box. This is what makes "Utilities"/"Terminal" clickable whether Vision
        // splits the menu into per-item boxes (some frames) or merges it (others).
        func pointForLabel(_ recognized: [(text: String, box: CGRect)], _ label: String)
            -> (x: Int, y: Int)?
        {
            if let box = firstBox(recognized, [label]) {
                return Self.clickPoint(forNormalizedBox: box)
            }
            let target = Self.ocrFold(label)
            guard !target.isEmpty else { return nil }
            for row in recognized {
                let folded = Self.ocrFold(row.text)
                guard let r = folded.range(of: target) else { continue }
                let startOffset = folded.distance(from: folded.startIndex, to: r.lowerBound)
                let midChar = Double(startOffset) + Double(target.count) / 2.0
                let frac = midChar / Double(max(folded.count, 1))
                let x = Int((row.box.minX + CGFloat(frac) * row.box.width) * 1920)
                let y = Int((1 - row.box.midY) * 1200)
                return (x, max(0, y))
            }
            return nil
        }
        func clickPoint(_ point: (x: Int, y: Int)) async {
            await surface.send(name: Self.baseRecoverySurfaceName, .move(x: point.x, y: point.y))
            await pause(0.2)
            await surface.send(
                name: Self.baseRecoverySurfaceName,
                .click(x: point.x, y: point.y, button: .left, count: 1))
        }

        let deadline = Date().addingTimeInterval(timeout)
        let agentPassword = try? resolveAgentPassword()
        let user = MacVMSettings.sshUser
        var selectedUser = false
        var answeredAuth = false
        var lastLoggedText = ""
        var frameSaves = 0
        var nextFrameSave = Date()  // save a frame roughly every 30s so we see the stuck screen
        // Recovery presents its first screen a beat after boot; let it settle before the first OCR.
        await pause(12)

        while Date() < deadline {
            let recognized = await lines()
            let joined = recognized.map(\.text).joined(separator: " ")
            // Trace what OCR actually reads, but only when it changes (so the log isn't spammed). This
            // is the single most useful signal for a stuck nav: it names the screen we're looking at.
            if joined != lastLoggedText {
                lastLoggedText = joined
                let shown = joined.isEmpty ? "<no text recognized>" : String(joined.prefix(200))
                hostLog("recovery(\(label)) screen reads: \(shown)")
            }
            if Date() >= nextFrameSave {
                nextFrameSave = Date().addingTimeInterval(30)
                _ = await saveRecoveryFrame(surface: surface, into: diagDir, index: frameSaves)
                frameSaves += 1
            }
            // The MAIN Recovery window's center list ("Restore From Time Machine", "Reinstall macOS",
            // "Disk Utility"). Activating any of it — a click on its "Continue" button, or a Return —
            // opens a modal that BLOCKS the Utilities menu we actually need (the default selection,
            // "Restore From Time Machine", is exactly the trap). Detect it so we never do that.
            let inRecoveryWindow = ["Restore", "Reinstall", "Disk Utility", "Time Machine"]
                .contains { Self.fuzzyContains(joined, $0) }

            // (a) Inside recoveryOS: open Terminal from the MENU BAR — Utilities ▸ Terminal. This is the
            //     ONLY correct way in; the center list must never be activated. Checked first, so a frame
            //     that reads the menu bar wins over the center-window guard below. `pointForLabel` finds
            //     "Utilities" even when Vision merges the whole menu bar into one line.
            if let utilities = pointForLabel(recognized, "Utilities") {
                await clickPoint(utilities)
                await pause(1.2)  // let the menu drop before re-reading it
                if let terminal = pointForLabel(await lines(), "Terminal") {
                    await clickPoint(terminal)
                    await pause(3.5)  // Terminal cold-launch in recoveryOS
                    await runInRecoveryTerminal(
                        surface: surface, command: command, agentPassword: agentPassword)
                    return
                }
                // Menu didn't reveal Terminal (OCR/timing miss) — close it and retry from the top.
                await surface.send(name: Self.baseRecoverySurfaceName, .key(chord: "esc"))
                await pause(0.8)
                continue
            }

            // (b) The Apple-silicon STARTUP PICKER ("Macintosh HD" + "Options"). Detected by OCR, but
            //     SELECTED by KEYBOARD — no gear-icon coordinate guess, no "Continue"-button hunt, both
            //     of which proved fragile. "Options" is always the RIGHTMOST item, so arrow all the way
            //     right (extra presses are no-ops once it's the rightmost selection) to land on it, then
            //     Return to enter recoveryOS. Arrowing to Options FIRST is what makes the Return safe:
            //     it never boots the default system disk.
            // Detected by CONTAINMENT (Vision often merges "Macintosh HD Options" into one line, so a
            // whole-line match misses it) — and driven by keyboard, so we only need to know we're here,
            // not the exact box. The center-window guard below ensures this can't fire in the Recovery
            // window (which has no "Options").
            if !inRecoveryWindow, Self.fuzzyContains(joined, "Options") {
                for _ in 0..<4 {
                    await surface.send(name: Self.baseRecoverySurfaceName, .key(chord: "right"))
                    await pause(0.3)
                }
                await surface.send(name: Self.baseRecoverySurfaceName, .key(chord: "return"))
                await pause(6.0)  // recoveryOS takes a while to come up after Options is confirmed
                continue
            }

            // (c) "Select a user you know the password for": pick the agent account, then advance with
            //     the Next/Continue button if one follows the selection.
            if !selectedUser, let userRow = firstBox(recognized, [user]) {
                await click(userRow)
                await pause(0.8)
                if let next = firstBox(await lines(), ["Next", "Continue"]) { await click(next) }
                selectedUser = true
                await pause(2.0)
                continue
            }

            // (d) A password SHEET (GUI). Answer it once with the agent password; field fuzzily located.
            if !answeredAuth, let password = agentPassword,
               let field = Self.passwordFieldBox(in: recognized)
            {
                await click(field)
                await pause(0.4)
                await surface.send(name: Self.baseRecoverySurfaceName, .text(password))
                await surface.send(name: Self.baseRecoverySurfaceName, .key(chord: "return"))
                answeredAuth = true
                await pause(2.5)
                continue
            }

            // (e) A language / welcome pane's Continue / Next — but ONLY when we're NOT in the main
            //     Recovery window (where "Continue" activates the blocking center selection) and NOT on
            //     the picker (handled above). Otherwise just wait and re-OCR — never a blind Return.
            if !inRecoveryWindow, let cont = firstBox(recognized, ["Continue", "Next"]) {
                await click(cont)
            }
            await pause(2.0)
        }
        // Timed out — grab a few final frames of the stuck screen for the host, then fail closed.
        _ = await saveRecoveryFrame(surface: surface, into: diagDir, index: 900)
        await pause(2.0)
        _ = await saveRecoveryFrame(surface: surface, into: diagDir, index: 901)
        hostLog(
            "recovery(\(label)) TIMED OUT after \(Int(timeout))s; last OCR text: "
                + (lastLoggedText.isEmpty ? "<none>" : String(lastLoggedText.prefix(200)))
                + "; frames → \(diagDir.path)")
        throw MacVMError.provisionFailed(
            "could not reach recoveryOS Terminal to run `\(command)` within \(Int(timeout))s "
                + "(the fully-automated Recovery navigation did not get past the startup picker / "
                + "Utilities → Terminal path). Host-side frames of the stuck screen: \(diagDir.path)")
    }

    /// Save one non-empty framebuffer grab of the recovery boot into `dir`. A real actor method (not a
    /// nested closure) so the actor owns its `FileManager` use across the capture suspension.
    private func saveRecoveryFrame(
        surface: any MacVMSurfaceHost, into dir: URL, index: Int
    ) async -> Bool {
        guard let jpeg = await surface.capture(name: Self.baseRecoverySurfaceName), !jpeg.isEmpty
        else { return false }
        let url = dir.appendingPathComponent(String(format: "recovery-%03d.jpg", index))
        guard (try? jpeg.write(to: url)) != nil else { return false }
        try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
        return true
    }

    /// With Terminal focused in recoveryOS, type the fixed `csrutil` command, answer its `[y/n]`
    /// confirmation and its command-line `getpass` admin-password prompt, then halt. Kept separate so
    /// the typing sequence is one readable unit.
    @available(macOS 13.0, *)
    private func runInRecoveryTerminal(
        surface: any MacVMSurfaceHost, command: String, agentPassword: String?
    ) async {
        func pause(_ seconds: Double) async {
            try? await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
        }
        func screenText() async -> String {
            let recognized = await Self.recognizeTextLines(
                inCapture: await surface.captureForRecognition(name: name) ?? Data(),
                lexicon: Self.recoveryLexicon).lines
            return recognized.map(\.text).joined(separator: " ")
        }
        let name = Self.baseRecoverySurfaceName
        // Focus the Terminal window (center) — recoveryOS opens it foreground, but a click makes sure.
        await surface.send(name: name, .click(x: 960, y: 600, button: .left, count: 1))
        await pause(0.6)

        // Run csrutil up to twice: a mistyped `getpass` answer prints "failed to set credential" /
        // "Sorry, try again" and returns to the shell, where re-running the command is safe. A dropped
        // keystroke into getpass is the likely cause of a wrong password, and the retry types clean.
        for _ in 1...2 {
            // A dropped-first-keystroke guard (as in `typeBootstrapLaunch`): a throwaway space absorbs a
            // dropped key, and the real command leads with a space so a still-dropped key costs the
            // space, not the `c` of `csrutil`.
            await surface.send(name: name, .text(" "))
            await pause(0.4)
            await surface.send(name: name, .text(" \(command)"))
            await surface.send(name: name, .key(chord: "return"))
            await pause(3.0)
            // `csrutil` prints a confirmation line and waits for "y" before applying the change.
            await surface.send(name: name, .text("y"))
            await surface.send(name: name, .key(chord: "return"))
            await pause(3.0)
            // It then prompts (getpass, at the command line) for an admin/volume-owner password. Answer
            // it. getpass reads with NO echo but in canonical mode, so a leading Backspace is a harmless
            // no-op on the empty buffer that ABSORBS a dropped first keystroke — without it, a dropped
            // key silently shortens the password, which reads back as a wrong password ("failed to set
            // credential"). Typed even absent a detected prompt: recoveryOS csrutil always asks, and a
            // stray line is just an unknown command.
            if let password = agentPassword {
                await surface.send(name: name, .key(chord: "delete"))
                await pause(0.3)
                await surface.send(name: name, .text(password))
                await surface.send(name: name, .key(chord: "return"))
                await pause(3.0)
            }
            let screen = await screenText()
            let failed = ["failed", "failure", "try again", "incorrect", "not authorized"]
                .contains { Self.fuzzyContains(screen, $0) }
            if !failed { break }  // no visible failure → assume it took (the next boot's read-back confirms)
        }
        await pause(1.0)
        // Halt recoveryOS so the host owns the next (normal) boot; the toggle persists in NVRAM.
        await surface.send(name: name, .text(" shutdown -h now"))
        await surface.send(name: name, .key(chord: "return"))
        await pause(2.0)
    }

    /// Boot the base normally (SIP off after step 1, or on after step 3), run
    /// `macvm-policy-grants.sh` over the vsock exec channel, and return the parsed read-back. No share
    /// is mounted, so the run itself can't trip the Network Volumes prompt it grants. Powers the guest
    /// off cleanly on the way out. `verifyOnly` skips the writes and only re-reads (step 4).
    private func runPolicyGrants(
        bundle: MacVMBundle, verifyOnly: Bool
    ) async throws -> PolicyGrantsReadback {
        guard let surface = surfaceHost else {
            throw MacVMError.provisionFailed("the policy-grants write pass needs the display surface")
        }
        guard let scriptURL = resolvePolicyGrantsScript() else {
            throw MacVMError.provisionFailed(
                "the bundled scripts/macvm-policy-grants.sh could not be located")
        }
        let scriptBase64 = try Data(contentsOf: scriptURL).base64EncodedString()
        // Derive the agent's compiled code-requirement blob from the EXACT staged app, so the row is
        // pinned to the signature actually installed (Developer-ID or ad-hoc dev build alike). A build
        // without a derivable requirement writes csreq-NULL rows and cannot require them.
        var csreqHex: String?
        if let app = resolveVMAgentApp(),
           let blob = Self.designatedRequirementData(forAgentAt: app)
        {
            csreqHex = blob.map { String(format: "%02x", $0) }.joined()
        } else if !verifyOnly {
            policyLog.error(
                """
                no derivable designated requirement for the staged VM agent — writing csreq-NULL TCC \
                rows (allowed by bundle id on current builds, but not pinned to the signature; sign the \
                agent with a stable identity for durable grants)
                """)
        }
        let body = Self.policyGrantsRemoteBody(
            scriptBase64: scriptBase64, csreqHex: csreqHex,
            requireCsreq: csreqHex != nil, user: MacVMSettings.sshUser, verifyOnly: verifyOnly)

        baseMaintenance = MacVMMaintenanceInfo(
            name: Self.basePolicyGrantsSurfaceName, os: .macOS,
            activity: verifyOnly ? "Verifying policy grants…" : "Writing policy grants…",
            booting: true)
        let config = try Self.makeConfiguration(
            bundle: bundle, cpus: MacVMSettings.vmCPUs, memoryGiB: MacVMSettings.vmMemoryGiB,
            mac: Self.randomMAC(), mounts: [])
        let instance = MacVMInstance(
            configuration: config, label: Self.basePolicyGrantsSurfaceName)
        do {
            try await instance.start()
        } catch {
            throw MacVMError.provisionFailed(
                "could not boot the base for the policy-grants \(verifyOnly ? "verify" : "write") "
                    + "pass: \(error)")
        }
        await surface.attach(
            name: Self.basePolicyGrantsSurfaceName,
            virtualMachine: UncheckedSendableBox(value: instance.vm as AnyObject))
        if baseObserverRequested {
            await surface.setObserverVisible(
                name: Self.basePolicyGrantsSurfaceName, visible: true)
        }
        do {
            hostLog("policy-grants \(verifyOnly ? "verify" : "write") boot: waiting for the agent vsock ping")
            try await awaitAgentReady(
                onInstance: instance, label: Self.basePolicyGrantsSurfaceName)
            hostLog("policy-grants \(verifyOnly ? "verify" : "write") boot: agent answered ✓")
            baseMaintenance = MacVMMaintenanceInfo(
                name: Self.basePolicyGrantsSurfaceName, os: .macOS,
                activity: verifyOnly ? "Verifying policy grants…" : "Writing policy grants…",
                booting: false)
            let channel = try await openExecChannel(
                instance: instance, label: Self.basePolicyGrantsSurfaceName,
                workdir: nil, env: [:], remoteBody: body)
            async let outText = Self.drainLines(channel.stdoutLines)
            async let errText = Self.drainLines(channel.stderrLines)
            let outcome = await Self.awaitExit(channel, timeoutNanos: 120 * 1_000_000_000)
            let stdout = await outText
            let stderr = await errText
            guard case .exited = outcome else {
                throw MacVMError.provisionFailed(
                    "the policy-grants script never returned within 120s"
                        + (stderr.isEmpty ? "" : " (stderr: \(stderr.suffix(300)))"))
            }
            let readback = Self.parsePolicyGrantsMarkers(stdout)
            await surface.detach(name: Self.basePolicyGrantsSurfaceName)
            await shutdownBaseGuest(instance)
            return readback
        } catch {
            await surface.detach(name: Self.basePolicyGrantsSurfaceName)
            await instance.stop()
            throw error
        }
    }
}
#endif
