import { exec as execCb } from 'child_process';
import { promisify } from 'util';
import http from 'http';
import { logger } from '../utils/logger';

const exec = promisify(execCb);

const EXEC_TIMEOUT = 120_000; // 2 minutes
const DOCKER_SOCKET = '/var/run/docker.sock';

/** Validate a service/project name to prevent shell injection. */
function validateName(name: string, label: string): string {
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name)) {
    throw new Error(`Invalid ${label}: ${name}`);
  }
  return name;
}

/** Validate a Docker duration format (e.g., "1h", "30m", "24h"). */
function validateDuration(value: string): string {
  if (!/^\d+[smhd]$/.test(value)) {
    throw new Error(`Invalid duration: ${value}`);
  }
  return value;
}

/** Validate a tail count (positive integer, capped). */
function validateTail(value: number): number {
  const n = Math.max(1, Math.min(value, 5000));
  return Math.floor(n);
}

/** Parsed container status from `docker compose ps --format json` */
export interface ContainerInfo {
  name: string;
  service: string;
  status: string;
  state: string;
  health: string;
  ports: string;
  createdAt: string;
  exitCode: number;
}

/**
 * Execute a shell command with timeout and proper error handling.
 */
async function execCmd(
  command: string,
  cwd: string,
  timeoutMs = EXEC_TIMEOUT
): Promise<{ stdout: string; stderr: string }> {
  logger.debug(`[docker] exec: ${command} (cwd: ${cwd})`);
  try {
    const result = await exec(command, {
      cwd,
      timeout: timeoutMs,
      maxBuffer: 10 * 1024 * 1024, // 10MB
      env: { ...process.env, COMPOSE_ANSI: 'never' },
    });
    return result;
  } catch (err: unknown) {
    const error = err as { stdout?: string; stderr?: string; message?: string; killed?: boolean };
    if (error.killed) {
      throw new Error(`Command timed out after ${timeoutMs}ms: ${command}`);
    }
    // Include stderr in the error for debugging
    const msg = error.stderr || error.message || 'Unknown exec error';
    throw new Error(`Command failed: ${command}\n${msg}`);
  }
}

/**
 * Build the base compose command with project name.
 */
function composeCmd(project: string): string {
  return `docker compose -p ${validateName(project, 'project')}`;
}

// ─── Docker Compose CLI Operations ───────────────────────────────────

export async function composeUp(
  projectDir: string,
  project: string,
  services?: string[]
): Promise<string> {
  const svc = services?.length ? ` ${services.map((s) => validateName(s, 'service')).join(' ')}` : '';
  const orphanFlag = services?.length ? '' : ' --remove-orphans';
  const { stdout, stderr } = await execCmd(
    `${composeCmd(project)} up -d${orphanFlag}${svc}`,
    projectDir
  );
  return stdout || stderr;
}

export async function composeDown(
  projectDir: string,
  project: string,
  removeVolumes = false
): Promise<string> {
  const flags = removeVolumes ? ' -v' : '';
  const { stdout, stderr } = await execCmd(
    `${composeCmd(project)} down${flags}`,
    projectDir
  );
  return stdout || stderr;
}

export async function composeStop(
  projectDir: string,
  project: string
): Promise<string> {
  const { stdout, stderr } = await execCmd(
    `${composeCmd(project)} stop`,
    projectDir
  );
  return stdout || stderr;
}

export async function composeRestart(
  projectDir: string,
  project: string,
  service?: string
): Promise<string> {
  const svc = service ? ` ${validateName(service, 'service')}` : '';
  const { stdout, stderr } = await execCmd(
    `${composeCmd(project)} restart${svc}`,
    projectDir
  );
  return stdout || stderr;
}

export async function composePull(
  projectDir: string,
  project: string
): Promise<string> {
  const { stdout, stderr } = await execCmd(
    `${composeCmd(project)} pull`,
    projectDir,
    300_000 // 5 min for pulls
  );
  return stdout || stderr;
}

export async function composeBuild(
  projectDir: string,
  project: string
): Promise<string> {
  const { stdout, stderr } = await execCmd(
    `${composeCmd(project)} build`,
    projectDir,
    600_000 // 10 min for builds
  );
  return stdout || stderr;
}

/**
 * List containers with status. Returns parsed container info.
 */
export async function composePs(
  projectDir: string,
  project: string
): Promise<ContainerInfo[]> {
  const { stdout } = await execCmd(
    `${composeCmd(project)} ps --format json`,
    projectDir
  );

  if (!stdout.trim()) return [];

  // docker compose ps --format json outputs one JSON object per line
  const containers: ContainerInfo[] = [];
  for (const line of stdout.trim().split('\n')) {
    if (!line.trim()) continue;
    try {
      const raw = JSON.parse(line);
      containers.push({
        name: raw.Name || raw.name || '',
        service: raw.Service || raw.service || '',
        status: raw.Status || raw.status || '',
        state: raw.State || raw.state || '',
        health: raw.Health || raw.health || '',
        ports: raw.Ports || raw.ports || '',
        createdAt: raw.CreatedAt || raw.created_at || '',
        exitCode: raw.ExitCode ?? raw.exit_code ?? 0,
      });
    } catch {
      logger.warn(`[docker] Failed to parse container line: ${line}`);
    }
  }
  return containers;
}

/**
 * Get logs from a specific service.
 */
export async function composeLogs(
  projectDir: string,
  project: string,
  service?: string,
  tail = 200,
  since?: string
): Promise<string> {
  const parts = [composeCmd(project), 'logs', '--no-color'];
  if (tail > 0) parts.push(`--tail=${validateTail(tail)}`);
  if (since) parts.push(`--since=${validateDuration(since)}`);
  if (service) parts.push(validateName(service, 'service'));

  const { stdout, stderr } = await execCmd(parts.join(' '), projectDir);
  return stdout || stderr;
}

/**
 * Execute a command inside a running service container.
 * Optionally pass environment variables via -e flags.
 */
export async function composeExec(
  projectDir: string,
  project: string,
  service: string,
  command: string,
  timeoutMs = EXEC_TIMEOUT,
  envVars?: Record<string, string>
): Promise<string> {
  const envFlags = envVars
    ? Object.entries(envVars).map(([k, v]) => `-e ${k}='${v.replace(/'/g, "'\\''")}'`).join(' ') + ' '
    : '';
  const { stdout, stderr } = await execCmd(
    `${composeCmd(project)} exec -T ${envFlags}${validateName(service, 'service')} ${command}`,
    projectDir,
    timeoutMs
  );
  return stdout || stderr;
}

/**
 * Run a one-off command in a service container (docker compose run).
 * Uses --entrypoint "" to skip the service's entrypoint script.
 * Useful for running setup commands (prisma db push, seed) without the entrypoint.
 */
export async function composeRun(
  projectDir: string,
  project: string,
  service: string,
  command: string,
  timeoutMs = EXEC_TIMEOUT
): Promise<string> {
  const { stdout, stderr } = await execCmd(
    `${composeCmd(project)} run --rm --no-deps -T --entrypoint "" ${validateName(service, 'service')} ${command}`,
    projectDir,
    timeoutMs
  );
  return stdout || stderr;
}

// ─── Docker Socket API ───────────────────────────────────────────────

/**
 * Make a request to the Docker Engine API via Unix socket.
 */
function dockerSocketRequest(path: string): Promise<string> {
  return new Promise((resolve, reject) => {
    const req = http.request(
      {
        socketPath: DOCKER_SOCKET,
        path,
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
      },
      (res) => {
        let data = '';
        res.on('data', (chunk) => (data += chunk));
        res.on('end', () => {
          if (res.statusCode && res.statusCode >= 400) {
            reject(new Error(`Docker API returned ${res.statusCode}: ${data}`));
          } else {
            resolve(data);
          }
        });
      }
    );
    req.on('error', reject);
    req.setTimeout(10_000, () => {
      req.destroy();
      reject(new Error('Docker socket request timed out'));
    });
    req.end();
  });
}

/**
 * Get status of a specific container by name via Docker socket API.
 */
export async function getContainerStatus(
  containerName: string
): Promise<{ state: string; health: string; running: boolean } | null> {
  try {
    const data = await dockerSocketRequest(
      `/containers/${encodeURIComponent(containerName)}/json`
    );
    const info = JSON.parse(data);
    return {
      state: info.State?.Status || 'unknown',
      health: info.State?.Health?.Status || 'none',
      running: info.State?.Running === true,
    };
  } catch {
    return null;
  }
}

/**
 * Poll until a container reaches healthy state or timeout expires.
 */
export async function waitForHealthy(
  containerName: string,
  timeoutMs = 60_000,
  pollIntervalMs = 2_000
): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const status = await getContainerStatus(containerName);
    if (status?.health === 'healthy') return true;
    if (status?.state === 'exited' || status?.state === 'dead') {
      throw new Error(`Container ${containerName} exited unexpectedly`);
    }
    await new Promise((r) => setTimeout(r, pollIntervalMs));
  }
  throw new Error(`Container ${containerName} did not become healthy within ${timeoutMs}ms`);
}

/**
 * Wait for an HTTP endpoint to respond with 200.
 */
export async function waitForHttp(
  url: string,
  timeoutMs = 120_000,
  pollIntervalMs = 3_000
): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      const response = await fetch(url, { signal: AbortSignal.timeout(5_000) });
      if (response.ok) return true;
    } catch {
      // Expected while service is starting
    }
    await new Promise((r) => setTimeout(r, pollIntervalMs));
  }
  throw new Error(`HTTP endpoint ${url} did not respond within ${timeoutMs}ms`);
}
