import Foundation

/// Brings a containerized run's container + control plane up in the platform's required
/// ORDER (docs/WINDOWS_PORT.md §5), shared by every backend so the sequencing can't drift:
///
///   • macOS (vsock relay): create/start the container FIRST — the engine relays the control
///     unix socket into the guest at create — then bind the approval server on that relayed
///     socket and assert it is actually listening.
///   • Windows (gateway TCP): bind the approval server on the wslc session's WSL-facing
///     gateway FIRST, bake the endpoint into the spec env (`NUCLEIC_CONTROL_HOST`/`PORT`,
///     read by the in-guest bridge at container create), THEN create/start.
///
/// Either way the agent-side contract is identical: inside the guest, the control endpoint
/// is loopback `ContainerSpec.controlBridgePort`, served by the supervised bridge.
public enum ContainerControlPlane {
    public struct Ready: Sendable {
        /// The (logical) container name `ensureRunning` returned.
        public let containerName: String
        /// macOS: the relayed control socket path — host-side nash `host_exec` shell events
        /// post here (NASH.md §7.7.3). nil on Windows (nash isn't used host-side there, §9.4).
        public let hostShellSocketPath: String?
    }

    /// `server == nil` (no approval registry for this run) skips control-plane binding and
    /// just brings the container up — the pre-existing behavior of the registry-less paths.
    public static func bringUp(
        spec: ContainerSpec, manager: ContainerManager, server: MCPApprovalServer?
    ) async throws -> Ready {
        #if os(Windows)
        var spec = spec
        if let server {
            guard let gateway = try await manager.sessionHostGateway() else {
                throw BackendError.spawnFailed(
                    "no sandbox session gateway (control endpoint unreachable)")
            }
            let tcpPort = try await server.start(host: gateway)
            guard tcpPort != 0 else {
                throw BackendError.spawnFailed(
                    "approval server did not bind a port (control endpoint unreachable)")
            }
            spec = spec.withEnvironment(merging: [
                "NUCLEIC_CONTROL_HOST": gateway,
                "NUCLEIC_CONTROL_PORT": String(tcpPort),
            ])
        }
        let (name, _) = try await manager.ensureRunning(spec)
        return Ready(containerName: name, hostShellSocketPath: nil)
        #else
        let (name, _) = try await manager.ensureRunning(spec)
        guard let server else {
            return Ready(containerName: name, hostShellSocketPath: nil)
        }
        // Every containerized spec carries a control socket here (SessionController sets it
        // unconditionally). A spec without one would silently fall back to a TCP endpoint the
        // guest can't reach — enforce the invariant loudly instead.
        guard let controlSock = spec.controlSocketHostPath else {
            throw BackendError.spawnFailed(
                "containerized run has no control socket — the vsock control plane is mandatory")
        }
        try await server.start(unixSocketPath: controlSock)
        // Assert we're actually listening. A desynced/unbound socket would otherwise surface
        // only as the CLI's opaque "Available MCP tools: none" and — because this server is
        // shared by the whole container — poison every session in the box. Fail the run
        // loudly and retryably instead.
        guard await server.isUnixSocketListening(atPath: controlSock) else {
            throw BackendError.spawnFailed(
                "approval control socket \(controlSock) is not listening "
                    + "(control endpoint unreachable)")
        }
        return Ready(containerName: name, hostShellSocketPath: controlSock)
        #endif
    }
}
