import { execFile as execFileCb } from 'node:child_process';
import { promisify } from 'node:util';
import type { Config } from '../../config/index.js';

const execFile = promisify(execFileCb);

export type DockerDependencyId = string;
export type DockerDependencyAction = 'start' | 'restart' | 'stop' | 'update';

export interface DockerDependencyStatus {
  id: DockerDependencyId;
  name: string;
  service: string;
  configured: boolean;
  state: string;
  health: string;
  statusText: string;
  containerName: string | null;
  availableActions: DockerDependencyAction[];
  error?: string;
}

export interface DockerDependencyControlResult {
  dependency: DockerDependencyId;
  action: DockerDependencyAction;
  status: DockerDependencyStatus;
  message?: string;
}

type DockerComposeResult = { stdout: string; stderr: string };
type DockerComposeRunner = (args: string[]) => Promise<DockerComposeResult>;

interface ComposePsEntry {
  Name?: string;
  Service?: string;
  State?: string;
  Status?: string;
  Health?: string;
}

interface ComposeDiscovery {
  profileArgs: string[];
  services: string[];
}

interface DockerDependencyDescriptor {
  id: DockerDependencyId;
  name: string;
  service: string;
  configured: boolean;
}

const COMPOSE_FILE = 'docker-compose.yml';
const FLYNN_SERVICE = 'flynn';
const WHISPER_SERVICE = 'whisper-server';
const WHISPER_DEPENDENCY_ID = 'whisper';
const BRAVE_SEARCH_SERVICE = 'brave-search';
const SEARXNG_SERVICE = 'searxng';

function runCompose(args: string[], timeout: number): Promise<DockerComposeResult> {
  return execFile('docker', ['compose', '-f', COMPOSE_FILE, ...args], {
    timeout,
    maxBuffer: 4 * 1024 * 1024,
  }) as Promise<DockerComposeResult>;
}

function defaultRunner(args: string[]): Promise<DockerComposeResult> {
  return runCompose(args, 10_000);
}

function defaultControlRunner(args: string[]): Promise<DockerComposeResult> {
  return runCompose(args, 15 * 60_000);
}

function normalizeError(error: unknown): string {
  if (error && typeof error === 'object') {
    const maybe = error as { stderr?: string; stdout?: string; message?: string };
    const stderr = maybe.stderr?.trim();
    if (stderr) {return stderr;}
    const stdout = maybe.stdout?.trim();
    if (stdout) {return stdout;}
    if (typeof maybe.message === 'string' && maybe.message.trim().length > 0) {
      return maybe.message.trim();
    }
  }
  if (error instanceof Error && error.message.trim().length > 0) {
    return error.message.trim();
  }
  return String(error);
}

function parseServiceList(output: string): string[] {
  return output
    .split('\n')
    .map((line) => line.trim())
    .filter((line) => line.length > 0);
}

function unique(items: string[]): string[] {
  const seen = new Set<string>();
  const out: string[] = [];
  for (const item of items) {
    const key = item.trim();
    if (!key || seen.has(key)) {continue;}
    seen.add(key);
    out.push(key);
  }
  return out;
}

function buildProfileArgs(profiles: string[]): string[] {
  return unique(profiles).flatMap((profile) => ['--profile', profile]);
}

function withProfiles(profileArgs: string[], args: string[]): string[] {
  return [...profileArgs, ...args];
}

function parseComposePsOutput(output: string): ComposePsEntry[] {
  const trimmed = output.trim();
  if (!trimmed) {return [];}

  try {
    const parsed = JSON.parse(trimmed) as unknown;
    if (Array.isArray(parsed)) {
      return parsed.filter((entry): entry is ComposePsEntry => Boolean(entry && typeof entry === 'object'));
    }
    if (parsed && typeof parsed === 'object') {
      return [parsed as ComposePsEntry];
    }
  } catch {
    // Some compose versions emit newline-delimited JSON objects instead of an array.
  }

  const entries: ComposePsEntry[] = [];
  for (const line of trimmed.split('\n')) {
    const item = line.trim();
    if (!item) {continue;}
    try {
      const parsed = JSON.parse(item) as unknown;
      if (parsed && typeof parsed === 'object') {
        entries.push(parsed as ComposePsEntry);
      }
    } catch {
      // Ignore non-JSON lines.
    }
  }
  return entries;
}

function normalizeState(rawState: string | undefined): string {
  const state = String(rawState ?? '').trim().toLowerCase();
  if (!state) {return 'unknown';}
  if (state === 'running') {return 'running';}
  if (state === 'exited' || state === 'dead' || state === 'stopped') {return 'stopped';}
  if (state === 'created' || state === 'restarting' || state === 'paused') {return state;}
  return state;
}

function buildStatusText(state: string, health: string, statusField: string): string {
  if (statusField.trim().length > 0) {
    return statusField.trim();
  }
  if (state === 'running' && health && health !== 'none' && health !== 'unknown') {
    return `running (${health})`;
  }
  return state || 'unknown';
}

function computeAvailableActions(state: string): DockerDependencyAction[] {
  if (state === 'not-found' || state === 'unavailable') {
    return [];
  }
  if (state === 'running') {
    return ['restart', 'stop', 'update'];
  }
  if (state === 'stopped' || state === 'not-created' || state === 'created') {
    return ['start', 'restart', 'update'];
  }
  if (state === 'restarting' || state === 'paused') {
    return ['restart', 'stop', 'update'];
  }
  return ['start', 'restart', 'stop', 'update'];
}

function isLocalWhisperEndpoint(endpoint: string): boolean {
  let parsed: URL;
  try {
    parsed = new URL(endpoint);
  } catch {
    return false;
  }
  const isLocalHost = parsed.hostname === 'localhost'
    || parsed.hostname === '127.0.0.1'
    || parsed.hostname === '::1'
    || parsed.hostname === '0.0.0.0';
  if (!isLocalHost) {return false;}
  return parsed.pathname.includes('/audio/transcriptions');
}

function isWhisperConfigured(config: Config): boolean {
  if (!config.audio.enabled) {return false;}
  const endpoint = config.audio.provider?.endpoint;
  if (typeof endpoint !== 'string' || endpoint.trim().length === 0) {
    return false;
  }
  return isLocalWhisperEndpoint(endpoint);
}

function isBraveSearchConfigured(config: Config): boolean {
  if (config.web_search.provider !== 'brave') {return false;}
  const apiKey = config.web_search.api_key;
  return typeof apiKey === 'string' && apiKey.trim().length > 0;
}

function isSearxngConfigured(config: Config): boolean {
  if (config.web_search.provider !== 'searxng') {return false;}
  const endpoint = config.web_search.endpoint;
  return typeof endpoint === 'string' && endpoint.trim().length > 0;
}

function toDependencyId(service: string): DockerDependencyId {
  if (service === WHISPER_SERVICE) {
    return WHISPER_DEPENDENCY_ID;
  }
  return service;
}

function toDependencyName(service: string): string {
  if (service === WHISPER_SERVICE) {
    return 'Whisper (whisper.cpp)';
  }
  return service
    .split(/[-_]+/g)
    .filter(Boolean)
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(' ');
}

function describeDependency(service: string, config: Config): DockerDependencyDescriptor {
  if (service === WHISPER_SERVICE) {
    return {
      id: WHISPER_DEPENDENCY_ID,
      name: 'Whisper (whisper.cpp)',
      service,
      configured: isWhisperConfigured(config),
    };
  }
  if (service === BRAVE_SEARCH_SERVICE) {
    return {
      id: BRAVE_SEARCH_SERVICE,
      name: 'Brave Search',
      service,
      configured: isBraveSearchConfigured(config),
    };
  }
  if (service === SEARXNG_SERVICE) {
    return {
      id: SEARXNG_SERVICE,
      name: 'SearXNG',
      service,
      configured: isSearxngConfigured(config),
    };
  }
  return {
    id: toDependencyId(service),
    name: toDependencyName(service),
    service,
    configured: true,
  };
}

function unavailableStatus(error: unknown): DockerDependencyStatus {
  return {
    id: 'compose',
    name: 'Docker Compose',
    service: COMPOSE_FILE,
    configured: false,
    state: 'unavailable',
    health: 'none',
    statusText: 'unavailable',
    containerName: null,
    availableActions: [],
    error: normalizeError(error),
  };
}

async function discoverCompose(runner: DockerComposeRunner): Promise<ComposeDiscovery> {
  let profiles: string[] = [];
  try {
    const response = await runner(['config', '--profiles']);
    profiles = parseServiceList(response.stdout);
  } catch {
    profiles = [];
  }

  const profileArgs = buildProfileArgs(profiles);
  const response = await runner(withProfiles(profileArgs, ['config', '--services']));
  const configuredServices = parseServiceList(response.stdout)
    .filter((service) => service !== FLYNN_SERVICE);

  // Compose `ps --all` can surface running services even when profile discovery is unavailable.
  let runtimeServices: string[] = [];
  try {
    const psResponse = await runner(withProfiles(profileArgs, ['ps', '--all', '--format', 'json']));
    runtimeServices = parseComposePsOutput(psResponse.stdout)
      .map((entry) => String(entry.Service ?? '').trim())
      .filter((service) => service.length > 0 && service !== FLYNN_SERVICE);
  } catch {
    runtimeServices = [];
  }

  return {
    profileArgs,
    services: unique([...configuredServices, ...runtimeServices]),
  };
}

export async function listDockerDependencyStatuses(
  config: Config,
  runner: DockerComposeRunner = defaultRunner,
): Promise<DockerDependencyStatus[]> {
  let discovery: ComposeDiscovery;
  try {
    discovery = await discoverCompose(runner);
  } catch (error) {
    return [unavailableStatus(error)];
  }

  if (discovery.services.length === 0) {
    return [];
  }

  return Promise.all(discovery.services.map(async (service): Promise<DockerDependencyStatus> => {
    const descriptor = describeDependency(service, config);
    const baseStatus: DockerDependencyStatus = {
      id: descriptor.id,
      name: descriptor.name,
      service: descriptor.service,
      configured: descriptor.configured,
      state: 'unknown',
      health: 'unknown',
      statusText: 'unknown',
      containerName: null,
      availableActions: [],
    };

    try {
      const response = await runner(withProfiles(discovery.profileArgs, ['ps', service, '--format', 'json']));
      const rows = parseComposePsOutput(response.stdout)
        .filter((entry) => (entry.Service ?? '') === service || !entry.Service);

      if (rows.length === 0) {
        return {
          ...baseStatus,
          state: 'not-created',
          health: 'none',
          statusText: 'defined, not started',
          availableActions: computeAvailableActions('not-created'),
        };
      }

      const row = rows[0];
      const state = normalizeState(row.State);
      const health = String(row.Health ?? '').trim().toLowerCase() || 'none';
      const statusField = String(row.Status ?? '');
      return {
        ...baseStatus,
        state,
        health,
        statusText: buildStatusText(state, health, statusField),
        containerName: row.Name?.trim() || null,
        availableActions: computeAvailableActions(state),
      };
    } catch (error) {
      return {
        ...baseStatus,
        statusText: 'unknown',
        availableActions: computeAvailableActions('unknown'),
        error: normalizeError(error),
      };
    }
  }));
}

function ensureValidAction(action: string): asserts action is DockerDependencyAction {
  if (action !== 'start' && action !== 'restart' && action !== 'stop' && action !== 'update') {
    throw new Error(`Unsupported action: ${action}`);
  }
}

async function resolveDiscoveredDependency(
  config: Config,
  dependency: string,
  runner: DockerComposeRunner,
): Promise<{ discovery: ComposeDiscovery; descriptor: DockerDependencyDescriptor }> {
  const discovery = await discoverCompose(runner);
  const descriptors = discovery.services.map((service) => describeDependency(service, config));
  const descriptor = descriptors.find((entry) => entry.id === dependency || entry.service === dependency);
  if (!descriptor) {
    throw new Error(`Unsupported dependency: ${dependency}`);
  }
  return { discovery, descriptor };
}

export async function controlDockerDependency(
  config: Config,
  dependency: string,
  action: string,
  runner: DockerComposeRunner = defaultControlRunner,
): Promise<DockerDependencyControlResult> {
  ensureValidAction(action);
  const { discovery, descriptor } = await resolveDiscoveredDependency(config, dependency, runner);
  const service = descriptor.service;

  let message: string | undefined;
  if (action === 'start') {
    await runner(withProfiles(discovery.profileArgs, ['up', '-d', service]));
    message = `Started ${service} container.`;
  } else if (action === 'restart') {
    try {
      await runner(withProfiles(discovery.profileArgs, ['restart', service]));
      message = `Restarted ${service} container.`;
    } catch (error) {
      const detail = normalizeError(error).toLowerCase();
      if (detail.includes('no containers to restart')) {
        await runner(withProfiles(discovery.profileArgs, ['up', '-d', service]));
        message = `${service} container was not running; started it.`;
      } else {
        throw error;
      }
    }
  } else if (action === 'stop') {
    await runner(withProfiles(discovery.profileArgs, ['stop', service]));
    message = `Stopped ${service} container.`;
  } else if (action === 'update') {
    await runner(withProfiles(discovery.profileArgs, ['pull', service]));
    await runner(withProfiles(discovery.profileArgs, ['up', '-d', service]));
    message = `Pulled latest ${service} image and reconciled container.`;
  }

  const status = (await listDockerDependencyStatuses(config, runner))
    .find((entry) => entry.id === descriptor.id || entry.service === descriptor.service);
  if (!status) {
    throw new Error('Failed to load docker dependency status after action.');
  }

  return {
    dependency: descriptor.id,
    action,
    status,
    message,
  };
}
