import CoreGraphics
import Foundation
import VMAgentCore

/// Low-level input synthesis — CGEvent posted to the HID event tap (§8). The raw fallback for
/// targets with no AX action (custom-drawn UI); AX actions (§6) are preferred where they exist.
/// All of it needs the `kTCCServicePostEvent` grant and an Aqua session.
extension ConnectionHandler {
    private var canPostEvents: Bool { CGPreflightPostEventAccess() }

    private func intField(_ request: [String: Any], _ key: String) -> Int? {
        (request[key] as? Int) ?? (request[key] as? Double).map(Int.init)
    }

    /// `click` — move the pointer to (x, y), then press/release. `count` 2 raises the click-state
    /// sequence AppKit needs to see a real double-click.
    func opClick(_ request: [String: Any]) -> [String: Any] {
        guard canPostEvents else { return AgentReply.err(Self.postEventDenied) }
        guard let x = intField(request, "x"), let y = intField(request, "y") else {
            return AgentReply.err("click requires x and y")
        }
        let position = CGPoint(x: x, y: y)
        let count = max(1, min(intField(request, "count") ?? 1, 3))
        let (down, up, button): (CGEventType, CGEventType, CGMouseButton) = {
            switch request["button"] as? String {
            case "right": return (.rightMouseDown, .rightMouseUp, .right)
            case "middle": return (.otherMouseDown, .otherMouseUp, .center)
            default: return (.leftMouseDown, .leftMouseUp, .left)
            }
        }()
        post(mouse: .mouseMoved, at: position, button: .left)
        for clickState in 1...count {
            guard
                let downEvent = CGEvent(
                    mouseEventSource: nil, mouseType: down, mouseCursorPosition: position,
                    mouseButton: button),
                let upEvent = CGEvent(
                    mouseEventSource: nil, mouseType: up, mouseCursorPosition: position,
                    mouseButton: button)
            else { return AgentReply.err("could not create the mouse event") }
            downEvent.setIntegerValueField(.mouseEventClickState, value: Int64(clickState))
            upEvent.setIntegerValueField(.mouseEventClickState, value: Int64(clickState))
            downEvent.post(tap: .cghidEventTap)
            usleep(40_000)
            upEvent.post(tap: .cghidEventTap)
            if clickState < count { usleep(90_000) }  // within the double-click interval
        }
        return AgentReply.ok()
    }

    /// `move` — reposition the pointer without clicking.
    func opMove(_ request: [String: Any]) -> [String: Any] {
        guard canPostEvents else { return AgentReply.err(Self.postEventDenied) }
        guard let x = intField(request, "x"), let y = intField(request, "y") else {
            return AgentReply.err("move requires x and y")
        }
        post(mouse: .mouseMoved, at: CGPoint(x: x, y: y), button: .left)
        return AgentReply.ok()
    }

    /// `drag` — press at (fromX, fromY) (default: the current pointer), glide in interpolated
    /// dragged events (apps ignore a teleporting drag), release at (toX, toY).
    func opDrag(_ request: [String: Any]) -> [String: Any] {
        guard canPostEvents else { return AgentReply.err(Self.postEventDenied) }
        guard let toX = intField(request, "toX"), let toY = intField(request, "toY") else {
            return AgentReply.err("drag requires toX and toY")
        }
        let current = CGEvent(source: nil)?.location ?? .zero
        let from = CGPoint(
            x: intField(request, "fromX").map(Double.init) ?? current.x,
            y: intField(request, "fromY").map(Double.init) ?? current.y)
        let to = CGPoint(x: toX, y: toY)

        post(mouse: .mouseMoved, at: from, button: .left)
        usleep(60_000)
        post(mouse: .leftMouseDown, at: from, button: .left)
        let steps = 12
        for step in 1...steps {
            let t = Double(step) / Double(steps)
            let waypoint = CGPoint(
                x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t)
            usleep(15_000)
            post(mouse: .leftMouseDragged, at: waypoint, button: .left)
        }
        usleep(60_000)
        post(mouse: .leftMouseUp, at: to, button: .left)
        return AgentReply.ok()
    }

    /// `type` — inject literal text as unicode keyboard events (no key-code mapping, so any text
    /// works regardless of layout). Chunked: `keyboardSetUnicodeString` reliably carries only short
    /// runs per event.
    func opType(_ request: [String: Any]) -> [String: Any] {
        guard canPostEvents else { return AgentReply.err(Self.postEventDenied) }
        guard let text = request["text"] as? String, !text.isEmpty else {
            return AgentReply.err("type requires non-empty \"text\"")
        }
        let units = Array(text.utf16)
        var index = 0
        while index < units.count {
            let chunk = Array(units[index..<min(index + 16, units.count)])
            guard
                let down = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: true),
                let up = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: false)
            else { return AgentReply.err("could not create the keyboard event") }
            down.keyboardSetUnicodeString(stringLength: chunk.count, unicodeString: chunk)
            up.keyboardSetUnicodeString(stringLength: chunk.count, unicodeString: chunk)
            down.post(tap: .cghidEventTap)
            up.post(tap: .cghidEventTap)
            usleep(12_000)
            index += 16
        }
        return AgentReply.ok()
    }

    /// `key` — a chord like `cmd+s` / `return` / `cmd+shift+4`, as a real key-code press with the
    /// modifier flags held (VMAgentCore.KeyMap owns the parse).
    func opKey(_ request: [String: Any]) -> [String: Any] {
        guard canPostEvents else { return AgentReply.err(Self.postEventDenied) }
        guard let chord = request["chord"] as? String, !chord.isEmpty else {
            return AgentReply.err("key requires a \"chord\" (e.g. \"cmd+s\")")
        }
        guard let stroke = KeyMap.stroke(forChord: chord) else {
            return AgentReply.err("unrecognized key in chord \"\(chord)\"")
        }
        guard
            let down = CGEvent(keyboardEventSource: nil, virtualKey: stroke.keyCode, keyDown: true),
            let up = CGEvent(keyboardEventSource: nil, virtualKey: stroke.keyCode, keyDown: false)
        else { return AgentReply.err("could not create the keyboard event") }
        down.flags = stroke.flags
        up.flags = stroke.flags
        down.post(tap: .cghidEventTap)
        usleep(30_000)
        up.post(tap: .cghidEventTap)
        return AgentReply.ok()
    }

    /// `scroll` — a REAL wheel event (dy > 0 scrolls content up, dx > 0 scrolls left, in lines),
    /// unlike the SSH path's arrow-key approximation.
    func opScroll(_ request: [String: Any]) -> [String: Any] {
        guard canPostEvents else { return AgentReply.err(Self.postEventDenied) }
        let dy = max(-40, min(intField(request, "dy") ?? 0, 40))
        let dx = max(-40, min(intField(request, "dx") ?? 0, 40))
        guard dx != 0 || dy != 0 else { return AgentReply.err("scroll requires dx and/or dy") }
        guard
            let event = CGEvent(
                scrollWheelEvent2Source: nil, units: .line, wheelCount: 2,
                wheel1: Int32(dy), wheel2: Int32(dx), wheel3: 0)
        else { return AgentReply.err("could not create the scroll event") }
        event.post(tap: .cghidEventTap)
        return AgentReply.ok()
    }

    private func post(mouse type: CGEventType, at position: CGPoint, button: CGMouseButton) {
        CGEvent(
            mouseEventSource: nil, mouseType: type, mouseCursorPosition: position,
            mouseButton: button)?
            .post(tap: .cghidEventTap)
    }

    private static let postEventDenied =
        "input synthesis (kTCCServicePostEvent) is not granted to the agent — re-run provisioning"
}
