/**
 * Gateway Manager
 *
 * Manages a single shared Caddy reverse proxy (the "gateway") that routes
 * subdomain-based requests to per-preview compose stacks on a shared Docker network.
 *
 * Architecture:
 *   .cw-previews/gateway/
 *     docker-compose.yml   ← single Caddy container
 *     Caddyfile            ← regenerated on each preview add/remove
 *
 * Caddy runs with `--watch` so it auto-reloads when the Caddyfile changes on disk.
 */

import { join } from 'node:path';
import { mkdir, writeFile, rm } from 'node:fs/promises';
import yaml from 'js-yaml';
import { GATEWAY_PROJECT_NAME, GATEWAY_NETWORK } from './types.js';
import {
  ensureDockerNetwork,
  removeDockerNetwork,
  composeUp,
  composeDown,
  composePs,
} from './docker-client.js';
import { allocatePort } from './port-allocator.js';
import { createModuleLogger } from '../logger/index.js';

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

/** Directory for preview deployment artifacts (relative to workspace root) */
const PREVIEWS_DIR = '.cw-previews';

/**
 * A route entry for the gateway Caddyfile.
 */
export interface GatewayRoute {
  containerName: string;
  port: number;
  route: string;
}

export class GatewayManager {
  private readonly workspaceRoot: string;
  private cachedPort: number | null = null;

  constructor(workspaceRoot: string) {
    this.workspaceRoot = workspaceRoot;
  }

  /**
   * Ensure the gateway is running. Idempotent — returns the port if already up.
   *
   * 1. Create the shared Docker network
   * 2. Check if gateway already running → return existing port
   * 3. Allocate a port, write compose + empty Caddyfile, start
   */
  async ensureGateway(): Promise<number> {
    await ensureDockerNetwork(GATEWAY_NETWORK);

    // Check if already running
    const existingPort = await this.getPort();
    if (existingPort !== null) {
      this.cachedPort = existingPort;
      log.info({ port: existingPort }, 'gateway already running');
      return existingPort;
    }

    // Allocate a port for the gateway
    const port = await allocatePort();
    this.cachedPort = port;

    // Write gateway compose + empty Caddyfile
    const gatewayDir = join(this.workspaceRoot, PREVIEWS_DIR, 'gateway');
    await mkdir(gatewayDir, { recursive: true });

    const composeContent = this.generateGatewayCompose(port);
    await writeFile(join(gatewayDir, 'docker-compose.yml'), composeContent, 'utf-8');

    // Start with an empty Caddyfile — will be populated by updateRoutes()
    const emptyCaddyfile = '{\n  auto_https off\n}\n';
    await writeFile(join(gatewayDir, 'Caddyfile'), emptyCaddyfile, 'utf-8');

    const composePath = join(gatewayDir, 'docker-compose.yml');
    await composeUp(composePath, GATEWAY_PROJECT_NAME);

    log.info({ port }, 'gateway started');
    return port;
  }

  /**
   * Regenerate the Caddyfile from all active previews.
   * Caddy's `--watch` flag picks up the file change automatically.
   */
  async updateRoutes(previews: Map<string, GatewayRoute[]>): Promise<void> {
    const port = this.cachedPort ?? (await this.getPort());
    if (port === null) {
      log.warn('cannot update routes — gateway not running');
      return;
    }

    const caddyfile = generateGatewayCaddyfile(previews, port);
    const caddyfilePath = join(this.workspaceRoot, PREVIEWS_DIR, 'gateway', 'Caddyfile');
    await writeFile(caddyfilePath, caddyfile, 'utf-8');

    log.info({ previewCount: previews.size }, 'gateway routes updated');
  }

  /**
   * Stop the gateway and remove the shared network.
   */
  async stopGateway(): Promise<void> {
    await composeDown(GATEWAY_PROJECT_NAME).catch(() => {});
    await removeDockerNetwork(GATEWAY_NETWORK);

    const gatewayDir = join(this.workspaceRoot, PREVIEWS_DIR, 'gateway');
    await rm(gatewayDir, { recursive: true, force: true }).catch(() => {});

    this.cachedPort = null;
    log.info('gateway stopped');
  }

  /**
   * Check if the gateway compose project is running.
   */
  async isRunning(): Promise<boolean> {
    const services = await composePs(GATEWAY_PROJECT_NAME);
    return services.some((s) => s.state === 'running');
  }

  /**
   * Read the gateway port from the running container's labels.
   * Returns null if the gateway isn't running.
   */
  async getPort(): Promise<number | null> {
    if (this.cachedPort !== null) {
      // Verify the container is still running
      if (await this.isRunning()) {
        return this.cachedPort;
      }
      this.cachedPort = null;
    }

    try {
      const { execa } = await import('execa');
      const result = await execa('docker', [
        'ps',
        '--filter', `label=cw.gateway=true`,
        '--format', `{{.Label "cw.gateway-port"}}`,
      ], { timeout: 15000 });

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

      const port = parseInt(result.stdout.trim().split('\n')[0], 10);
      if (isNaN(port)) return null;

      this.cachedPort = port;
      return port;
    } catch {
      return null;
    }
  }

  /**
   * Generate the gateway docker-compose.yml content.
   */
  private generateGatewayCompose(port: number): string {
    const compose = {
      services: {
        caddy: {
          image: 'caddy:2-alpine',
          command: ['caddy', 'run', '--config', '/etc/caddy/Caddyfile', '--adapter', 'caddyfile', '--watch'],
          ports: [`${port}:80`],
          volumes: ['./Caddyfile:/etc/caddy/Caddyfile:rw'],
          networks: [GATEWAY_NETWORK],
          labels: {
            'cw.gateway': 'true',
            'cw.gateway-port': String(port),
          },
        },
      },
      networks: {
        [GATEWAY_NETWORK]: {
          external: true,
        },
      },
    };

    return yaml.dump(compose, { lineWidth: 120, noRefs: true });
  }
}

/**
 * Generate a Caddyfile for the gateway from all active preview routes.
 *
 * Uses subdomain-based routing: each preview gets its own `<previewId>.localhost:80` block.
 * Chrome/Firefox resolve `*.localhost` to 127.0.0.1 natively — no DNS setup needed.
 * Routes within a preview are sorted by specificity (longest path first).
 */
export function generateGatewayCaddyfile(
  previews: Map<string, GatewayRoute[]>,
  _port: number,
): string {
  // Caddy runs inside a container where Docker maps host:${port} → container:80.
  // The Caddyfile must listen on the container-internal port (80), not the host port.
  const lines: string[] = [
    '{',
    '  auto_https off',
    '}',
  ];

  for (const [previewId, routes] of previews) {
    // Sort routes by specificity (longer paths first, root last)
    const sorted = [...routes].sort((a, b) => {
      if (a.route === '/') return 1;
      if (b.route === '/') return -1;
      return b.route.length - a.route.length;
    });

    lines.push('');
    lines.push(`${previewId}.localhost:80 {`);

    for (const route of sorted) {
      if (route.route === '/') {
        lines.push(`  handle /* {`);
        lines.push(`    reverse_proxy ${route.containerName}:${route.port}`);
        lines.push(`  }`);
      } else {
        const path = route.route.endsWith('/') ? route.route.slice(0, -1) : route.route;
        lines.push(`  handle_path ${path}/* {`);
        lines.push(`    reverse_proxy ${route.containerName}:${route.port}`);
        lines.push(`  }`);
      }
    }

    lines.push('}');
  }

  lines.push('');

  return lines.join('\n');
}
