// ABOUTME: Container system type definitions and interfaces
// ABOUTME: Defines container lifecycle, configuration, and runtime contracts

import type { Readable, Writable } from 'node:stream';

export interface PortMapping {
  host: number;
  container: number;
}

export interface DockerCreateConfig {
  // Image to run. Required: every container must know its image at create time.
  image: string;

  // Filesystem configuration
  workingDirectory: string;
  mounts: ContainerMount[];

  // Process configuration
  command?: string[];
  environment?: Record<string, string>;

  // Host->container port publishing. Optional; runtimes that lack a port concept
  // may ignore (see AppleContainerRuntime).
  ports?: PortMapping[];

  // Docker --restart policy. Only 'unless-stopped' is supported in v1; used by
  // persistent container runtimes so the daemon resurrects them after host reboot.
  // Absent ⇒ no restart flag emitted (default no-restart behavior).
  restartPolicy?: 'unless-stopped';

  // Linux kernel sysctls forwarded to `docker create --sysctl key=value`.
  // Absent or empty ⇒ no --sysctl flag emitted. The browser persona may need
  // `net.ipv6.conf.lo.disable_ipv6=0` so chrome's port-availability check
  // can bind to `::1`.
  sysctls?: Record<string, string>;

  // Linux capabilities forwarded to `docker create --cap-add <cap>` per entry.
  // Absent or empty ⇒ no --cap-add flags emitted. Used by direct docker
  // container specs that need extra kernel authority.
  capAdd?: string[];

  // Docker network name forwarded to `docker create --network <name>`.
  // Absent ⇒ no --network flag emitted (docker default).
  network?: string;

  // IPv4 address of the egress gateway broker.
  gatewayRoute?: string;

  // Docker object labels stamped at create (`docker create --label key=value`).
  // Absent or empty ⇒ no --label flags.
  labels?: Record<string, string>;

  // Privileged-runtime SELECTOR fields (copied from ContainerSpec by
  // materializeOnce). SELECTOR ONLY — never an authority source: the privileged
  // runtime rebuilds the full container config from its own catalog using only
  // the persona. The docker/apple runtimes ignore them.
  persona?: string;
  // The spawned persona's role name. SELECTOR ONLY (credential-helper authz);
  // PlaneRuntime forwards it as the spawn argv's role. Docker/apple ignore it.
  role?: string;
  parentSessionId?: string;
  childSessionId?: string;
}

type AssertNoForbiddenDockerCreateConfigKeys<T extends never> = T;
type _DockerCreateConfigHasNoResourceLimitFields = AssertNoForbiddenDockerCreateConfigKeys<
  Extract<keyof DockerCreateConfig, 'memory' | 'cpuShares'>
>;

export interface PlaneSpawnRequest {
  // Container identity fields may be supplied by ContainerManager for fallback naming.
  id?: string;
  name?: string;

  // Selector fields for plane spawn:
  // `spawn <persona/env> <parent> <child> <jobId> <role>`.
  // `persona` is the ENVIRONMENT (the shim's environment allowlist value);
  // `role` is the spawned persona's role name, used by the credential helper
  // for egress source-IP → role authz. Both are required for a plane spawn.
  persona: string;
  role?: string;
  parentSession?: string;
  childSession?: string;
  jobId?: string;
}

export type ContainerConfig = DockerCreateConfig &
  Partial<PlaneSpawnRequest> & {
    // Container identification (optional - will be generated if not provided)
    id?: string;
    name?: string;
  };

export interface ContainerMount {
  source: string; // Host path
  target: string; // Container path
  readonly?: boolean;
}

export interface ContainerInfo {
  id: string;
  state: ContainerState;
  mounts?: ContainerMount[];
  pid?: number;
  startedAt?: Date;
  stoppedAt?: Date;
  exitCode?: number;
  // Docker object labels read back from `docker inspect` (.Config.Labels).
  // PRI-2012: the spawn broker reads its `sen.broker.*` identity labels here to
  // reconstruct + re-validate ownership when adopting a container after restart.
  labels?: Record<string, string>;
}

export type ContainerState = 'created' | 'running' | 'stopped' | 'failed';
export type ExecEnvironmentMode = 'inherit' | 'replace';

export interface ExecOptions {
  command: string[];
  workingDirectory?: string;
  environment?: Record<string, string>;
  environmentMode?: ExecEnvironmentMode;
  stdin?: string;
  timeout?: number;
}

export interface ExecResult {
  stdout: string;
  stderr: string;
  exitCode: number;
}

export interface ExecStreamOptions {
  command: string[];
  workingDirectory?: string;
  environment?: Record<string, string>;
  environmentMode?: ExecEnvironmentMode;
  /**
   * Mark this as a long-lived service stream (an in-container MCP server) rather
   * than a one-shot brokered exec. The plane shim then skips the one-shot stream
   * budgets (wall-clock lifetime, cumulative output, stdin idle timeout) that
   * would otherwise SIGKILL a persistent server mid-session. Ignored by runtimes
   * without a brokering shim.
   */
  longLived?: boolean;
}

export interface ExecStreamHandle {
  stdin: Writable;
  stdout: Readable;
  stderr: Readable;
  wait(): Promise<{ exitCode: number }>;
  kill(signal?: NodeJS.Signals): void;
}

export interface ContainerRuntime {
  // Lifecycle management
  create(config: ContainerConfig): string | Promise<string>; // Returns container ID
  start(containerId: string): Promise<void>;
  stop(containerId: string, timeout?: number): Promise<void>;
  remove(containerId: string): Promise<void>;

  // Execution
  exec(containerId: string, options: ExecOptions): Promise<ExecResult>;
  execStream(containerId: string, options: ExecStreamOptions): Promise<ExecStreamHandle>;

  // Information
  inspect(containerId: string): ContainerInfo | Promise<ContainerInfo>;
  list(): ContainerInfo[] | Promise<ContainerInfo[]>;

  /**
   * Live-inspect against the daemon (not the in-process cache). Returns null
   * when the container does not exist on the daemon side. Used by
   * ContainerManager to adopt boxes that survived a parent restart.
   *
   * Distinct from sync `inspect()`: the latter reads only the in-memory cache.
   * Subclasses may fall back to the cached inspect when the daemon-side
   * concept does not apply (e.g. AppleContainerRuntime).
   */
  daemonInspect(containerId: string): Promise<ContainerInfo | null>;

  /**
   * Register an existing daemon-side container into the runtime's in-process
   * caches so subsequent start/exec calls work. Idempotent and no-op when the
   * runtime has no caches to populate.
   */
  adopt(config: ContainerConfig, state: ContainerState): Promise<void>;

  /**
   * Release a per_invocation child: the runtime owns destroying the child's
   * container AND removing its `/work` workspace. Only the plane runtime
   * implements this — it routes the shim's `release` verb. Runtimes without a
   * shim leave this undefined; ContainerManager falls back to destroying the
   * container only (the shim is the production owner of `/work`).
   */
  releasePerInvocation?(parentSession: string, childSession: string): Promise<void>;
}

export class ContainerError extends Error {
  constructor(
    message: string,
    public readonly containerId?: string,
    public readonly cause?: Error
  ) {
    super(message);
    this.name = 'ContainerError';
  }
}

export class ContainerNotFoundError extends ContainerError {
  constructor(containerId: string) {
    super(`Container not found: ${containerId}`, containerId);
    this.name = 'ContainerNotFoundError';
  }
}

export class ContainerExecError extends ContainerError {
  constructor(
    containerId: string,
    public readonly exitCode: number,
    public readonly stderr: string
  ) {
    super(`Container exec failed with exit code ${exitCode}`, containerId);
    this.name = 'ContainerExecError';
  }
}
