import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import type { IUnifiedEmailServerOptions } from '@push.rocks/smartmta';
import type { IRoute } from '../../ts_interfaces/data/route-management.js';
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
import type { DcRouter } from '../classes.dcrouter.js';

type TInboundProxyProtocolPolicy = NonNullable<plugins.smartproxy.IRouteMatch['inboundProxyProtocol']>;

/**
 * Generates SmartProxy routes for the email ports and hydrates persisted
 * email routes into runtime routes: server-first SMTP ports get a raw
 * socket-handler proxy that injects PROXY protocol toward the backend.
 */
export class EmailRouteBuilder {
  constructor(private dcRouterRef: DcRouter) {}

  public generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): IDcRouterRouteConfig[] {
    const emailRoutes: IDcRouterRouteConfig[] = [];

    // Create routes for each email port
    for (const port of emailConfig.ports) {
      // Create a descriptive name for the route based on the port
      let routeName = 'email-route';
      let tlsMode: 'terminate' | undefined;

      // Handle different email ports differently
      switch (port) {
        case 25: // SMTP
          routeName = 'smtp-route';
          break;

        case 587: // Submission
          routeName = 'submission-route';
          break;

        case 465: // SMTPS
          routeName = 'smtps-route';
          tlsMode = 'terminate'; // SmartProxy owns public TLS; backend remains server-first SMTP
          break;

        default:
          routeName = `email-port-${port}-route`;

          // Check if we have specific settings for this port
          if (this.dcRouterRef.options.emailPortConfig?.portSettings &&
              this.dcRouterRef.options.emailPortConfig.portSettings[port]) {
            const portSettings = this.dcRouterRef.options.emailPortConfig.portSettings[port];

            // If this port requires TLS termination, set the mode accordingly
            if (portSettings.terminateTls) {
              tlsMode = 'terminate';
            }

            // Override the route name if specified
            if (portSettings.routeName) {
              routeName = portSettings.routeName;
            }
          }
          break;
      }

      // Create forward action to route to internal email server ports
      const defaultPortMapping: Record<number, number> = {
        25: 10025,   // SMTP
        587: 10587,  // Submission
        465: 10465   // SMTPS
      };

      const portMapping = this.dcRouterRef.options.emailPortConfig?.portMapping || defaultPortMapping;
      const internalPort = portMapping[port] || port + 10000;

      let action: any = {
        type: 'forward',
        sendProxyProtocol: true,
        targets: [{
          host: 'localhost', // Forward to internal email server
          port: internalPort,
          sendProxyProtocol: true,
        }]
      };

      // Plain SMTP/STARTTLS ports must not carry TLS metadata, or SmartProxy waits for TLS/SNI first.
      if (tlsMode === 'terminate') {
        action.tls = {
          mode: tlsMode,
          certificate: 'auto',
        };
      }

      // Create the route configuration
      const routeConfig: IDcRouterRouteConfig = {
        name: routeName,
        match: {
          ports: [port],
          transport: 'tcp',
        },
        action: action
      };

      if (this.dcRouterRef.isRemoteIngressHubEnabled()) {
        routeConfig.remoteIngress = { enabled: true };
        const inboundProxyProtocol = this.getRemoteIngressEmailInboundProxyPolicy(port);
        if (inboundProxyProtocol) {
          routeConfig.match.inboundProxyProtocol = inboundProxyProtocol;
        }
      }

      // Add the route to our list
      emailRoutes.push(routeConfig);
    }

    return emailRoutes;
  }

  public getRuntimeEmailRoutes(emailRoutes: IDcRouterRouteConfig[]): plugins.smartproxy.IRouteConfig[] {
    return emailRoutes.map((route) => this.createServerFirstEmailRuntimeRoute(route) || route);
  }

  /**
   * Hydrate a persisted route into its runtime form: generated email routes
   * get the server-first socket-handler treatment, DoH routes get the DNS
   * socket handler. Returns undefined when the stored route runs as-is.
   */
  public hydrateStoredRouteForRuntime(storedRoute: IRoute): plugins.smartproxy.IRouteConfig | undefined {
    const routeName = storedRoute.route.name || '';
    const isDohRoute = storedRoute.origin === 'dns'
      && storedRoute.route.action?.type === 'socket-handler'
      && routeName.startsWith('dns-over-https-');

    if (!isDohRoute) {
      if (this.shouldHydrateGeneratedEmailRoute(storedRoute)) {
        return this.createServerFirstEmailRuntimeRoute(storedRoute.route);
      }
      return undefined;
    }

    return {
      ...storedRoute.route,
      action: {
        ...storedRoute.route.action,
        type: 'socket-handler' as any,
        socketHandler: this.dcRouterRef.dnsServerRuntime.createSocketHandler(),
      } as any,
    };
  }

  private getCurrentGeneratedEmailRouteNames(): Set<string> {
    if (this.dcRouterRef.options.dbConfig?.enabled === false) {
      return new Set();
    }
    const sourceRoutes = this.dcRouterRef.seedEmailRoutes.length > 0
      ? this.dcRouterRef.seedEmailRoutes
      : this.dcRouterRef.options.emailConfig
        ? this.generateEmailRoutes(this.dcRouterRef.options.emailConfig)
        : [];
    return new Set(sourceRoutes.map((route) => route.name).filter(Boolean) as string[]);
  }

  private shouldHydrateGeneratedEmailRoute(storedRoute: IRoute): boolean {
    if (storedRoute.origin !== 'email') {
      return false;
    }
    const routeName = storedRoute.route.name;
    if (!routeName || !this.getCurrentGeneratedEmailRouteNames().has(routeName)) {
      return false;
    }
    const expectedSystemKey = `email:${routeName}`;
    return !storedRoute.systemKey || storedRoute.systemKey === expectedSystemKey;
  }

  private createServerFirstEmailRuntimeRoute(
    route: plugins.smartproxy.IRouteConfig,
  ): plugins.smartproxy.IRouteConfig | undefined {
    const action = route.action as any;
    if (action?.type !== 'forward') {
      return undefined;
    }
    const tlsMode = action.tls?.mode;
    if (tlsMode === 'terminate-and-reencrypt') {
      return undefined;
    }
    const routePorts = plugins.smartproxy.expandPortRange(route.match?.ports as any) as number[];
    if (routePorts.length !== 1) {
      return undefined;
    }

    const target = action.targets?.[0];
    if (!target || action.targets.length !== 1 || typeof target.port !== 'number') {
      return undefined;
    }
    if (typeof target.host !== 'string') {
      return undefined;
    }

    const targetHost = target.host === 'localhost' ? '127.0.0.1' : target.host;
    const inboundProxyProtocol = this.getRemoteIngressEmailInboundProxyPolicy(routePorts[0]);
    return {
      ...route,
      match: {
        ...route.match,
        ...(inboundProxyProtocol
          ? { inboundProxyProtocol }
          : {}),
      },
      action: {
        type: 'socket-handler' as any,
        ...(action.tls
          ? { tls: action.tls }
          : {}),
        socketHandler: this.createEmailSocketProxyHandler(targetHost, target.port),
      } as any,
    };
  }

  private getRemoteIngressEmailInboundProxyPolicy(
    port: number,
  ): TInboundProxyProtocolPolicy | undefined {
    if (!this.dcRouterRef.isRemoteIngressHubEnabled()) {
      return undefined;
    }
    return { mode: port === 25 || port === 587 ? 'required' : 'optional' };
  }

  private createEmailSocketProxyHandler(
    targetHost: string,
    targetPort: number,
  ): NonNullable<plugins.smartproxy.IRouteConfig['action']['socketHandler']> {
    return (clientSocket, context) => {
      let backendSocket: plugins.net.Socket | undefined;
      let connectTimeout: ReturnType<typeof setTimeout> & { unref?: () => void };
      let cleanupDone = false;

      const cleanup = () => {
        if (cleanupDone) return;
        cleanupDone = true;
        clearTimeout(connectTimeout);
        clientSocket.removeListener('timeout', cleanup);
        clientSocket.removeListener('error', cleanup);
        clientSocket.removeListener('end', cleanup);
        clientSocket.removeListener('close', cleanup);
        backendSocket?.removeListener('timeout', cleanup);
        backendSocket?.removeListener('error', cleanup);
        backendSocket?.removeListener('end', cleanup);
        backendSocket?.removeListener('close', cleanup);
        clientSocket.destroy();
        backendSocket?.destroy();
      };

      connectTimeout = setTimeout(() => {
        cleanup();
      }, 30_000);
      connectTimeout.unref?.();

      clientSocket.setTimeout(300_000);
      clientSocket.on('timeout', cleanup);
      clientSocket.on('error', cleanup);
      clientSocket.on('end', cleanup);
      clientSocket.on('close', cleanup);

      backendSocket = plugins.net.connect(targetPort, targetHost, () => {
        clearTimeout(connectTimeout);
        backendSocket?.setTimeout(300_000);
        const proxyHeader = this.createProxyProtocolV1Header(
          context?.clientIp,
          targetHost,
          0,
          targetPort,
        );
        if (!proxyHeader) {
          cleanup();
          return;
        }
        backendSocket!.write(proxyHeader, () => {
          clientSocket.pipe(backendSocket!);
          backendSocket!.pipe(clientSocket);
        });
      });
      backendSocket.setTimeout(30_000);
      backendSocket.on('timeout', cleanup);
      backendSocket.on('error', cleanup);
      backendSocket.on('end', cleanup);
      backendSocket.on('close', cleanup);
    };
  }

  private createProxyProtocolV1Header(
    sourceIp: string | undefined,
    destinationIp: string,
    sourcePort: number,
    destinationPort: number,
  ): string | undefined {
    if (!sourceIp || !plugins.net.isIP(sourceIp)) {
      logger.log('warn', `Cannot create email PROXY protocol header for invalid source IP: ${sourceIp || 'unknown'}`);
      return undefined;
    }
    const sourceFamily = plugins.net.isIP(sourceIp);
    const destinationAddress = destinationIp === 'localhost' || destinationIp === '127.0.0.1' || destinationIp === '::1'
      ? sourceFamily === 6 ? '::1' : '127.0.0.1'
      : destinationIp;
    const destinationFamily = plugins.net.isIP(destinationAddress);
    if (!destinationFamily) {
      logger.log('warn', `Cannot create email PROXY protocol header for invalid destination IP: ${destinationIp}`);
      return undefined;
    }
    if (sourceFamily !== destinationFamily) {
      return undefined;
    }
    const protocol = sourceFamily === 6 ? 'TCP6' : 'TCP4';
    return `PROXY ${protocol} ${sourceIp} ${destinationAddress} ${sourcePort} ${destinationPort}\r\n`;
  }
}
