/**
 * Docker Client
 *
 * Thin wrapper around Docker CLI via execa for preview lifecycle management.
 * No SDK dependency — uses `docker compose` subprocess calls.
 */

import { execa } from 'execa';
import { createModuleLogger } from '../logger/index.js';
import { COMPOSE_PROJECT_PREFIX, PREVIEW_LABELS } from './types.js';

const log = createModuleLogger('preview:docker');

/**
 * Service status from `docker compose ps`.
 */
export interface ServiceStatus {
  name: string;
  state: string;
  health: string;
}

/**
 * Compose project from `docker compose ls`.
 */
export interface ComposeProject {
  Name: string;
  Status: string;
  ConfigFiles: string;
}

/**
 * Check if Docker is available and running.
 */
export async function isDockerAvailable(): Promise<boolean> {
  try {
    await execa('docker', ['info'], { timeout: 10000 });
    return true;
  } catch {
    return false;
  }
}

/**
 * Ensure a Docker network exists. Creates it if missing.
 */
export async function ensureDockerNetwork(name: string): Promise<void> {
  try {
    await execa('docker', ['network', 'create', '--driver', 'bridge', name], {
      timeout: 15000,
    });
    log.info({ name }, 'created docker network');
  } catch (error) {
    // Ignore "already exists" error
    if ((error as Error).message?.includes('already exists')) {
      log.debug({ name }, 'docker network already exists');
      return;
    }
    throw error;
  }
}

/**
 * Remove a Docker network. Ignores errors (e.g., network in use or not found).
 */
export async function removeDockerNetwork(name: string): Promise<void> {
  try {
    await execa('docker', ['network', 'rm', name], { timeout: 15000 });
    log.info({ name }, 'removed docker network');
  } catch {
    log.debug({ name }, 'failed to remove docker network (may not exist or be in use)');
  }
}

/**
 * Check if a Docker network exists.
 */
export async function dockerNetworkExists(name: string): Promise<boolean> {
  try {
    await execa('docker', ['network', 'inspect', name], { timeout: 15000 });
    return true;
  } catch {
    return false;
  }
}

/**
 * Start a compose project (build and run in background).
 */
export async function composeUp(composePath: string, projectName: string): Promise<void> {
  log.info({ composePath, projectName }, 'starting compose project');
  const cwd = composePath.substring(0, composePath.lastIndexOf('/'));

  await execa('docker', [
    'compose',
    '-p', projectName,
    '-f', composePath,
    'up',
    '--build',
    '-d',
  ], {
    cwd,
    timeout: 600000, // 10 minutes for builds
  });
}

/**
 * Stop and remove a compose project.
 */
export async function composeDown(projectName: string): Promise<void> {
  log.info({ projectName }, 'stopping compose project');
  await execa('docker', [
    'compose',
    '-p', projectName,
    'down',
    '--volumes',
    '--remove-orphans',
  ], {
    timeout: 60000,
  });
}

/**
 * Execute a command inside a running service container.
 * Used for seed commands (migrations, fixture loading, etc.) after health checks pass.
 */
export async function execInContainer(
  projectName: string,
  serviceName: string,
  command: string,
): Promise<{ stdout: string; stderr: string }> {
  const result = await execa('docker', [
    'compose', '-p', projectName,
    'exec', '-T', serviceName,
    'sh', '-c', command,
  ], { timeout: 300000 }); // 5 min per seed command
  return { stdout: result.stdout, stderr: result.stderr };
}

/**
 * Get service statuses for a compose project.
 */
export async function composePs(projectName: string): Promise<ServiceStatus[]> {
  try {
    const result = await execa('docker', [
      'compose',
      '-p', projectName,
      'ps',
      '--format', 'json',
    ], {
      timeout: 15000,
    });

    if (!result.stdout.trim()) {
      return [];
    }

    // docker compose ps --format json outputs one JSON object per line
    const lines = result.stdout.trim().split('\n');
    return lines.map((line) => {
      const container = JSON.parse(line);
      return {
        name: container.Service || container.Name || '',
        state: container.State || 'unknown',
        health: container.Health || 'none',
      };
    });
  } catch (error) {
    log.warn({ projectName, err: error }, 'failed to get compose ps');
    return [];
  }
}

/**
 * List all preview compose projects.
 */
export async function listPreviewProjects(): Promise<ComposeProject[]> {
  try {
    const result = await execa('docker', [
      'compose',
      'ls',
      '--filter', `name=${COMPOSE_PROJECT_PREFIX}`,
      '--format', 'json',
    ], {
      timeout: 15000,
    });

    if (!result.stdout.trim()) {
      return [];
    }

    return JSON.parse(result.stdout);
  } catch (error) {
    log.warn({ err: error }, 'failed to list preview projects');
    return [];
  }
}

/**
 * Get container labels for a compose project.
 * Returns labels from the first container that has cw.preview=true.
 */
export async function getContainerLabels(projectName: string): Promise<Record<string, string>> {
  try {
    const result = await execa('docker', [
      'ps',
      '--filter', `label=${PREVIEW_LABELS.preview}=true`,
      '--filter', `label=com.docker.compose.project=${projectName}`,
      '--format', '{{json .Labels}}',
    ], {
      timeout: 15000,
    });

    if (!result.stdout.trim()) {
      return {};
    }

    // Parse the first line's label string: "key=val,key=val,..."
    const firstLine = result.stdout.trim().split('\n')[0];
    const labelStr = firstLine.replace(/^"|"$/g, '');
    const labels: Record<string, string> = {};

    for (const pair of labelStr.split(',')) {
      const eqIdx = pair.indexOf('=');
      if (eqIdx > 0) {
        const key = pair.substring(0, eqIdx);
        const value = pair.substring(eqIdx + 1);
        if (key.startsWith('cw.')) {
          labels[key] = value;
        }
      }
    }

    return labels;
  } catch (error) {
    log.warn({ projectName, err: error }, 'failed to get container labels');
    return {};
  }
}
