import AppKit
import ApplicationServices
import CoreGraphics
import Foundation
import VMAgentCore

extension ConnectionHandler {
    /// `ping` — capability negotiation: protocol version, displays, and TCC readiness, so the host
    /// can distinguish "agent present" from "agent present but not authorized" (§5).
    ///
    /// `version` is the host's HARD gate for "agent reachable" (`awaitAgentReady`), and the display +
    /// TCC-readiness calls below (`CGGetActiveDisplayList`, `AXIsProcessTrusted`,
    /// `CGPreflightScreenCaptureAccess`, …) all talk to the WindowServer / tccd, which can be slow to
    /// answer while the Aqua session is still coming up — most visibly on the first heavy boot after the
    /// toolchain install, once the agent actually holds Screen Recording / Accessibility grants. So the
    /// version is returned even if those stall: they're gathered on a background thread bounded by a
    /// short deadline, and a ping that beats them replies with the version alone (a later ping fills in
    /// readiness). Without this bound, a slow WindowServer could hang every ping and leave the agent
    /// looking unreachable for the host's whole deadline even though the desktop is up.
    func opPing() -> [String: Any] {
        var extras: [String: Any] = [:]
        let ready = DispatchSemaphore(value: 0)
        Thread.detachNewThread {
            var displayIDs = [CGDirectDisplayID](repeating: 0, count: 8)
            var displayCount: UInt32 = 0
            CGGetActiveDisplayList(UInt32(displayIDs.count), &displayIDs, &displayCount)
            let displays: [[String: Any]] = (0..<Int(displayCount)).map { i in
                let id = displayIDs[i]
                return [
                    "id": Int(id),
                    "width": CGDisplayPixelsWide(id),
                    "height": CGDisplayPixelsHigh(id),
                    "main": CGDisplayIsMain(id) != 0,
                ]
            }
            extras = [
                "displays": displays,
                "readiness": [
                    "accessibility": AXIsProcessTrusted(),
                    "screenRecording": CGPreflightScreenCaptureAccess(),
                    "postEvent": CGPreflightPostEventAccess(),
                ],
            ]
            ready.signal()
        }
        var reply: [String: Any] = ["version": AgentWire.version]
        if ready.wait(timeout: .now() + 2) == .success {
            reply.merge(extras) { current, _ in current }
        }
        return AgentReply.ok(reply)
    }

    /// `cursor_position` — the pointer's current screen location (top-left-origin pixels).
    func opCursorPosition() -> [String: Any] {
        guard let event = CGEvent(source: nil) else {
            return AgentReply.err("could not read the cursor position (no event source)")
        }
        let p = event.location
        return AgentReply.ok(["x": Int(p.x.rounded()), "y": Int(p.y.rounded())])
    }

    /// `launch_app` — open an application by name, `NSWorkspace` when the bundle is findable in the
    /// standard app directories, else `/usr/bin/open -a` (which searches LaunchServices).
    func opLaunchApp(_ request: [String: Any]) -> [String: Any] {
        guard let name = request["name"] as? String, !name.isEmpty else {
            return AgentReply.err("launch_app requires a non-empty \"name\"")
        }
        let leaf = name.hasSuffix(".app") ? name : name + ".app"
        let candidates = [
            "/Applications/\(leaf)",
            "/System/Applications/\(leaf)",
            "/System/Applications/Utilities/\(leaf)",
        ]
        if let path = candidates.first(where: { FileManager.default.fileExists(atPath: $0) }) {
            let semaphore = DispatchSemaphore(value: 0)
            var failure: String?
            NSWorkspace.shared.openApplication(
                at: URL(fileURLWithPath: path), configuration: NSWorkspace.OpenConfiguration()
            ) { _, error in
                failure = error.map { String(describing: $0) }
                semaphore.signal()
            }
            _ = semaphore.wait(timeout: .now() + 15)
            if let failure { return AgentReply.err("could not launch \(name): \(failure)") }
            return AgentReply.ok()
        }
        // Not in the standard directories — let LaunchServices resolve it.
        let open = Process()
        open.executableURL = URL(fileURLWithPath: "/usr/bin/open")
        open.arguments = ["-a", name]
        do {
            try open.run()
        } catch {
            return AgentReply.err("could not launch \(name): \(error)")
        }
        open.waitUntilExit()
        guard open.terminationStatus == 0 else {
            return AgentReply.err("no application named \"\(name)\" (open -a exited \(open.terminationStatus))")
        }
        return AgentReply.ok()
    }
}
