/**
 * Reverse Proxy for Onebox
 *
 * Delegates to CoreTraffic (running as Docker service) for production-grade reverse proxy
 * with TLS termination, WebSocket proxying, and zero-downtime configuration updates.
 *
 * Routes use Docker service names (e.g., onebox-hello-world:80) for container-to-container
 * communication within the Docker overlay network.
 */

import { logger } from '../logging.js';
import { getErrorMessage } from '../utils/error.js';
import { normalizeHostname } from '../utils/domain.js';
import * as plugins from '../plugins.js';
import { OneboxDatabase } from '../database/index.js';
import {
  CoreTrafficManager,
  type ICoreTrafficPublicPortRoute,
  type ICoreTrafficStatusObservation,
} from './coretraffic.js';
import type { IService } from '../types.js';

const adminUiRouteName = 'onebox-admin-ui';
const adminUiPort = 3000;

interface IProxyRoute {
  domain: string;
  targetHost: string;
  targetPort: number;
  serviceId?: number;
  serviceName?: string;
  routeType: 'service' | 'admin-ui';
}

export class OneboxReverseProxy {
  private oneboxRef: any;
  private database: OneboxDatabase;
  private coreTraffic: CoreTrafficManager;
  private routes: Map<string, IProxyRoute> = new Map();
  private httpPort = 8080;  // Default to dev ports (will be overridden if production)
  private httpsPort = 8443;

  constructor(oneboxRef: any) {
    this.oneboxRef = oneboxRef;
    this.database = oneboxRef.database;
    this.coreTraffic = new CoreTrafficManager({
      httpPort: this.httpPort,
      httpsPort: this.httpsPort,
      getAdminToken: async () => await this.ensureCoreTrafficAdminToken(),
    });
  }

  private async ensureCoreTrafficAdminToken(): Promise<string> {
    const existingToken = await this.database.getSecretSetting('coretrafficAdminToken');
    if (existingToken) {
      return existingToken;
    }

    const token = `obct_${globalThis.crypto.randomUUID().replaceAll('-', '')}${globalThis.crypto.randomUUID().replaceAll('-', '')}`;
    await this.database.setSecretSetting('coretrafficAdminToken', token);
    return token;
  }

  /**
   * Initialize reverse proxy - CoreTraffic runs as Docker service, no setup needed
   */
  async init(): Promise<void> {
    logger.info('Reverse proxy initialized (CoreTraffic Docker service)');
  }

  /**
   * Start the HTTP/HTTPS reverse proxy server
   * CoreTraffic handles both HTTP and HTTPS on the configured ports
   */
  async startHttp(port?: number): Promise<void> {
    if (port) {
      this.httpPort = port;
      this.coreTraffic.setPorts(this.httpPort, this.httpsPort);
    }

    try {
      await this.reloadPublicPortRoutes();
      await this.coreTraffic.start();
      logger.success(`Reverse proxy started on port ${this.httpPort} (CoreTraffic Docker service)`);
    } catch (error) {
      logger.error(`Failed to start reverse proxy: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Start HTTPS - CoreTraffic already handles HTTPS when started
   * This method exists for interface compatibility
   */
  async startHttps(port?: number): Promise<void> {
    if (port) {
      this.httpsPort = port;
      this.coreTraffic.setPorts(this.httpPort, this.httpsPort);
    }
    const status = this.coreTraffic.getStatus();
    if (status.running) {
      logger.info(`HTTPS already running on port ${this.httpsPort} via CoreTraffic`);
    } else {
      logger.warn('Skipping HTTPS reverse proxy startup because CoreTraffic is not running');
    }
  }

  /**
   * Stop the reverse proxy
   */
  async stop(): Promise<void> {
    await this.coreTraffic.stop();
    logger.info('Reverse proxy stopped');
  }

  /**
   * Add a route for a service
   * Uses Docker service name for upstream (CoreTraffic runs in same Docker network)
   */
  async addRoute(serviceId: number, domain: string, targetPort: number): Promise<void> {
    try {
      // Get service info from database
      const service = await this.database.getServiceByID(serviceId);
      if (!service) {
        throw new Error(`Service not found: ${serviceId}`);
      }

      // Use Docker service name as upstream target
      // CoreTraffic runs on the same Docker network, so it can resolve service names directly
      const serviceName = `onebox-${service.name}`;
      const targetHost = serviceName;

      const route: IProxyRoute = {
        domain,
        targetHost,
        targetPort,
        serviceId,
        serviceName,
        routeType: 'service',
      };

      this.routes.set(domain, route);

      // Add route to CoreTraffic using Docker service name
      const upstream = `${targetHost}:${targetPort}`;
      await this.coreTraffic.addRoute(domain, upstream);

      logger.success(`Added proxy route: ${domain} -> ${upstream}`);
    } catch (error) {
      logger.error(`Failed to add route for ${domain}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  async addAdminUiRoute(domain: string): Promise<void> {
    const normalizedDomain = normalizeHostname(domain);
    if (!normalizedDomain) return;

    const targetHost = await this.getAdminUiTargetHost();
    const route: IProxyRoute = {
      domain: normalizedDomain,
      targetHost,
      targetPort: adminUiPort,
      serviceName: adminUiRouteName,
      routeType: 'admin-ui',
    };

    this.routes.set(normalizedDomain, route);
    const upstream = `${targetHost}:${adminUiPort}`;
    await this.coreTraffic.addRoute(normalizedDomain, upstream);
    logger.success(`Added Admin UI proxy route: ${normalizedDomain} -> ${upstream}`);
  }

  /**
   * Remove a route
   */
  async removeRoute(domain: string): Promise<void> {
    if (this.routes.delete(domain)) {
      await this.coreTraffic.removeRoute(domain);
      logger.success(`Removed proxy route: ${domain}`);
    } else {
      logger.warn(`Route not found: ${domain}`);
    }
  }

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

  /**
   * Reload routes from database
   */
  async reloadRoutes(): Promise<void> {
    try {
      logger.info('Reloading proxy routes...');

      // Clear local and CoreTraffic routes
      this.routes.clear();
      this.coreTraffic.clear();

      const services = await this.database.getAllServices();

      for (const service of services) {
        // Route by domain if running (containerID is the service ID for Swarm services)
        if (service.status === 'running' && service.containerID) {
          for (const domain of this.getServiceDomainNames(service)) {
            await this.addRoute(service.id!, domain, this.resolveServiceRoutePort(service, domain));
          }
        }
      }

      const adminUiDomain = await this.getAdminUiDomain();
      if (adminUiDomain) {
        await this.addAdminUiRoute(adminUiDomain);
      }

      await this.reloadPublicPortRoutes();

      logger.success(`Loaded ${this.routes.size} proxy routes`);
    } catch (error) {
      logger.error(`Failed to reload routes: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Reapply the desired runtime config without rebuilding it from the database.
   * This rehydrates CoreTraffic after Docker replaces its service task.
   */
  async reapplyRuntimeConfig(): Promise<void> {
    await this.coreTraffic.reloadConfig();
  }

  async reloadPublicPortRoutes(optionsArg: { excludeServiceId?: number } = {}): Promise<void> {
    const services = await this.database.getAllServices();
    const routes = services
      .filter((serviceArg) => serviceArg.id !== optionsArg.excludeServiceId)
      .filter((serviceArg) => serviceArg.status === 'running' && serviceArg.containerID)
      .flatMap((serviceArg) => this.getServicePublicPortRoutes(serviceArg));
    await this.coreTraffic.setPublicPortRoutes(routes);
  }

  validatePublicPortRoutesForServices(servicesArg: IService[]): void {
    const routes = servicesArg.flatMap((serviceArg) => this.getServicePublicPortRoutes(serviceArg));
    this.coreTraffic.validatePublicPortRoutes(routes);
  }

  private normalizeServicePortConfig(
    serviceArg: Pick<IService, 'name' | 'domain' | 'targetPorts' | 'domains' | 'publicPortMappings' | 'port'>,
  ): plugins.servezoneInterfaces.data.INormalizedServicePortConfig {
    return plugins.servezoneInterfaces.data.normalizeServicePortConfig({
      name: serviceArg.name,
      targetPorts: serviceArg.targetPorts,
      domains: serviceArg.domains?.length
        ? serviceArg.domains
        : serviceArg.domain
          ? [{ name: serviceArg.domain, protocol: 'https' }]
          : [],
      publicPortMappings: serviceArg.publicPortMappings || [],
    }, { allowLegacyDomainPort: false });
  }

  private resolveServiceRoutePort(
    serviceArg: Pick<IService, 'name' | 'domain' | 'domains' | 'targetPorts' | 'publicPortMappings' | 'port'>,
    domainArg?: string,
  ): number {
    const normalizedPortConfig = this.normalizeServicePortConfig(serviceArg);
    const normalizedDomain = domainArg?.trim().toLowerCase();
    const domainRoute = normalizedDomain
      ? normalizedPortConfig.domains.find((domainEntryArg) => domainEntryArg.name.trim().toLowerCase() === normalizedDomain)
      : normalizedPortConfig.domains[0];
    const routePort = domainRoute?.target.port || normalizedPortConfig.defaultTargetPort?.port;
    if (!routePort) {
      throw new Error(`Service ${serviceArg.name} has no declared target port`);
    }
    return routePort;
  }

  private getServiceDomainNames(
    serviceArg: Pick<IService, 'name' | 'domain' | 'domains' | 'targetPorts' | 'publicPortMappings' | 'port'>,
  ): string[] {
    return this.normalizeServicePortConfig(serviceArg).domains.map((domainEntryArg) => domainEntryArg.name);
  }

  private getServicePublicPortRoutes(serviceArg: IService): ICoreTrafficPublicPortRoute[] {
    const routes: ICoreTrafficPublicPortRoute[] = [];
    const normalizedPortConfig = this.normalizeServicePortConfig(serviceArg);
    for (const mapping of normalizedPortConfig.publicPortMappings) {
      for (let offset = 0; offset <= mapping.publicPortEnd - mapping.publicPort; offset++) {
        routes.push({
          name: `${serviceArg.name}-${mapping.name}-${mapping.protocol}-${mapping.publicPort + offset}`,
          listenPort: mapping.publicPort + offset,
          transport: mapping.protocol,
          targetHost: `onebox-${serviceArg.name}`,
          targetPort: mapping.targetPortStart + offset,
        });
      }
    }
    return routes;
  }

  private async getAdminUiDomain(): Promise<string> {
    return normalizeHostname((await this.database.getSetting('adminUiDomain')) || '');
  }

  private async getAdminUiTargetHost(): Promise<string> {
    const serverIP = await this.database.getSetting('serverIP');
    if (!serverIP) {
      logger.warn('serverIP is not configured; Admin UI proxy route will use host.docker.internal');
    }
    return serverIP || 'host.docker.internal';
  }

  /**
   * Add TLS certificate for a domain
   * Sends PEM content to CoreTraffic via Admin API
   */
  async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> {
    if (!certPem || !keyPem) {
      logger.warn(`Cannot add certificate for ${domain}: missing PEM content`);
      return;
    }

    await this.coreTraffic.addCertificate(domain, certPem, keyPem);
  }

  /**
   * Remove TLS certificate for a domain
   */
  async removeCertificate(domain: string): Promise<void> {
    await this.coreTraffic.removeCertificate(domain);
  }

  /**
   * Reload TLS certificates from database
   */
  async reloadCertificates(): Promise<void> {
    try {
      logger.info('Reloading TLS certificates from database...');

      const certificates = await this.database.getAllSSLCertificates();

      for (const cert of certificates) {
        // Use fullchainPem for the cert (includes intermediates) and keyPem for the key
        if (cert.domain && cert.fullchainPem && cert.keyPem) {
          await this.coreTraffic.addCertificate(cert.domain, cert.fullchainPem, cert.keyPem);
        } else {
          logger.warn(`Skipping certificate for ${cert.domain}: missing PEM content`);
        }
      }

      logger.success(`Loaded ${this.coreTraffic.getCertificates().length} TLS certificates`);
    } catch (error) {
      logger.error(`Failed to reload certificates: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Get status of reverse proxy
   */
  async observeStatus(): Promise<{
    state: ICoreTrafficStatusObservation['state'];
    running: boolean | null;
    observedAt: number;
    http: { running: boolean | null; port: number };
    https: { running: boolean | null; port: number; certificates: number };
    routes: number;
    backend: 'coretraffic-docker';
  }> {
    const coreTrafficStatus = await this.coreTraffic.observeStatus();
    return {
      state: coreTrafficStatus.state,
      running: coreTrafficStatus.running,
      observedAt: coreTrafficStatus.observedAt,
      http: { running: coreTrafficStatus.running, port: coreTrafficStatus.httpPort },
      https: {
        running: coreTrafficStatus.running,
        port: coreTrafficStatus.httpsPort,
        certificates: coreTrafficStatus.certificates,
      },
      routes: coreTrafficStatus.routes,
      backend: 'coretraffic-docker',
    };
  }

  getStatus() {
    const coreTrafficStatus = this.coreTraffic.getStatus();
    return {
      http: {
        running: coreTrafficStatus.running,
        port: coreTrafficStatus.httpPort,
      },
      https: {
        running: coreTrafficStatus.running,
        port: coreTrafficStatus.httpsPort,
        certificates: coreTrafficStatus.certificates,
      },
      routes: coreTrafficStatus.routes,
      backend: 'coretraffic-docker',
    };
  }
}
