/**
 * Preview Config Reader
 *
 * Discovers and parses preview configuration from a project directory.
 * Discovery order: .cw-preview.yml → docker-compose.yml/compose.yml → Dockerfile
 */

import { readFile, access } from 'node:fs/promises';
import { join } from 'node:path';
import yaml from 'js-yaml';
import type { PreviewConfig, PreviewServiceConfig } from './types.js';
import { createModuleLogger } from '../logger/index.js';

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

/** Files to check for existing Docker Compose config */
const COMPOSE_FILES = [
  'docker-compose.yml',
  'docker-compose.yaml',
  'compose.yml',
  'compose.yaml',
];

/**
 * Discover and parse preview configuration from a project directory.
 *
 * Discovery order:
 * 1. `.cw-preview.yml` — explicit CW preview config
 * 2. `docker-compose.yml` / `compose.yml` (+ variants) — wrap existing compose
 * 3. `Dockerfile` at root — single-service fallback (assumes port 3000)
 *
 * @param projectPath - Absolute path to the project directory (at the target branch)
 * @returns Parsed and normalized PreviewConfig
 * @throws If no config can be discovered
 */
export async function discoverConfig(projectPath: string): Promise<PreviewConfig> {
  // 1. Check for explicit .cw-preview.yml
  const cwPreviewPath = join(projectPath, '.cw-preview.yml');
  if (await fileExists(cwPreviewPath)) {
    log.info({ path: cwPreviewPath }, 'found .cw-preview.yml');
    const raw = await readFile(cwPreviewPath, 'utf-8');
    return parseCwPreviewConfig(raw);
  }

  // 2. Check for existing compose files
  for (const composeFile of COMPOSE_FILES) {
    const composePath = join(projectPath, composeFile);
    if (await fileExists(composePath)) {
      log.info({ path: composePath }, 'found existing compose file');
      return parseExistingCompose(composePath, composeFile);
    }
  }

  // 3. Check for Dockerfile
  const dockerfilePath = join(projectPath, 'Dockerfile');
  if (await fileExists(dockerfilePath)) {
    log.info({ path: dockerfilePath }, 'found Dockerfile, using single-service fallback');
    return createDockerfileFallback();
  }

  throw new Error(
    `No preview configuration found in ${projectPath}. ` +
    `Expected one of: .cw-preview.yml, docker-compose.yml, compose.yml, or Dockerfile`
  );
}

/**
 * Parse a `.cw-preview.yml` file into a PreviewConfig.
 */
export function parseCwPreviewConfig(raw: string): PreviewConfig {
  const parsed = yaml.load(raw) as Record<string, unknown>;

  if (!parsed || typeof parsed !== 'object') {
    throw new Error('Invalid .cw-preview.yml: expected a YAML object');
  }

  if (!parsed.services || typeof parsed.services !== 'object') {
    throw new Error('Invalid .cw-preview.yml: missing "services" key');
  }

  const services: Record<string, PreviewServiceConfig> = {};
  const rawServices = parsed.services as Record<string, Record<string, unknown>>;

  for (const [name, svc] of Object.entries(rawServices)) {
    if (!svc || typeof svc !== 'object') {
      throw new Error(`Invalid service "${name}": expected an object`);
    }

    const port = svc.port as number | undefined;
    if (port === undefined && !svc.internal) {
      throw new Error(`Service "${name}" must specify a "port" (or be marked "internal: true")`);
    }

    services[name] = {
      name,
      port: port ?? 0,
      ...(svc.build !== undefined && { build: normalizeBuild(svc.build) }),
      ...(svc.image !== undefined && { image: svc.image as string }),
      ...(svc.route !== undefined && { route: svc.route as string }),
      ...(svc.internal !== undefined && { internal: svc.internal as boolean }),
      ...(svc.healthcheck !== undefined && { healthcheck: svc.healthcheck as PreviewServiceConfig['healthcheck'] }),
      ...(svc.env !== undefined && { env: svc.env as Record<string, string> }),
      ...(svc.volumes !== undefined && { volumes: svc.volumes as string[] }),
      ...(Array.isArray(svc.seed) && { seed: svc.seed as string[] }),
      ...(svc.dev !== undefined && {
        dev: {
          image: (svc.dev as Record<string, unknown>).image as string,
          ...(typeof (svc.dev as Record<string, unknown>).command === 'string' && {
            command: (svc.dev as Record<string, unknown>).command as string,
          }),
          ...(typeof (svc.dev as Record<string, unknown>).workdir === 'string' && {
            workdir: (svc.dev as Record<string, unknown>).workdir as string,
          }),
        },
      }),
    };
  }

  return {
    version: 1,
    services,
  };
}

/**
 * Wrap an existing Docker Compose file as a passthrough config.
 */
function parseExistingCompose(composePath: string, composeFile: string): PreviewConfig {
  return {
    version: 1,
    compose: composeFile,
    services: {},
  };
}

/**
 * Create a single-service fallback config from a Dockerfile.
 */
function createDockerfileFallback(): PreviewConfig {
  return {
    version: 1,
    services: {
      app: {
        name: 'app',
        build: '.',
        port: 3000,
      },
    },
  };
}

/**
 * Normalize build config to a consistent format.
 */
function normalizeBuild(build: unknown): PreviewServiceConfig['build'] {
  if (typeof build === 'string') {
    return build;
  }
  if (typeof build === 'object' && build !== null) {
    const b = build as Record<string, unknown>;
    return {
      context: (b.context as string) ?? '.',
      dockerfile: (b.dockerfile as string) ?? 'Dockerfile',
    };
  }
  return '.';
}

async function fileExists(path: string): Promise<boolean> {
  try {
    await access(path);
    return true;
  } catch {
    return false;
  }
}
