import { readFileSync } from 'fs';
import { parse } from 'yaml';
import { configSchema, type Config } from './schema.js';

function expandEnvVars(value: string): string {
  return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
    const envValue = process.env[envVar];
    if (envValue === undefined) {
      throw new Error(`Environment variable ${envVar} is not set`);
    }
    return envValue;
  });
}

function expandEnvVarsInObject(obj: unknown): unknown {
  if (typeof obj === 'string') {
    return expandEnvVars(obj);
  }
  if (Array.isArray(obj)) {
    return obj.map(expandEnvVarsInObject);
  }
  if (obj !== null && typeof obj === 'object') {
    const result: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(obj)) {
      result[key] = expandEnvVarsInObject(value);
    }
    return result;
  }
  return obj;
}

/**
 * Deep merge two plain objects. Overlay values override base values.
 * Arrays in overlay replace base arrays (not concatenated).
 * null in overlay explicitly sets key to null.
 * Only plain objects recurse; arrays, primitives, and null are overwritten.
 */
export function deepMerge(
  base: Record<string, unknown>,
  overlay: Record<string, unknown>,
): Record<string, unknown> {
  const result = { ...base };
  for (const [key, value] of Object.entries(overlay)) {
    if (
      value !== null &&
      typeof value === 'object' &&
      !Array.isArray(value) &&
      typeof result[key] === 'object' &&
      result[key] !== null &&
      !Array.isArray(result[key])
    ) {
      result[key] = deepMerge(
        result[key] as Record<string, unknown>,
        value as Record<string, unknown>,
      );
    } else {
      result[key] = value;
    }
  }
  return result;
}

/**
 * Load and validate a Flynn config from YAML file(s).
 * If overlayPath is provided, the overlay YAML is deep-merged on top of
 * the base config BEFORE env var expansion and Zod validation.
 */
export function loadConfig(configPath: string, overlayPath?: string): Config {
  const rawContent = readFileSync(configPath, 'utf-8');
  let rawConfig = parse(rawContent);

  if (overlayPath) {
    const overlayContent = readFileSync(overlayPath, 'utf-8');
    const overlayConfig = parse(overlayContent);
    if (overlayConfig && typeof overlayConfig === 'object') {
      rawConfig = deepMerge(
        rawConfig as Record<string, unknown>,
        overlayConfig as Record<string, unknown>,
      );
    }
  }

  const expandedConfig = expandEnvVarsInObject(rawConfig);
  const config = configSchema.parse(expandedConfig);

  // PaaS convention: if PORT is set, bind the gateway to it.
  // This is intentionally an override (even if config.server.port is set)
  // to make deployments on Fly/Railway/Render/Heroku-style platforms work.
  const envPort = process.env.PORT;
  if (envPort && /^\d+$/.test(envPort)) {
    const port = Number(envPort);
    if (Number.isFinite(port) && port > 0 && port <= 65535) {
      config.server.port = port;
    }
  }

  return config;
}
