import * as plugins from './plugins.js';
import * as paths from './paths.js';

// Certificate types are available via plugins.tsclass

// Import the email server and its configuration from smartmta
import {
  UnifiedEmailServer,
  type IUnifiedEmailServerOptions,
  type IEmailRoute,
  type IEmailDomainConfig,
} from '@push.rocks/smartmta';
import { logger } from './logger.js';
import { StorageBackedCertManager } from './classes.storage-cert-manager.js';
import { CertProvisionScheduler } from './classes.cert-provision-scheduler.js';
// Import unified database
import { DcRouterDb, type IDcRouterDbConfig, CacheCleaner, ProxyCertDoc, AcmeCertDoc } from './db/index.js';

import { OpsServer } from './opsserver/index.js';
import { MetricsManager } from './monitoring/index.js';
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder } from './config/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';

export interface IDcRouterOptions {
  /** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
  baseDir?: string;

  /**
   * Direct SmartProxy configuration - gives full control over HTTP/HTTPS and TCP/SNI traffic
   * This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic
   */
  smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions;
  
  /**
   * Email server configuration
   * This enables all email handling with pattern-based routing
   */
  emailConfig?: IUnifiedEmailServerOptions;
  
  /**
   * Custom email port configuration
   * Allows configuring specific ports for email handling
   * This overrides the default port mapping in the emailConfig
   */
  emailPortConfig?: {
    /** External to internal port mapping */
    portMapping?: Record<number, number>;
    /** Custom port configuration for specific ports */
    portSettings?: Record<number, any>;
    /** Path to store received emails */
    receivedEmailsPath?: string;
  };
  
  /** TLS/certificate configuration */
  tls?: {
    /** Contact email for ACME certificates */
    contactEmail: string;
    /** Domain for main certificate */
    domain?: string;
    /** Path to certificate file (if not using auto-provisioning) */
    certPath?: string;
    /** Path to key file (if not using auto-provisioning) */
    keyPath?: string;
    /** Path to CA certificate file (for custom CAs) */
    caPath?: string;
  };
  
  /**
   * The nameserver domains (e.g., ['ns1.example.com', 'ns2.example.com'])
   * These will automatically get A records pointing to publicIp or proxyIps[0]
   * These are what go in the NS records for ALL domains in dnsScopes
   */
  dnsNsDomains?: string[];
  
  /**
   * Domains this DNS server is authoritative for (e.g., ['example.com', 'mail.example.org'])
   * NS records will be auto-generated for these domains
   * Any DNS record outside these scopes will trigger a warning
   * Email domains with `internal-dns` mode must be included here
   */
  dnsScopes?: string[];
  
  /**
   * IPs of proxies that forward traffic to your server (optional)
   * When defined AND useIngressProxy is true, A records with server IP are replaced with proxy IPs
   * If not defined or empty, all A records use the real server IP
   * Helps hide real server IP for security/privacy
   */
  proxyIps?: string[];
  
  /**
   * Public IP address for nameserver A records (required if proxyIps not set)
   * This is the IP that will be used for the nameserver domains (dnsNsDomains)
   * If proxyIps is set, the first proxy IP will be used instead
   */
  publicIp?: string;
  
  /**
   * DNS records to register
   * Must be within the defined dnsScopes (or receive warning)
   * Only need A, CNAME, TXT, MX records (NS records auto-generated, SOA handled by smartdns)
   * Can use `useIngressProxy: false` to expose real server IP (defaults to true)
   */
  dnsRecords?: Array<{
    name: string;
    type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA';
    value: string;
    ttl?: number;
    useIngressProxy?: boolean;  // Whether to replace server IP with proxy IP (default: true)
  }>;
  
  /** DNS challenge configuration for ACME (optional) */
  dnsChallenge?: {
    /** Cloudflare API key for DNS challenges */
    cloudflareApiKey?: string;
    /** Other DNS providers can be added here */
  };
  
  /**
   * Unified database configuration.
   * All persistent data (config, certs, VPN, cache, etc.) is stored via smartdata.
   * If mongoDbUrl is provided, connects to external MongoDB.
   * Otherwise, starts an embedded LocalSmartDb automatically.
   */
  dbConfig?: {
    /** Enable database (default: true). Set to false in tests to skip DB startup. */
    enabled?: boolean;
    /** External MongoDB connection URL. If absent, uses embedded LocalSmartDb. */
    mongoDbUrl?: string;
    /** Storage path for embedded database data (default: ~/.serve.zone/dcrouter/tsmdb) */
    storagePath?: string;
    /** Database name (default: dcrouter) */
    dbName?: string;
    /** Cache cleanup interval in hours (default: 1) */
    cleanupIntervalHours?: number;
    /** Seed default security profiles and network targets when DB is empty on first startup. */
    seedOnEmpty?: boolean;
    /** Custom seed data for profiles and targets (overrides built-in defaults). */
    seedData?: import('./config/classes.db-seeder.js').ISeedData;
  };

  /**
   * RADIUS server configuration for network authentication
   * Enables MAC Authentication Bypass (MAB) and VLAN assignment
   */
  radiusConfig?: IRadiusServerConfig;

  /**
   * Remote Ingress configuration for edge tunnel nodes
   * Enables edge nodes to accept incoming connections and tunnel them to this DcRouter
   */
  /**
   * HTTP/3 (QUIC) configuration for HTTPS routes.
   * Enabled by default — qualifying HTTPS routes on port 443 are automatically
   * augmented with QUIC/H3 fields. Set { enabled: false } to disable globally.
   * Individual routes can opt out via action.options.http3 = false.
   */
  http3?: IHttp3Config;

  /** Port for the OpsServer web UI (default: 3000) */
  opsServerPort?: number;

  remoteIngressConfig?: {
    /** Enable remote ingress hub (default: false) */
    enabled?: boolean;
    /** Port for tunnel connections from edge nodes (default: 8443) */
    tunnelPort?: number;
    /** External hostname of this hub, embedded in connection tokens */
    hubDomain?: string;
    /** TLS configuration for the tunnel server */
    tls?: {
      certPath?: string;
      keyPath?: string;
    };
  };

  /**
   * VPN server configuration.
   * Enables VPN-based access control: routes with vpn.enabled are only
   * accessible from VPN clients. Supports WireGuard + native (WS/QUIC) transports.
   */
  vpnConfig?: {
    /** Enable VPN server (default: false) */
    enabled?: boolean;
    /** VPN subnet CIDR (default: '10.8.0.0/24') */
    subnet?: string;
    /** WireGuard UDP listen port (default: 51820) */
    wgListenPort?: number;
    /** DNS servers pushed to VPN clients */
    dns?: string[];
    /** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
    serverEndpoint?: string;
    /** Pre-defined VPN clients created on startup */
    clients?: Array<{
      clientId: string;
      serverDefinedClientTags?: string[];
      description?: string;
    }>;
    /** Destination routing policy for VPN client traffic.
     *  Default in socket mode: { default: 'forceTarget', target: '127.0.0.1' } (all traffic → SmartProxy).
     *  Default in tun mode: not set (all traffic passes through). */
    destinationPolicy?: {
      default: 'forceTarget' | 'block' | 'allow';
      target?: string;
      allowList?: string[];
      blockList?: string[];
    };
    /** Forwarding mode: 'socket' (default, userspace NAT), 'bridge' (L2 bridge to host LAN),
     *  or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
    forwardingMode?: 'socket' | 'bridge' | 'hybrid';
    /** LAN subnet CIDR for bridge mode (e.g., '192.168.1.0/24') */
    bridgeLanSubnet?: string;
    /** Physical network interface for bridge mode (auto-detected if omitted) */
    bridgePhysicalInterface?: string;
    /** Start of VPN client IP range in LAN subnet (host offset, default: 200) */
    bridgeIpRangeStart?: number;
    /** End of VPN client IP range in LAN subnet (host offset, default: 250) */
    bridgeIpRangeEnd?: number;
  };
}

/**
 * DcRouter can be run on ingress and egress to and from a datacenter site.
 */
/**
 * Context passed to HTTP routing rules
 */
/**
 * Context passed to port proxy (SmartProxy) routing rules
 */
export interface PortProxyRuleContext {
  proxy: plugins.smartproxy.SmartProxy;
  routes: plugins.smartproxy.IRouteConfig[];
}

export class DcRouter {
  public options: IDcRouterOptions;
  public resolvedPaths: ReturnType<typeof paths.resolvePaths>;

  // Core services
  public smartProxy?: plugins.smartproxy.SmartProxy;
  public smartAcme?: plugins.smartacme.SmartAcme;
  public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
  public emailServer?: UnifiedEmailServer;
  public radiusServer?: RadiusServer;
  public opsServer!: OpsServer;
  public metricsManager?: MetricsManager;

  // Compatibility shim for smartmta's DkimManager which calls dcRouter.storageManager.set()
  public storageManager: any = {
    get: async (_key: string) => null,
    set: async (_key: string, _value: string) => {
      // DKIM keys from smartmta — logged but not yet migrated to smartdata
      logger.log('debug', `storageManager.set() called (compat shim) for key: ${_key}`);
    },
  };

  // Unified database (smartdata + LocalSmartDb or external MongoDB)
  public dcRouterDb?: DcRouterDb;
  public cacheCleaner?: CacheCleaner;

  // Remote Ingress
  public remoteIngressManager?: RemoteIngressManager;
  public tunnelManager?: TunnelManager;

  // VPN
  public vpnManager?: VpnManager;

  // Programmatic config API
  public routeConfigManager?: RouteConfigManager;
  public apiTokenManager?: ApiTokenManager;
  public referenceResolver?: ReferenceResolver;

  // Auto-discovered public IP (populated by generateAuthoritativeRecords)
  public detectedPublicIp: string | null = null;

  // DNS query logging rate limiter state
  private dnsLogWindowSecond: number = 0; // epoch second of current window
  private dnsLogWindowCount: number = 0;  // queries logged this second
  private dnsBatchCount: number = 0;
  private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null;

  // Certificate status tracking from SmartProxy events (keyed by domain)
  public certificateStatusMap = new Map<string, {
    status: 'valid' | 'failed';
    routeNames: string[];
    expiryDate?: string;
    issuedAt?: string;
    source?: string;
    error?: string;
  }>();

  // Certificate provisioning scheduler with per-domain backoff
  public certProvisionScheduler?: CertProvisionScheduler;

  // Service lifecycle management
  public serviceManager: plugins.taskbuffer.ServiceManager;
  private serviceSubjectSubscription?: plugins.smartrx.rxjs.Subscription;
  public smartAcmeReady = false;

  // TypedRouter for API endpoints
  public typedrouter = new plugins.typedrequest.TypedRouter();

  // Cached constructor routes (computed once during setupSmartProxy, used by RouteConfigManager)
  private constructorRoutes: plugins.smartproxy.IRouteConfig[] = [];

  // Environment access
  private qenv = new plugins.qenv.Qenv('./', '.nogit/');
  
  constructor(optionsArg: IDcRouterOptions) {
    // Set defaults in options
    this.options = {
      ...optionsArg
    };

    // Resolve all data paths from baseDir
    this.resolvedPaths = paths.resolvePaths(this.options.baseDir);

    // Initialize service manager and register all services
    this.serviceManager = new plugins.taskbuffer.ServiceManager({
      name: 'dcrouter',
      startupTimeoutMs: 120_000,
      shutdownTimeoutMs: 30_000,
    });
    this.registerServices();
  }

  /**
   * Register all dcrouter services with the ServiceManager.
   * Services are started in dependency order, with failure isolation for optional services.
   */
  private registerServices(): void {
    // OpsServer: critical, no dependencies — provides visibility
    this.serviceManager.addService(
      new plugins.taskbuffer.Service('OpsServer')
        .critical()
        .withStart(async () => {
          this.opsServer = new OpsServer(this);
          await this.opsServer.start();
        })
        .withStop(async () => {
          await this.opsServer?.stop();
        })
        .withRetry({ maxRetries: 0 }),
    );

    // DcRouterDb: optional, no dependencies — unified database for all persistence
    if (this.options.dbConfig?.enabled !== false) {
      this.serviceManager.addService(
        new plugins.taskbuffer.Service('DcRouterDb')
          .optional()
          .withStart(async () => {
            await this.setupDcRouterDb();
          })
          .withStop(async () => {
            if (this.cacheCleaner) {
              this.cacheCleaner.stop();
              this.cacheCleaner = undefined;
            }
            if (this.dcRouterDb) {
              await this.dcRouterDb.stop();
              DcRouterDb.resetInstance();
              this.dcRouterDb = undefined;
            }
          })
          .withRetry({ maxRetries: 2, baseDelayMs: 1000, maxDelayMs: 5000 }),
      );
    }

    // MetricsManager: optional, depends on OpsServer
    this.serviceManager.addService(
      new plugins.taskbuffer.Service('MetricsManager')
        .optional()
        .dependsOn('OpsServer')
        .withStart(async () => {
          this.metricsManager = new MetricsManager(this);
          await this.metricsManager.start();
        })
        .withStop(async () => {
          if (this.metricsManager) {
            await this.metricsManager.stop();
            this.metricsManager = undefined;
          }
        })
        .withRetry({ maxRetries: 1, baseDelayMs: 1000 }),
    );

    // SmartProxy: critical, depends on DcRouterDb (if enabled)
    const smartProxyDeps: string[] = [];
    if (this.options.dbConfig?.enabled !== false) {
      smartProxyDeps.push('DcRouterDb');
    }
    this.serviceManager.addService(
      new plugins.taskbuffer.Service('SmartProxy')
        .critical()
        .dependsOn(...smartProxyDeps)
        .withStart(async () => {
          await this.setupSmartProxy();
        })
        .withStop(async () => {
          if (this.smartProxy) {
            this.smartProxy.removeAllListeners();
            await this.smartProxy.stop();
            this.smartProxy = undefined;
          }
        })
        .withRetry({ maxRetries: 0 }),
    );

    // SmartAcme: optional, depends on SmartProxy — aggressive retry for rate limits
    // Only registered if DNS challenge is configured
    if (this.options.dnsChallenge?.cloudflareApiKey) {
      this.serviceManager.addService(
        new plugins.taskbuffer.Service('SmartAcme')
          .optional()
          .dependsOn('SmartProxy')
          .withStart(async () => {
            if (this.smartAcme) {
              await this.smartAcme.start();
              this.smartAcmeReady = true;
              logger.log('info', 'SmartAcme DNS-01 provider is now ready');

              // Re-trigger certificate provisioning for all auto-cert routes.
              // During startup, certProvisionFunction returned 'http01' (SmartAcme not ready),
              // but Rust ACME is disabled when certProvisionFunction is set — so all domains
              // failed silently (SmartProxy doesn't emit certificate-failed for this path).
              // Calling updateRoutes() re-triggers provisionCertificatesViaCallback internally,
              // which calls certProvisionFunction again — now with smartAcmeReady === true.
              if (this.smartProxy) {
                if (this.certProvisionScheduler) {
                  this.certProvisionScheduler.clear();
                }
                const currentRoutes = this.smartProxy.routeManager.getRoutes();
                logger.log('info', `Re-triggering certificate provisioning for ${currentRoutes.length} routes`);
                this.smartProxy.updateRoutes(currentRoutes).catch((err: any) => {
                  logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
                });
              }
            }
          })
          .withStop(async () => {
            this.smartAcmeReady = false;
            if (this.smartAcme) {
              await this.smartAcme.stop();
              this.smartAcme = undefined;
            }
          })
          .withRetry({ maxRetries: 20, baseDelayMs: 5000, maxDelayMs: 3_600_000, backoffFactor: 2 }),
      );
    }

    // ConfigManagers: optional, depends on SmartProxy + DcRouterDb
    // Requires DcRouterDb to be enabled (document classes need the database)
    if (this.options.dbConfig?.enabled !== false) {
      this.serviceManager.addService(
        new plugins.taskbuffer.Service('ConfigManagers')
          .optional()
          .dependsOn('SmartProxy', 'DcRouterDb')
          .withStart(async () => {
            // Initialize reference resolver first (profiles + targets)
            this.referenceResolver = new ReferenceResolver();
            await this.referenceResolver.initialize();

            this.routeConfigManager = new RouteConfigManager(
              () => this.getConstructorRoutes(),
              () => this.smartProxy,
              () => this.options.http3,
              this.options.vpnConfig?.enabled
                ? (tags?: string[]) => {
                    if (tags?.length && this.vpnManager) {
                      return this.vpnManager.getClientIpsForServerDefinedTags(tags);
                    }
                    return [this.options.vpnConfig?.subnet || '10.8.0.0/24'];
                  }
                : undefined,
              this.referenceResolver,
              // Sync merged routes to RemoteIngressManager whenever routes change,
              // then push updated derived ports to the Rust hub binary
              (routes) => {
                if (this.remoteIngressManager) {
                  this.remoteIngressManager.setRoutes(routes as any[]);
                }
                if (this.tunnelManager) {
                  this.tunnelManager.syncAllowedEdges();
                }
              },
            );
            this.apiTokenManager = new ApiTokenManager();
            await this.apiTokenManager.initialize();
            await this.routeConfigManager.initialize();

            // Seed default profiles/targets if DB is empty and seeding is enabled
            const seeder = new DbSeeder(this.referenceResolver);
            await seeder.seedIfEmpty(
              this.options.dbConfig?.seedOnEmpty,
              this.options.dbConfig?.seedData,
            );
          })
          .withStop(async () => {
            this.routeConfigManager = undefined;
            this.apiTokenManager = undefined;
            this.referenceResolver = undefined;
          })
          .withRetry({ maxRetries: 2, baseDelayMs: 1000 }),
      );
    }

    // Email Server: optional, depends on SmartProxy
    if (this.options.emailConfig) {
      this.serviceManager.addService(
        new plugins.taskbuffer.Service('EmailServer')
          .optional()
          .dependsOn('SmartProxy')
          .withStart(async () => {
            await this.setupUnifiedEmailHandling();
          })
          .withStop(async () => {
            if (this.emailServer) {
              if ((this.emailServer as any).deliverySystem) {
                (this.emailServer as any).deliverySystem.removeAllListeners();
              }
              this.emailServer.removeAllListeners();
              await this.emailServer.stop();
              this.emailServer = undefined;
            }
          })
          .withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
      );
    }

    // DNS Server: optional, depends on SmartProxy
    if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 && this.options.dnsScopes && this.options.dnsScopes.length > 0) {
      this.serviceManager.addService(
        new plugins.taskbuffer.Service('DnsServer')
          .optional()
          .dependsOn('SmartProxy')
          .withStart(async () => {
            await this.setupDnsWithSocketHandler();
          })
          .withStop(async () => {
            // Flush pending DNS batch log
            if (this.dnsBatchTimer) {
              clearTimeout(this.dnsBatchTimer);
              if (this.dnsBatchCount > 0) {
                logger.log('info', `DNS: ${this.dnsBatchCount} queries processed (final flush)`, { zone: 'dns' });
              }
              this.dnsBatchTimer = null;
              this.dnsBatchCount = 0;
              this.dnsLogWindowSecond = 0;
              this.dnsLogWindowCount = 0;
            }
            if (this.dnsServer) {
              this.dnsServer.removeAllListeners();
              await this.dnsServer.stop();
              this.dnsServer = undefined;
            }
          })
          .withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
      );
    }

    // RADIUS Server: optional, no dependency on SmartProxy
    if (this.options.radiusConfig) {
      this.serviceManager.addService(
        new plugins.taskbuffer.Service('RadiusServer')
          .optional()
          .withStart(async () => {
            await this.setupRadiusServer();
          })
          .withStop(async () => {
            if (this.radiusServer) {
              await this.radiusServer.stop();
              this.radiusServer = undefined;
            }
          })
          .withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
      );
    }

    // Remote Ingress: optional, depends on SmartProxy
    if (this.options.remoteIngressConfig?.enabled) {
      this.serviceManager.addService(
        new plugins.taskbuffer.Service('RemoteIngress')
          .optional()
          .dependsOn('SmartProxy')
          .withStart(async () => {
            await this.setupRemoteIngress();
          })
          .withStop(async () => {
            if (this.tunnelManager) {
              await this.tunnelManager.stop();
              this.tunnelManager = undefined;
            }
            this.remoteIngressManager = undefined;
          })
          .withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
      );
    }

    // VPN Server: optional, depends on SmartProxy
    if (this.options.vpnConfig?.enabled) {
      this.serviceManager.addService(
        new plugins.taskbuffer.Service('VpnServer')
          .optional()
          .dependsOn('SmartProxy')
          .withStart(async () => {
            await this.setupVpnServer();
          })
          .withStop(async () => {
            if (this.vpnManager) {
              await this.vpnManager.stop();
              this.vpnManager = undefined;
            }
          })
          .withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
      );
    }

    // Wire up aggregated events for logging
    this.serviceSubjectSubscription = this.serviceManager.serviceSubject.subscribe((event) => {
      const level = event.type === 'failed' ? 'error' : event.type === 'retrying' ? 'warn' : 'info';
      logger.log(level as any, `Service '${event.serviceName}': ${event.type}`, {
        state: event.state,
        ...(event.error ? { error: event.error } : {}),
        ...(event.attempt ? { attempt: event.attempt } : {}),
      });
    });
  }

  public async start() {
    await this.checkSystemLimits();
    logger.log('info', 'Starting DcRouter Services');
    await this.serviceManager.start();
    this.logStartupSummary();
  }

  /**
   * Detect OS-level resource limits and warn if they are too low for production use.
   * This is detection only — no attempts to raise limits.
   */
  private async checkSystemLimits(): Promise<void> {
    try {
      const fs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
      const limitsContent = await fs.file('/proc/self/limits').encoding('utf8').read() as string;
      const nofileLine = limitsContent.split('\n').find((line: string) => line.startsWith('Max open files'));
      if (nofileLine) {
        const parts = nofileLine.split(/\s{2,}/);
        const softLimit = parseInt(parts[1], 10);
        const hardLimit = parseInt(parts[2], 10);
        if (softLimit < 65536) {
          logger.log('warn', `File descriptor soft limit is ${softLimit} (hard: ${hardLimit}). ` +
            `For production use, set --ulimit nofile=65536:65536 on the container runtime.`);
        } else {
          logger.log('info', `File descriptor limits: soft=${softLimit}, hard=${hardLimit}`);
        }
      }
    } catch {
      // Non-Linux or /proc not available — silently skip
    }
  }
  
  /**
   * Log comprehensive startup summary
   */
  private logStartupSummary(): void {
    logger.log('info', 'DcRouter Started Successfully');

    // Metrics summary
    if (this.metricsManager) {
      logger.log('info', 'Metrics Service: SmartMetrics active, SmartProxy stats active, real-time tracking enabled');
    }

    // SmartProxy summary
    if (this.smartProxy) {
      const routeCount = this.options.smartProxyConfig?.routes?.length || 0;
      const acmeEnabled = this.options.smartProxyConfig?.acme?.enabled || false;
      const acmeMode = acmeEnabled
        ? `email=${this.options.smartProxyConfig!.acme!.email || 'not set'}, mode=${this.options.smartProxyConfig!.acme!.useProduction ? 'production' : 'staging'}`
        : 'disabled';
      logger.log('info', `SmartProxy Service: ${routeCount} routes, ACME: ${acmeMode}`);
    }

    // Email service summary
    if (this.emailServer && this.options.emailConfig) {
      const ports = this.options.emailConfig.ports || [];
      const domainCount = this.options.emailConfig.domains?.length || 0;
      const domainNames = this.options.emailConfig.domains?.map(d => `${d.domain} (${d.dnsMode || 'default'})`).join(', ') || 'none';
      logger.log('info', `Email Service: ports=[${ports.join(', ')}], hostname=${this.options.emailConfig.hostname || 'localhost'}, domains=${domainCount} [${domainNames}], DKIM initialized`);
    }

    // DNS service summary
    if (this.dnsServer && this.options.dnsNsDomains && this.options.dnsScopes) {
      logger.log('info', `DNS Service: nameservers=[${this.options.dnsNsDomains.join(', ')}], authoritative for ${this.options.dnsScopes.length} domains [${this.options.dnsScopes.join(', ')}], UDP:53, DoH enabled`);
    }

    // RADIUS service summary
    if (this.radiusServer && this.options.radiusConfig) {
      const vlanStats = this.radiusServer.getVlanManager().getStats();
      logger.log('info', `RADIUS Service: auth=${this.options.radiusConfig.authPort || 1812}, acct=${this.options.radiusConfig.acctPort || 1813}, clients=${this.options.radiusConfig.clients?.length || 0}, VLANs=${vlanStats.totalMappings}, accounting=${this.options.radiusConfig.accounting?.enabled ? 'enabled' : 'disabled'}`);
    }

    // VPN summary
    if (this.vpnManager && this.options.vpnConfig?.enabled) {
      const subnet = this.vpnManager.getSubnet();
      const wgPort = this.options.vpnConfig.wgListenPort ?? 51820;
      const clientCount = this.vpnManager.listClients().length;
      logger.log('info', `VPN Service: subnet=${subnet}, wg=:${wgPort}, clients=${clientCount}`);
    }

    // Remote Ingress summary
    if (this.tunnelManager && this.options.remoteIngressConfig?.enabled) {
      const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0;
      const connectedCount = this.tunnelManager.getConnectedCount();
      logger.log('info', `Remote Ingress: tunnel port=${this.options.remoteIngressConfig.tunnelPort || 8443}, edges=${edgeCount} registered/${connectedCount} connected`);
    }

    // Database summary
    if (this.dcRouterDb) {
      logger.log('info', `Database: ${this.dcRouterDb.isEmbedded() ? 'embedded' : 'external'}, db=${this.dcRouterDb.getDbName()}, cleaner=${this.cacheCleaner?.isActive() ? 'active' : 'inactive'} (${(this.options.dbConfig?.cleanupIntervalHours || 1)}h interval)`);
    }

    // Service status summary from ServiceManager
    const health = this.serviceManager.getHealth();
    const statuses = health.services;
    const running = statuses.filter(s => s.state === 'running').length;
    const failed = statuses.filter(s => s.state === 'failed').length;
    const retrying = statuses.filter(s => s.state === 'starting' || s.state === 'degraded').length;

    if (failed > 0) {
      const failedNames = statuses.filter(s => s.state === 'failed').map(s => `${s.name}: ${s.lastError || 'unknown'}`);
      logger.log('warn', `DcRouter started in degraded mode — ${running} running, ${failed} failed: ${failedNames.join('; ')}`);
    } else if (retrying > 0) {
      logger.log('info', `DcRouter started — ${running} running, ${retrying} still initializing`);
    } else {
      logger.log('info', `All ${running} services are running`);
    }
  }
  
  /**
   * Set up the unified database (smartdata + LocalSmartDb or external MongoDB)
   */
  private async setupDcRouterDb(): Promise<void> {
    logger.log('info', 'Setting up DcRouterDb...');

    const dbConfig = this.options.dbConfig || {};

    // Initialize DcRouterDb singleton
    this.dcRouterDb = DcRouterDb.getInstance({
      mongoDbUrl: dbConfig.mongoDbUrl,
      storagePath: dbConfig.storagePath || this.resolvedPaths.defaultTsmDbPath,
      dbName: dbConfig.dbName || 'dcrouter',
      debug: false,
    });

    await this.dcRouterDb.start();

    // Start the cache cleaner for TTL-based document cleanup
    const cleanupIntervalMs = (dbConfig.cleanupIntervalHours || 1) * 60 * 60 * 1000;
    this.cacheCleaner = new CacheCleaner(this.dcRouterDb, {
      intervalMs: cleanupIntervalMs,
      verbose: false,
    });
    this.cacheCleaner.start();

    logger.log('info', `DcRouterDb ready (${this.dcRouterDb.isEmbedded() ? 'embedded' : 'external'})`);
  }

  /**
   * Set up SmartProxy with direct configuration and automatic email routes
   */
  private async setupSmartProxy(): Promise<void> {
    logger.log('info', 'Setting up SmartProxy...');

    // Clean up any existing SmartProxy instance (e.g. from a retry)
    if (this.smartProxy) {
      this.smartProxy.removeAllListeners();
      this.smartProxy = undefined;
    }

    let routes: plugins.smartproxy.IRouteConfig[] = [];
    let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
    
    // If user provides full SmartProxy config, use it directly
    if (this.options.smartProxyConfig) {
      routes = this.options.smartProxyConfig.routes || [];
      acmeConfig = this.options.smartProxyConfig.acme;
      logger.log('info', `Found ${routes.length} routes in config, ACME config present: ${!!acmeConfig}`);
    }
    
    // If email config exists, automatically add email routes
    if (this.options.emailConfig) {
      const emailRoutes = this.generateEmailRoutes(this.options.emailConfig);
      logger.log('debug', 'Email routes generated', { routes: JSON.stringify(emailRoutes) });
      routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
    }
    
    // If DNS is configured, add DNS routes
    if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
      const dnsRoutes = this.generateDnsRoutes();
      logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(dnsRoutes) });
      routes = [...routes, ...dnsRoutes];
    }
    
    // Merge TLS/ACME configuration if provided at root level
    if (this.options.tls && !acmeConfig) {
      acmeConfig = {
        accountEmail: this.options.tls.contactEmail,
        enabled: true,
        useProduction: true,
        autoRenew: true,
        renewThresholdDays: 30
      };
    }
    
    // Configure DNS challenge if available
    let challengeHandlers: any[] = [];
    if (this.options.dnsChallenge?.cloudflareApiKey) {
      logger.log('info', 'Configuring Cloudflare DNS challenge for ACME');
      const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey);
      const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount);
      challengeHandlers.push(dns01Handler);
    }
    
    // HTTP/3 augmentation (enabled by default unless explicitly disabled)
    if (this.options.http3?.enabled !== false) {
      const http3Config: IHttp3Config = { enabled: true, ...this.options.http3 };
      routes = augmentRoutesWithHttp3(routes, http3Config);
      logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
    }

    // Cache constructor routes for RouteConfigManager (without VPN security baked in —
    // applyRoutes() injects VPN security dynamically so it stays current with client changes)
    this.constructorRoutes = [...routes];

    // If we have routes or need a basic SmartProxy instance, create it
    if (routes.length > 0 || this.options.smartProxyConfig) {
      logger.log('info', 'Setting up SmartProxy with combined configuration');

      // Track cert entries loaded from cert store so we can populate certificateStatusMap after start
      const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];

      // Create SmartProxy configuration with sensible gateway defaults.
      // User's smartProxyConfig overrides these defaults via spread.
      const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
        // --- dcrouter gateway defaults ---
        maxConnectionsPerIP: 100,
        connectionRateLimitPerMinute: 600,
        socketTimeout: 120_000,
        inactivityTimeout: 120_000,
        keepAlive: true,
        noDelay: true,
        gracefulShutdownTimeout: 30_000,
        // --- user overrides ---
        ...this.options.smartProxyConfig,
        // --- deep-merge defaults.security so user can override maxConnections ---
        defaults: {
          ...this.options.smartProxyConfig?.defaults,
          security: {
            maxConnections: 50_000,
            ...this.options.smartProxyConfig?.defaults?.security,
          },
        },
        // --- always set by dcrouter (after spread) ---
        routes,
        acme: acmeConfig,
        certStore: {
          loadAll: async () => {
            const docs = await ProxyCertDoc.findAll();
            const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = [];
            for (const doc of docs) {
              certs.push({ domain: doc.domain, publicKey: doc.publicKey, privateKey: doc.privateKey, ca: doc.ca });
              loadedCertEntries.push({ domain: doc.domain, publicKey: doc.publicKey, validUntil: doc.validUntil, validFrom: doc.validFrom });
            }
            return certs;
          },
          save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => {
            let validUntil: number | undefined;
            let validFrom: number | undefined;
            try {
              const x509 = new plugins.crypto.X509Certificate(publicKey);
              validUntil = new Date(x509.validTo).getTime();
              validFrom = new Date(x509.validFrom).getTime();
            } catch { /* PEM parsing failed */ }
            let doc = await ProxyCertDoc.findByDomain(domain);
            if (!doc) {
              doc = new ProxyCertDoc();
              doc.domain = domain;
            }
            doc.publicKey = publicKey;
            doc.privateKey = privateKey;
            doc.ca = ca || '';
            doc.validUntil = validUntil || 0;
            doc.validFrom = validFrom || 0;
            await doc.save();
          },
          remove: async (domain: string) => {
            const doc = await ProxyCertDoc.findByDomain(domain);
            if (doc) {
              await doc.delete();
            }
          },
        },
      };
      
      // Initialize cert provision scheduler
      this.certProvisionScheduler = new CertProvisionScheduler();

      // If we have DNS challenge handlers, create SmartAcme instance and wire certProvisionFunction
      // Note: SmartAcme.start() is NOT called here — it runs as a separate optional service
      // via the ServiceManager, with aggressive retry for rate-limit resilience.
      if (challengeHandlers.length > 0) {
        // Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
        if (this.smartAcme) {
          this.smartAcmeReady = false;
          await this.smartAcme.stop().catch(err =>
            logger.log('error', 'Error stopping old SmartAcme', { error: String(err) })
          );
        }
        this.smartAcme = new plugins.smartacme.SmartAcme({
          accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
          certManager: new StorageBackedCertManager(),
          environment: 'production',
          challengeHandlers: challengeHandlers,
          challengePriority: ['dns-01'],
        });

        const scheduler = this.certProvisionScheduler;
        smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
          // If SmartAcme is not yet ready (still starting or retrying), fall back to HTTP-01
          if (!this.smartAcmeReady) {
            eventComms.warn(`SmartAcme not yet initialized, falling back to http-01 for ${domain}`);
            return 'http01';
          }

          // Check backoff before attempting provision
          if (await scheduler.isInBackoff(domain)) {
            const info = await scheduler.getBackoffInfo(domain);
            const msg = `Domain ${domain} is in backoff (${info?.failures} failures), retry after ${info?.retryAfter}`;
            eventComms.warn(msg);
            throw new Error(msg);
          }

          try {
            // smartacme v9 handles concurrency, per-domain dedup, and rate limiting internally
            eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
            eventComms.setSource('smartacme-dns-01');
            const isWildcardDomain = domain.startsWith('*.');
            const cert = await this.smartAcme!.getCertificateForDomain(domain, {
              includeWildcard: !isWildcardDomain,
            });
            // Parse real X509 expiry from PEM (defense-in-depth over SmartAcme's estimate)
            let realValidUntil = cert.validUntil;
            if (cert.publicKey) {
              try {
                const x509 = new plugins.crypto.X509Certificate(cert.publicKey);
                realValidUntil = new Date(x509.validTo).getTime();
              } catch { /* fallback to SmartAcme's value */ }
            }
            if (realValidUntil) {
              eventComms.setExpiryDate(new Date(realValidUntil));
            }
            const result = {
              id: cert.id,
              domainName: cert.domainName,
              created: cert.created,
              validUntil: realValidUntil,
              privateKey: cert.privateKey,
              publicKey: cert.publicKey,
              csr: cert.csr,
            };

            // Success — clear any backoff
            await scheduler.clearBackoff(domain);
            return result;
          } catch (err: unknown) {
            // Record failure for backoff tracking
            await scheduler.recordFailure(domain, (err as Error).message);
            eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${(err as Error).message}, falling back to http-01`);
            return 'http01';
          }
        };
      }
      
      // When remoteIngress is enabled, the hub binary forwards tunneled connections
      // to SmartProxy with PROXY protocol v1 headers to preserve client IPs.
      if (this.options.remoteIngressConfig?.enabled) {
        smartProxyConfig.acceptProxyProtocol = true;
        smartProxyConfig.proxyIPs = ['127.0.0.1'];
      }

      // VPN uses socket mode with PP v2 — SmartProxy must accept proxy protocol from localhost
      if (this.options.vpnConfig?.enabled) {
        smartProxyConfig.acceptProxyProtocol = true;
        if (!smartProxyConfig.proxyIPs) {
          smartProxyConfig.proxyIPs = [];
        }
        if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
          smartProxyConfig.proxyIPs.push('127.0.0.1');
        }
      }

      // Create SmartProxy instance
      logger.log('info', `Creating SmartProxy instance: routes=${smartProxyConfig.routes?.length}, acme=${smartProxyConfig.acme?.enabled}, certProvisionFunction=${!!smartProxyConfig.certProvisionFunction}`);
      
      this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
      
      // Set up event listeners
      this.smartProxy.on('error', (err) => {
        logger.log('error', `SmartProxy error: ${err.message}`, { stack: err.stack });
      });
      
      // Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
      // Events are keyed by domain for domain-centric certificate tracking
      this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
        logger.log('info', `Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
        const routeNames = this.findRouteNamesForDomain(event.domain);
        this.certificateStatusMap.set(event.domain, {
          status: 'valid', routeNames,
          expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
          source: event.source,
        });
      });

      this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
        logger.log('info', `Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
        const routeNames = this.findRouteNamesForDomain(event.domain);
        this.certificateStatusMap.set(event.domain, {
          status: 'valid', routeNames,
          expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
          source: event.source,
        });
      });

      this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
        logger.log('error', `Certificate failed for ${event.domain} (${event.source}): ${event.error}`);
        const routeNames = this.findRouteNamesForDomain(event.domain);
        this.certificateStatusMap.set(event.domain, {
          status: 'failed', routeNames, error: event.error,
          source: event.source,
        });
      });
      
      // Start SmartProxy
      logger.log('info', 'Starting SmartProxy...');
      await this.smartProxy.start();
      logger.log('info', 'SmartProxy started successfully');

      // Populate certificateStatusMap for certs loaded from store at startup
      for (const entry of loadedCertEntries) {
        if (!this.certificateStatusMap.has(entry.domain)) {
          const routeNames = this.findRouteNamesForDomain(entry.domain);
          let expiryDate: string | undefined;
          let issuedAt: string | undefined;

          // Use validUntil/validFrom from stored proxy-certs data if available
          if (entry.validUntil) {
            expiryDate = new Date(entry.validUntil).toISOString();
          }
          if (entry.validFrom) {
            issuedAt = new Date(entry.validFrom).toISOString();
          }

          // Try SmartAcme AcmeCertDoc metadata as secondary source
          if (!expiryDate) {
            try {
              const cleanDomain = entry.domain.replace(/^\*\.?/, '');
              const certDoc = await AcmeCertDoc.findByDomain(cleanDomain);
              if (certDoc?.validUntil) {
                expiryDate = new Date(certDoc.validUntil).toISOString();
              }
              if (certDoc?.created && !issuedAt) {
                issuedAt = new Date(certDoc.created).toISOString();
              }
            } catch { /* no metadata available */ }
          }

          // Fallback: parse X509 from PEM to get expiry
          if (!expiryDate && entry.publicKey) {
            try {
              const x509 = new plugins.crypto.X509Certificate(entry.publicKey);
              expiryDate = new Date(x509.validTo).toISOString();
              if (!issuedAt) {
                issuedAt = new Date(x509.validFrom).toISOString();
              }
            } catch { /* PEM parsing failed */ }
          }

          this.certificateStatusMap.set(entry.domain, {
            status: 'valid',
            routeNames,
            expiryDate,
            issuedAt,
            source: 'cert-store',
          });
        }
      }
      if (loadedCertEntries.length > 0) {
        logger.log('info', `Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
      }

      logger.log('info', `SmartProxy started with ${routes.length} routes`);
    }
  }
  
  

  /**
   * Generate SmartProxy routes for email configuration
   */
  private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): plugins.smartproxy.IRouteConfig[] {
    const emailRoutes: plugins.smartproxy.IRouteConfig[] = [];
    
    // 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 = 'passthrough';
      
      // Handle different email ports differently
      switch (port) {
        case 25: // SMTP
          routeName = 'smtp-route';
          tlsMode = 'passthrough'; // STARTTLS handled by email server
          break;
          
        case 587: // Submission
          routeName = 'submission-route';
          tlsMode = 'passthrough'; // STARTTLS handled by email server
          break;
          
        case 465: // SMTPS
          routeName = 'smtps-route';
          tlsMode = 'terminate'; // Terminate TLS and re-encrypt to email server
          break;
          
        default:
          routeName = `email-port-${port}-route`;
          tlsMode = 'passthrough';
          
          // Check if we have specific settings for this port
          if (this.options.emailPortConfig?.portSettings && 
              this.options.emailPortConfig.portSettings[port]) {
            const portSettings = this.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.options.emailPortConfig?.portMapping || defaultPortMapping;
      const internalPort = portMapping[port] || port + 10000;

      let action: any = {
        type: 'forward',
        targets: [{
          host: 'localhost', // Forward to internal email server
          port: internalPort
        }],
        tls: {
          mode: tlsMode as any
        }
      };
      
      // For TLS terminate mode, add certificate info
      if (tlsMode === 'terminate' && action.tls) {
        action.tls.certificate = 'auto';
      }
      
      // Create the route configuration
      const routeConfig: plugins.smartproxy.IRouteConfig = {
        name: routeName,
        match: {
          ports: [port]
        },
        action: action
      };
      
      // Add the route to our list
      emailRoutes.push(routeConfig);
    }
    
    return emailRoutes;
  }
  
  /**
   * Generate SmartProxy routes for DNS configuration
   */
  private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] {
    if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
      return [];
    }
    
    const dnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
    
    // Create routes for DNS-over-HTTPS paths
    const dohPaths = ['/dns-query', '/resolve'];
    
    // Use the first nameserver domain for DoH routes
    const primaryNameserver = this.options.dnsNsDomains[0];
    
    for (const path of dohPaths) {
      const dohRoute: plugins.smartproxy.IRouteConfig = {
        name: `dns-over-https-${path.replace('/', '')}`,
        match: {
          ports: [443], // HTTPS port for DoH
          domains: [primaryNameserver],
          path: path
        },
        action: {
          type: 'socket-handler' as any,
          socketHandler: this.createDnsSocketHandler()
        } as any
      };
      
      dnsRoutes.push(dohRoute);
    }
    
    return dnsRoutes;
  }

  /**
   * Check if a domain matches a pattern (including wildcard support)
   * @param domain The domain to check
   * @param pattern The pattern to match against (e.g., "*.example.com")
   * @returns Whether the domain matches the pattern
   */
  private isDomainMatch(domain: string, pattern: string): boolean {
    domain = domain.toLowerCase();
    pattern = pattern.toLowerCase();

    if (domain === pattern) return true;

    // Routing-glob: *example.com matches example.com, sub.example.com, *.example.com
    if (pattern.startsWith('*') && !pattern.startsWith('*.')) {
      const baseDomain = pattern.slice(1); // *nevermind.cloud → nevermind.cloud
      if (domain === baseDomain || domain === `*.${baseDomain}`) return true;
      if (domain.endsWith(baseDomain) && domain.length > baseDomain.length) return true;
    }

    // Standard wildcard: *.example.com matches sub.example.com and example.com
    if (pattern.startsWith('*.')) {
      const suffix = pattern.slice(2);
      if (domain === suffix) return true;
      return domain.endsWith(suffix) && domain.length > suffix.length;
    }

    return false;
  }

  /**
   * Find ALL route names that match a given domain
   */
  public findRouteNamesForDomain(domain: string): string[] {
    if (!this.smartProxy) return [];
    const names: string[] = [];
    for (const route of this.smartProxy.routeManager.getRoutes()) {
      if (!route.match.domains || !route.name) continue;
      const routeDomains = Array.isArray(route.match.domains)
        ? route.match.domains
        : [route.match.domains];
      for (const pattern of routeDomains) {
        if (this.isDomainMatch(domain, pattern)) {
          names.push(route.name);
          break; // This route already matched, no need to check other patterns
        }
      }
    }
    return names;
  }

  /**
   * Get the routes derived from constructor config (smartProxy + email + DNS).
   * Used by RouteConfigManager as the "hardcoded" base.
   */
  public getConstructorRoutes(): plugins.smartproxy.IRouteConfig[] {
    return this.constructorRoutes;
  }

  public async stop() {
    logger.log('info', 'Stopping DcRouter services...');

    // Unsubscribe from service events before stopping services
    if (this.serviceSubjectSubscription) {
      this.serviceSubjectSubscription.unsubscribe();
      this.serviceSubjectSubscription = undefined;
    }

    // ServiceManager handles reverse-dependency-ordered shutdown
    await this.serviceManager.stop();

    // Clear backoff cache in cert scheduler
    if (this.certProvisionScheduler) {
      this.certProvisionScheduler.clear();
      this.certProvisionScheduler = undefined;
    }

    this.certificateStatusMap.clear();

    // Reset security singletons to allow GC
    SecurityLogger.resetInstance();
    ContentScanner.resetInstance();
    IPReputationChecker.resetInstance();

    logger.log('info', 'All DcRouter services stopped');
  }

  /**
   * Update SmartProxy configuration
   * @param config New SmartProxy configuration
   */
  public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
    // Stop existing SmartProxy if running
    if (this.smartProxy) {
      this.smartProxy.removeAllListeners();
      await this.smartProxy.stop();
      this.smartProxy = undefined;
    }

    // Update configuration
    this.options.smartProxyConfig = config;

    // Update routes on RemoteIngressManager so derived ports stay in sync
    if (this.remoteIngressManager && config.routes) {
      this.remoteIngressManager.setRoutes(config.routes as any[]);
    }

    // Start new SmartProxy with updated configuration (will include email routes if configured)
    await this.setupSmartProxy();

    // Re-apply programmatic routes and overrides after SmartProxy restart
    if (this.routeConfigManager) {
      await this.routeConfigManager.initialize();
    }

    logger.log('info', 'SmartProxy configuration updated');
  }
  
  
  
  /**
   * Set up unified email handling with pattern-based routing
   * This implements the consolidated emailConfig approach
   */
  private async setupUnifiedEmailHandling(): Promise<void> {
    if (!this.options.emailConfig) {
      throw new Error('Email configuration is required for unified email handling');
    }

    // Apply port mapping if behind SmartProxy
    const portMapping = this.options.emailPortConfig?.portMapping || {
      25: 10025,   // SMTP
      587: 10587,  // Submission
      465: 10465   // SMTPS
    };
    
    // Transform domains if they are provided as strings
    let transformedDomains = this.options.emailConfig.domains;
    if (transformedDomains && transformedDomains.length > 0) {
      // Check if domains are strings (for backward compatibility)
      if (typeof transformedDomains[0] === 'string') {
        transformedDomains = (transformedDomains as any).map((domain: string) => ({
          domain,
          dnsMode: 'external-dns' as const,
          dkim: {
            selector: 'default',
            keySize: 2048,
            rotateKeys: false,
            rotationInterval: 90
          }
        }));
      }
    }
    
    // Create config with mapped ports
    const emailConfig: IUnifiedEmailServerOptions = {
      ...this.options.emailConfig,
      domains: transformedDomains,
      ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
      hostname: 'localhost' // Listen on localhost for SmartProxy forwarding
    };
    
    // Create unified email server
    this.emailServer = new UnifiedEmailServer(this, emailConfig);
    
    // Set up error handling
    this.emailServer.on('error', (err: Error) => {
      logger.log('error', `UnifiedEmailServer error: ${err.message}`);
    });
    
    // Start the server
    await this.emailServer.start();

    // Wire delivery events to MetricsManager and logger
    if (this.metricsManager && this.emailServer.deliverySystem) {
      this.emailServer.deliverySystem.on('deliveryStart', (item: any) => {
        this.metricsManager!.trackEmailReceived(item?.from);
        logger.log('info', `Email delivery started: ${item?.from} → ${item?.to}`, { zone: 'email' });
      });
      this.emailServer.deliverySystem.on('deliverySuccess', (item: any) => {
        this.metricsManager!.trackEmailSent(item?.to);
        logger.log('info', `Email delivered to ${item?.to}`, { zone: 'email' });
      });
      this.emailServer.deliverySystem.on('deliveryFailed', (item: any, error: any) => {
        this.metricsManager!.trackEmailFailed(item?.to, error?.message);
        logger.log('warn', `Email delivery failed to ${item?.to}: ${error?.message}`, { zone: 'email' });
      });
    }
    if (this.metricsManager && this.emailServer) {
      this.emailServer.on('bounceProcessed', () => {
        this.metricsManager!.trackEmailBounced();
        logger.log('warn', 'Email bounce processed', { zone: 'email' });
      });
    }

    logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
  }
  
  /**
   * Update the unified email configuration
   * @param config New email configuration
   */
  public async updateEmailConfig(config: IUnifiedEmailServerOptions): Promise<void> {
    // Stop existing email components
    await this.stopUnifiedEmailComponents();
    
    // Update configuration
    this.options.emailConfig = config;
    
    // Start email handling with new configuration
    await this.setupUnifiedEmailHandling();
    
    logger.log('info', 'Unified email configuration updated');
  }
  
  /**
   * Stop all unified email components
   */
  private async stopUnifiedEmailComponents(): Promise<void> {
    try {
      // Stop the unified email server which contains all components
      if (this.emailServer) {
        // Remove listeners before stopping to prevent leaks on config update cycles
        if ((this.emailServer as any).deliverySystem) {
          (this.emailServer as any).deliverySystem.removeAllListeners();
        }
        this.emailServer.removeAllListeners();
        await this.emailServer.stop();
        logger.log('info', 'Unified email server stopped');
        this.emailServer = undefined;
      }
      
      logger.log('info', 'All unified email components stopped');
    } catch (error: unknown) {
      logger.log('error', `Error stopping unified email components: ${(error as Error).message}`);
      throw error;
    }
  }

  /**
   * Update domain rules for email routing
   * @param rules New domain rules to apply
   */
  public async updateEmailRoutes(routes: IEmailRoute[]): Promise<void> {
    // Validate that email config exists
    if (!this.options.emailConfig) {
      throw new Error('Email configuration is required before updating routes');
    }
    
    // Update the configuration
    this.options.emailConfig.routes = routes;
    
    // Update the unified email server if it exists
    if (this.emailServer) {
      this.emailServer.updateEmailRoutes(routes);
    }
    
    logger.log('info', `Email routes updated with ${routes.length} routes`);
  }
  
  /**
   * Get statistics from all components
   */
  public getStats(): any {
    const stats: any = {
      emailServer: this.emailServer?.getStats()
    };
    
    return stats;
  }
  
  /**
   * Register DNS records with the DNS server
   * @param records Array of DNS records to register
   */
  private registerDnsRecords(records: Array<{name: string; type: string; value: string; ttl?: number}>): void {
    if (!this.dnsServer) return;
    
    // Register a separate handler for each record
    // This ensures multiple records of the same type (like NS records) are all served
    for (const record of records) {
      // Register handler for this specific record
      this.dnsServer.registerHandler(record.name, [record.type], (question) => {
        // Check if this handler matches the question
        if (question.name === record.name && question.type === record.type) {
          return {
            name: record.name,
            type: record.type,
            class: 'IN',
            ttl: record.ttl || 300,
            data: this.parseDnsRecordData(record.type, record.value)
          };
        }
        
        return null;
      });
    }
    
    logger.log('info', `Registered ${records.length} DNS handlers (one per record)`);
  }
  
  /**
   * Parse DNS record data based on record type
   * @param type DNS record type
   * @param value DNS record value
   * @returns Parsed data for the DNS response
   */
  private parseDnsRecordData(type: string, value: string): any {
    switch (type) {
      case 'A':
        return value; // IP address as string
      case 'MX':
        const [priority, exchange] = value.split(' ');
        return { priority: parseInt(priority), exchange };
      case 'TXT':
        return value;
      case 'NS':
        return value;
      case 'SOA':
        // SOA format: primary-ns admin-email serial refresh retry expire minimum
        const parts = value.split(' ');
        return {
          mname: parts[0],
          rname: parts[1],
          serial: parseInt(parts[2]),
          refresh: parseInt(parts[3]),
          retry: parseInt(parts[4]),
          expire: parseInt(parts[5]),
          minimum: parseInt(parts[6])
        };
      default:
        return value;
    }
  }
  
  /**
   * Set up DNS server with socket handler for DoH
   */
  private async setupDnsWithSocketHandler(): Promise<void> {
    if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
      throw new Error('dnsNsDomains is required for DNS server setup');
    }
    
    if (!this.options.dnsScopes || this.options.dnsScopes.length === 0) {
      throw new Error('dnsScopes is required for DNS server setup');
    }
    
    const primaryNameserver = this.options.dnsNsDomains[0];
    logger.log('info', `Setting up DNS server with primary nameserver: ${primaryNameserver}`);
    
    // Get VM IP address for UDP binding
    const networkInterfaces = plugins.os.networkInterfaces();
    let vmIpAddress = '0.0.0.0'; // Default to all interfaces
    
    // Try to find the VM's internal IP address
    for (const [_name, interfaces] of Object.entries(networkInterfaces)) {
      if (interfaces) {
        for (const iface of interfaces) {
          if (!iface.internal && iface.family === 'IPv4') {
            vmIpAddress = iface.address;
            break;
          }
        }
      }
    }
    
    // Create DNS server instance with manual HTTPS mode
    this.dnsServer = new plugins.smartdns.dnsServerMod.DnsServer({
      udpPort: 53,
      udpBindInterface: vmIpAddress,
      httpsPort: 443, // Required but won't bind due to manual mode
      manualHttpsMode: true, // Enable manual HTTPS socket handling
      dnssecZone: primaryNameserver,
      primaryNameserver: primaryNameserver, // Automatically generates correct SOA records
      // For now, use self-signed cert until we integrate with Let's Encrypt
      httpsKey: '',
      httpsCert: ''
    });
    
    // Start the DNS server (UDP only)
    await this.dnsServer.start();
    logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);

    // Wire DNS query events to MetricsManager and logger with adaptive rate limiting
    if (this.metricsManager && this.dnsServer) {
      const flushDnsBatch = () => {
        if (this.dnsBatchCount > 0) {
          logger.log('info', `DNS: ${this.dnsBatchCount} queries processed (rate limited)`, { zone: 'dns' });
          this.dnsBatchCount = 0;
        }
        this.dnsBatchTimer = null;
      };

      this.dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
        // Metrics tracking
        for (const question of event.questions) {
          this.metricsManager?.trackDnsQuery(
            question.type,
            question.name,
            false,
            event.responseTimeMs,
            event.answered,
          );
        }

        // Adaptive logging: individual logs up to 2/sec, then batch
        const nowSec = Math.floor(Date.now() / 1000);
        if (nowSec !== this.dnsLogWindowSecond) {
          this.dnsLogWindowSecond = nowSec;
          this.dnsLogWindowCount = 0;
        }

        if (this.dnsLogWindowCount < 2) {
          this.dnsLogWindowCount++;
          const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
          logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
        } else {
          this.dnsBatchCount++;
          if (!this.dnsBatchTimer) {
            this.dnsBatchTimer = setTimeout(flushDnsBatch, 5000);
          }
        }
      });
    }
    
    // Validate DNS configuration
    await this.validateDnsConfiguration();
    
    // Generate and register authoritative records
    const authoritativeRecords = await this.generateAuthoritativeRecords();
    
    // Generate email DNS records
    const emailDnsRecords = await this.generateEmailDnsRecords();
    
    // Initialize DKIM for all email domains
    await this.initializeDkimForEmailDomains();
    
    // Load DKIM records from JSON files (they should now exist)
    const dkimRecords = await this.loadDkimRecords();
    
    // Combine all records: authoritative, email, DKIM, and user-defined
    const allRecords = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
    if (this.options.dnsRecords && this.options.dnsRecords.length > 0) {
      allRecords.push(...this.options.dnsRecords);
    }
    
    // Apply proxy IP replacement if configured
    await this.applyProxyIpReplacement(allRecords);
    
    // Register all DNS records
    if (allRecords.length > 0) {
      this.registerDnsRecords(allRecords);
      logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${this.options.dnsRecords?.length || 0} user-defined)`);
    }
  }
  
  /**
   * Create DNS socket handler for DoH
   */
  private createDnsSocketHandler(): (socket: plugins.net.Socket) => Promise<void> {
    return async (socket: plugins.net.Socket) => {
      if (!this.dnsServer) {
        logger.log('error', 'DNS socket handler called but DNS server not initialized');
        socket.end();
        return;
      }
      
      // Prevent uncaught exception from socket 'error' events
      socket.on('error', (err) => {
        logger.log('error', `DNS socket error: ${err.message}`);
        if (!socket.destroyed) {
          socket.destroy();
        }
      });

      logger.log('debug', 'DNS socket handler: passing socket to DnsServer');

      try {
        // Use the built-in socket handler from smartdns
        // This handles HTTP/2, DoH protocol, etc.
        await (this.dnsServer as any).handleHttpsSocket(socket);
      } catch (error: unknown) {
        logger.log('error', `DNS socket handler error: ${(error as Error).message}`);
        if (!socket.destroyed) {
          socket.destroy();
        }
      }
    };
  }
  
  /**
   * Validate DNS configuration
   */
  private async validateDnsConfiguration(): Promise<void> {
    if (!this.options.dnsNsDomains || !this.options.dnsScopes) {
      return;
    }
    
    logger.log('info', 'Validating DNS configuration...');
    
    // Check if email domains with internal-dns are in dnsScopes
    if (this.options.emailConfig?.domains) {
      for (const domainConfig of this.options.emailConfig.domains) {
        if (domainConfig.dnsMode === 'internal-dns' && 
            !this.options.dnsScopes.includes(domainConfig.domain)) {
          logger.log('warn', `Email domain '${domainConfig.domain}' with internal-dns mode is not in dnsScopes. It should be added to dnsScopes.`);
        }
      }
    }
    
    // Validate user-provided DNS records are within scopes
    if (this.options.dnsRecords) {
      for (const record of this.options.dnsRecords) {
        const recordDomain = this.extractDomain(record.name);
        const isInScope = this.options.dnsScopes.some(scope => 
          recordDomain === scope || recordDomain.endsWith(`.${scope}`)
        );
        
        if (!isInScope) {
          logger.log('warn', `DNS record for '${record.name}' is outside defined scopes [${this.options.dnsScopes.join(', ')}]`);
        }
      }
    }
  }
  
  /**
   * Generate email DNS records for domains with internal-dns mode
   */
  private async generateEmailDnsRecords(): Promise<Array<{name: string; type: string; value: string; ttl?: number}>> {
    const records: Array<{name: string; type: string; value: string; ttl?: number}> = [];
    
    if (!this.options.emailConfig?.domains) {
      return records;
    }
    
    // Filter domains with internal-dns mode
    const internalDnsDomains = this.options.emailConfig.domains.filter(
      domain => domain.dnsMode === 'internal-dns'
    );
    
    for (const domainConfig of internalDnsDomains) {
      const domain = domainConfig.domain;
      const ttl = domainConfig.dns?.internal?.ttl || 3600;
      const mxPriority = domainConfig.dns?.internal?.mxPriority || 10;
      
      // MX record - points to the domain itself for email handling
      records.push({
        name: domain,
        type: 'MX',
        value: `${mxPriority} ${domain}`,
        ttl
      });
      
      // SPF record - using sensible defaults
      const spfRecord = 'v=spf1 a mx ~all';
      records.push({
        name: domain,
        type: 'TXT',
        value: spfRecord,
        ttl
      });
      
      // DMARC record - using sensible defaults
      const dmarcPolicy = 'none'; // Start with 'none' policy for monitoring
      const dmarcEmail = `dmarc@${domain}`;
      records.push({
        name: `_dmarc.${domain}`,
        type: 'TXT',
        value: `v=DMARC1; p=${dmarcPolicy}; rua=mailto:${dmarcEmail}`,
        ttl
      });
      
      // Note: DKIM records will be generated later when DKIM keys are available
      // They require the DKIMCreator which is part of the email server
    }
    
    logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`);
    return records;
  }
  
  /**
   * Load DKIM records from JSON files
   * Reads all *.dkimrecord.json files from the DNS records directory
   */
  private async loadDkimRecords(): Promise<Array<{name: string; type: string; value: string; ttl?: number}>> {
    const records: Array<{name: string; type: string; value: string; ttl?: number}> = [];
    
    try {
      // Ensure paths are imported
      const dnsDir = this.resolvedPaths.dnsRecordsDir;
      
      // Check if directory exists
      if (!plugins.fs.existsSync(dnsDir)) {
        logger.log('debug', 'No DNS records directory found, skipping DKIM record loading');
        return records;
      }
      
      // Read all files in the directory
      const files = plugins.fs.readdirSync(dnsDir);
      const dkimFiles = files.filter(f => f.endsWith('.dkimrecord.json'));
      
      logger.log('info', `Found ${dkimFiles.length} DKIM record files`);
      
      // Load each DKIM record
      for (const file of dkimFiles) {
        try {
          const filePath = plugins.path.join(dnsDir, file);
          const fileContent = plugins.fs.readFileSync(filePath, 'utf8');
          const dkimRecord = JSON.parse(fileContent);
          
          // Validate record structure
          if (dkimRecord.name && dkimRecord.type === 'TXT' && dkimRecord.value) {
            records.push({
              name: dkimRecord.name,
              type: 'TXT',
              value: dkimRecord.value,
              ttl: 3600 // Standard DKIM TTL
            });
            
            logger.log('info', `Loaded DKIM record for ${dkimRecord.name}`);
          } else {
            logger.log('warn', `Invalid DKIM record structure in ${file}`);
          }
        } catch (error: unknown) {
          logger.log('error', `Failed to load DKIM record from ${file}: ${(error as Error).message}`);
        }
      }
    } catch (error: unknown) {
      logger.log('error', `Failed to load DKIM records: ${(error as Error).message}`);
    }

    return records;
  }
  
  /**
   * Initialize DKIM keys for all configured email domains
   * This ensures DKIM records are available immediately at startup
   */
  private async initializeDkimForEmailDomains(): Promise<void> {
    if (!this.options.emailConfig?.domains || !this.emailServer) {
      return;
    }
    
    logger.log('info', 'Initializing DKIM keys for email domains...');
    
    // Get DKIMCreator instance from email server (public in smartmta)
    const dkimCreator = this.emailServer.dkimCreator;
    if (!dkimCreator) {
      logger.log('warn', 'DKIMCreator not available, skipping DKIM initialization');
      return;
    }
    
    // Ensure necessary directories exist
    paths.ensureDataDirectories(this.resolvedPaths);
    
    // Generate DKIM keys for each email domain
    for (const domainConfig of this.options.emailConfig.domains) {
      try {
        // Generate DKIM keys for all domains, regardless of DNS mode
        // This ensures keys are ready even if DNS mode changes later
        await dkimCreator.handleDKIMKeysForDomain(domainConfig.domain);
        logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`);
      } catch (error: unknown) {
        logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${(error as Error).message}`);
      }
    }

    logger.log('info', 'DKIM initialization complete');
  }
  
  /**
   * Generate authoritative DNS records (NS only) for all domains in dnsScopes
   * SOA records are now automatically generated by smartdns with primaryNameserver setting
   */
  private async generateAuthoritativeRecords(): Promise<Array<{name: string; type: string; value: string; ttl?: number}>> {
    const records: Array<{name: string; type: string; value: string; ttl?: number}> = [];
    
    if (!this.options.dnsNsDomains || !this.options.dnsScopes) {
      return records;
    }
    
    // Determine the public IP for nameserver A records
    let publicIp: string | null = null;
    
    // Use proxy IPs if configured (these should be public IPs)
    if (this.options.proxyIps && this.options.proxyIps.length > 0) {
      publicIp = this.options.proxyIps[0]; // Use first proxy IP
      logger.log('info', `Using proxy IP for nameserver A records: ${publicIp}`);
    } else if (this.options.publicIp) {
      // Use explicitly configured public IP
      publicIp = this.options.publicIp;
      this.detectedPublicIp = publicIp;
      logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`);
    } else {
      // Auto-discover public IP using smartnetwork
      try {
        logger.log('info', 'Auto-discovering public IP address...');
        const smartNetwork = new plugins.smartnetwork.SmartNetwork();
        const publicIps = await smartNetwork.getPublicIps();
        
        if (publicIps.v4) {
          publicIp = publicIps.v4;
          this.detectedPublicIp = publicIp;
          logger.log('info', `Auto-discovered public IPv4: ${publicIp}`);
        } else {
          logger.log('warn', 'Could not auto-discover public IPv4 address');
        }
      } catch (error: unknown) {
        logger.log('error', `Failed to auto-discover public IP: ${(error as Error).message}`);
      }

      if (!publicIp) {
        logger.log('warn', 'No public IP available. Nameserver A records require either proxyIps, publicIp, or successful auto-discovery.');
      }
    }
    
    // Generate A records for nameservers if we have a public IP
    if (publicIp) {
      for (const nsDomain of this.options.dnsNsDomains) {
        records.push({
          name: nsDomain,
          type: 'A',
          value: publicIp,
          ttl: 3600
        });
      }
      logger.log('info', `Generated A records for ${this.options.dnsNsDomains.length} nameservers`);
    }
    
    // Generate NS records for each domain in scopes
    for (const domain of this.options.dnsScopes) {
      // Add NS records for all nameservers
      for (const nsDomain of this.options.dnsNsDomains) {
        records.push({
          name: domain,
          type: 'NS',
          value: nsDomain,
          ttl: 3600
        });
      }
      
      // SOA records are now automatically generated by smartdns DnsServer
      // with the primaryNameserver configuration option
    }
    
    logger.log('info', `Generated ${records.length} total records (A + NS) for ${this.options.dnsScopes.length} domains`);
    return records;
  }
  
  /**
   * Extract the base domain from a DNS record name
   */
  private extractDomain(recordName: string): string {
    // Handle wildcards
    if (recordName.startsWith('*.')) {
      recordName = recordName.substring(2);
    }
    return recordName;
  }
  
  /**
   * Apply proxy IP replacement logic to DNS records
   */
  private async applyProxyIpReplacement(records: Array<{name: string; type: string; value: string; ttl?: number; useIngressProxy?: boolean}>): Promise<void> {
    if (!this.options.proxyIps || this.options.proxyIps.length === 0) {
      return; // No proxy IPs configured, skip replacement
    }
    
    // Get server's public IP
    const serverIp = await this.detectServerPublicIp();
    if (!serverIp) {
      logger.log('warn', 'Could not detect server public IP, skipping proxy IP replacement');
      return;
    }
    
    logger.log('info', `Applying proxy IP replacement. Server IP: ${serverIp}, Proxy IPs: ${this.options.proxyIps.join(', ')}`);
    
    let proxyIndex = 0;
    for (const record of records) {
      if (record.type === 'A' && 
          record.value === serverIp && 
          record.useIngressProxy !== false) {
        // Round-robin through proxy IPs
        const proxyIp = this.options.proxyIps[proxyIndex % this.options.proxyIps.length];
        logger.log('info', `Replacing A record for ${record.name}: ${record.value} → ${proxyIp}`);
        record.value = proxyIp;
        proxyIndex++;
      }
    }
  }
  
  /**
   * Detect the server's public IP address
   */
  private async detectServerPublicIp(): Promise<string | null> {
    try {
      const smartNetwork = new plugins.smartnetwork.SmartNetwork();
      const publicIps = await smartNetwork.getPublicIps();
      
      if (publicIps.v4) {
        return publicIps.v4;
      }
      
      return null;
    } catch (error: unknown) {
      logger.log('warn', `Failed to detect public IP: ${(error as Error).message}`);
      return null;
    }
  }
  
  /**
   * Set up Remote Ingress hub for edge tunnel connections
   */
  private async setupRemoteIngress(): Promise<void> {
    if (!this.options.remoteIngressConfig?.enabled) {
      return;
    }

    logger.log('info', 'Setting up Remote Ingress hub...');

    // Initialize the edge registration manager
    this.remoteIngressManager = new RemoteIngressManager();
    await this.remoteIngressManager.initialize();

    // Pass current routes so the manager can derive edge ports from remoteIngress-tagged routes
    const currentRoutes = this.constructorRoutes;
    this.remoteIngressManager.setRoutes(currentRoutes as any[]);

    // Race-condition fix: if ConfigManagers finished before us, re-apply routes
    // so the callback delivers the full merged set (including DB-stored routes)
    // to our newly-created remoteIngressManager.
    if (this.routeConfigManager) {
      await this.routeConfigManager.applyRoutes();
    }

    // Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
    const riCfg = this.options.remoteIngressConfig;
    let tlsConfig: { certPem: string; keyPem: string } | undefined;

    // Priority 1: Explicit cert/key file paths
    if (riCfg.tls?.certPath && riCfg.tls?.keyPath) {
      try {
        const certPem = plugins.fs.readFileSync(riCfg.tls.certPath, 'utf8');
        const keyPem = plugins.fs.readFileSync(riCfg.tls.keyPath, 'utf8');
        tlsConfig = { certPem, keyPem };
        logger.log('info', 'Using explicit TLS cert/key for RemoteIngress tunnel');
      } catch (err: unknown) {
        logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${(err as Error).message}`);
      }
    }

    // Priority 2: Existing cert from SmartProxy cert store for hubDomain
    if (!tlsConfig && riCfg.hubDomain) {
      try {
        const stored = await ProxyCertDoc.findByDomain(riCfg.hubDomain);
        if (stored?.publicKey && stored?.privateKey) {
          tlsConfig = { certPem: stored.publicKey, keyPem: stored.privateKey };
          logger.log('info', `Using stored ACME cert for RemoteIngress tunnel TLS: ${riCfg.hubDomain}`);
        }
      } catch { /* no stored cert, fall through */ }
    }

    if (!tlsConfig) {
      logger.log('info', 'No TLS cert configured for RemoteIngress tunnel — using auto-generated self-signed');
    }

    // Create and start the tunnel manager
    this.tunnelManager = new TunnelManager(this.remoteIngressManager, {
      tunnelPort: riCfg.tunnelPort ?? 8443,
      targetHost: '127.0.0.1',
      tls: tlsConfig,
    });
    await this.tunnelManager.start();

    const edgeCount = this.remoteIngressManager.getAllEdges().length;
    logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
  }

  /**
   * Set up VPN server for VPN-based route access control.
   */
  private async setupVpnServer(): Promise<void> {
    if (!this.options.vpnConfig?.enabled) {
      return;
    }

    logger.log('info', 'Setting up VPN server...');

    this.vpnManager = new VpnManager({
      subnet: this.options.vpnConfig.subnet,
      wgListenPort: this.options.vpnConfig.wgListenPort,
      dns: this.options.vpnConfig.dns,
      serverEndpoint: this.options.vpnConfig.serverEndpoint,
      initialClients: this.options.vpnConfig.clients,
      destinationPolicy: this.options.vpnConfig.destinationPolicy,
      forwardingMode: this.options.vpnConfig.forwardingMode,
      bridgeLanSubnet: this.options.vpnConfig.bridgeLanSubnet,
      bridgePhysicalInterface: this.options.vpnConfig.bridgePhysicalInterface,
      bridgeIpRangeStart: this.options.vpnConfig.bridgeIpRangeStart,
      bridgeIpRangeEnd: this.options.vpnConfig.bridgeIpRangeEnd,
      onClientChanged: () => {
        // Re-apply routes so tag-based ipAllowLists get updated
        this.routeConfigManager?.applyRoutes();
      },
      getClientAllowedIPs: async (clientTags: string[]) => {
        const subnet = this.options.vpnConfig?.subnet || '10.8.0.0/24';
        const ips = new Set<string>([subnet]);

        // Check routes for VPN-gated tag match and collect domains
        const routes = this.options.smartProxyConfig?.routes || [];
        const domainsToResolve = new Set<string>();
        for (const route of routes) {
          const dcRoute = route as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig;
          if (!dcRoute.vpn?.enabled) continue;

          const routeTags = dcRoute.vpn.allowedServerDefinedClientTags;
          if (!routeTags?.length || clientTags.some(t => routeTags.includes(t))) {
            // Collect domains from this route
            const domains = (route.match as any)?.domains;
            if (Array.isArray(domains)) {
              for (const d of domains) {
                // Strip wildcard prefix for DNS resolution (*.example.com → example.com)
                domainsToResolve.add(d.replace(/^\*\./, ''));
              }
            }
          }
        }

        // Resolve DNS A records for matched domains (with caching)
        for (const domain of domainsToResolve) {
          const resolvedIps = await this.resolveVpnDomainIPs(domain);
          for (const ip of resolvedIps) {
            ips.add(`${ip}/32`);
          }
        }

        return [...ips];
      },
    });

    await this.vpnManager.start();

    // Re-apply routes now that VPN clients are loaded — ensures hardcoded routes
    // get correct tag-based ipAllowLists (not possible during setupSmartProxy since
    // VPN server wasn't ready yet)
    this.routeConfigManager?.applyRoutes();
  }

  /** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
  private vpnDomainIpCache = new Map<string, { ips: string[]; expiresAt: number }>();

  /**
   * Resolve a domain's A record(s) for VPN AllowedIPs, with a 5-minute cache.
   */
  private async resolveVpnDomainIPs(domain: string): Promise<string[]> {
    const cached = this.vpnDomainIpCache.get(domain);
    if (cached && cached.expiresAt > Date.now()) {
      return cached.ips;
    }
    try {
      const { promises: dnsPromises } = await import('dns');
      const ips = await dnsPromises.resolve4(domain);
      this.vpnDomainIpCache.set(domain, { ips, expiresAt: Date.now() + 5 * 60 * 1000 });
      return ips;
    } catch (err) {
      logger.log('warn', `VPN: Failed to resolve ${domain} for AllowedIPs: ${(err as Error).message}`);
      return cached?.ips || []; // Return stale cache on failure, or empty
    }
  }

  // VPN security injection is now handled dynamically by RouteConfigManager.applyRoutes()
  // via the getVpnAllowList callback — no longer a separate method here.

  /**
   * Set up RADIUS server for network authentication
   */
  private async setupRadiusServer(): Promise<void> {
    if (!this.options.radiusConfig) {
      return;
    }

    logger.log('info', 'Setting up RADIUS server...');

    this.radiusServer = new RadiusServer(this.options.radiusConfig);
    await this.radiusServer.start();

    logger.log('info', `RADIUS server started on ports ${this.options.radiusConfig.authPort || 1812} (auth) and ${this.options.radiusConfig.acctPort || 1813} (acct)`);
  }

  /**
   * Update RADIUS configuration at runtime
   */
  public async updateRadiusConfig(config: IRadiusServerConfig): Promise<void> {
    // Stop existing RADIUS server if running
    if (this.radiusServer) {
      await this.radiusServer.stop();
      this.radiusServer = undefined;
    }

    // Update configuration
    this.options.radiusConfig = config;

    // Start with new configuration
    await this.setupRadiusServer();

    logger.log('info', 'RADIUS configuration updated');
  }
}

// Re-export email server types for convenience
export type { IUnifiedEmailServerOptions };

// Re-export RADIUS types for convenience
export type { IRadiusServerConfig };

export default DcRouter;
