// GestureController — hand-gesture control for Jarvis.
//
// A Python tracker (voice/hands.py, a child of the engine) watches the webcam
// and streams gesture datagrams over UDP 127.0.0.1:47831 — one JSON object per
// datagram (protocol below). This controller drives the REAL macOS mouse with
// EITHER hand and paints a reticle halo on each:
//   • The ACTIVE hand (the one that most recently pinched or right-clicked; else
//     the "R" hand if present, else "L") moves the actual system cursor by
//     POSTING CGEvents, so hover/UI states update and the user aims with the
//     real pointer they can see.
//   • ANY hand pinch = synthetic left-button AT THAT HAND'S OWN (stabilized)
//     point: pinch_start→mouseMoved+leftMouseDown, pinch_move→leftMouseDragged
//     (only past a 12px dead-zone — see below), pinch_end→leftMouseUp. A quick
//     down→up is a native click (Dock, buttons, apps); a down→move→up is a
//     native drag (title bars, text selection, Dock rearrange). macOS derives
//     click-vs-drag itself — no timers. The FIRST hand to pinch OWNS the button;
//     a second hand's pinch (and its move/end) is ignored until release.
//   • rclick event (thumb+middle "mpinch", one-shot) = a right-click
//     (rightMouseDown+Up) at that hand's stabilized point, when no left button
//     is down. (This REPLACES the old secondary-hand pinch→right-click, which
//     confused left-hand users trying to drag panels.)
//   • fist-hold = toggle-maximize the window under that hand (visibleFrame, via
//     the Accessibility API — kept because it's distinct from pinch and handy).
//
// Protocol (one JSON object per datagram) — FROZEN, matches voice/hands.py:
//   STREAM (30 Hz, BOTH hands in one message):
//     {"t":"hands","armed":true,
//      "hands":[{"hand":"R","x":0..1,"y":0..1,
//                "pose":"point|pinch|mpinch|fist|palm|none"}, …0..2…]}
//   ARM/DISARM/READY:
//     {"t":"gesture","name":"arm"} / {"name":"disarm"}
//     {"name":"ready"}   — once when the tracker's camera is up (visible confirm)
//   PER-HAND EVENTS (each carries "hand":"R"|"L" + absolute "x","y" 0..1 top-left):
//     {"t":"gesture","name":"pinch_start","hand":"R","x":..,"y":..}  ← PRE-STABILIZED
//     {"name":"pinch_move","hand":"R","x":..,"y":..}   (live, every frame)
//     {"name":"pinch_end","hand":"R","x":..,"y":..}    (live)
//     {"name":"rclick","hand":"R","x":..,"y":..}       ← one-shot, stabilized
//     {"name":"fist_hold","hand":"R","x":..,"y":..}
//   (There are NO spread_* events; window dragging/resizing is now native mouse.)
//   pinch_start/rclick coords are the position ~120ms BEFORE the pinch, so the
//   click lands on the target the user was aiming at (not where the pinch motion
//   dragged the hand). pinch_move/pinch_end are live.
//
// ─────────────────────────────────────────────────────────────────────────────
// COORDINATE SPACES (the classic bug — verified twice, keep it right)
// ─────────────────────────────────────────────────────────────────────────────
// The stream's x,y are normalized 0..1 with origin at the TOP-LEFT of the main
// screen (already mirrored + smoothed by the tracker). Two conversions:
//   • REAL MOUSE (CGEvent) + CGWindowList/AX → top-left global space (== stream):
//         cgX = x·W           cgY = y·H          →  cgPoint(n)
//     CGEvent mouseCursorPosition uses exactly this: global-display coords with a
//     TOP-LEFT origin on the main display. Use cgPoint for ALL mouse events.
//   • NSWindow work (the reticle halos) → Cocoa space, BOTTOM-LEFT origin:
//         cocoaX = x·W        cocoaY = (1 − y)·H  →  cocoaPoint(n)
//     cocoaPoint is ONLY for positioning reticle NSWindows — never mouse events.
//
// v1 LIMITATION: MAIN (primary) screen only — normalized coords map onto
// NSScreen.screens[0], which is the display CGMainDisplayID() reports (Cocoa
// frame (0,0,W,H) == CG bounds (0,0,W,H), so one W/H serves both spaces).
// Multi-display needs the tracker to say which screen it means; punt for now.

import AppKit
import CoreGraphics   // CGEvent / CGEventSource — the real synthetic mouse
import Network
import SwiftUI

// MARK: - Reticle (per-hand cursor halo)

final class ReticleModel: ObservableObject {
    @Published var pose = "none"      // point | pinch | mpinch | fist | palm | none
    @Published var isPrimary = true   // active hand = accent ring on the real cursor
    @Published var rightFlash = false // right-click momentary pop (orange accent)
    @Published var pulse = false      // brief scale/opacity swell (ready/arm confirm)
}

/// Liquid-glass-ish hand halo. The ACTIVE hand's reticle is a full accent ring
/// riding on the real system cursor (filled/contracted when its pinch is live,
/// solid dot when fisting). The other hand's reticle is dimmer + smaller. Either
/// reticle flashes a warm accent when its hand fires a right-click ("mpinch").
struct ReticleView: View {
    @ObservedObject var model: ReticleModel

    private var contracted: Bool { model.pose == "pinch" || model.pose == "mpinch" || model.pose == "fist" }
    private var base: CGFloat { contracted ? 17 : 26 }
    private var diameter: CGFloat { model.isPrimary ? base : base * 0.7 }
    private var ring: Color { model.rightFlash ? .orange : .cyan }
    private var ringOpacity: Double { model.rightFlash ? 0.95 : (model.isPrimary ? 0.9 : 0.5) }
    private var fill: Color {
        if model.rightFlash { return .orange.opacity(0.7) }
        switch model.pose {
        case "pinch", "mpinch": return model.isPrimary ? .cyan.opacity(0.85) : .clear
        case "fist":            return .white
        default:                return .clear
        }
    }

    var body: some View {
        ZStack {
            Circle().fill(fill)
            Circle().strokeBorder(ring.opacity(ringOpacity),
                                  lineWidth: model.isPrimary ? 1.5 : 1.2)
        }
        .frame(width: diameter, height: diameter)
        .shadow(color: ring.opacity(model.isPrimary ? 0.8 : 0.4),
                radius: model.isPrimary ? 6 : 4)
        .scaleEffect(model.pulse ? 1.35 : 1.0)
        .opacity(model.pulse ? 0.9 : 1.0)
        .frame(width: GestureController.reticleSize,
               height: GestureController.reticleSize)
        .animation(.easeOut(duration: 0.1), value: model.pose)
        .animation(.easeOut(duration: 0.1), value: model.rightFlash)
        .animation(.easeInOut(duration: 0.4), value: model.pulse)
    }
}

/// One always-on-top, click-through NSWindow hosting a ReticleView. Reused per
/// hand ("R"/"L") — plus a "C" instance for the center-screen "ready" pulse — so
/// we never spawn more than three.
@MainActor
final class HandReticle {
    let model = ReticleModel()
    let window: NSWindow
    private let size: CGFloat

    init(size: CGFloat) {
        self.size = size
        window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: size, height: size),
                          styleMask: .borderless, backing: .buffered, defer: false)
        window.isOpaque = false
        window.backgroundColor = .clear
        window.hasShadow = false
        window.ignoresMouseEvents = true                 // never steals the click
        window.level = .screenSaver                       // above everything we manage
        window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
        window.isReleasedWhenClosed = false
        window.contentView = NSHostingView(rootView: ReticleView(model: model))
    }

    /// Center the halo on a Cocoa (bottom-left) point.
    func place(atCocoa c: NSPoint) {
        window.setFrameOrigin(NSPoint(x: c.x - size / 2, y: c.y - size / 2))
        if !window.isVisible { window.orderFrontRegardless() }
    }

    func hide() { if window.isVisible { window.orderOut(nil) } }
}

// MARK: - GestureController

@MainActor
final class GestureController {
    static let port: UInt16 = 47831
    static let reticleSize: CGFloat = 36

    private let windows = WindowManager()   // AX plumbing (fist-hold maximize)

    private(set) var enabled = false        // mirrors the engine's tracker state
    private var armed = false
    private var lastSeen = Date.distantPast // last stream datagram

    /// Fired on real gesture activity (an armed hand streaming / acting) so the
    /// HUD can treat hand gestures as interaction — e.g. to cancel idle auto-dim.
    var onActivity: (() -> Void)?

    // MARK: Real mouse (CGEvent)

    private let eventSource = CGEventSource(stateID: .hidSystemState)
    /// Latest cursor point in CG top-left global space (kept in sync by the
    /// stream while idle, and by pinch_move while dragging).
    private var cursor = CGPoint.zero

    /// The hand currently holding the LEFT button down (nil = up) — the OWNER.
    /// The first hand to pinch owns the down/drag/release; a second hand's pinch
    /// is ignored entirely until the owner releases, and a vanished owner can't
    /// strand the button (the stale-stream release in tick() backstops).
    private var owner: String?
    /// Where the left button went down (the stabilized point).
    private var downLoc = CGPoint.zero
    /// Dead-zone latch: false until the pinch has moved >12px from downLoc; while
    /// false we suppress ALL drags so a quick pinch-release is a clean click on
    /// the stabilized point (protects clicks on small targets).
    private var dragStarted = false

    /// The hand that most recently acted (pinch/rclick). Drives cursor hover-
    /// follow: this hand if present, else the other present hand, else nil.
    /// nil until any hand has ever acted (then R-else-L is the default).
    private var activeHand: String?

    // MARK: Reticle windows (created on first use, keyed by hand — "C" = center)

    private var reticles: [String: HandReticle] = [:]
    private func reticle(_ key: String) -> HandReticle {
        if let r = reticles[key] { return r }
        let r = HandReticle(size: Self.reticleSize)
        reticles[key] = r
        return r
    }
    private func hideAllReticles() { for r in reticles.values { r.hide() } }
    private var staleTimer: Timer?
    private var centerPulseGen = 0   // cancels a stale center-pulse hide

    // MARK: Network

    private var listener: NWListener?
    private var conns: [NWConnection] = []
    private nonisolated static let netQueue = DispatchQueue(label: "jarvis.gestures.udp")

    private var savedFrames: [CGWindowID: CGRect] = [:]   // fist-maximize restore (AX space)

    // MARK: Enable / disable (driven by the menu + the engine's "gestures" event)

    func setEnabled(_ on: Bool) {
        guard on != enabled else { return }
        enabled = on
        if on {
            WindowManager.ensureAccessibilityPermission()   // AX = both window ops AND synthetic events
            startListener()
            staleTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) {
                [weak self] _ in Task { @MainActor in self?.tick() }
            }
            // Gestures are now on (engine reported it) → visible confirmation the
            // system is live, even before any hand appears or the user arms.
            centerPulse()
        } else {
            stopListener()
            staleTimer?.invalidate()
            staleTimer = nil
            releaseLeftIfNeeded()
            armed = false
            activeHand = nil
            hideAllReticles()
        }
    }

    /// Hide reticles when the stream goes quiet, and release a held left button
    /// if the tracker died mid-drag (no pinch_end would ever come → stuck click).
    private func tick() {
        if Date().timeIntervalSince(lastSeen) > 0.5 {
            hideAllReticles()
            releaseLeftIfNeeded()
        }
    }

    /// Fail-safe: never leave the physical left button stuck down. Also clears
    /// the owner + dead-zone state so the next pinch starts clean.
    private func releaseLeftIfNeeded() {
        guard owner != nil else { return }
        postMouse(.leftMouseUp, at: cursor, button: .left, clickState: 1)
        owner = nil
        dragStarted = false
    }

    // MARK: UDP listener (loopback only)

    private func startListener() {
        guard listener == nil else { return }
        let params = NWParameters.udp
        params.allowLocalEndpointReuse = true
        params.requiredLocalEndpoint = NWEndpoint.hostPort(
            host: "127.0.0.1", port: NWEndpoint.Port(rawValue: Self.port)!)
        guard let l = try? NWListener(using: params) else { return }
        l.newConnectionHandler = { [weak self] conn in
            guard let self else { conn.cancel(); return }
            Task { @MainActor in self.adopt(conn) }
        }
        l.start(queue: Self.netQueue)
        listener = l
    }

    private func stopListener() {
        listener?.cancel()
        listener = nil
        conns.forEach { $0.cancel() }
        conns.removeAll()
    }

    private func adopt(_ conn: NWConnection) {
        conns.append(conn)
        if conns.count > 8 { conns.removeFirst().cancel() }   // stray-sender paranoia
        conn.start(queue: Self.netQueue)
        receive(on: conn)
    }

    nonisolated private func receive(on conn: NWConnection) {
        conn.receiveMessage { [weak self] data, _, _, error in
            guard let self else { conn.cancel(); return }
            if let data, !data.isEmpty,
               let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] {
                Task { @MainActor in self.handle(obj) }       // AX/NSWindow/CGEvent = main thread
            }
            if error == nil { self.receive(on: conn) }
        }
    }

    // MARK: Datagram dispatch

    private func handle(_ m: [String: Any]) {
        guard enabled else { return }
        if (m["t"] as? String) == "hands" { handsStream(m); return }
        guard let name = m["name"] as? String else { return }
        switch name {
        case "arm":    setArmed(true)
        case "disarm": setArmed(false)
        case "ready":  centerPulse()          // camera up — confirm gestures are live
        default:
            // Per-hand events carry hand + absolute (stabilized/live) point.
            guard let hand = m["hand"] as? String,
                  let x = num(m["x"]), let y = num(m["y"]) else { return }
            let n = CGPoint(x: x, y: y)
            switch name {
            case "pinch_start": pinchStart(hand, n)
            case "pinch_move":  pinchMove(hand, n)
            case "pinch_end":   pinchEnd(hand, n)
            case "rclick":      rclick(hand, n)
            case "fist_hold":   fistHold(n)
            default: break
            }
        }
    }

    // MARK: Stream — two hands, two reticles, real cursor

    /// Which hand drives the real cursor: the active hand if present, else the
    /// other present hand (fallback when the active one leaves), else R-else-L.
    private func drivingHand(_ present: [String: (n: CGPoint, pose: String)]) -> String? {
        if let a = activeHand {
            if present[a] != nil { return a }
            let other = a == "R" ? "L" : "R"
            if present[other] != nil { return other }
            return nil
        }
        return present["R"] != nil ? "R" : (present["L"] != nil ? "L" : nil)
    }

    private func handsStream(_ m: [String: Any]) {
        lastSeen = Date()
        if let a = m["armed"] as? Bool, a != armed { setArmed(a) }

        // Parse present hands (0..2).
        var present: [String: (n: CGPoint, pose: String)] = [:]
        if let arr = m["hands"] as? [[String: Any]] {
            for h in arr {
                guard let hand = h["hand"] as? String,
                      let x = num(h["x"]), let y = num(h["y"]) else { continue }
                present[hand] = (CGPoint(x: x, y: y), (h["pose"] as? String) ?? "none")
            }
        }

        guard enabled, armed else { hideAllReticles(); return }
        onActivity?()   // armed hand controlling = interaction (keeps HUD awake)

        // A live hand stream supersedes the transient center "ready" pulse.
        if !present.isEmpty { reticles["C"]?.hide() }

        let dh = drivingHand(present)

        // Reticle per hand; the driving (active) hand gets the primary look.
        for key in ["R", "L"] {
            if let info = present[key] {
                let r = reticle(key)
                if r.model.pose != info.pose { r.model.pose = info.pose }
                let primary = key == dh
                if r.model.isPrimary != primary { r.model.isPrimary = primary }
                r.place(atCocoa: cocoaPoint(info.n))
            } else {
                reticles[key]?.hide()
            }
        }

        // Drive the REAL cursor with the active hand while no button is held
        // (during a drag, pinch_move is authoritative, so we don't fight it).
        if owner == nil, let dh, let info = present[dh] {
            let p = cgPoint(info.n)
            cursor = p
            postMouse(.mouseMoved, at: p, button: .left)
        }
    }

    private func setArmed(_ on: Bool) {
        guard on != armed else { return }
        armed = on
        if on {
            centerPulse()               // arming is visible
        } else {
            releaseLeftIfNeeded()
            hideAllReticles()
        }
    }

    // MARK: Pinch → synthetic mouse (either hand = left button)

    private func pinchStart(_ hand: String, _ n: CGPoint) {
        guard armed else { return }
        guard owner == nil else { return }   // another hand already owns the button → ignore this pinch
        let p = cgPoint(n)
        cursor = p
        downLoc = p
        dragStarted = false
        owner = hand
        activeHand = hand
        postMouse(.mouseMoved, at: p, button: .left)   // move to the stabilized point first…
        postMouse(.leftMouseDown, at: p, button: .left, clickState: 1)   // …then press there
    }

    private func pinchMove(_ hand: String, _ n: CGPoint) {
        guard hand == owner else { return }   // only the owner drags; other hand's moves ignored
        let p = cgPoint(n)
        if !dragStarted {
            // Dead-zone: suppress drags (and any cursor move) until we clear 12px.
            let dx = p.x - downLoc.x, dy = p.y - downLoc.y
            guard dx * dx + dy * dy > 144 else { return }   // 12² — still a clean click
            dragStarted = true
        }
        cursor = p
        postMouse(.leftMouseDragged, at: p, button: .left)
    }

    private func pinchEnd(_ hand: String, _ n: CGPoint) {
        guard hand == owner else { return }   // ignore a non-owner hand's end
        // Release at the live point if a drag actually started, else at the
        // stabilized down point so macOS sees a clean single click on-target.
        let p = dragStarted ? cgPoint(n) : downLoc
        cursor = p
        owner = nil
        dragStarted = false
        postMouse(.leftMouseUp, at: p, button: .left, clickState: 1)
    }

    /// One-shot right-click (thumb+middle "mpinch") at this hand's stabilized
    /// point — only when no left button is down (never mid-drag).
    private func rclick(_ hand: String, _ n: CGPoint) {
        guard armed, owner == nil else { return }
        let p = cgPoint(n)
        cursor = p
        activeHand = hand
        postMouse(.mouseMoved, at: p, button: .left)
        postMouse(.rightMouseDown, at: p, button: .right, clickState: 1)
        postMouse(.rightMouseUp,   at: p, button: .right, clickState: 1)
        flashRight(hand)
    }

    private func flashRight(_ hand: String) {
        let r = reticle(hand)
        r.model.rightFlash = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) { [weak self] in
            self?.reticles[hand]?.model.rightFlash = false
        }
    }

    /// Brief reticle swell at the CENTER of the main screen — visible proof that
    /// gesture mode is running (fired on "ready", on enable, and on arm). Auto-
    /// hides after ~1.2s unless a live hand stream takes over the "C" reticle.
    private func centerPulse() {
        guard let f = primaryFrame else { return }
        let r = reticle("C")
        r.model.isPrimary = true
        r.model.rightFlash = false
        r.model.pose = "point"
        r.place(atCocoa: NSPoint(x: f.midX, y: f.midY))
        r.model.pulse = true
        centerPulseGen += 1
        let gen = centerPulseGen
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
            self?.reticles["C"]?.model.pulse = false
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { [weak self] in
            guard let self, self.centerPulseGen == gen else { return }
            self.reticles["C"]?.hide()
        }
    }

    /// Post one synthetic mouse event to the HID tap (moves the real cursor and
    /// delivers the click to whatever is under it). Requires Accessibility trust,
    /// which the app already holds.
    private func postMouse(_ type: CGEventType, at p: CGPoint,
                           button: CGMouseButton, clickState: Int64 = 0) {
        guard let e = CGEvent(mouseEventSource: eventSource, mouseType: type,
                              mouseCursorPosition: p, mouseButton: button) else { return }
        if clickState > 0 { e.setIntegerValueField(.mouseEventClickState, value: clickState) }
        e.post(tap: .cghidEventTap)
    }

    // MARK: Fist-hold = toggle maximize (visibleFrame, not full-screen Spaces)

    private func fistHold(_ n: CGPoint) {
        guard ownPanel(at: cocoaPoint(n)) == nil else { return }  // not through our glass
        guard let hit = topWindow(atCG: cgPoint(n)),
              let win = windows.axWindow(pid: hit.pid, matching: hit.bounds),
              let cur = windows.axFrame(win),
              let sc = windows.screens().first else { return }
        let target = windows.axRect(fromCocoa: sc.visibleFrame)
        let tol: CGFloat = 20
        let isMaximized = abs(cur.minX - target.minX) <= tol
                       && abs(cur.minY - target.minY) <= tol
                       && abs(cur.width - target.width) <= tol
                       && abs(cur.height - target.height) <= tol
        if isMaximized, let prev = savedFrames[hit.id] {
            savedFrames[hit.id] = nil
            windows.axSetFrame(win, prev)
        } else {
            savedFrames[hit.id] = cur
            if savedFrames.count > 64 { savedFrames = [hit.id: cur] }  // crude bound
            windows.axSetFrame(win, target)
        }
    }

    // MARK: Hit-testing (fist-hold only)

    /// Frontmost of OUR glass panels containing a Cocoa point, so a fist over the
    /// HUD doesn't maximize a window hiding behind it. Reticles are plain
    /// NSWindows (not GlassPanel), so they never hit-test against themselves.
    private func ownPanel(at p: NSPoint) -> NSWindow? {
        NSApp.windows
            .filter { $0 is GlassPanel && $0.isVisible && $0.frame.contains(p) }
            .min { $0.orderedIndex < $1.orderedIndex }         // 0 = frontmost
    }

    /// Topmost normal (layer-0) system window under a CG top-left-space point,
    /// excluding this app. CGWindowListCopyWindowInfo returns front→back order,
    /// so the first match is the visually topmost one.
    private func topWindow(atCG p: CGPoint) -> (id: CGWindowID, pid: pid_t, bounds: CGRect)? {
        let opts: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
        guard let list = CGWindowListCopyWindowInfo(opts, kCGNullWindowID)
                as? [[String: Any]] else { return nil }
        let me = ProcessInfo.processInfo.processIdentifier
        for info in list {
            guard (info[kCGWindowLayer as String] as? Int) == 0,
                  let pid = (info[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value,
                  pid != me,
                  let idNum = (info[kCGWindowNumber as String] as? NSNumber)?.uint32Value,
                  let bd = info[kCGWindowBounds as String] as? NSDictionary,
                  let bounds = CGRect(dictionaryRepresentation: bd),
                  (info[kCGWindowAlpha as String] as? Double ?? 1) > 0.05,
                  bounds.width >= 40, bounds.height >= 40,     // skip status/junk slivers
                  bounds.contains(p)
            else { continue }
            return (CGWindowID(idNum), pid_t(pid), bounds)
        }
        return nil
    }

    // MARK: Coordinate conversion (see file header)

    /// Primary screen frame — Cocoa (0,0,W,H), same W/H as CG main-display bounds.
    private var primaryFrame: CGRect? { NSScreen.screens.first?.frame }

    /// Normalized top-left → Cocoa bottom-left (reticle NSWindow placement ONLY).
    private func cocoaPoint(_ n: CGPoint) -> NSPoint {
        guard let f = primaryFrame else { return .zero }
        return NSPoint(x: f.minX + n.x * f.width,
                       y: f.minY + (1 - n.y) * f.height)
    }

    /// Normalized top-left → CG/AX top-left (real mouse events + CGWindowList/AX).
    private func cgPoint(_ n: CGPoint) -> CGPoint {
        guard let f = primaryFrame else { return .zero }
        return CGPoint(x: n.x * f.width, y: n.y * f.height)
    }

    private func num(_ v: Any?) -> Double? { (v as? NSNumber)?.doubleValue }
}
