#if arch(arm64)
import AppKit
import NucleicCore
import Virtualization

/// The **host-side computer-use surface** (docs/MACOS_VM.md §12): the app-layer implementation of
/// `MacVMSurfaceHost`. For each computer-use VM it owns a `VZVirtualMachineView` bound to the
/// (main-queue) guest and drives it entirely host-side — capturing the guest **framebuffer** via
/// `cacheDisplay` and injecting **keyboard/mouse HID** via synthesized `NSEvent`s delivered to the
/// view. The guest grants nothing (no in-guest agent, no TCC, no SIP); these are ordinary virtual
/// hardware. Proven end-to-end by `macvm-spike`'s host-IO mode.
///
/// The view lives in an **off-screen** borderless window so it renders (the WindowServer composites
/// ordered-in windows regardless of position) without showing a VM window to the user. Everything runs
/// on the main actor, as `VZVirtualMachineView` requires.
@MainActor
final class MacVMComputerSurface: MacVMSurfaceHost {
    /// Guest logical display size in points (matches `MacVMEngine.makeGraphics` — a 3840×2400 @ 2×
    /// Retina panel that renders "looks like 1920×1200"). Screenshots are normalized to this so
    /// image pixels == input coordinates == guest points, all 1:1 regardless of the panel's density.
    static let fbWidth = 1920
    static let fbHeight = 1200

    private struct Entry {
        let window: KeyableWindow
        let view: VZVirtualMachineView
        /// Host-tracked pointer position in guest space (top-left origin), since we synthesize moves.
        var cursor: CGPoint
    }
    private var entries: [String: Entry] = [:]

    init() {}

    // MARK: - MacVMSurfaceHost

    func attach(name: String, virtualMachine box: UncheckedSendableBox<AnyObject>) async {
        guard let vm = box.value as? VZVirtualMachine else { return }
        let frame = NSRect(x: 0, y: 0, width: Self.fbWidth, height: Self.fbHeight)
        let view = VZVirtualMachineView(frame: frame)
        view.virtualMachine = vm
        // Off-screen (agent-driven) the view must NEVER capture the user's physical keyboard: the agent
        // injects HID by handing synthesized events straight to the view's responder (`postStroke` /
        // `postMouse`), which needs neither a key window nor system-key capture. `capturesSystemKeys` is
        // turned on ONLY while the window is deliberately on-screen for the diagnostic monitor /
        // operator-assist hand-off, where the user IS driving the guest (see `setWindowOnScreen`).
        view.capturesSystemKeys = false
        // Scale with the window so the diagnostic monitor can present at a smaller size (the view
        // letterboxes the guest to fit); off-screen it stays at native 1920×1200 for capture/HID.
        view.autoresizingMask = [.width, .height]

        // Off-screen host window: ordered-in (so the view's Metal layer renders) but positioned far
        // off any display. It must never become key while off-screen — a `VZVirtualMachineView` in a key
        // window routes the user's real keyboard (system keys included) into the guest, so an off-screen
        // window winning key focus on app activation is exactly what would "capture" the user's typing.
        // `KeyableWindow.isKeyable` stays `false` here (so `canBecomeKey` is `false`); it's flipped on
        // only when the window is brought on-screen for the diagnostic monitor / operator-assist
        // hand-off, the sole cases where the user should type into the guest. If a future macOS ever
        // refuses to render an off-screen window, host this view in the VM monitor instead.
        let window = KeyableWindow(
            contentRect: frame, styleMask: [.borderless], backing: .buffered, defer: false)
        window.contentView = view
        window.isReleasedWhenClosed = false
        window.setFrameOrigin(NSPoint(x: -30_000, y: -30_000))
        window.orderFrontRegardless()
        window.makeFirstResponder(view)

        entries[name] = Entry(
            window: window, view: view,
            cursor: CGPoint(x: Self.fbWidth / 2, y: Self.fbHeight / 2))
    }

    func detach(name: String) async {
        guard let e = entries.removeValue(forKey: name) else { return }
        if e.window.isKeyWindow { e.window.resignKey() }
        e.view.virtualMachine = nil
        e.window.orderOut(nil)
    }

    func setObserverVisible(name: String, visible: Bool) async {
        // The base-build diagnostic monitor is an explicit, operator-driven hand-off (watch — and, if
        // needed, click into — a misbehaving base build), so it opts into user control.
        setWindowOnScreen(
            name, visible: visible, title: "Nucleic base VM — diagnostic monitor", interactive: true)
    }

    func presentOperatorAssist(name: String, visible: Bool) async {
        // Same on-screen mechanism as the diagnostic monitor, but titled for the user-facing hand-off
        // (`mac_vm_request_operator`): the user drives the guest by hand to clear a captcha / login.
        setWindowOnScreen(
            name, visible: visible, title: "macOS VM — you're driving (agent needs help)",
            interactive: true)
    }

    /// Bring the surface's live `VZVirtualMachineView` on-screen in a titled window (or send it back
    /// off-screen). Shared by the base-build diagnostic monitor and the operator-assist viewer.
    ///
    /// `interactive` is the ONLY thing that ever hands the guest's keyboard/mouse to the user, and it's
    /// deliberately decoupled from mere visibility: showing the window never grants control on its own.
    /// The agent-driven default (off-screen) AND any non-interactive on-screen use keep the window
    /// unkeyable with system-key capture off, so the user can't type or click into the VM — only the
    /// two explicit, user-consented hand-offs (`setObserverVisible`, `presentOperatorAssist`) pass
    /// `interactive: true`. The agent's synthesized HID goes straight to the view's responder and is
    /// unaffected by any of this.
    private func setWindowOnScreen(
        _ name: String, visible: Bool, title: String, interactive: Bool = false
    ) {
        guard let e = entries[name] else { return }
        let w = e.window
        if visible {
            // Give the (normally off-screen, borderless) HID window real chrome and bring it on-screen.
            // The VM view scales to fit (autoresizing), so a 1280×800 window shows the whole 1920×1200
            // guest. Hand the user the keyboard/mouse ONLY for an explicit `interactive` hand-off —
            // where they're deliberately driving the guest; a non-interactive show stays observe-only
            // (unkeyable, no system-key capture), so the guest still can't take the user's input.
            e.view.capturesSystemKeys = interactive
            w.isKeyable = interactive
            w.styleMask = [.titled, .closable, .resizable, .miniaturizable]
            w.title = title
            w.setContentSize(NSSize(width: 1280, height: 800))
            w.center()
            if interactive {
                w.makeKeyAndOrderFront(nil)
                w.makeFirstResponder(e.view)
            } else {
                w.orderFrontRegardless()
            }
            NSApp.activate(ignoringOtherApps: true)
        } else {
            // Back to the invisible, native-size, borderless HID/capture window — and lock it back down
            // so it can never again become key or capture the user's keyboard while the agent drives.
            if w.isKeyWindow { w.resignKey() }
            w.isKeyable = false
            e.view.capturesSystemKeys = false
            w.orderOut(nil)
            w.styleMask = [.borderless]
            w.setContentSize(NSSize(width: Self.fbWidth, height: Self.fbHeight))
            w.setFrameOrigin(NSPoint(x: -30_000, y: -30_000))
            w.orderFrontRegardless()
        }
    }

    func cursorPosition(name: String) async -> Point? {
        guard let e = entries[name] else { return nil }
        return Point(x: Int(e.cursor.x), y: Int(e.cursor.y))
    }

    /// Capture the framebuffer and normalize to a 1920×1200 JPEG (`cacheDisplay` returns the view's
    /// backing-scale bitmap; downscaling keeps the 1:1 coordinate contract).
    func capture(name: String) async -> Data? {
        guard let e = entries[name] else { return nil }
        let view = e.view
        guard let rep = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { return nil }
        view.cacheDisplay(in: view.bounds, to: rep)

        let target = NSSize(width: CGFloat(Self.fbWidth), height: CGFloat(Self.fbHeight))
        guard let out = NSBitmapImageRep(
            bitmapDataPlanes: nil, pixelsWide: Self.fbWidth, pixelsHigh: Self.fbHeight,
            bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
            colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)
        else { return nil }
        out.size = target
        NSGraphicsContext.saveGraphicsState()
        NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: out)
        NSGraphicsContext.current?.imageInterpolation = .high
        let img = NSImage(size: target)
        img.addRepresentation(rep)
        img.draw(in: NSRect(origin: .zero, size: target))
        NSGraphicsContext.restoreGraphicsState()

        return out.representation(using: .jpeg, properties: [.compressionFactor: 0.7])
    }

    func send(name: String, _ input: MacVMSurfaceInput) async {
        guard var e = entries[name] else { return }
        let view = e.view, window = e.window
        switch input {
        case let .move(x, y):
            postMouse(.mouseMoved, at: guestPoint(x, y), window: window, view: view)
            e.cursor = CGPoint(x: x, y: y)

        case let .click(x, y, button, count):
            let p = guestPoint(x, y)
            postMouse(.mouseMoved, at: p, window: window, view: view)
            let (down, up): (NSEvent.EventType, NSEvent.EventType) =
                button == .right ? (.rightMouseDown, .rightMouseUp) : (.leftMouseDown, .leftMouseUp)
            postMouse(down, at: p, window: window, view: view, clickCount: count)
            postMouse(up, at: p, window: window, view: view, clickCount: count)
            e.cursor = CGPoint(x: x, y: y)

        case let .drag(fromX, fromY, toX, toY):
            let from = guestPoint(fromX, fromY), to = guestPoint(toX, toY)
            postMouse(.leftMouseDown, at: from, window: window, view: view)
            postMouse(.leftMouseDragged, at: to, window: window, view: view)
            postMouse(.leftMouseUp, at: to, window: window, view: view)
            e.cursor = CGPoint(x: toX, y: toY)

        case let .scroll(_, _, dx, dy):
            postScroll(dx: dx, dy: dy, view: view)

        case let .key(chord):
            if let s = MacVMKeyMap.stroke(forChord: chord) { postStroke(s, view: view, name: name) }

        case let .text(text):
            for ch in text {
                if let s = MacVMKeyMap.stroke(forCharacter: ch) { postStroke(s, view: view, name: name) }
            }
        }
        entries[name] = e
    }

    // MARK: - NSEvent synthesis (delivered to the view; no system event posting ⇒ no permissions)

    /// Guest (top-left origin) → window/view point (AppKit bottom-left origin).
    private func guestPoint(_ x: Int, _ y: Int) -> NSPoint {
        NSPoint(x: CGFloat(x), y: CGFloat(Self.fbHeight - y))
    }

    private func postMouse(
        _ type: NSEvent.EventType, at p: NSPoint, window: NSWindow, view: NSView, clickCount: Int = 1
    ) {
        guard let e = NSEvent.mouseEvent(
            with: type, location: p, modifierFlags: [],
            timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber,
            context: nil, eventNumber: 0, clickCount: clickCount, pressure: type.isMouseDown ? 1 : 0)
        else { return }
        window.sendEvent(e)
    }

    private func postScroll(dx: Int, dy: Int, view: NSView) {
        // NSEvent has no scroll constructor; build a line-unit scroll CGEvent and wrap it (we deliver
        // it to the view directly, never posting it to the system, so no accessibility grant needed).
        guard let cg = CGEvent(
            scrollWheelEvent2Source: nil, units: .line, wheelCount: 2,
            wheel1: Int32(dy), wheel2: Int32(dx), wheel3: 0),
            let e = NSEvent(cgEvent: cg)
        else { return }
        view.scrollWheel(with: e)
    }

    /// Append the keyboard-injection preconditions for the base-build VM to a host-readable log, so a
    /// stuck provisioning run can be diagnosed without eyes on the guest: is the VM window key, is the
    /// VZ view first responder, is the app active? Provisioning-only (keyed off the base surface name)
    /// and best-effort — never throws into the input path.
    /// Append one line to the base-VM HID diagnostic log (host-readable at
    /// `$TMPDIR/nucleic-hid-diag.log`). Provisioning-only (keyed off the base surface name) and
    /// best-effort — never throws into the input path.
    private func diagWrite(_ name: String, _ line: String) {
        guard name == MacVMEngine.baseProvisionSurfaceName else { return }
        let url = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent("nucleic-hid-diag.log")
        guard let data = "\(Date()) \(line)\n".data(using: .utf8) else { return }
        if let fh = try? FileHandle(forWritingTo: url) {
            defer { try? fh.close() }
            fh.seekToEndOfFile()
            fh.write(data)
        } else {
            try? data.write(to: url)
        }
    }

    private func postStroke(_ s: MacVMKeyStroke, view: NSView, name: String) {
        // Deliver keystrokes to the guest in the BACKGROUND, exactly like the mouse path — hand the
        // events straight to the VZVirtualMachineView's responder instead of routing them through
        // `NSApp`/key-window dispatch. That routing is what forced the app to activate and steal focus
        // (AppKit sends key events only to the *key* window); calling the view's methods directly
        // bypasses it, so the surface window never needs to be key or on-screen.
        //
        // Two subtleties:
        //  • CGEvent backing — VZVirtualMachineView derives the guest HID scancode from the event's
        //    underlying CGEvent (a hand-built `NSEvent.keyEvent` has none and types nothing).
        //  • Modifiers are their OWN `flagsChanged` events — the view tracks the guest's modifier state
        //    from those, NOT from a `.command` flag on a keyDown. So ⌘Space must be: press ⌘
        //    (flagsChanged) → Space down/up → release ⌘, or it arrives as a bare Space.

        // (virtualKey, flag) for each held modifier, in a stable press order. Each flag combines the
        // device-INDEPENDENT mask (.maskCommand …) with the device-DEPENDENT left-key bit (NX_DEVICE*
        // masks: LCMD 0x8, LSHIFT 0x2, LALT 0x20, LCTL 0x1). A real keypress carries both; when we hand
        // events straight to the view (rather than posting them through the system, which would fill
        // these in), VZVirtualMachineView needs the device bit to recognize the modifier — with only
        // the independent mask, ⌘Space arrives as a bare Space.
        func flag(_ mask: CGEventFlags, _ deviceBit: UInt64) -> CGEventFlags {
            CGEventFlags(rawValue: mask.rawValue | deviceBit)
        }
        var mods: [(UInt16, CGEventFlags)] = []
        if s.command { mods.append((0x37, flag(.maskCommand, 0x8))) }    // kVK_Command  + NX_DEVICELCMD
        if s.shift { mods.append((0x38, flag(.maskShift, 0x2))) }        // kVK_Shift    + NX_DEVICELSHIFT
        if s.option { mods.append((0x3A, flag(.maskAlternate, 0x20))) }  // kVK_Option   + NX_DEVICELALT
        if s.control { mods.append((0x3B, flag(.maskControl, 0x1))) }    // kVK_Control  + NX_DEVICELCTL
        if s.function { mods.append((0x3F, .maskSecondaryFn)) }          // kVK_Function

        var trace: [String] = []
        var flags: CGEventFlags = []
        // Press modifiers: accumulate the flag and emit a flagsChanged for each.
        for (code, flag) in mods {
            flags.insert(flag)
            trace.append("fcDown(\(code))=\(postCGKey(virtualKey: code, keyDown: true, flags: flags, type: .flagsChanged, to: view))")
        }
        // The key itself, down then up, with the modifiers held.
        trace.append("down=\(postCGKey(virtualKey: s.keyCode, keyDown: true, flags: flags, type: .keyDown, to: view))")
        trace.append("up=\(postCGKey(virtualKey: s.keyCode, keyDown: false, flags: flags, type: .keyUp, to: view))")
        // Release modifiers in reverse: clear the flag and emit a flagsChanged for each.
        for (code, flag) in mods.reversed() {
            flags.remove(flag)
            trace.append("fcUp(\(code))=\(postCGKey(virtualKey: code, keyDown: false, flags: [], type: .flagsChanged, to: view))")
        }
        if !mods.isEmpty {
            diagWrite(name, "stroke key=0x\(String(s.keyCode, radix: 16)) mods=\(mods.count) \(trace.joined(separator: " "))")
        }
    }

    /// Build one CGEvent-backed key/modifier event and hand it straight to the VM view's responder.
    /// Returns whether an `NSEvent` was actually produced and delivered (for diagnostics).
    @discardableResult
    private func postCGKey(
        virtualKey: UInt16, keyDown: Bool, flags: CGEventFlags, type: CGEventType, to view: NSView
    ) -> Bool {
        guard let cg = CGEvent(keyboardEventSource: nil, virtualKey: virtualKey, keyDown: keyDown)
        else { return false }
        cg.flags = flags
        cg.type = type  // a modifier press is a flagsChanged, not a keyDown
        guard let e = NSEvent(cgEvent: cg) else { return false }
        switch type {
        case .flagsChanged: view.flagsChanged(with: e)
        case .keyUp: view.keyUp(with: e)
        default: view.keyDown(with: e)
        }
        return true
    }
}

extension NSEvent.EventType {
    fileprivate var isMouseDown: Bool { self == .leftMouseDown || self == .rightMouseDown }
}

/// A borderless window whose ability to become key/main is opt-in via ``isKeyable``, defaulting to
/// `false`. This matters for input isolation: a `VZVirtualMachineView` in a key window routes the
/// host's real keyboard into the guest, so the normally off-screen, agent-driven HID window must NOT be
/// able to take key — otherwise it can win key focus on app activation and silently "capture" the
/// user's typing. Background agent input needs no key window (keystrokes go straight to the view's
/// responder). Only when the window is deliberately brought on-screen for the diagnostic monitor /
/// operator-assist hand-off — where the user drives the guest by hand — is ``isKeyable`` flipped `true`
/// so it can take key. Plain `.borderless` windows return `false` for both unconditionally.
private final class KeyableWindow: NSWindow {
    /// When `true`, the window may become key/main (on-screen, user-driven). `false` off-screen so the
    /// guest can never capture the user's keyboard while the agent has exclusive control.
    var isKeyable = false
    override var canBecomeKey: Bool { isKeyable }
    override var canBecomeMain: Bool { isKeyable }
}
#endif
