import Foundation
import Network

/// Localhost HTTP MCP server hosted inside the app (ADAPTERS §1.2). Claude Code
/// is pointed at it via `--mcp-config` + `--permission-prompt-tool`; it exposes a
/// single tool `approve` whose call SUSPENDS until a human resolves the approval.
///
/// One server can serve all sessions: requests are authenticated by a per-session
/// bearer token, which keys the registered handler.
public actor MCPApprovalServer {
    public struct ApprovalCall: Sendable {
        public let toolName: String
        public let input: JSONValue
        public let toolUseID: String?
    }

    public enum Reply: Sendable {
        case allow(updatedInput: JSONValue)
        case deny(message: String)

        /// The confirmed wire shape: a JSON-stringified `{behavior,…}` object
        /// placed inside an MCP text content block — `behavior`/`message`/
        /// `updatedInput`, NOT `decision`/`reason` (ADAPTERS §1.2 ✅).
        public func wireJSON() -> String {
            switch self {
            case .allow(let updatedInput):
                return JSONValue.object([
                    "behavior": .string("allow"), "updatedInput": updatedInput,
                ]).canonicalString()
            case .deny(let message):
                return JSONValue.object([
                    "behavior": .string("deny"), "message": .string(message),
                ]).canonicalString()
            }
        }
    }

    /// A `check_conflict` tool call: the agent's task and the files/areas it expects to
    /// touch. The handler suspends (like `approve`) until Nucleic decides + the user
    /// answers, then returns a `ConflictReply` the agent reads to know how to proceed.
    public struct ConflictCall: Sendable {
        public let task: String
        public let files: [String]
    }

    public struct ConflictReply: Sendable {
        public let resolution: ConflictResolution

        public init(resolution: ConflictResolution) { self.resolution = resolution }

        /// Text-block JSON the agent reads. `conflict` is true unless there was no
        /// overlap; `action` + `message` tell it exactly what to do.
        public func wireJSON() -> String {
            switch resolution {
            case .noConflict:
                return JSONValue.object(["conflict": .bool(false)]).canonicalString()
            case .deferred:
                return conflictJSON(
                    "deferred",
                    "This task was added to the Nucleic to-do list because it conflicts with "
                        + "another active agent. Stop now and do NOT work on it.")
            case .cancelled:
                return conflictJSON(
                    "cancelled",
                    "The user cancelled this task because it conflicts with another active "
                        + "agent. Stop now and do NOT work on it.")
            case .proceed:
                return conflictJSON(
                    "proceed",
                    "This task conflicts with another active agent, but the user chose to "
                        + "proceed. You may continue.")
            }
        }

        private func conflictJSON(_ action: String, _ message: String) -> String {
            JSONValue.object([
                "conflict": .bool(true),
                "action": .string(action),
                "message": .string(message),
            ]).canonicalString()
        }
    }

    /// A `host_exec` tool call: the agent asks to run a shell command on the *host* machine,
    /// outside the sandbox container (e.g. to build/run a macOS binary the Linux container
    /// can't). The handler suspends (like `approve`) until the user approves and the host
    /// command finishes, then returns its output. Always gated; never auto-approved.
    public struct HostExecCall: Sendable {
        public let command: String
        public init(command: String) { self.command = command }
    }

    public enum HostExecReply: Sendable {
        case denied(message: String)
        case ran(exitCode: Int32, stdout: String, stderr: String)

        /// Text-block JSON the agent reads back from the tool result.
        public func wireJSON() -> String {
            switch self {
            case .denied(let message):
                return JSONValue.object([
                    "denied": .bool(true), "message": .string(message),
                ]).canonicalString()
            case .ran(let exitCode, let stdout, let stderr):
                return JSONValue.object([
                    "exit_code": .number(Double(exitCode)),
                    "stdout": .string(stdout),
                    "stderr": .string(stderr),
                ]).canonicalString()
            }
        }
    }

    /// A ground-truth report from the in-container `git` interceptor shim: the real git the
    /// agent ran (argv + cwd + exit code) in a Nucleic Control container. Delivered over a
    /// dedicated `POST /git-event` route (not an MCP tool), so the shim needs no MCP handshake.
    public struct GitReportCall: Sendable {
        public let argv: [String]
        public let cwd: String
        public let exitCode: Int
        public let subcommand: String?
        public let sessionID: String?
        public init(argv: [String], cwd: String, exitCode: Int, subcommand: String?, sessionID: String?) {
            self.argv = argv
            self.cwd = cwd
            self.exitCode = exitCode
            self.subcommand = subcommand
            self.sessionID = sessionID
        }
    }

    /// A ground-truth report from the in-container `gh` interceptor shim — the agentic GitHub
    /// action the agent ran (argv + cwd + exit code). Same shape and `POST /gh-event` transport as
    /// `GitReportCall`; the host classifies it via `GhCommandSummary`.
    public struct GhReportCall: Sendable {
        public let argv: [String]
        public let cwd: String
        public let exitCode: Int
        public let subcommand: String?
        public let sessionID: String?
        public init(argv: [String], cwd: String, exitCode: Int, subcommand: String?, sessionID: String?) {
            self.argv = argv
            self.cwd = cwd
            self.exitCode = exitCode
            self.subcommand = subcommand
            self.sessionID = sessionID
        }
    }

    /// A ground-truth report of one ordinary (non-git) command the in-container interceptor
    /// observed — from the output-capturing shim (`source == .shim`, carrying argv + a capped
    /// stdout/stderr head) or the bash command tracer (`source == .tracer`, a raw command line +
    /// metadata only). Delivered over `POST /command-event`. One HTTP request may carry many
    /// (the tracer batches a whole Bash tool call), so the route fans out to one call each.
    public struct CommandReportCall: Sendable {
        public enum Source: String, Sendable { case shim, tracer }
        /// The resolved command name (shim), or nil when only a raw command line is available
        /// (tracer) — the consumer derives it by tokenizing `commandLine`.
        public let command: String?
        /// Full argv (shim), or empty when only `commandLine` is available (tracer).
        public let argv: [String]
        /// The raw command line (tracer), or nil (shim).
        public let commandLine: String?
        public let cwd: String
        public let exitCode: Int
        /// Wall-clock duration in milliseconds, when the reporter measured it.
        public let durationMs: Int?
        /// Capped head of stdout / stderr (shim only; empty for the tracer).
        public let stdout: String
        public let stderr: String
        /// True when stdout/stderr exceeded the capture cap (shim).
        public let truncated: Bool
        public let source: Source
        public let sessionID: String?

        public init(
            command: String?, argv: [String], commandLine: String?, cwd: String, exitCode: Int,
            durationMs: Int?, stdout: String, stderr: String, truncated: Bool, source: Source,
            sessionID: String?
        ) {
            self.command = command
            self.argv = argv
            self.commandLine = commandLine
            self.cwd = cwd
            self.exitCode = exitCode
            self.durationMs = durationMs
            self.stdout = stdout
            self.stderr = stderr
            self.truncated = truncated
            self.source = source
            self.sessionID = sessionID
        }
    }

    public typealias Handler = @Sendable (ApprovalCall) async -> Reply
    public typealias ConflictHandler = @Sendable (ConflictCall) async -> ConflictReply
    public typealias HostExecHandler = @Sendable (HostExecCall) async -> HostExecReply
    /// Synchronous + non-gated: it just records the observed op and returns. No user approval,
    /// so a git report never blocks the agent's git call.
    public typealias GitReportHandler = @Sendable (GitReportCall) -> Void
    /// As `GitReportHandler`, for the `gh` interceptor's `POST /gh-event`.
    public typealias GhReportHandler = @Sendable (GhReportCall) -> Void
    /// As ``GitReportHandler``, for the non-git command interceptor (`POST /command-event`).
    public typealias CommandReportHandler = @Sendable (CommandReportCall) -> Void

    /// The dedicated POST path the `git` interceptor shim reports to — bearer-token gated like
    /// the MCP route, but a plain JSON POST rather than JSON-RPC.
    public static let gitEventPath = "/git-event"
    /// The dedicated POST path the `gh` interceptor shim reports to (sibling of `gitEventPath`).
    public static let ghEventPath = "/gh-event"
    /// The dedicated POST path the non-git command shims + bash tracer report to.
    public static let commandEventPath = CommandInterceptor.eventPath

    public static let toolName = "approve"
    /// Fully-qualified name passed to `--permission-prompt-tool`.
    public static let qualifiedToolName = "mcp__nucleic__approve"
    public static let conflictToolName = "check_conflict"
    /// Fully-qualified name — pre-allowed so the agent calls it without an approval round-trip.
    public static let qualifiedConflictToolName = "mcp__nucleic__check_conflict"
    public static let hostExecToolName = "host_exec"
    /// Fully-qualified name — pre-allowed via `--allowedTools` so the call reaches our handler
    /// directly (the handler is the sole gate); never routed through Claude's permission path,
    /// so `auto` mode can't auto-approve it. Only advertised when a handler is registered.
    public static let qualifiedHostExecToolName = "mcp__nucleic__host_exec"

    private var listener: NWListener?
    /// In-flight TCP bind, so concurrent `start(host:)` callers (the default once sessions share one
    /// control-container server via ``ApprovalServerRegistry``) join the SAME bind and all receive
    /// the real bound port. Without this, a second caller entering while the first is mid-bind saw a
    /// half-initialized listener and returned `port == 0`, which the agent's MCP config baked into an
    /// unreachable `:0` endpoint — stalling that session's first gated tool. Cleared when the bind
    /// settles (success or failure), so a failed bind can be retried.
    private var tcpStartTask: Task<UInt16, Error>?
    private var handlers: [String: Handler] = [:]
    private var conflictHandlers: [String: ConflictHandler] = [:]
    private var hostExecHandlers: [String: HostExecHandler] = [:]
    private var gitReportHandlers: [String: GitReportHandler] = [:]
    private var ghReportHandlers: [String: GhReportHandler] = [:]
    private var commandReportHandlers: [String: CommandReportHandler] = [:]
    private var connectionTasks: [Int: Task<Void, Never>] = [:]
    private var nextConnectionID = 0
    public private(set) var port: UInt16 = 0
    /// The bound unix-socket path when started via ``start(unixSocketPath:)`` (nil for a TCP
    /// bind). Recorded so ``stop()`` can unlink the socket file it created.
    public private(set) var boundUnixSocketPath: String?
    /// The listening AF_UNIX socket fd + its accept source, for the unix-socket transport.
    private var unixListenFD: Int32?
    private var unixAcceptSource: DispatchSourceRead?

    public init() {}

    /// Returns the bound ephemeral port. `host` is the local interface to bind to;
    /// default loopback for host runs, `0.0.0.0` for sandbox runs so the containerized
    /// `claude` can reach the approval server over the VM gateway (bearer-token gated).
    @discardableResult
    public func start(host: String = "127.0.0.1") async throws -> UInt16 {
        if listener != nil { return port }  // already bound → real port is published
        if let task = tcpStartTask { return try await task.value }  // bind in flight → join it
        let task = Task { [self] in try await bind(host: host) }
        tcpStartTask = task
        defer { tcpStartTask = nil }  // settle the single-flight (success or throw); allow a retry
        return try await task.value
    }

    /// Bind the server to a **unix domain socket** instead of a TCP port — the transport for a
    /// sandboxed agent, whose connection is relayed in over vsock (the framework's
    /// `UnixSocketConfiguration`). No IP listener means macOS raises no incoming-connection /
    /// local-network prompts. The HTTP/JSON-RPC dispatch is byte-for-byte identical to the TCP
    /// path; only the listening socket differs.
    ///
    /// A real `AF_UNIX` listener (not `NWListener` with a `.unix` endpoint, which Network.framework
    /// matches in-process and never materializes on disk) so the framework's host-side relay — an
    /// ordinary socket client — can connect to it. Any stale socket file at `path` (e.g. left by a
    /// crash) is removed first, since `bind` fails if the path already exists. Returns the bound path.
    @discardableResult
    public func start(unixSocketPath path: String) async throws -> String {
        if unixListenFD != nil { return boundUnixSocketPath ?? path }
        let fm = FileManager.default
        try? fm.createDirectory(
            atPath: (path as NSString).deletingLastPathComponent,
            withIntermediateDirectories: true)
        try? fm.removeItem(atPath: path)  // a bind fails if the path already exists

        let fd = socket(AF_UNIX, SOCK_STREAM, 0)
        guard fd >= 0 else {
            throw UnixSocketError("socket() failed: \(String(cString: strerror(errno)))")
        }
        _ = fcntl(fd, F_SETFD, FD_CLOEXEC)

        var addr = sockaddr_un()
        addr.sun_family = sa_family_t(AF_UNIX)
        let pathBytes = Array(path.utf8)
        let capacity = MemoryLayout.size(ofValue: addr.sun_path)
        guard pathBytes.count < capacity else {
            close(fd)
            throw UnixSocketError("socket path too long (\(pathBytes.count) ≥ \(capacity)): \(path)")
        }
        withUnsafeMutablePointer(to: &addr.sun_path) { raw in
            raw.withMemoryRebound(to: CChar.self, capacity: capacity) { dst in
                for (i, byte) in pathBytes.enumerated() { dst[i] = CChar(bitPattern: byte) }
                dst[pathBytes.count] = 0
            }
        }
        let bindRC = withUnsafePointer(to: &addr) { ptr in
            ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                Darwin.bind(fd, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
            }
        }
        guard bindRC == 0 else {
            let err = String(cString: strerror(errno))
            close(fd)
            throw UnixSocketError("bind(\(path)) failed: \(err)")
        }
        // Only the host-side relay connects here; lock the socket to the owner.
        chmod(path, 0o600)
        guard listen(fd, 16) == 0 else {
            let err = String(cString: strerror(errno))
            close(fd)
            try? fm.removeItem(atPath: path)
            throw UnixSocketError("listen(\(path)) failed: \(err)")
        }

        // Accept off a background source so the actor never blocks; each accepted fd drives the same
        // HTTP/JSON-RPC serve loop as a TCP connection.
        let source = DispatchSource.makeReadSource(
            fileDescriptor: fd, queue: .global(qos: .userInitiated))
        source.setEventHandler { [weak self] in
            let client = accept(fd, nil, nil)
            guard client >= 0 else { return }
            _ = fcntl(client, F_SETFD, FD_CLOEXEC)
            guard let self else {
                close(client)
                return
            }
            Task { await self.adopt(UnixSocketByteConn(fd: client)) }
        }
        source.setCancelHandler { close(fd) }
        source.resume()

        unixListenFD = fd
        unixAcceptSource = source
        boundUnixSocketPath = path
        return path
    }

    /// Bind a TCP listener and resume once it is `.ready` (or throw on failure). The connection-
    /// serving pipeline (``adopt(_:)`` → ``serve(_:)``) is transport-agnostic, so this drives the
    /// exact same HTTP/JSON-RPC dispatch as the unix-socket path. Returns the bound ephemeral port.
    ///
    /// `self.listener` and `self.port` are published only AFTER `.ready` — never before the await —
    /// so a concurrent caller can't observe a half-bound listener with a 0 port, and a `.failed`
    /// bind leaves both unset (no stale listener pinning a permanent port 0). The connection handler
    /// is wired before `start`; it captures `self` directly, so it works whether or not
    /// `self.listener` is assigned yet.
    private func bind(host: String) async throws -> UInt16 {
        let parameters = NWParameters.tcp
        parameters.requiredLocalEndpoint = NWEndpoint.hostPort(
            host: NWEndpoint.Host(host), port: .any)
        let listener = try NWListener(using: parameters)

        listener.newConnectionHandler = { [weak self] connection in
            guard let self else {
                connection.cancel()
                return
            }
            Task { await self.adopt(NWByteConn(connection)) }
        }

        let resumeOnce = OnceBox()
        try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
            listener.stateUpdateHandler = { state in
                switch state {
                case .ready:
                    if resumeOnce.claim() { cont.resume() }
                case .failed(let error), .waiting(let error):
                    if resumeOnce.claim() { cont.resume(throwing: error) }
                default:
                    break
                }
            }
            listener.start(queue: .global(qos: .userInitiated))
        }
        // Ready and bound: publish atomically (no suspension between these and the return).
        self.listener = listener
        self.port = listener.port?.rawValue ?? 0
        return self.port
    }

    public func stop() {
        tcpStartTask?.cancel()
        tcpStartTask = nil
        listener?.cancel()
        listener = nil
        unixAcceptSource?.cancel()  // its cancel handler closes the listening fd
        unixAcceptSource = nil
        unixListenFD = nil
        if let path = boundUnixSocketPath {
            try? FileManager.default.removeItem(atPath: path)
            boundUnixSocketPath = nil
        }
        for task in connectionTasks.values { task.cancel() }
        connectionTasks.removeAll()
        handlers.removeAll()
        conflictHandlers.removeAll()
        hostExecHandlers.removeAll()
        gitReportHandlers.removeAll()
        ghReportHandlers.removeAll()
        commandReportHandlers.removeAll()
    }

    public func register(token: String, handler: @escaping Handler) {
        handlers[token] = handler
    }

    /// Register the per-session `check_conflict` handler under the same bearer token.
    public func registerConflict(token: String, handler: @escaping ConflictHandler) {
        conflictHandlers[token] = handler
    }

    /// Register the per-session `host_exec` handler. Registering it both advertises the tool
    /// in `tools/list` for this token and routes calls to the host runner (HOST_EXEC).
    public func registerHostExec(token: String, handler: @escaping HostExecHandler) {
        hostExecHandlers[token] = handler
    }

    /// Register the per-session `git` interceptor report handler under the same bearer token.
    /// Called for the `POST /git-event` route the in-container shim uses.
    public func registerGitReport(token: String, handler: @escaping GitReportHandler) {
        gitReportHandlers[token] = handler
    }

    /// Register the per-session `gh` interceptor report handler under the same bearer token.
    /// Called for the `POST /gh-event` route the in-container shim uses.
    public func registerGhReport(token: String, handler: @escaping GhReportHandler) {
        ghReportHandlers[token] = handler
    }

    /// Register the per-session non-git command report handler (`POST /command-event`).
    public func registerCommandReport(token: String, handler: @escaping CommandReportHandler) {
        commandReportHandlers[token] = handler
    }

    public func unregister(token: String) {
        handlers.removeValue(forKey: token)
        conflictHandlers.removeValue(forKey: token)
        hostExecHandlers.removeValue(forKey: token)
        gitReportHandlers.removeValue(forKey: token)
        ghReportHandlers.removeValue(forKey: token)
        commandReportHandlers.removeValue(forKey: token)
    }

    /// The MCP server entry for `--mcp-config` (inline JSON). `host` is the address the
    /// child reaches the server at — loopback for host runs, the VM gateway IP for a
    /// containerized child (which can't see the host's `127.0.0.1`).
    public nonisolated func mcpConfigJSON(
        host: String = "127.0.0.1", port: UInt16, token: String
    ) -> String {
        JSONValue.object([
            "mcpServers": .object([
                "nucleic": .object([
                    "type": .string("http"),
                    "url": .string("http://\(host):\(port)/mcp"),
                    "headers": .object(["Authorization": .string("Bearer \(token)")]),
                ])
            ])
        ]).canonicalString()
    }

    // MARK: - Connection handling

    private func adopt(_ conn: any ByteConn) {
        let id = nextConnectionID
        nextConnectionID += 1
        let task = Task {
            await self.serve(conn)
            self.forget(id)
        }
        connectionTasks[id] = task
    }

    private func forget(_ id: Int) {
        connectionTasks.removeValue(forKey: id)
    }

    private func serve(_ conn: any ByteConn) async {
        defer { conn.close() }
        var buffer = Data()
        while !Task.isCancelled {
            guard let request = await nextRequest(on: conn, buffer: &buffer) else { return }
            let response = await handle(request)
            do {
                try await sendResponse(response, on: conn)
            } catch {
                return
            }
            if request.headers["connection"]?.lowercased() == "close" { return }
        }
    }

    private struct HTTPRequest {
        let method: String
        let path: String
        let headers: [String: String]  // lowercased keys
        let body: Data
    }

    private struct HTTPResponse {
        let status: Int
        let statusText: String
        let contentType: String?
        let body: Data
    }

    /// Hard ceiling on a single request's bytes (headers + body). The only clients are our own
    /// in-container shims/tracer over loopback/VM-gateway, but a compromised agent shares the
    /// per-session token, so cap the buffer to bound memory: a request that grows past this drops
    /// the connection rather than letting an attacker stream gigabytes into `Data`. Generous enough
    /// for a full command-event batch (capped at the source too) plus the largest MCP payload.
    static let maxRequestBytes = 4 * 1024 * 1024

    private func nextRequest(on conn: any ByteConn, buffer: inout Data) async -> HTTPRequest? {
        while true {
            if let request = Self.parseRequest(from: &buffer) { return request }
            // Bound memory: an over-long request (or a lying Content-Length) drops the connection.
            if buffer.count > Self.maxRequestBytes { return nil }
            do {
                guard let chunk = try await conn.receive(maxLength: 1 << 16) else { return nil }
                buffer.append(chunk)
            } catch {
                return nil
            }
        }
    }

    private static func parseRequest(from buffer: inout Data) -> HTTPRequest? {
        guard let headerEnd = buffer.range(of: Data("\r\n\r\n".utf8)) else { return nil }
        let headerData = buffer.subdata(in: buffer.startIndex..<headerEnd.lowerBound)
        guard let headerText = String(data: headerData, encoding: .utf8) else { return nil }
        let lines = headerText.components(separatedBy: "\r\n")
        guard let requestLine = lines.first else { return nil }
        let parts = requestLine.split(separator: " ")
        guard parts.count >= 2 else { return nil }

        var headers: [String: String] = [:]
        for line in lines.dropFirst() {
            guard let colon = line.firstIndex(of: ":") else { continue }
            let key = line[..<colon].trimmingCharacters(in: .whitespaces).lowercased()
            let value = line[line.index(after: colon)...].trimmingCharacters(in: .whitespaces)
            headers[key] = value
        }

        let contentLength = headers["content-length"].flatMap(Int.init) ?? 0
        let bodyStart = headerEnd.upperBound
        guard buffer.distance(from: bodyStart, to: buffer.endIndex) >= contentLength else {
            return nil
        }
        let body = buffer.subdata(in: bodyStart..<buffer.index(bodyStart, offsetBy: contentLength))
        buffer.removeSubrange(buffer.startIndex..<buffer.index(bodyStart, offsetBy: contentLength))

        return HTTPRequest(
            method: String(parts[0]), path: String(parts[1]), headers: headers, body: body)
    }

    private func sendResponse(_ response: HTTPResponse, on conn: any ByteConn) async throws {
        var head = "HTTP/1.1 \(response.status) \(response.statusText)\r\n"
        if let contentType = response.contentType {
            head += "Content-Type: \(contentType)\r\n"
        }
        head += "Content-Length: \(response.body.count)\r\nConnection: keep-alive\r\n\r\n"
        var payload = Data(head.utf8)
        payload.append(response.body)
        try await conn.send(payload)
    }

    // MARK: - MCP / JSON-RPC dispatch

    private func handle(_ request: HTTPRequest) async -> HTTPResponse {
        guard request.method == "POST" else {
            // Streamable-HTTP clients may probe GET (server-push channel) — we
            // don't offer one, 405 is the spec-sanctioned answer.
            return HTTPResponse(
                status: 405, statusText: "Method Not Allowed", contentType: nil, body: Data())
        }

        // The `git` / `gh` interceptor shims post here (not JSON-RPC). Route before MCP dispatch.
        if request.path == Self.gitEventPath {
            return handleGitEvent(request)
        }
        if request.path == Self.ghEventPath {
            return handleGhEvent(request)
        }
        // The non-git command shims + bash tracer post here (not JSON-RPC).
        if request.path == Self.commandEventPath {
            return handleCommandEvent(request)
        }

        let authorization = request.headers["authorization"] ?? ""
        let token = authorization.hasPrefix("Bearer ") ? String(authorization.dropFirst(7)) : ""
        guard let handler = handlers[token] else {
            return HTTPResponse(
                status: 401, statusText: "Unauthorized", contentType: "application/json",
                body: Data(#"{"error":"invalid bearer token"}"#.utf8))
        }

        guard let message = try? JSONValue(parsing: request.body) else {
            return rpcError(id: .null, code: -32700, message: "Parse error")
        }

        let id = message["id"] ?? .null
        let method = message["method"]?.stringValue ?? ""

        // Notifications (no id) are acknowledged with 202 and no body.
        if message["id"] == nil {
            return HTTPResponse(status: 202, statusText: "Accepted", contentType: nil, body: Data())
        }

        switch method {
        case "initialize":
            let requested = message["params"]?["protocolVersion"]?.stringValue ?? "2025-06-18"
            return rpcResult(
                id: id,
                result: .object([
                    "protocolVersion": .string(requested),
                    "capabilities": .object(["tools": .object(["listChanged": .bool(false)])]),
                    "serverInfo": .object([
                        "name": .string("nucleic-approval"), "version": .string("0.1.0"),
                    ]),
                ]))

        case "ping":
            return rpcResult(id: id, result: .object([:]))

        case "tools/list":
            var tools: [JSONValue] = [
                .object([
                    "name": .string(Self.toolName),
                    "description": .string(
                        "Ask the Nucleic user to approve or deny a gated tool call."),
                    "inputSchema": .object([
                        "type": .string("object"),
                        "properties": .object([
                            "tool_name": .object(["type": .string("string")]),
                            "input": .object(["type": .string("object")]),
                            "tool_use_id": .object(["type": .string("string")]),
                        ]),
                        "required": .array([.string("tool_name"), .string("input")]),
                    ]),
                ]),
                .object([
                    "name": .string(Self.conflictToolName),
                    "description": .string(
                        "Before starting a distinct task that edits files, check whether it "
                            + "conflicts with another currently-active Nucleic agent. Pass a "
                            + "one-line task description and the repo-relative files/areas you "
                            + "expect to change. If the result says the task was deferred or "
                            + "cancelled, STOP and do not edit; if it says proceed (or there is "
                            + "no conflict), continue."),
                    "inputSchema": .object([
                        "type": .string("object"),
                        "properties": .object([
                            "task": .object(["type": .string("string")]),
                            "files": .object([
                                "type": .string("array"),
                                "items": .object(["type": .string("string")]),
                            ]),
                        ]),
                        "required": .array([.string("task")]),
                    ]),
                ]),
            ]
            // Only advertise host_exec when this session opted into host build/run.
            if hostExecHandlers[token] != nil {
                tools.append(
                    .object([
                        "name": .string(Self.hostExecToolName),
                        "description": .string(
                            "Build and/or run a command on the HOST machine (macOS), OUTSIDE this "
                                + "Linux sandbox container. Use ONLY when you must compile or run an "
                                + "executable the sandbox cannot — e.g. building and running a macOS "
                                + "app or binary. Pass a single shell `command`; it runs in this "
                                + "session's worktree on the host (combine build + run into one "
                                + "command, e.g. \"swift build && swift run MyTool\"). Returns "
                                + "{exit_code, stdout, stderr}. Every call needs explicit user "
                                + "approval and is NEVER auto-approved, so only call it when host "
                                + "execution is genuinely required."),
                        "inputSchema": .object([
                            "type": .string("object"),
                            "properties": .object([
                                "command": .object(["type": .string("string")])
                            ]),
                            "required": .array([.string("command")]),
                        ]),
                    ]))
            }
            return rpcResult(id: id, result: .object(["tools": .array(tools)]))

        case "tools/call":
            let params = message["params"]
            let toolName = params?["name"]?.stringValue
            let arguments = params?["arguments"]
            switch toolName {
            case Self.toolName:
                let call = ApprovalCall(
                    toolName: arguments?["tool_name"]?.stringValue ?? "unknown",
                    input: arguments?["input"] ?? .object([:]),
                    toolUseID: arguments?["tool_use_id"]?.stringValue)
                // This await is the bridge: it suspends the HTTP response until the
                // human answers (ApprovalCoordinator), then returns the mapped reply.
                let reply = await handler(call)
                return toolResult(id: id, text: reply.wireJSON())

            case Self.conflictToolName:
                let files = (arguments?["files"]?.arrayValue ?? []).compactMap { $0.stringValue }
                let call = ConflictCall(
                    task: arguments?["task"]?.stringValue ?? "", files: files)
                // No handler registered (shouldn't happen) → fail open: no conflict.
                guard let conflictHandler = conflictHandlers[token] else {
                    return toolResult(id: id, text: ConflictReply(resolution: .noConflict).wireJSON())
                }
                let reply = await conflictHandler(call)
                return toolResult(id: id, text: reply.wireJSON())

            case Self.hostExecToolName:
                let call = HostExecCall(command: arguments?["command"]?.stringValue ?? "")
                // No handler (tool shouldn't be visible) → refuse rather than run anything.
                guard let hostExecHandler = hostExecHandlers[token] else {
                    return toolResult(
                        id: id,
                        text: HostExecReply.denied(message: "Host execution is not enabled.")
                            .wireJSON())
                }
                // Suspends until the user approves and the host command finishes (HOST_EXEC).
                let reply = await hostExecHandler(call)
                return toolResult(id: id, text: reply.wireJSON())

            default:
                return rpcError(id: id, code: -32602, message: "Unknown tool")
            }

        default:
            return rpcError(id: id, code: -32601, message: "Method not found: \(method)")
        }
    }

    /// Handle a `POST /git-event` from the in-container `git` interceptor shim: bearer-token
    /// gated like the MCP route, but a plain JSON body `{argv, cwd, exitCode, subcommand,
    /// sessionId}`. Fire-and-forget — the registered handler returns immediately (it offloads
    /// any real work), so a report never blocks the agent's git call. Always 202s a known
    /// token (even on a malformed body) so the shim never retries or stalls; 401s an unknown one.
    private func handleGitEvent(_ request: HTTPRequest) -> HTTPResponse {
        let authorization = request.headers["authorization"] ?? ""
        let token = authorization.hasPrefix("Bearer ") ? String(authorization.dropFirst(7)) : ""
        guard let handler = gitReportHandlers[token] else {
            return HTTPResponse(
                status: 401, statusText: "Unauthorized", contentType: "application/json",
                body: Data(#"{"error":"invalid bearer token"}"#.utf8))
        }
        if let message = try? JSONValue(parsing: request.body) {
            let argv = (message["argv"]?.arrayValue ?? []).compactMap { $0.stringValue }
            handler(GitReportCall(
                argv: argv,
                cwd: message["cwd"]?.stringValue ?? "",
                exitCode: message["exitCode"]?.intValue ?? 0,
                subcommand: message["subcommand"]?.stringValue,
                sessionID: message["sessionId"]?.stringValue))
        }
        return HTTPResponse(status: 202, statusText: "Accepted", contentType: nil, body: Data())
    }

    /// Handle a `POST /gh-event` from the in-container `gh` interceptor shim — the exact twin of
    /// `handleGitEvent`: bearer-token gated, plain JSON `{argv, cwd, exitCode, subcommand,
    /// sessionId}`, fire-and-forget (202 on any known-token body, 401 on an unknown token).
    private func handleGhEvent(_ request: HTTPRequest) -> HTTPResponse {
        let authorization = request.headers["authorization"] ?? ""
        let token = authorization.hasPrefix("Bearer ") ? String(authorization.dropFirst(7)) : ""
        guard let handler = ghReportHandlers[token] else {
            return HTTPResponse(
                status: 401, statusText: "Unauthorized", contentType: "application/json",
                body: Data(#"{"error":"invalid bearer token"}"#.utf8))
        }
        if let message = try? JSONValue(parsing: request.body) {
            let argv = (message["argv"]?.arrayValue ?? []).compactMap { $0.stringValue }
            handler(GhReportCall(
                argv: argv,
                cwd: message["cwd"]?.stringValue ?? "",
                exitCode: message["exitCode"]?.intValue ?? 0,
                subcommand: message["subcommand"]?.stringValue,
                sessionID: message["sessionId"]?.stringValue))
        }
        return HTTPResponse(status: 202, statusText: "Accepted", contentType: nil, body: Data())
    }

    /// Handle a `POST /command-event` from the non-git command interceptor: bearer-token gated
    /// like `/git-event`. Two body shapes, both fanned out to one `CommandReportCall` per command:
    ///
    ///   * shim — `{type:"command", command, argv, cwd, exitCode, durationMs, stdout, stderr,
    ///     truncated, sessionId}` — one rich, output-carrying report.
    ///   * tracer — `{type:"command-batch", sessionId, events:[{line, cwd, exitCode, durationMs}]}`
    ///     — a batch of metadata-only reports for a whole Bash tool call.
    ///
    /// Fire-and-forget: handlers return immediately. Always 202s a known token (even on a malformed
    /// body) so the shim/tracer never retry or stall; 401s an unknown one.
    private func handleCommandEvent(_ request: HTTPRequest) -> HTTPResponse {
        let authorization = request.headers["authorization"] ?? ""
        let token = authorization.hasPrefix("Bearer ") ? String(authorization.dropFirst(7)) : ""
        guard let handler = commandReportHandlers[token] else {
            return HTTPResponse(
                status: 401, statusText: "Unauthorized", contentType: "application/json",
                body: Data(#"{"error":"invalid bearer token"}"#.utf8))
        }
        if let message = try? JSONValue(parsing: request.body) {
            for call in Self.parseCommandReports(message) { handler(call) }
        }
        return HTTPResponse(status: 202, statusText: "Accepted", contentType: nil, body: Data())
    }

    /// Decode a `/command-event` body into zero or more `CommandReportCall`s. A `command-batch`
    /// (tracer) yields one per `events[]` entry; anything else is treated as a single shim report.
    static func parseCommandReports(_ message: JSONValue) -> [CommandReportCall] {
        let sessionID = message["sessionId"]?.stringValue
        if message["type"]?.stringValue == "command-batch" {
            return (message["events"]?.arrayValue ?? []).map { event in
                CommandReportCall(
                    command: nil, argv: [], commandLine: event["line"]?.stringValue ?? "",
                    cwd: event["cwd"]?.stringValue ?? "",
                    exitCode: event["exitCode"]?.intValue ?? 0,
                    durationMs: event["durationMs"]?.intValue,
                    stdout: "", stderr: "", truncated: false, source: .tracer, sessionID: sessionID)
            }
        }
        let argv = (message["argv"]?.arrayValue ?? []).compactMap { $0.stringValue }
        return [CommandReportCall(
            command: message["command"]?.stringValue, argv: argv,
            commandLine: message["commandLine"]?.stringValue,
            cwd: message["cwd"]?.stringValue ?? "",
            exitCode: message["exitCode"]?.intValue ?? 0,
            durationMs: message["durationMs"]?.intValue,
            stdout: message["stdout"]?.stringValue ?? "",
            stderr: message["stderr"]?.stringValue ?? "",
            truncated: message["truncated"]?.boolValue ?? false,
            source: .shim, sessionID: sessionID)]
    }

    /// A successful `tools/call` result wrapping `text` in a single MCP text content block.
    private func toolResult(id: JSONValue, text: String) -> HTTPResponse {
        rpcResult(
            id: id,
            result: .object([
                "content": .array([
                    .object(["type": .string("text"), "text": .string(text)])
                ]),
                "isError": .bool(false),
            ]))
    }

    private func rpcResult(id: JSONValue, result: JSONValue) -> HTTPResponse {
        let body = JSONValue.object(["jsonrpc": .string("2.0"), "id": id, "result": result])
        return HTTPResponse(
            status: 200, statusText: "OK", contentType: "application/json",
            body: (try? body.encodedData()) ?? Data())
    }

    private func rpcError(id: JSONValue, code: Int, message: String) -> HTTPResponse {
        let body = JSONValue.object([
            "jsonrpc": .string("2.0"), "id": id,
            "error": .object(["code": .number(Double(code)), "message": .string(message)]),
        ])
        return HTTPResponse(
            status: 200, statusText: "OK", contentType: "application/json",
            body: (try? body.encodedData()) ?? Data())
    }
}

// MARK: - Transport abstraction

/// A bidirectional byte stream the HTTP/JSON-RPC serve loop drives, so the same dispatch runs over
/// either a Network.framework TCP connection (host loopback) or a raw unix-domain socket (the
/// vsock-relayed control plane). `receive` returns nil at EOF; `close` is idempotent.
private protocol ByteConn: Sendable {
    func receive(maxLength: Int) async throws -> Data?
    func send(_ data: Data) async throws
    func close()
}

/// ``ByteConn`` over a Network.framework connection (the TCP transport). Started on creation so the
/// serve loop can receive immediately.
private final class NWByteConn: ByteConn, @unchecked Sendable {
    private let connection: NWConnection
    init(_ connection: NWConnection) {
        self.connection = connection
        connection.start(queue: .global(qos: .userInitiated))
    }
    func receive(maxLength: Int) async throws -> Data? {
        try await withCheckedThrowingContinuation { cont in
            connection.receive(minimumIncompleteLength: 1, maximumLength: maxLength) {
                data, _, isComplete, error in
                if let error {
                    cont.resume(throwing: error)
                } else if let data, !data.isEmpty {
                    cont.resume(returning: data)
                } else if isComplete {
                    cont.resume(returning: nil)
                } else {
                    cont.resume(returning: Data())
                }
            }
        }
    }
    func send(_ data: Data) async throws {
        try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
            connection.send(
                content: data,
                completion: .contentProcessed { error in
                    if let error { cont.resume(throwing: error) } else { cont.resume() }
                })
        }
    }
    func close() { connection.cancel() }
}

/// ``ByteConn`` over an accepted `AF_UNIX` stream socket (the vsock-relayed transport). Reads/writes
/// are blocking POSIX calls offloaded to a background queue, so the actor never blocks and a turn
/// suspended on an approval holds no thread (no read is in flight between request and response). The
/// control plane's live connection count is small (one MCP connection plus short-lived interceptor
/// posts per container), so a thread parked on `read` per live connection is acceptable.
private final class UnixSocketByteConn: ByteConn, @unchecked Sendable {
    private let fd: Int32
    private let lock = NSLock()
    private var closed = false
    init(fd: Int32) { self.fd = fd }

    func receive(maxLength: Int) async throws -> Data? {
        let fd = self.fd
        return try await withCheckedThrowingContinuation { cont in
            DispatchQueue.global(qos: .userInitiated).async {
                var buffer = [UInt8](repeating: 0, count: maxLength)
                while true {
                    let n = buffer.withUnsafeMutableBytes { read(fd, $0.baseAddress, maxLength) }
                    if n > 0 {
                        cont.resume(returning: Data(buffer[0..<n]))
                    } else if n == 0 {
                        cont.resume(returning: nil)
                    } else if errno == EINTR {
                        continue
                    } else {
                        cont.resume(throwing: POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO))
                    }
                    return
                }
            }
        }
    }

    func send(_ data: Data) async throws {
        let fd = self.fd
        try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
            DispatchQueue.global(qos: .userInitiated).async {
                let result: Result<Void, Error> = data.withUnsafeBytes { raw in
                    guard let base = raw.baseAddress else { return .success(()) }
                    var offset = 0
                    while offset < raw.count {
                        let n = write(fd, base + offset, raw.count - offset)
                        if n > 0 {
                            offset += n
                        } else if n < 0 && errno == EINTR {
                            continue
                        } else {
                            return .failure(POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO))
                        }
                    }
                    return .success(())
                }
                cont.resume(with: result)
            }
        }
    }

    func close() {
        lock.lock()
        defer { lock.unlock() }
        guard !closed else { return }
        closed = true
        shutdown(fd, SHUT_RDWR)
        _ = Darwin.close(fd)
    }
}

/// Failure binding/listening the control-plane unix socket.
private struct UnixSocketError: Error, CustomStringConvertible {
    let description: String
    init(_ description: String) { self.description = description }
}

/// Resume-once guard for NWListener state callbacks (ready may be followed by
/// failed; the continuation must fire exactly once).
private final class OnceBox: @unchecked Sendable {
    private let lock = NSLock()
    private var claimed = false

    func claim() -> Bool {
        lock.lock()
        defer { lock.unlock() }
        if claimed { return false }
        claimed = true
        return true
    }
}
