import Foundation

/// Vends one shared ``MCPApprovalServer`` per **control container**.
///
/// Nucleic Control runs every session as an `exec` inside a single shared container (the primary
/// `nucleic-control`, or the per-family split-outs). Those sessions must therefore share **one**
/// approval server — a container has exactly one relayed control socket, and the server already
/// multiplexes sessions by their per-session bearer token ("one server can serve all sessions"). A
/// per-backend server can't satisfy that: two sessions in the same box would both try to own the
/// container's single socket.
///
/// The server is intentionally long-lived (keyed by the stable *logical* container name): it
/// outlives container restarts — e.g. a memory-balloon stop+recreate — because the relay re-attaches
/// to the same stable host socket path on recreate, and the still-listening server is right there.
///
/// Host (non-containerized) runs and per-session sandbox containers do **not** use this; they keep a
/// per-backend server (loopback for host, the per-session container gateway otherwise).
public actor ApprovalServerRegistry {
    private var servers: [String: MCPApprovalServer] = [:]

    public init() {}

    /// The shared server for `containerName`, created on first use. The caller starts it
    /// (idempotently) and registers its per-session token; teardown only unregisters that token, so
    /// the server keeps serving the other sessions in the container.
    public func server(for containerName: String) -> MCPApprovalServer {
        if let existing = servers[containerName] { return existing }
        let server = MCPApprovalServer()
        servers[containerName] = server
        return server
    }

    /// Stop and drop the server for a container (best-effort; e.g. the container was deleted).
    public func teardown(containerName: String) async {
        guard let server = servers.removeValue(forKey: containerName) else { return }
        await server.stop()
    }

    /// Stop every server (app shutdown).
    public func stopAll() async {
        let all = Array(servers.values)
        servers.removeAll()
        for server in all { await server.stop() }
    }

    /// The container-stable **host** path for a control container's approval socket, relayed into the
    /// guest at ``ContainerSpec/controlSocketGuestPath``. Lives under an app-owned runtime dir in
    /// Application Support — deliberately NOT `/tmp`, which the OS can reap out from under a long-lived
    /// listener (one of the desyncs that used to permanently brick a shared control server; the server's
    /// `start(unixSocketPath:)` self-heal and the spawn-time listening check are the other guards).
    /// Only the short `Nucleic/run/<name>.sock` segment lives here, so it stays well under the AF_UNIX
    /// `sun_path` ~104-byte cap even though the full Application-Support *session* path would overflow;
    /// a pathological home path that still overran would make the bind throw and fail the run loudly.
    /// Stable per logical container name, so the persistent server and the relay (re-attached on
    /// container recreate) always agree on where to meet.
    public nonisolated static func controlSocketPath(for containerName: String) -> String {
        "\(NucleicPaths.runDirectory.path)/\(containerName).sock"
    }

    /// The **host** path for a container's Claude token-proxy socket (opt-in), a sibling of
    /// ``controlSocketPath(for:)`` in the same short, reaper-safe `Nucleic/run` dir so it stays under
    /// the AF_UNIX `sun_path` cap. Relayed into the guest at ``ContainerSpec/proxySocketGuestPath``.
    public nonisolated static func proxySocketPath(for containerName: String) -> String {
        "\(NucleicPaths.runDirectory.path)/\(containerName)-proxy.sock"
    }
}
