/**
 * SmartProxy Manager for Onebox
 *
 * Manages SmartProxy as a Docker Swarm service so it can route to services on
 * the Onebox overlay network.
 */

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

const SMARTPROXY_SERVICE_NAME = 'onebox-smartproxy';
const LEGACY_REVERSE_PROXY_SERVICE_NAME = 'onebox-caddy';
const SMARTPROXY_IMAGE = 'code.foss.global/host.today/ht-docker-smartproxy:latest';
const SMARTPROXY_ADMIN_CONTAINER_PORT = 3000;
const SMARTPROXY_HTTP_CONTAINER_PORT = 80;
const SMARTPROXY_HTTPS_CONTAINER_PORT = 443;

export interface ISmartProxyRoute {
  domain: string;
  upstream: string;
}

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

interface ISmartProxyRouteConfig {
  name: string;
  match: {
    ports: number;
    domains: string;
    protocol?: 'http' | 'tcp' | 'udp' | 'quic' | 'http3';
  };
  action: {
    type: 'forward';
    targets: Array<{ host: string; port: number }>;
    tls?: {
      mode: 'terminate';
      certificate: {
        key: string;
        cert: string;
      };
    };
    websocket?: {
      enabled: boolean;
    };
  };
  priority?: number;
}

export class SmartProxyManager {
  private dockerClient: InstanceType<typeof plugins.docker.Docker> | null = null;
  private certsDir: string;
  private adminUrl: string;
  private adminPort: number;
  private httpPort: number;
  private httpsPort: number;
  private routes: Map<string, ISmartProxyRoute> = new Map();
  private certificates: Map<string, ISmartProxyCertificate> = new Map();
  private networkName = 'onebox-network';
  private serviceRunning = false;

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

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

  setPorts(httpPort: number, httpsPort: number): void {
    this.httpPort = httpPort;
    this.httpsPort = httpsPort;
  }

  async start(): Promise<void> {
    if (this.serviceRunning) {
      logger.warn('SmartProxy service is already running');
      return;
    }

    try {
      await this.ensureDockerClient();
      await Deno.mkdir(this.certsDir, { recursive: true });

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

      const legacyService = await this.getExistingService(LEGACY_REVERSE_PROXY_SERVICE_NAME);
      if (legacyService) {
        logger.info(
          `Legacy reverse proxy service ${LEGACY_REVERSE_PROXY_SERVICE_NAME} exists, removing it before SmartProxy startup...`,
        );
        await this.removeService(LEGACY_REVERSE_PROXY_SERVICE_NAME);
        await new Promise((resolve) => setTimeout(resolve, 2000));
      }

      const existingService = await this.getExistingService();
      if (existingService) {
        logger.info('SmartProxy service exists, removing old service...');
        await this.removeService();
        await new Promise((resolve) => setTimeout(resolve, 2000));
      }

      const networkId = await this.getNetworkId();

      const response = await this.dockerClient!.request('POST', '/services/create', {
        Name: SMARTPROXY_SERVICE_NAME,
        Labels: {
          'managed-by': 'onebox',
          'onebox-type': 'smartproxy',
        },
        TaskTemplate: {
          ContainerSpec: {
            Image: SMARTPROXY_IMAGE,
            Env: [
              'SMARTPROXY_ADMIN_HOST=0.0.0.0',
              `SMARTPROXY_ADMIN_PORT=${SMARTPROXY_ADMIN_CONTAINER_PORT}`,
            ],
          },
          Networks: [
            {
              Target: networkId,
            },
          ],
          RestartPolicy: {
            Condition: 'any',
            MaxAttempts: 0,
          },
        },
        Mode: {
          Replicated: {
            Replicas: 1,
          },
        },
        EndpointSpec: {
          Ports: [
            {
              Protocol: 'tcp',
              TargetPort: SMARTPROXY_HTTP_CONTAINER_PORT,
              PublishedPort: this.httpPort,
              PublishMode: 'host',
            },
            {
              Protocol: 'tcp',
              TargetPort: SMARTPROXY_HTTPS_CONTAINER_PORT,
              PublishedPort: this.httpsPort,
              PublishMode: 'host',
            },
            {
              Protocol: 'tcp',
              TargetPort: SMARTPROXY_ADMIN_CONTAINER_PORT,
              PublishedPort: this.adminPort,
              PublishMode: 'host',
            },
          ],
        },
      });

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

      const serviceId = response.body.ID;
      logger.info(`SmartProxy service created: ${serviceId}`);

      await this.waitForServiceTaskRunning(serviceId);
      await this.waitForReady();
      this.serviceRunning = true;
      await this.reloadConfig({ skipRunningCheck: true });

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

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

  private async removeService(serviceNameArg = SMARTPROXY_SERVICE_NAME): Promise<void> {
    try {
      await this.dockerClient!.request('DELETE', `/services/${serviceNameArg}`, {});
    } catch {
      // Service may not exist.
    }
  }

  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;
  }

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

  private async waitForServiceTaskRunning(
    serviceIdArg: string,
    maxAttempts = 30,
    intervalMs = 1000,
  ): Promise<void> {
    let lastState = 'unknown';

    for (let i = 0; i < maxAttempts; i++) {
      const tasksResponse = await this.dockerClient!.request(
        'GET',
        `/tasks?filters=${encodeURIComponent(JSON.stringify({ service: [serviceIdArg] }))}`,
        {},
      );

      if (tasksResponse.statusCode === 200 && Array.isArray(tasksResponse.body)) {
        const tasks = tasksResponse.body;
        const runningTask = tasks.find((task: any) => task.Status?.State === 'running');
        if (runningTask) {
          return;
        }

        const latestTask = tasks[0];
        lastState = latestTask?.Status?.State || lastState;
      }

      await new Promise((resolve) => setTimeout(resolve, intervalMs));
    }

    throw new Error(`SmartProxy service task did not reach running state (last state: ${lastState})`);
  }

  async stop(): Promise<void> {
    try {
      await this.ensureDockerClient();

      if (!this.serviceRunning && !(await this.getExistingService())) {
        return;
      }

      logger.info('Stopping SmartProxy service...');
      await this.removeService();

      this.serviceRunning = false;
      logger.info('SmartProxy service stopped');
    } catch (error) {
      logger.error(`Failed to stop SmartProxy: ${getErrorMessage(error)}`);
    } finally {
      if (this.dockerClient) {
        try {
          await this.dockerClient.stop();
        } catch (error) {
          logger.error(`Failed to stop SmartProxy Docker client: ${getErrorMessage(error)}`);
        } finally {
          this.dockerClient = null;
        }
      }
    }
  }

  async isHealthy(): Promise<boolean> {
    try {
      const response = await fetch(`${this.adminUrl}/health`);
      return response.ok;
    } catch {
      return false;
    }
  }

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

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

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

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

  private routeName(prefixArg: string, domainArg: string): string {
    return `${prefixArg}-${domainArg.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '')}`;
  }

  private parseUpstream(upstreamArg: string): { host: string; port: number } {
    const separatorIndex = upstreamArg.lastIndexOf(':');
    if (separatorIndex <= 0 || separatorIndex === upstreamArg.length - 1) {
      throw new Error(`Invalid upstream target: ${upstreamArg}`);
    }

    const host = upstreamArg.slice(0, separatorIndex);
    const port = Number(upstreamArg.slice(separatorIndex + 1));
    if (!Number.isInteger(port) || port < 1 || port > 65535) {
      throw new Error(`Invalid upstream port in target: ${upstreamArg}`);
    }

    return { host, port };
  }

  private buildRoutes(): ISmartProxyRouteConfig[] {
    const routeConfigs: ISmartProxyRouteConfig[] = [];

    for (const [domain, route] of this.routes) {
      const target = this.parseUpstream(route.upstream);
      const baseAction = {
        type: 'forward' as const,
        targets: [target],
        websocket: {
          enabled: true,
        },
      };

      routeConfigs.push({
        name: this.routeName('http', domain),
        match: {
          ports: SMARTPROXY_HTTP_CONTAINER_PORT,
          domains: domain,
          protocol: 'http',
        },
        action: baseAction,
        priority: 10,
      });

      const certificate = this.certificates.get(domain);
      if (certificate) {
        routeConfigs.push({
          name: this.routeName('https', domain),
          match: {
            ports: SMARTPROXY_HTTPS_CONTAINER_PORT,
            domains: domain,
            protocol: 'http',
          },
          action: {
            ...baseAction,
            tls: {
              mode: 'terminate',
              certificate: {
                key: certificate.keyPem,
                cert: certificate.certPem,
              },
            },
          },
          priority: 20,
        });
      }
    }

    return routeConfigs;
  }

  async reloadConfig(options: { skipRunningCheck?: boolean } = {}): Promise<void> {
    if (!options.skipRunningCheck) {
      const isRunning = await this.isRunning();
      if (!isRunning) {
        logger.warn('SmartProxy not running, cannot reload config');
        return;
      }
    }

    const routes = this.buildRoutes();

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

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

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

  async addRoute(domain: string, upstream: string): Promise<void> {
    this.routes.set(domain, { domain, upstream });

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

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

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

  async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> {
    this.certificates.set(domain, {
      domain,
      certPem,
      keyPem,
    });

    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}`);
  }

  async removeCertificate(domain: string): Promise<void> {
    if (this.certificates.delete(domain)) {
      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}`);
    }
  }

  getRoutes(): ISmartProxyRoute[] {
    return Array.from(this.routes.values());
  }

  getCertificates(): ISmartProxyCertificate[] {
    return Array.from(this.certificates.values());
  }

  clear(): void {
    this.routes.clear();
    this.certificates.clear();
  }

  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,
    };
  }
}
