/**
 * Caddy Manager for Onebox
 *
 * Manages Caddy as a Docker Swarm service instead of a host binary.
 * This allows Caddy to access services on the Docker overlay network.
 */

import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';

const CADDY_SERVICE_NAME = 'onebox-caddy';
const CADDY_IMAGE = 'caddy:2-alpine';
const DOCKER_GATEWAY_IP = '172.17.0.1'; // Docker bridge gateway for container-to-host communication

export interface ICaddyRoute {
  domain: string;
  upstream: string; // e.g., "onebox-hello-world:80"
}

export interface ICaddyCertificate {
  domain: string;
  certPem: string;
  keyPem: string;
}

interface ICaddyLoggingConfig {
  logs: {
    [name: string]: {
      writer: {
        output: string;
        address?: string;
        dial_timeout?: string;
        soft_start?: boolean;
      };
      encoder?: { format: string };
      level?: string;
      include?: string[];
    };
  };
}

interface ICaddyConfig {
  admin: {
    listen: string;
  };
  logging?: ICaddyLoggingConfig;
  apps: {
    http: {
      servers: {
        [key: string]: {
          listen: string[];
          routes: ICaddyRouteConfig[];
          automatic_https?: {
            disable?: boolean;
            disable_redirects?: boolean;
          };
          logs?: {
            default_logger_name: string;
          };
        };
      };
    };
    tls?: {
      automation?: {
        policies: Array<{ issuers: never[] }>;
      };
      certificates?: {
        load_pem?: Array<{
          certificate: string;
          key: string;
          tags?: string[];
        }>;
      };
    };
  };
}

interface ICaddyRouteConfig {
  match: Array<{ host: string[] }>;
  handle: Array<{
    handler: string;
    upstreams?: Array<{ dial: string }>;
    routes?: ICaddyRouteConfig[];
  }>;
  terminal?: boolean;
}

export class CaddyManager {
  private dockerClient: InstanceType<typeof plugins.docker.Docker> | null = null;
  private certsDir: string;
  private adminUrl: string;
  private httpPort: number;
  private httpsPort: number;
  private logReceiverPort: number;
  private loggingEnabled: boolean;
  private routes: Map<string, ICaddyRoute> = new Map();
  private certificates: Map<string, ICaddyCertificate> = new Map();
  private networkName = 'onebox-network';
  private serviceRunning = false;

  constructor(options?: {
    certsDir?: string;
    adminPort?: number;
    httpPort?: number;
    httpsPort?: number;
    logReceiverPort?: number;
    loggingEnabled?: boolean;
  }) {
    this.certsDir = options?.certsDir || './.nogit/certs';
    this.adminUrl = `http://localhost:${options?.adminPort || 2019}`;
    this.httpPort = options?.httpPort || 8080;
    this.httpsPort = options?.httpsPort || 8443;
    this.logReceiverPort = options?.logReceiverPort || 9999;
    this.loggingEnabled = options?.loggingEnabled ?? true;
  }

  /**
   * Initialize Docker client for Caddy service management
   */
  private async ensureDockerClient(): Promise<void> {
    if (!this.dockerClient) {
      this.dockerClient = new plugins.docker.Docker({
        socketPath: 'unix:///var/run/docker.sock',
      });
      await this.dockerClient.start();
    }
  }

  /**
   * Update listening ports (must call reloadConfig after if running)
   */
  setPorts(httpPort: number, httpsPort: number): void {
    this.httpPort = httpPort;
    this.httpsPort = httpsPort;
  }

  /**
   * Start Caddy as a Docker Swarm service
   */
  async start(): Promise<void> {
    if (this.serviceRunning) {
      logger.warn('Caddy service is already running');
      return;
    }

    try {
      await this.ensureDockerClient();

      // Create certs directory for backup/persistence
      await Deno.mkdir(this.certsDir, { recursive: true });

      logger.info('Starting Caddy Docker service...');

      // Check if service already exists
      const existingService = await this.getExistingService();
      if (existingService) {
        logger.info('Caddy service exists, removing old service...');
        await this.removeService();
        // Wait for service to be removed
        await new Promise((resolve) => setTimeout(resolve, 2000));
      }

      // Get network ID
      const networkId = await this.getNetworkId();

      // Create Caddy Docker service
      const response = await this.dockerClient!.request('POST', '/services/create', {
        Name: CADDY_SERVICE_NAME,
        Labels: {
          'managed-by': 'onebox',
          'onebox-type': 'caddy',
        },
        TaskTemplate: {
          ContainerSpec: {
            Image: CADDY_IMAGE,
            // Start Caddy with admin listening on all interfaces so we can reach it from host
            // Write minimal config to /tmp and start Caddy with that config
            Command: ['sh', '-c', 'printf \'{"admin":{"listen":"0.0.0.0:2019"}}\' > /tmp/caddy.json && caddy run --config /tmp/caddy.json'],
          },
          Networks: [
            {
              Target: networkId,
            },
          ],
          RestartPolicy: {
            Condition: 'any',
            MaxAttempts: 0,
          },
        },
        Mode: {
          Replicated: {
            Replicas: 1,
          },
        },
        EndpointSpec: {
          Ports: [
            {
              Protocol: 'tcp',
              TargetPort: 80,
              PublishedPort: this.httpPort,
              PublishMode: 'host',
            },
            {
              Protocol: 'tcp',
              TargetPort: 443,
              PublishedPort: this.httpsPort,
              PublishMode: 'host',
            },
            {
              Protocol: 'tcp',
              TargetPort: 2019,
              PublishedPort: 2019,
              PublishMode: 'host',
            },
          ],
        },
      });

      if (response.statusCode >= 300) {
        throw new Error(`Failed to create Caddy service: HTTP ${response.statusCode} - ${JSON.stringify(response.body)}`);
      }

      logger.info(`Caddy service created: ${response.body.ID}`);

      // Wait for Admin API to be ready
      await this.waitForReady();

      this.serviceRunning = true;

      // Now configure via Admin API with current routes and certificates
      await this.reloadConfig();

      logger.success(`Caddy started (HTTP: ${this.httpPort}, HTTPS: ${this.httpsPort}, Admin: ${this.adminUrl})`);
    } catch (error) {
      logger.error(`Failed to start Caddy: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Get existing Caddy service if any
   */
  private async getExistingService(): Promise<any | null> {
    try {
      const response = await this.dockerClient!.request('GET', `/services/${CADDY_SERVICE_NAME}`, {});
      if (response.statusCode === 200) {
        return response.body;
      }
      return null;
    } catch {
      return null;
    }
  }

  /**
   * Remove the Caddy service
   */
  private async removeService(): Promise<void> {
    try {
      await this.dockerClient!.request('DELETE', `/services/${CADDY_SERVICE_NAME}`, {});
    } catch {
      // Service may not exist
    }
  }

  /**
   * Get network ID by name
   */
  private async getNetworkId(): Promise<string> {
    const networks = await this.dockerClient!.listNetworks();
    const network = networks.find((n: any) => n.Name === this.networkName);
    if (!network) {
      throw new Error(`Network not found: ${this.networkName}`);
    }
    return network.Id;
  }

  /**
   * Wait for Caddy Admin API to be ready
   */
  private async waitForReady(maxAttempts = 60, intervalMs = 500): Promise<void> {
    for (let i = 0; i < maxAttempts; i++) {
      try {
        const response = await fetch(`${this.adminUrl}/config/`);
        if (response.ok) {
          return;
        }
      } catch {
        // Not ready yet
      }
      await new Promise((resolve) => setTimeout(resolve, intervalMs));
    }
    throw new Error('Caddy service failed to start within timeout');
  }

  /**
   * Stop Caddy Docker service
   */
  async stop(): Promise<void> {
    if (!this.serviceRunning && !(await this.getExistingService())) {
      return;
    }

    try {
      await this.ensureDockerClient();

      logger.info('Stopping Caddy service...');

      await this.removeService();

      this.serviceRunning = false;
      logger.info('Caddy service stopped');
    } catch (error) {
      logger.error(`Failed to stop Caddy: ${getErrorMessage(error)}`);
    }
  }

  /**
   * Check if Caddy Admin API is healthy
   */
  async isHealthy(): Promise<boolean> {
    try {
      const response = await fetch(`${this.adminUrl}/config/`);
      return response.ok;
    } catch {
      return false;
    }
  }

  /**
   * Check if Caddy service is running
   */
  async isRunning(): Promise<boolean> {
    try {
      await this.ensureDockerClient();
      const service = await this.getExistingService();
      if (!service) return false;

      // Check if service has running tasks
      const tasksResponse = await this.dockerClient!.request(
        'GET',
        `/tasks?filters=${encodeURIComponent(JSON.stringify({ service: [CADDY_SERVICE_NAME] }))}`,
        {}
      );

      if (tasksResponse.statusCode !== 200) return false;

      const tasks = tasksResponse.body;
      return tasks.some((task: any) => task.Status?.State === 'running');
    } catch {
      return false;
    }
  }

  /**
   * Build Caddy JSON configuration from current routes and certificates
   */
  private buildConfig(): ICaddyConfig {
    const routes: ICaddyRouteConfig[] = [];

    // Add routes
    for (const [domain, route] of this.routes) {
      routes.push({
        match: [{ host: [domain] }],
        handle: [
          {
            handler: 'reverse_proxy',
            upstreams: [{ dial: route.upstream }],
          },
        ],
        terminal: true,
      });
    }

    // Build certificate load_pem entries (inline PEM content)
    const loadPem: Array<{ certificate: string; key: string; tags?: string[] }> = [];
    for (const [domain, cert] of this.certificates) {
      loadPem.push({
        certificate: cert.certPem,
        key: cert.keyPem,
        tags: [domain],
      });
    }

    const config: ICaddyConfig = {
      admin: {
        listen: '0.0.0.0:2019', // Listen on all interfaces inside container
      },
      apps: {
        http: {
          servers: {
            main: {
              listen: [':80', ':443'],
              routes,
              // Disable automatic HTTPS to prevent Caddy from trying to obtain certs
              automatic_https: {
                disable: true,
              },
            },
          },
        },
      },
    };

    // Add access logging configuration if enabled
    if (this.loggingEnabled) {
      config.logging = {
        logs: {
          access: {
            writer: {
              output: 'net',
              // Use Docker bridge gateway IP to reach log receiver on host
              address: `tcp/${DOCKER_GATEWAY_IP}:${this.logReceiverPort}`,
              dial_timeout: '5s',
              soft_start: true, // Continue even if log receiver is down
            },
            encoder: { format: 'json' },
            level: 'INFO',
            include: ['http.log.access'],
          },
        },
      };

      // Associate server with access logger
      config.apps.http.servers.main.logs = {
        default_logger_name: 'access',
      };
    }

    // Add TLS config if we have certificates
    if (loadPem.length > 0) {
      config.apps.tls = {
        automation: {
          // Disable automatic HTTPS - we manage certs ourselves
          policies: [{ issuers: [] }],
        },
        certificates: {
          load_pem: loadPem,
        },
      };
    }

    return config;
  }

  /**
   * Reload Caddy configuration via Admin API
   */
  async reloadConfig(): Promise<void> {
    const isRunning = await this.isRunning();
    if (!isRunning) {
      logger.warn('Caddy not running, cannot reload config');
      return;
    }

    const config = this.buildConfig();

    try {
      const response = await fetch(`${this.adminUrl}/load`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(config),
      });

      if (!response.ok) {
        const text = await response.text();
        throw new Error(`Failed to reload Caddy config: ${response.status} ${text}`);
      }

      logger.debug('Caddy configuration reloaded');
    } catch (error) {
      logger.error(`Failed to reload Caddy config: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Add or update a route
   */
  async addRoute(domain: string, upstream: string): Promise<void> {
    this.routes.set(domain, { domain, upstream });

    if (await this.isRunning()) {
      await this.reloadConfig();
    }

    logger.success(`Added Caddy route: ${domain} -> ${upstream}`);
  }

  /**
   * Remove a route
   */
  async removeRoute(domain: string): Promise<void> {
    if (this.routes.delete(domain)) {
      if (await this.isRunning()) {
        await this.reloadConfig();
      }
      logger.success(`Removed Caddy route: ${domain}`);
    }
  }

  /**
   * Add or update a TLS certificate
   * Stores PEM content in memory for Admin API, also writes to disk for backup
   */
  async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> {
    // Store PEM content in memory for buildConfig()
    this.certificates.set(domain, {
      domain,
      certPem,
      keyPem,
    });

    // Also write to disk for backup/persistence
    try {
      await Deno.mkdir(this.certsDir, { recursive: true });
      await Deno.writeTextFile(`${this.certsDir}/${domain}.crt`, certPem);
      await Deno.writeTextFile(`${this.certsDir}/${domain}.key`, keyPem);
    } catch (error) {
      logger.warn(`Failed to write certificate backup for ${domain}: ${getErrorMessage(error)}`);
    }

    if (await this.isRunning()) {
      await this.reloadConfig();
    }

    logger.success(`Added TLS certificate for ${domain}`);
  }

  /**
   * Remove a TLS certificate
   */
  async removeCertificate(domain: string): Promise<void> {
    if (this.certificates.delete(domain)) {
      // Remove backup files
      try {
        await Deno.remove(`${this.certsDir}/${domain}.crt`);
        await Deno.remove(`${this.certsDir}/${domain}.key`);
      } catch {
        // Files may not exist
      }

      if (await this.isRunning()) {
        await this.reloadConfig();
      }

      logger.success(`Removed TLS certificate for ${domain}`);
    }
  }

  /**
   * Get all current routes
   */
  getRoutes(): ICaddyRoute[] {
    return Array.from(this.routes.values());
  }

  /**
   * Get all current certificates
   */
  getCertificates(): ICaddyCertificate[] {
    return Array.from(this.certificates.values());
  }

  /**
   * Clear all routes and certificates (useful for reload from database)
   */
  clear(): void {
    this.routes.clear();
    this.certificates.clear();
  }

  /**
   * Get status
   */
  getStatus(): {
    running: boolean;
    httpPort: number;
    httpsPort: number;
    routes: number;
    certificates: number;
  } {
    return {
      running: this.serviceRunning,
      httpPort: this.httpPort,
      httpsPort: this.httpsPort,
      routes: this.routes.size,
      certificates: this.certificates.size,
    };
  }
}
