import CoreGraphics
import CoreText
import Foundation
import ImageIO
import os

#if canImport(Vision)
import Vision
#endif

private let consentLog = Logger(subsystem: "com.nucleic", category: "macvm.consent")

// MARK: - Network Volumes policy/readiness validation
//
// On a macOS guest the virtiofs workspace share counts as a **network volume**, so the first exec'd
// child that touches it raises a per-user `kTCCServiceSystemPolicyNetworkVolumes` consent prompt
// attributed to `NucleicVMAgent.app` (the responsible process behind every exec). Headless use never
// sees that dialog, so share-touching execs — including the boot-time host-path mount step — blocked
// until their timeout: the "mac_vm_exec never returns" hang.
//
// Production recipe v8 delivers this permission through User-Approved MDM. This boot remains the
// functional, production-shaped gate: it must touch the exact custom mount successfully without UI.
// No consent alert is answered here. The pass boots with a throwaway custom-tagged share, runs one
// bounded agent-attributed mount/read, and fails publication if the MDM grant is not already effective.
//
// The pass gates the provisioning-recipe stamp: a base that provisioned but could not be granted is
// left recipe-stale so the next launch retries, and clones of it fail their (now bounded) host-path
// step with an error naming this consent rather than wedging.
extension MacVMEngine {
    /// Surface/VM label for the consent-grant maintenance boot (Control-panel visible).
    public static let baseConsentSurfaceName = "base-consent"
    /// The throwaway share's name (its custom virtiofs tag's basis).
    static let consentShareName = "nucleic-consent"

    /// Where the probe share is mounted **inside the guest**. An explicit mountpoint
    /// (``MacVMSpec/Mount/guestPath``) rather than the share's own host path, for the same reason
    /// the provisioning share has one (``provisionMountPoint``): the host path is a per-run
    /// `FileManager.temporaryDirectory` URL, so re-creating it verbatim would mount a virtiofs
    /// device inside the *guest's* system-managed `/var/folders` TMPDIR tree — unrepresentative of
    /// production and needlessly fragile. This keeps the production *mechanism* (custom-tagged
    /// single-directory device + `mount_virtiofs`) while pinning a stable path in the same
    /// filesystem region a real workspace share lands in. The TCC network-volume grant is per-app,
    /// so the mountpoint doesn't narrow what it covers.
    static var consentProbeGuestPath: String {
        "/Users/\(MacVMSettings.sshUser)/nucleic-readiness-probe"
    }

    /// The probe share, built in ONE place: the VM configuration and the in-guest mount script must
    /// describe the same mount, and the virtiofs tag is derived from the mount's index in the
    /// configuration's array (``macOSShareTag(index:)``) — so a second construction that drifted in
    /// order or flags would mount the wrong device, or none. Callers pass it to
    /// `makeConfiguration(mounts:)` as the sole mount and to `hostPathRecreationScript` unchanged.
    static func consentProbeMount(hostDirectory: String) -> MacVMSpec.Mount {
        MacVMSpec.Mount(
            host: hostDirectory, name: consentShareName, readOnly: false,
            recreateHostPath: false, guestPath: consentProbeGuestPath)
    }

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

    /// Convert a Vision-normalized bounding box (bottom-left origin) to a click point in the surface's
    /// **input** coordinate space (top-left origin).
    ///
    /// `width`/`height` must be the space `MacVMSurfaceInput` coordinates live in —
    /// ``surfaceInputSize`` — NOT the pixel size of the capture the box was recognized in. The two are
    /// deliberately no longer the same: recognition runs on a native-resolution capture
    /// (``MacVMSurfaceHost/captureForRecognition(name:)``) that can be twice the input space on each
    /// axis, so measuring the frame would double every click and put it off the screen. Vision's box
    /// is resolution-independent, which is exactly what lets the two spaces differ safely; this is the
    /// only place a resolution enters at all.
    ///
    /// The defaults remain the input space so pure unit tests can pass a box alone.
    static func clickPoint(
        forNormalizedBox box: CGRect, width: Int = 1920, height: Int = 1200
    ) -> (x: Int, y: Int) {
        (
            x: Int((box.midX * CGFloat(width)).rounded()),
            y: Int(((1 - box.midY) * CGFloat(height)).rounded())
        )
    }

    /// Click point in a given coordinate space. Callers driving a real surface pass
    /// ``surfaceInputSize``; the parameter exists so the mapping stays a pure, testable function.
    static func clickPoint(forNormalizedBox box: CGRect, in frame: CGSize) -> (x: Int, y: Int) {
        clickPoint(
            forNormalizedBox: box,
            width: Int(frame.width.rounded()), height: Int(frame.height.rounded()))
    }

    /// The pixel dimensions of a capture, read from its header without decoding the image. `nil` when
    /// the data isn't a readable image. Recorded as evidence — a recognition capture that comes back
    /// at the input space rather than the guest's native panel size means the framebuffer grab was
    /// downsampled before Vision saw it, which is the difference between reliable OCR and character
    /// substitutions.
    static func imageSize(ofCapture data: Data) -> CGSize? {
        guard let source = CGImageSourceCreateWithData(data as CFData, nil),
              let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil)
                as? [CFString: Any],
              let width = properties[kCGImagePropertyPixelWidth] as? NSNumber,
              let height = properties[kCGImagePropertyPixelHeight] as? NSNumber,
              width.doubleValue > 0, height.doubleValue > 0
        else { return nil }
        return CGSize(width: width.doubleValue, height: height.doubleValue)
    }

    /// Re-encode a capture as a bounded JPEG for an evidence directory.
    ///
    /// Recognition captures are lossless and full-resolution — right for Vision, wrong for the ~40
    /// frames an approval run preserves beside a base. A human reading those frames needs to see which
    /// screen was up, not every pixel of it, so they are downsampled and compressed on the way to
    /// disk. `nil` when the data isn't a decodable image; the caller then writes it unchanged.
    static func evidenceJPEG(
        from data: Data, maxPixelSize: Int = 1920, quality: CGFloat = 0.6
    ) -> Data? {
        guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
        // One decode-and-downsample pass rather than a full decode followed by a resize.
        let options: [CFString: Any] = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
        ]
        guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)
        else { return nil }
        let out = NSMutableData()
        guard let destination = CGImageDestinationCreateWithData(
            out, "public.jpeg" as CFString, 1, nil)
        else { return nil }
        CGImageDestinationAddImage(
            destination, image,
            [kCGImageDestinationLossyCompressionQuality: quality] as CFDictionary)
        guard CGImageDestinationFinalize(destination) else { return nil }
        return out as Data
    }

    /// The surface's **input** coordinate space: the space `MacVMSurfaceInput` coordinates are
    /// interpreted in (`MacVMComputerSurface.fbWidth`/`fbHeight`), and therefore the space every click
    /// derived from a recognized box must be expressed in. Fixed, and deliberately independent of what
    /// a capture happens to measure — see ``clickPoint(forNormalizedBox:width:height:)``.
    static let surfaceInputSize = CGSize(width: 1920, height: 1200)

    /// How many alternate readings to ask Vision for per line. Vision computes these anyway; taking
    /// more than the top one costs nothing and lets ``bestCandidate(among:lexicon:)`` recover a line
    /// whose *first* reading was damaged but whose second says "Enroll".
    static let recognitionCandidateCount = 3

    /// Recognize text lines in a framebuffer capture, each with its normalized bounding box. Runs
    /// detached — Vision's synchronous `perform` must not park the engine actor.
    ///
    /// `lexicon` is the vocabulary the caller is going to match against (see
    /// ``MacVMEngine/approvalLexicon``). It is used twice, and both uses are conservative:
    ///
    ///  * as Vision's `customWords`, so language correction has the proper nouns and UI labels of this
    ///    flow ("Nucleic", "Enroll", "mobileconfig") in its prior rather than fighting them;
    ///  * to pick between the alternates Vision returns for a line, preferring the reading that
    ///    contains more known words. This never invents text — every candidate considered is one
    ///    Vision itself proposed — it only breaks the tie in favour of the vocabulary that actually
    ///    appears on these screens.
    ///
    /// Language correction is ON here, unlike the first version of this call. It was disabled to stop
    /// Vision "correcting" UI labels into ordinary English, but with an empty `customWords` list that
    /// merely removed the model's only defence against low-resolution glyph confusion, and the damage
    /// it was meant to prevent ("Password" → "Peseword") happened anyway. Correction plus the flow's
    /// own vocabulary is the configuration that fixes both, and the caller's matching is fuzzy in any
    /// case (``MacVMEngine/ocrFold(_:)``), so neither side depends on an exact spelling.
    /// One recognition pass: the lines read, and — when the recognizer itself failed — why.
    ///
    /// `failure` exists because its absence cost a live run and a whole investigation. `perform` was
    /// called as `try? handler.perform(...)`, so a recognizer that could not run at all was
    /// indistinguishable from a screen with no text on it. The loop classified 566 consecutive frames
    /// of a perfectly sharp screenshot as `.unknown`, cycled Settings panes for five minutes, and then
    /// reported *"the guest framebuffer appears blank"* — blaming the one component that was working.
    /// A failure that cannot be seen gets diagnosed as something else.
    struct TextRecognition: Sendable {
        var lines: [(text: String, box: CGRect)] = []
        /// `nil` when recognition ran. Non-`nil` names what went wrong, for the transcript.
        var failure: String?
    }

    nonisolated static func recognizeTextLines(
        inCapture data: Data, lexicon: Set<String> = []
    ) async -> TextRecognition {
        #if canImport(Vision)
        return await Task.detached(priority: .userInitiated) { () -> TextRecognition in
            // Decode explicitly rather than handing Vision the encoded bytes. `VNImageRequestHandler`
            // accepts `Data`, but then a decode failure and a recognition failure surface as the same
            // opaque throw — and ImageIO is already proven to work in this process (the same capture's
            // dimensions are read through it for the transcript). One decode, one error to attribute.
            guard let source = CGImageSourceCreateWithData(data as CFData, nil),
                  let image = CGImageSourceCreateImageAtIndex(source, 0, nil)
            else {
                return TextRecognition(
                    failure: "the capture (\(data.count) bytes) could not be decoded as an image")
            }
            let handler = VNImageRequestHandler(cgImage: image)

            func read(level: VNRequestTextRecognitionLevel) throws -> [(String, CGRect)] {
                let request = VNRecognizeTextRequest()
                request.recognitionLevel = level
                request.recognitionLanguages = ["en-US"]
                // Correction is only safe alongside this flow's own vocabulary; see `approvalLexicon`.
                request.usesLanguageCorrection = true
                request.customWords = lexicon.sorted()
                try handler.perform([request])
                return (request.results ?? []).compactMap { observation in
                    let readings = observation.topCandidates(recognitionCandidateCount).map(\.string)
                    guard let text = MacVMEngine.bestCandidate(among: readings, lexicon: lexicon)
                    else { return nil }
                    return (text, observation.boundingBox)
                }
            }

            // `.accurate` is the one we want; `.fast` is a different code path in Vision and is worth
            // one attempt before concluding that recognition is unavailable — a degraded reading of
            // the Enroll button beats five minutes of `.unknown`.
            do {
                let raw = try read(level: .accurate)
                if !raw.isEmpty { return TextRecognition(lines: raw.map { (text: $0.0, box: $0.1) }) }
            } catch {
                do {
                    let raw = try read(level: .fast)
                    return TextRecognition(
                        lines: raw.map { (text: $0.0, box: $0.1) },
                        failure: raw.isEmpty
                            ? "accurate recognition failed (\(error)) and fast recognition read nothing"
                            : nil)
                } catch let fastError {
                    return TextRecognition(
                        failure: "text recognition failed: accurate=\(error); fast=\(fastError)")
                }
            }
            // Ran cleanly and read nothing. Legitimate on a genuinely empty screen, so not a failure.
            return TextRecognition()
        }.value
        #else
        return TextRecognition(failure: "Vision is not available in this build")
        #endif
    }

    /// Prove — before the approval loop depends on it — that text recognition actually works **in this
    /// process**, and pay Vision's one-time model load while nothing is on the clock. Returns `nil` when
    /// recognition is healthy, otherwise the reason to fail with.
    ///
    /// Both halves are load-bearing. A live run showed the recognizer returning nothing for every
    /// frame while the framebuffer was fine, which is only diagnosable if something asserts the
    /// recognizer works on input whose answer is already known. And the first `.accurate` request in a
    /// process with cold Vision models took **45 seconds** on this machine — inside a loop that
    /// captures every 500 ms against a 300 s deadline, that alone can consume a sixth of the budget
    /// before the first frame is ever classified. Doing it here means the loop starts warm.
    static func verifyTextRecognition() async -> String? {
        let probeWords = ["Nucleic", "Enroll", "Password"]
        guard let capture = syntheticTextCapture(probeWords) else {
            return nil  // Could not build a probe; never fail the build on the self-test's own tooling.
        }
        let recognition = await recognizeTextLines(inCapture: capture, lexicon: approvalLexicon)
        if let failure = recognition.failure { return failure }
        let joined = recognition.lines.map(\.text).joined(separator: " ")
        guard probeWords.allSatisfy({ fuzzyContains(joined, $0) }) else {
            return "text recognition ran but could not read a synthetic image containing "
                + probeWords.joined(separator: ", ") + " — it read: "
                + (joined.isEmpty ? "<nothing>" : joined)
        }
        return nil
    }

    /// Render `words` as large dark text on a light background and encode it like a capture. Used only
    /// by ``verifyTextRecognition()``; deliberately synthesized rather than shipped as a fixture so the
    /// probe cannot drift from the code that reads it.
    static func syntheticTextCapture(_ words: [String]) -> Data? {
        let width = 1200, height = 400
        guard let context = CGContext(
            data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0,
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)
        else { return nil }
        context.setFillColor(CGColor(red: 0.96, green: 0.96, blue: 0.96, alpha: 1))
        context.fill(CGRect(x: 0, y: 0, width: width, height: height))
        let font = CTFontCreateWithName("Helvetica" as CFString, 44, nil)
        for (index, word) in words.enumerated() {
            let attributed = NSAttributedString(
                string: word,
                attributes: [
                    .font: font, .foregroundColor: CGColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 1),
                ])
            context.textPosition = CGPoint(x: 60, y: height - 90 - (index * 90))
            CTLineDraw(CTLineCreateWithAttributedString(attributed), context)
        }
        guard let image = context.makeImage() else { return nil }
        let out = NSMutableData()
        guard let destination = CGImageDestinationCreateWithData(
            out, "public.jpeg" as CFString, 1, nil)
        else { return nil }
        CGImageDestinationAddImage(destination, image, nil)
        guard CGImageDestinationFinalize(destination) else { return nil }
        return out as Data
    }

    /// Choose one reading from Vision's ranked alternates: the first one with the most `lexicon` words
    /// in it, falling back to Vision's own top choice. `nil` only when there are no readings at all.
    ///
    /// Ties keep Vision's ordering, so an empty lexicon (or a line with no known words in any reading)
    /// is exactly the old behaviour. Pure and `nonisolated` so it is unit-testable without Vision.
    nonisolated static func bestCandidate(among readings: [String], lexicon: Set<String>) -> String? {
        guard let top = readings.first else { return nil }
        guard !lexicon.isEmpty, readings.count > 1 else { return top }
        func score(_ reading: String) -> Int {
            reading.split(whereSeparator: { !$0.isLetter && !$0.isNumber })
                .reduce(into: 0) { total, word in
                    if lexicon.contains(word.lowercased()) { total += 1 }
                }
        }
        var best = top
        var bestScore = score(top)
        for reading in readings.dropFirst() where score(reading) > bestScore {
            bestScore = score(reading)
            best = reading
        }
        return best
    }
}

#if arch(arm64)
extension MacVMEngine {
    /// Boot the finished base with a throwaway production-shaped share and verify the whole production
    /// path — Network Volumes file access, nash, and an exec round-trip — then power off. There is
    /// deliberately no interactive fallback: a base whose baked-in policy didn't take is fatal (the
    /// recipe stamp is withheld) regardless of which lane delivered it.
    func networkVolumesConsentPass(bundle: MacVMBundle) async throws {
        guard let surface = surfaceHost else {
            throw MacVMError.provisionFailed(
                "the network-volumes readiness pass needs the app's display surface (not available in "
                    + "this headless context)")
        }
        // Publish the final policy/readiness pass in the Control panel and clear it on every exit.
        baseMaintenance = MacVMMaintenanceInfo(
            name: Self.baseConsentSurfaceName, os: .macOS,
            activity: "Verifying VM file access…", booting: true)
        defer { baseMaintenance = nil }
        let fm = FileManager.default
        let shareDir = fm.temporaryDirectory
            .appendingPathComponent("nucleic-consent-\(UUID().uuidString)", isDirectory: true)
        try fm.createDirectory(at: shareDir, withIntermediateDirectories: true)
        try Data("nucleic network-volumes consent probe\n".utf8)
            .write(to: shareDir.appendingPathComponent("probe"))
        defer { try? fm.removeItem(at: shareDir) }

        // The probe MUST replicate a session clone's mount MECHANISM: a custom-tagged
        // single-directory device the guest mounts with `mount_virtiofs` at an absolute path — what
        // `recreateHostPaths` performs at every session boot. (Probing the `/Volumes/My Shared
        // Files` automount instead is what made the first version of this pass vacuous: the
        // automount doesn't trip the network-volume gate, so the touch succeeded with no prompt, the
        // click loop never ran, and the recipe was stamped on a guest that still prompted for real
        // work.) Read-write, like the real workspace share.
        let mount = Self.consentProbeMount(hostDirectory: shareDir.path)

        // Boot the finished base and wait for its in-guest agent — RETRYING once with a fresh reboot.
        // A base that has just been through a big toolchain install plus several reboots occasionally
        // comes up on one boot WITHOUT finishing auto-login, so the Aqua-only agent never runs and the
        // vsock ping times out. That is a one-time transient far more often than a real break, and this
        // is the last gate of a ~40-minute build, so a single reboot self-heal is worth far more than
        // failing the whole build (the same "reboot → the login session comes back" the MDM drain uses).
        var instance: MacVMInstance!
        var agentError: Error?
        var ready = false
        var diagnosticsDir: URL?
        for attempt in 1...2 {
            let config = try Self.makeConfiguration(
                bundle: bundle, cpus: MacVMSettings.vmCPUs, memoryGiB: MacVMSettings.vmMemoryGiB,
                mac: Self.randomMAC(), mounts: [mount])
            instance = MacVMInstance(configuration: config, label: Self.baseConsentSurfaceName)
            do {
                try await instance.start()
            } catch {
                throw MacVMError.provisionFailed(
                    "could not boot the base for its readiness pass: \(error)")
            }
            hostLog("readiness boot \(attempt)/2: guest started; waiting up to 300s for the agent vsock ping")
            await surface.attach(
                name: Self.baseConsentSurfaceName,
                virtualMachine: UncheckedSendableBox(value: instance.vm as AnyObject))
            do {
                try await awaitAgentReady(
                    onInstance: instance, label: Self.baseConsentSurfaceName)
                ready = true
                hostLog("readiness boot \(attempt)/2: agent answered ✓")
                break
            } catch {
                agentError = error
                hostLog("readiness boot \(attempt)/2: agent DID NOT answer — \(error)")
                // The agent is unreachable, so nothing IN the guest can hand us evidence — but the host
                // can grab the guest's framebuffer directly (no agent, no share write, no TCC prompt).
                // Persist it OUTSIDE the throwaway boot, under the base's Diagnostics dir, so we can see
                // what the guest was actually showing (login window? a stuck/blank desktop? a dialog?).
                if let dir = await captureConsentFailureFrames(
                    surface: surface, bundle: bundle, attempt: attempt)
                {
                    diagnosticsDir = dir
                    hostLog("readiness boot \(attempt)/2: saved framebuffer diagnostics → \(dir.path)")
                } else {
                    hostLog("readiness boot \(attempt)/2: framebuffer capture unavailable")
                }
                await surface.detach(name: Self.baseConsentSurfaceName)
                await instance.stop()
                if attempt < 2 {
                    consentLog.error("readiness boot 1: agent unanswered — rebooting the base once and retrying")
                }
            }
        }
        guard ready else {
            let where_ = diagnosticsDir.map { "; a host-side screenshot of the stuck boot was saved to \($0.path)" } ?? ""
            consentLog.error(
                "base readiness FAILED: agent unreachable across two boots\(where_, privacy: .public)")
            throw MacVMError.agentUnavailable(
                (agentError.map { String(describing: $0) }
                    ?? "the base's in-guest agent never became reachable across two readiness boots")
                    + where_)
        }

        do {
            baseMaintenance = MacVMMaintenanceInfo(
                name: Self.baseConsentSurfaceName, os: .macOS,
                activity: "Verifying VM file access…", booting: false)
            // Trigger one bounded custom-mount touch, but NEVER click a dialog. A base whose Network
            // Volumes grant didn't actually take must fail here, visibly.
            guard try await shareTouchGranted(instance: instance, hostPath: shareDir.path) else {
                consentLog.error(
                    "network-volumes access NOT ready: production-shaped share touch still blocked")
                throw MacVMError.provisionFailed(
                    "the base's Network Volumes permission did not authorize the production-shaped "
                        + "mount; no interactive fallback exists")
            }
            // Error level so it PERSISTS in the unified log (info/debug are memory-only and were
            // absent exactly when this pass needed diagnosing).
            consentLog.error("network-volumes access verified against a production-shaped mount")

            // The readiness gate: consent is only ONE way a base can be born broken. Before anything
            // is stamped, exercise the whole production path on this same boot and refuse the base
            // if any leg fails (see `verifyBaseReadiness`).
            baseMaintenance = MacVMMaintenanceInfo(
                name: Self.baseConsentSurfaceName, os: .macOS,
                activity: "Verifying the base…", booting: false)
            let report = try await verifyBaseReadiness(instance: instance, hostPath: shareDir.path)
            guard report.isReady else {
                consentLog.error(
                    "base readiness FAILED: \(report.failures.joined(separator: "; "), privacy: .public)")
                throw MacVMError.provisionFailed(
                    "the base failed its readiness check, so it was not marked usable: "
                        + report.failures.joined(separator: "; ")
                        + ". Clones of it would break the same way; fix the cause and rebuild.")
            }
            consentLog.error("base readiness PASSED (\(report.summary, privacy: .public))")
            await surface.detach(name: Self.baseConsentSurfaceName)
            await shutdownBaseGuest(instance)  // clean power-off so the disk quiesces before cloning
        } catch {
            await surface.detach(name: Self.baseConsentSurfaceName)
            await instance.stop()
            throw error
        }
    }

    /// Grab host-side framebuffer frames of the readiness boot and persist them under the base's
    /// Diagnostics dir. Used only when the in-guest agent is unreachable — the framebuffer is the one
    /// piece of evidence obtainable with NO guest cooperation (no agent, no share write, no TCC
    /// prompt), and it lives OUTSIDE the throwaway boot so it survives for inspection on the host.
    ///
    /// Two passes:
    ///  1. `screen-before-*.jpg` — the raw state (is it the login window? a stuck/blank desktop? a
    ///     dialog?), which alone disambiguates "auto-login broke" from "agent broke".
    ///  2. `screen-agentlog-*.jpg` — if a desktop is up, HID-drive Terminal to print the agent's own
    ///     `/tmp/nucleic-vmagent.log` plus a little system state ON SCREEN, and capture that. This
    ///     surfaces the guest-internal reason the vsock server never came up (a crash, a missing
    ///     launch, a bad signature) as an image on the host, with no share/network-volume write.
    ///
    /// A macOS guest can transiently return a blank grab, so each pass takes several frames a beat
    /// apart and keeps whichever aren't empty. Returns the directory it wrote (nil if nothing saved).
    private func captureConsentFailureFrames(
        surface: any MacVMSurfaceHost, bundle: MacVMBundle, attempt: Int
    ) async -> URL? {
        let fm = FileManager.default
        let dir = bundle.provisioningDiagnosticsURL.appendingPathComponent(
            "consent-agent-unreachable-boot\(attempt)-\(UUID().uuidString.lowercased())",
            isDirectory: true)
        guard (try? fm.createDirectory(at: dir, withIntermediateDirectories: true)) != nil else {
            return nil
        }
        try? fm.setAttributes([.posixPermissions: 0o700], ofItemAtPath: dir.path)
        let name = Self.baseConsentSurfaceName
        func pause(_ s: Double) async { try? await Task.sleep(nanoseconds: UInt64(s * 1e9)) }

        var saved = await grabConsentFrames(surface: surface, into: dir, label: "before")

        // Print the guest's own agent log on screen and capture it. Best-effort: if there's no desktop
        // (login window), these keystrokes go nowhere and the "before" frames already tell that story.
        await surface.send(name: name, .key(chord: "esc"))
        await pause(0.6)
        await surface.send(name: name, .key(chord: "cmd+space"))
        await pause(1.2)
        await surface.send(name: name, .text("Terminal"))
        await pause(1.0)
        await surface.send(name: name, .key(chord: "return"))
        await pause(4.0)
        await surface.send(name: name, .text(" "))  // absorb a dropped first keystroke
        await pause(0.4)
        // One line, leading space (so a dropped key costs the space, not the command): clear the screen
        // then dump identity, SIP, whether the agent process is alive, and the tail of its log.
        // The definitive probe: try to EXEC the agent binary directly (a `perl alarm` bounds it to 3s).
        // Its own output tells us instantly which failure this is — "starting on vsock port…" means the
        // binary runs and it's a launchd/plist problem; nothing + "Killed: 9" means a pre-main kill
        // (code signature / dyld); a dyld line names a missing library. Then the launchd exit fields
        // (state/runs/last exit/reason) the previous capture cut off, plus codesign/spctl/log tail.
        let diag =
            " clear; echo '== DIAG =='; csrutil status 2>&1; "
            + "echo '-- DIRECT RUN 3s --'; "
            + "perl -e 'alarm 3; exec @ARGV' "
            + "/Applications/NucleicVMAgent.app/Contents/MacOS/NucleicVMAgent 2>&1 | head -5; "
            + "echo '-- CODESIGN --'; codesign -v /Applications/NucleicVMAgent.app 2>&1 | tail -1; "
            + "echo '-- SPCTL --'; spctl -a -t exec /Applications/NucleicVMAgent.app 2>&1 | head -1; "
            + "echo '-- LAUNCHD --'; "
            + "launchctl print gui/$(id -u)/xyz.blakeslee.nucleic.vmagent 2>&1 "
            + "| grep -iE 'state =|runs =|last exit|reason'; "
            + "echo '-- MOUNTS --'; mount | grep -iE 'virtiofs|Shared' | head -2; "
            + "echo '-- LOGTAIL --'; tail -2 /tmp/nucleic-vmagent.log 2>&1; echo '== END =='"
        await surface.send(name: name, .text(diag))
        await surface.send(name: name, .key(chord: "return"))
        await pause(4.0)
        saved += await grabConsentFrames(surface: surface, into: dir, label: "agentlog")
        await pause(3.0)
        saved += await grabConsentFrames(surface: surface, into: dir, label: "agentlog2")

        return saved > 0 ? dir : nil
    }

    /// Save up to three non-empty framebuffer grabs of the readiness boot into `dir`, a beat apart (a
    /// macOS guest can transiently return a blank frame). Returns how many were written. A real method
    /// (not a nested closure) so the actor owns its `FileManager` use across the capture suspensions.
    private func grabConsentFrames(
        surface: any MacVMSurfaceHost, into dir: URL, label: String
    ) async -> Int {
        let fm = FileManager.default
        var saved = 0
        for frame in 0..<3 {
            if frame > 0 { try? await Task.sleep(nanoseconds: 2_500_000_000) }
            guard let jpeg = await surface.capture(name: Self.baseConsentSurfaceName), !jpeg.isEmpty
            else { continue }
            let url = dir.appendingPathComponent("screen-\(label)-\(frame).jpg")
            if (try? jpeg.write(to: url)) != nil {
                try? fm.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
                saved += 1
            }
        }
        return saved
    }

    /// Bounded, agent-attributed, **production-shaped** share touch: run the very same
    /// `hostPathRecreationScript` a session clone runs at boot (`mount_virtiofs` of the custom-tagged
    /// share at an absolute mountpoint), then read that mountpoint. Exit 0 + marker ⇒ the grant is
    /// effective for the mount shape real work uses; the timeout sentinel ⇒ the consent prompt is
    /// (still) blocking the child. Two properties this must preserve:
    ///
    /// * **Attribution** — it runs through the agent's exec surface, so the prompt (and the grant)
    ///   belong to `xyz.blakeslee.nucleic.vmagent`, the responsible process behind every future
    ///   `mac_vm_exec`.
    /// * **Shape fidelity** — probing anything easier (the automount) can pass while real sessions
    ///   still prompt, which is exactly how a stamped-but-broken base shipped once already.
    private func shareTouchGranted(instance: MacVMInstance, hostPath: String) async throws -> Bool {
        let mount = Self.consentProbeMount(hostDirectory: hostPath)
        let mountScript = Self.hostPathRecreationScript(os: .macOS, mounts: [mount]) ?? ""
        let body = mountScript + "\n"
            + "/bin/ls \(Self.shQuote(Self.macOSMountPoint(mount))) >/dev/null 2>&1 && echo GRANTED"
        let channel = try await openExecChannel(
            instance: instance, label: Self.baseConsentSurfaceName, workdir: nil, env: [:],
            remoteBody: body)
        async let out = Self.drainLines(channel.stdoutLines)
        async let err = Self.drainLines(channel.stderrLines)
        let outcome = await Self.awaitExit(channel, timeoutNanos: 10 * 1_000_000_000)
        let stdout = await out
        _ = await err
        guard case .exited(let code) = outcome else { return false }
        return code == 0 && stdout.contains("GRANTED")
    }
}
#endif
