import Docker from 'dockerode';
import { PassThrough } from 'node:stream';
import type {
  McpOrchestrator,
  ContainerSpec,
  ContainerInfo,
  ContainerLogs,
  ExecResult,
  InteractiveExec,
} from '../orchestrator.js';
import { DEFAULT_MEMORY_LIMIT } from '../orchestrator.js';

const MCPCTL_LABEL = 'mcpctl.managed';

function mapState(state: string | undefined): ContainerInfo['state'] {
  switch (state?.toLowerCase()) {
    case 'running':
      return 'running';
    case 'created':
    case 'restarting':
      return 'starting';
    case 'exited':
    case 'dead':
    case 'paused':
      return 'stopped';
    default:
      return 'unknown';
  }
}

export class DockerContainerManager implements McpOrchestrator {
  private docker: Docker;

  constructor(opts?: Docker.DockerOptions) {
    this.docker = new Docker(opts);
  }

  async ping(): Promise<boolean> {
    try {
      await this.docker.ping();
      return true;
    } catch {
      return false;
    }
  }

  async pullImage(image: string): Promise<void> {
    const stream = await this.docker.pull(image);
    // Wait for pull to complete
    await new Promise<void>((resolve, reject) => {
      this.docker.modem.followProgress(stream, (err: Error | null) => {
        if (err) reject(err);
        else resolve();
      });
    });
  }

  async createContainer(spec: ContainerSpec): Promise<ContainerInfo> {
    const memoryLimit = spec.memoryLimit ?? DEFAULT_MEMORY_LIMIT;
    const nanoCpus = spec.nanoCpus;

    const portBindings: Record<string, Array<{ HostPort: string }>> = {};
    const exposedPorts: Record<string, Record<string, never>> = {};

    if (spec.containerPort) {
      const key = `${spec.containerPort}/tcp`;
      exposedPorts[key] = {};
      portBindings[key] = [{ HostPort: spec.hostPort ? String(spec.hostPort) : '0' }];
    }

    const labels: Record<string, string> = {
      [MCPCTL_LABEL]: 'true',
      ...spec.labels,
    };

    const envArr = spec.env
      ? Object.entries(spec.env).map(([k, v]) => `${k}=${v}`)
      : undefined;

    const createOpts: Docker.ContainerCreateOptions = {
      Image: spec.image,
      name: spec.name,
      Env: envArr,
      ExposedPorts: exposedPorts,
      Labels: labels,
      // Keep stdin open for STDIO MCP servers (they read from stdin)
      OpenStdin: true,
      StdinOnce: false,
      HostConfig: {
        PortBindings: portBindings,
        Memory: memoryLimit,
        ...(nanoCpus ? { NanoCpus: nanoCpus } : {}),
        NetworkMode: spec.network ?? 'bridge',
      },
    };
    if (spec.command) {
      createOpts.Cmd = spec.command;
    }

    const container = await this.docker.createContainer(createOpts);

    await container.start();

    return this.inspectContainer(container.id);
  }

  async stopContainer(containerId: string, timeoutSeconds = 10): Promise<void> {
    const container = this.docker.getContainer(containerId);
    await container.stop({ t: timeoutSeconds });
  }

  async removeContainer(containerId: string, force = false): Promise<void> {
    const container = this.docker.getContainer(containerId);
    await container.remove({ force, v: true });
  }

  async inspectContainer(containerId: string): Promise<ContainerInfo> {
    const container = this.docker.getContainer(containerId);
    const info = await container.inspect();

    let port: number | undefined;
    const ports = info.NetworkSettings?.Ports;
    if (ports) {
      for (const bindings of Object.values(ports)) {
        const arr = bindings as Array<{ HostIp: string; HostPort: string }> | undefined;
        if (arr && arr.length > 0 && arr[0]) {
          port = parseInt(arr[0].HostPort, 10);
          break;
        }
      }
    }

    const result: ContainerInfo = {
      containerId: info.Id,
      name: info.Name.replace(/^\//, ''),
      state: mapState(info.State?.Status),
      createdAt: new Date(info.Created),
    };
    if (port !== undefined) {
      result.port = port;
    }

    // Extract container IP from first non-default network
    const networks = info.NetworkSettings?.Networks;
    if (networks) {
      for (const [, net] of Object.entries(networks)) {
        const netInfo = net as { IPAddress?: string };
        if (netInfo.IPAddress) {
          result.ip = netInfo.IPAddress;
          break;
        }
      }
    }

    return result;
  }

  async getContainerLogs(
    containerId: string,
    opts?: { tail?: number; since?: number },
  ): Promise<ContainerLogs> {
    const container = this.docker.getContainer(containerId);
    const logOpts: Record<string, unknown> = {
      stdout: true,
      stderr: true,
      follow: false,
      tail: opts?.tail ?? 100,
    };
    if (opts?.since) {
      logOpts['since'] = opts.since;
    }

    const buffer = await container.logs(logOpts) as unknown as Buffer;
    const raw = buffer.toString('utf-8');

    // Docker multiplexes stdout/stderr with 8-byte headers.
    // For simplicity we return everything as stdout.
    return { stdout: raw, stderr: '' };
  }

  async execInContainer(
    containerId: string,
    cmd: string[],
    opts?: { stdin?: string; timeoutMs?: number },
  ): Promise<ExecResult> {
    const container = this.docker.getContainer(containerId);
    const hasStdin = opts?.stdin !== undefined;

    const exec = await container.exec({
      Cmd: cmd,
      AttachStdin: hasStdin,
      AttachStdout: true,
      AttachStderr: true,
    });

    const stream = await exec.start({ hijack: hasStdin, stdin: hasStdin });
    const timeoutMs = opts?.timeoutMs ?? 30_000;

    return new Promise<ExecResult>((resolve, reject) => {
      const stdout = new PassThrough();
      const stderr = new PassThrough();
      const stdoutChunks: Buffer[] = [];
      const stderrChunks: Buffer[] = [];

      stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
      stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));

      this.docker.modem.demuxStream(stream, stdout, stderr);

      if (hasStdin) {
        stream.write(opts!.stdin);
        stream.end();
      }

      const timer = setTimeout(() => {
        stream.destroy();
        reject(new Error(`Exec timed out after ${timeoutMs}ms`));
      }, timeoutMs);

      stream.on('end', () => {
        clearTimeout(timer);
        exec.inspect().then((info) => {
          resolve({
            exitCode: (info as { ExitCode: number }).ExitCode,
            stdout: Buffer.concat(stdoutChunks).toString('utf-8'),
            stderr: Buffer.concat(stderrChunks).toString('utf-8'),
          });
        }).catch((err) => {
          resolve({
            exitCode: -1,
            stdout: Buffer.concat(stdoutChunks).toString('utf-8'),
            stderr: err instanceof Error ? err.message : String(err),
          });
        });
      });

      stream.on('error', (err: Error) => {
        clearTimeout(timer);
        reject(err);
      });
    });
  }

  async execInteractive(containerId: string, cmd: string[]): Promise<InteractiveExec> {
    const container = this.docker.getContainer(containerId);

    const exec = await container.exec({
      Cmd: cmd,
      AttachStdin: true,
      AttachStdout: true,
      AttachStderr: true,
    });

    const stream = await exec.start({ hijack: true, stdin: true });

    // Demux Docker's multiplexed stream into separate stdout/stderr
    const stdout = new PassThrough();
    const stderr = new PassThrough();
    this.docker.modem.demuxStream(stream, stdout, stderr);

    return {
      stdout,
      write(data: string) {
        stream.write(data);
      },
      close() {
        try { stream.end(); } catch { /* ignore */ }
      },
    };
  }
}
