import Foundation

/// Result of resolving a public hostname from inside a sandbox. This deliberately separates a
/// real lookup failure from a probe that could not run: both warrant rebuilding networking after
/// Codex has already reported `getaddrinfo` failure, while `.responsive` cancels recovery when a
/// transient host-network transition has healed on its own.
public enum SandboxDNSProbe: Sendable, Equatable {
    case responsive
    case lookupFailed(String)
    case probeFailed(String)
}

/// The platform-neutral sandbox **mechanism** surface `ContainerManager` (the policy layer)
/// drives — exactly the slice of `ContainerEngine`'s API the manager and backends use, with
/// signatures lifted verbatim from `Container/ContainerEngine.swift` (docs/WINDOWS_PORT.md
/// §3.1). On macOS the Apple-containerization `ContainerEngine` conforms; on Windows the wslc
/// broker engine (`Container/Windows/WslcContainerEngine.swift`) does. Policy — busy
/// ref-counting, idle timers, shared naming, reconcile — stays single-sourced in
/// `ContainerManager` across both.
///
/// Compiles on every platform (Linux included, where only the LinuxSupport.swift stub manager
/// exists and no conformer is built) so the protocol itself never forks.
public protocol SandboxEngine: Sendable {
    /// Idempotent create-or-start; returns the container name and the address at which the
    /// guest reaches the host (macOS: vmnet gateway; Windows: WSL vEthernet gateway).
    func ensureRunning(_ spec: ContainerSpec) async throws
        -> (name: String, hostGateway: String)

    /// Run `argv` inside the named container as a fresh exec, returning a ``ProcessHandle``
    /// whose stdio is the guest process's stdio. The agent process flows through this.
    func exec(
        name: String, workdir: String, env: [String: String], argv: [String],
        uid: Int?, gid: Int?, memoryLimitBytes: UInt64?
    ) async throws -> any ProcessHandle

    /// Run `argv` to completion in the named **running** container, returning its combined,
    /// tail-capped output plus the exit code; `nil` only when the container isn't live.
    func runCapturing(
        name: String, argv: [String], env: [String: String], uid: Int, gid: Int
    ) async -> (exitCode: Int32, output: String)?

    /// Stop the container (freeing its memory) but keep its on-disk state for a fast recreate.
    func stop(name: String) async

    /// Stop then recreate the container in place; `false` when the recreate failed.
    @discardableResult
    func restart(name: String) async -> Bool

    /// Stop the container and delete its on-disk state. `true` once it's gone.
    @discardableResult
    func remove(name: String) async -> Bool

    /// True if the named container is currently running.
    func isRunning(_ name: String) async -> Bool

    /// True if the container is running or recreatable from persisted on-disk state.
    func containerExists(_ name: String) async -> Bool

    /// Names of all containers currently running.
    func list() async -> [String]

    /// Live CPU/memory reading for the Control panel's resource monitor; `nil` when the
    /// container isn't running or the probe fails.
    func sampleResourceUsage(name: String) async -> ContainerResourceSample?

    /// The artifact acquisition currently in flight (image pull / unpack), for the Control
    /// panel's progress bar; `nil` when everything is cached.
    func currentDownloadProgress() async -> ContainerDownloadProgress?

    /// End-to-end health of the container's control plane, probed from inside the guest.
    func probeControlPlane(name: String) async -> ContainerEngine.ControlPlaneProbe

    /// Resolve `hostname` from inside the guest. Used to distinguish a stale sandbox DNS/NAT
    /// service from a provider stream that merely disconnected.
    func probeDNS(name: String, hostname: String) async -> SandboxDNSProbe

    /// Tear down the sandbox mechanism's shared network state. The policy layer calls this only
    /// after every managed turn is idle and places a global lifecycle barrier in front of new
    /// starts. Implementations must leave persisted container files intact so the next
    /// `ensureRunning` recreates against a fresh network without reprovisioning.
    func resetNetwork() async

    /// Post-mortem for an agent that exited 137/SIGKILL in the named container.
    func diagnoseKill(name: String) async -> ContainerKillDiagnosis

    /// On-launch disk GC: keep only `keepNames`' per-container state, drop everything else
    /// this channel owns.
    func reconcileDisk(keepNames: Set<String>) async

    /// Delete the cached default sandbox image/rootfs so the next start rebuilds it fresh.
    func removeDefaultRootfs() async

    /// Whether the registry serves a newer sandbox base image than the cached one.
    func checkDefaultImageUpdate() async -> ImageUpdateStatus

    /// Force an immediate memory-reclaim pass across all live containers.
    func reclaimMemoryNow() async

    /// The address at which a guest reaches the host, when the engine can know it BEFORE any
    /// container exists — the wslc session's WSL-facing gateway (docs/WINDOWS_PORT.md §5),
    /// needed so a backend can bind the approval server and bake its TCP endpoint into the
    /// spec env ahead of `ensureRunning`. `nil` where the gateway is per-container
    /// (macOS vmnet — callers there use the address `ensureRunning` returns instead).
    func sessionHostGateway() async throws -> String?
}

extension SandboxEngine {
    /// Default: no pre-container gateway (the macOS engine allocates one per container).
    public func sessionHostGateway() async throws -> String? { nil }

    /// All current sandbox images carry Node (the agent CLIs require it), so use libc's resolver
    /// through `dns.lookup`: this exercises the same `/etc/resolv.conf` + `getaddrinfo` path whose
    /// `EAI_AGAIN` Codex reports. A hard deadline keeps a dead DNS proxy from wedging recovery.
    public func probeDNS(name: String, hostname: String) async -> SandboxDNSProbe {
        let script = """
            const dns = require('dns');
            let done = false;
            const finish = (code, msg) => {
              if (done) return;
              done = true;
              console.log(msg);
              process.exit(code);
            };
            dns.lookup(process.argv[1], (error, address) => {
              if (error) finish(2, `${error.code || 'DNS'}:${error.message}`);
              else finish(0, address || 'RESOLVED');
            });
            setTimeout(() => finish(3, 'DNS_TIMEOUT'), 4000);
            """
        guard let result = await runCapturing(
            name: name, argv: ["node", "-e", script, hostname], env: [:], uid: 0, gid: 0)
        else {
            return .probeFailed("container isn't running")
        }
        switch result.exitCode {
        case 0: return .responsive
        case 2, 3: return .lookupFailed(result.output)
        default: return .probeFailed("exit \(result.exitCode): \(result.output)")
        }
    }
}
