import { PassThrough, Writable } from 'node:stream';
import type {
  McpOrchestrator,
  ContainerSpec,
  ContainerInfo,
  ContainerLogs,
  ExecResult,
  InteractiveExec,
} from '../orchestrator.js';
import { K8sOfficialClient } from './k8s-client-official.js';
import type { K8sOfficialClientConfig } from './k8s-client-official.js';
import { generatePodSpec } from './manifest-generator.js';
import type { V1Pod } from '@kubernetes/client-node';

function mapPodState(pod: V1Pod): ContainerInfo['state'] {
  const cs = pod.status?.containerStatuses?.[0];
  if (cs) {
    if (cs.state?.running) return 'running';
    if (cs.state?.waiting) return 'starting';
    if (cs.state?.terminated) return 'stopped';
  }

  switch (pod.status?.phase) {
    case 'Running':
      return 'running';
    case 'Pending':
      return 'starting';
    case 'Succeeded':
    case 'Failed':
      return 'stopped';
    default:
      return 'unknown';
  }
}

function podToContainerInfo(pod: V1Pod): ContainerInfo {
  const info: ContainerInfo = {
    containerId: pod.metadata!.name!,
    name: pod.metadata!.name!,
    state: mapPodState(pod),
    createdAt: pod.metadata!.creationTimestamp
      ? new Date(pod.metadata!.creationTimestamp as unknown as string)
      : new Date(),
  };

  // Pod IP for internal network communication (replaces Docker container IP)
  if (pod.status?.podIP) {
    info.ip = pod.status.podIP;
  }

  // Extract port from first container spec
  const ports = pod.spec?.containers?.[0]?.ports;
  if (ports && ports.length > 0 && ports[0]?.containerPort) {
    info.port = ports[0].containerPort;
  }

  return info;
}

export class KubernetesOrchestrator implements McpOrchestrator {
  private client: K8sOfficialClient;
  private namespace: string;

  constructor(config?: K8sOfficialClientConfig) {
    this.client = new K8sOfficialClient(config);
    this.namespace = this.client.serversNamespace;
  }

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

  async pullImage(_image: string): Promise<void> {
    // K8s pulls images on pod scheduling — no pre-pull needed
  }

  async createContainer(spec: ContainerSpec): Promise<ContainerInfo> {
    await this.ensureNamespace(this.namespace);

    const manifest = generatePodSpec(spec, this.namespace);
    const pod = await this.client.core.createNamespacedPod({
      namespace: this.namespace,
      body: manifest as V1Pod,
    });

    // Wait briefly for pod to start scheduling
    await new Promise((resolve) => setTimeout(resolve, 500));

    return this.inspectContainer(pod.metadata!.name!);
  }

  async stopContainer(containerId: string): Promise<void> {
    await this.removeContainer(containerId);
  }

  async removeContainer(containerId: string, _force?: boolean): Promise<void> {
    try {
      await this.client.core.deleteNamespacedPod({
        name: containerId,
        namespace: this.namespace,
        gracePeriodSeconds: 5,
      });
    } catch (err: unknown) {
      const status = (err as { statusCode?: number }).statusCode
        ?? (err as { response?: { statusCode?: number } }).response?.statusCode;
      if (status !== 404) throw err;
    }
  }

  async inspectContainer(containerId: string): Promise<ContainerInfo> {
    const pod = await this.client.core.readNamespacedPod({
      name: containerId,
      namespace: this.namespace,
    });
    return podToContainerInfo(pod);
  }

  async getContainerLogs(
    containerId: string,
    opts?: { tail?: number; since?: number },
  ): Promise<ContainerLogs> {
    const stdout = new PassThrough();
    const chunks: Buffer[] = [];
    stdout.on('data', (chunk: Buffer) => chunks.push(chunk));

    const containerName = await this.getContainerName(containerId);

    const logOpts: { tailLines?: number; sinceSeconds?: number } = {
      tailLines: opts?.tail ?? 100,
    };
    if (opts?.since !== undefined) {
      logOpts.sinceSeconds = opts.since;
    }

    await new Promise<void>((resolve, reject) => {
      this.client.log
        .log(this.namespace, containerId, containerName, stdout, logOpts)
        .then(() => {
          stdout.on('end', resolve);
        })
        .catch(reject);
    });

    return { stdout: Buffer.concat(chunks).toString('utf-8'), stderr: '' };
  }

  async execInContainer(
    containerId: string,
    cmd: string[],
    opts?: { stdin?: string; timeoutMs?: number },
  ): Promise<ExecResult> {
    const containerName = await this.getContainerName(containerId);
    const stdoutChunks: Buffer[] = [];
    const stderrChunks: Buffer[] = [];

    const stdoutStream = new Writable({
      write(chunk: Buffer, _encoding, callback) {
        stdoutChunks.push(chunk);
        callback();
      },
    });

    const stderrStream = new Writable({
      write(chunk: Buffer, _encoding, callback) {
        stderrChunks.push(chunk);
        callback();
      },
    });

    let stdinStream: PassThrough | null = null;
    if (opts?.stdin) {
      stdinStream = new PassThrough();
      stdinStream.end(opts.stdin);
    }

    let exitCode = 0;

    const timeoutMs = opts?.timeoutMs ?? 30_000;

    await Promise.race([
      new Promise<void>((resolve, reject) => {
        this.client.exec
          .exec(
            this.namespace,
            containerId,
            containerName,
            cmd,
            stdoutStream,
            stderrStream,
            stdinStream,
            false, // tty
            (status) => {
              if (status.status === 'Failure') {
                exitCode = 1;
              }
              resolve();
            },
          )
          .catch(reject);
      }),
      new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error(`Exec timed out after ${timeoutMs}ms`)), timeoutMs),
      ),
    ]);

    return {
      exitCode,
      stdout: Buffer.concat(stdoutChunks).toString('utf-8'),
      stderr: Buffer.concat(stderrChunks).toString('utf-8'),
    };
  }

  async execInteractive(
    containerId: string,
    cmd: string[],
  ): Promise<InteractiveExec> {
    const containerName = await this.getContainerName(containerId);
    const stdout = new PassThrough();
    const stdinStream = new PassThrough();

    const stderrStream = new Writable({
      write(_chunk: Buffer, _encoding, callback) {
        // Discard stderr for interactive sessions (matches Docker behavior)
        callback();
      },
    });

    const wsPromise = this.client.exec.exec(
      this.namespace,
      containerId,
      containerName,
      cmd,
      stdout,
      stderrStream,
      stdinStream,
      false, // tty
    );

    // Wait for WebSocket connection to establish
    const ws = await wsPromise;

    return {
      stdout,
      write(data: string) {
        stdinStream.write(data);
      },
      close() {
        stdinStream.end();
        stdout.destroy();
        ws.close();
      },
    };
  }

  /**
   * Attach to a running container's main process (PID 1) stdin/stdout.
   * Used for docker-image STDIO servers where the entrypoint IS the MCP server.
   */
  async attachInteractive(
    containerId: string,
  ): Promise<InteractiveExec> {
    const containerName = await this.getContainerName(containerId);
    const stdout = new PassThrough();
    const stdinStream = new PassThrough();

    const stderrStream = new Writable({
      write(_chunk: Buffer, _encoding, callback) {
        callback();
      },
    });

    const ws = await this.client.attach.attach(
      this.namespace,
      containerId,
      containerName,
      stdout,
      stderrStream,
      stdinStream,
      false, // tty
    );

    return {
      stdout,
      write(data: string) {
        stdinStream.write(data);
      },
      close() {
        stdinStream.end();
        stdout.destroy();
        ws.close();
      },
    };
  }

  async listContainers(namespace?: string): Promise<ContainerInfo[]> {
    const ns = namespace ?? this.namespace;
    const podList = await this.client.core.listNamespacedPod({
      namespace: ns,
      labelSelector: 'mcpctl.managed=true',
    });

    return podList.items.map(podToContainerInfo);
  }

  async ensureNamespace(name: string): Promise<void> {
    try {
      await this.client.core.readNamespace({ name });
    } catch {
      try {
        await this.client.core.createNamespace({
          body: { apiVersion: 'v1', kind: 'Namespace', metadata: { name } },
        });
      } catch (createErr: unknown) {
        const status = (createErr as { statusCode?: number }).statusCode
          ?? (createErr as { response?: { statusCode?: number } }).response?.statusCode;
        if (status !== 409) throw createErr; // Already exists is fine
      }
    }
  }

  getNamespace(): string {
    return this.namespace;
  }

  /** Get the first container name in a pod (needed for exec/log APIs). */
  private async getContainerName(podName: string): Promise<string> {
    const pod = await this.client.core.readNamespacedPod({
      name: podName,
      namespace: this.namespace,
    });
    return pod.spec?.containers?.[0]?.name ?? podName;
  }
}
