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, } 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, CacheCleaner, ProxyCertDoc, AcmeCertDoc } from './db/index.js';
// Import migration runner and app version
import { createMigrationRunner } from '../dist_ts_migrations/index.js';
import { commitinfo } from './00_commitinfo_data.js';
import { OpsServer } from './opsserver/index.js';
import { MetricsManager } from './monitoring/index.js';
import { RadiusServer } from './radius/index.js';
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
import { VpnManager } from './vpn/index.js';
import { RouteConfigManager, ApiTokenManager, GatewayClientManager, ReferenceResolver, DbSeeder, TargetProfileManager } from './config/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyManager } from './security/index.js';
import { augmentRoutesWithHttp3 } from './http3/index.js';
import { DnsManager } from './dns/manager.dns.js';
import { AcmeConfigManager } from './acme/manager.acme-config.js';
import { EmailDomainManager, SmartMtaStorageManager, WorkAppMailManager, buildEmailDnsRecords } from './email/index.js';
export class DcRouter {
    options;
    resolvedPaths;
    // Core services
    smartProxy;
    smartAcme;
    dnsServer;
    emailServer;
    radiusServer;
    opsServer;
    metricsManager;
    emailEventSubscriptions = [];
    storageManager;
    // Unified database (smartdata + LocalSmartDb or external MongoDB)
    dcRouterDb;
    cacheCleaner;
    // Remote Ingress
    remoteIngressManager;
    tunnelManager;
    // VPN
    vpnManager;
    // Programmatic config API
    routeConfigManager;
    apiTokenManager;
    gatewayClientManager;
    referenceResolver;
    targetProfileManager;
    // Domain / DNS management (DB-backed providers, domains, records)
    dnsManager;
    // ACME configuration (DB-backed singleton, replaces tls.contactEmail)
    acmeConfigManager;
    emailDomainManager;
    workAppMailManager;
    securityPolicyManager;
    // Auto-discovered public IP (populated by generateAuthoritativeRecords)
    detectedPublicIp = null;
    // DNS query logging rate limiter state
    dnsLogWindowSecond = 0; // epoch second of current window
    dnsLogWindowCount = 0; // queries logged this second
    dnsBatchCount = 0;
    dnsBatchTimer = null;
    // Certificate status tracking from SmartProxy events (keyed by domain)
    certificateStatusMap = new Map();
    // Certificate provisioning scheduler with per-domain backoff
    certProvisionScheduler;
    // Service lifecycle management
    serviceManager;
    serviceSubjectSubscription;
    smartAcmeReady = false;
    // TypedRouter for API endpoints
    typedrouter = new plugins.typedrequest.TypedRouter();
    // Seed routes assembled during setupSmartProxy, passed to RouteConfigManager for DB seeding
    seedConfigRoutes = [];
    seedEmailRoutes = [];
    seedDnsRoutes = [];
    // Live DoH routes used during SmartProxy bootstrap before RouteConfigManager re-applies stored routes.
    runtimeDnsRoutes = [];
    // Environment access
    qenv = new plugins.qenv.Qenv('./', '.nogit/');
    constructor(optionsArg) {
        // Set defaults in options
        this.options = {
            ...optionsArg
        };
        // Resolve all data paths from baseDir
        this.resolvedPaths = paths.resolvePaths(this.options.baseDir);
        paths.ensureDataDirectories(this.resolvedPaths);
        this.storageManager = new SmartMtaStorageManager(plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-storage'));
        this.workAppMailManager = new WorkAppMailManager(this);
        // 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.
     */
    registerServices() {
        // 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 }));
        // DnsManager: optional, depends on DcRouterDb — owns DB-backed DNS state
        // (providers, domains, records). Must run before SmartProxy so ACME DNS-01
        // wiring can look up providers.
        if (this.options.dbConfig?.enabled !== false) {
            this.serviceManager.addService(new plugins.taskbuffer.Service('DnsManager')
                .optional()
                .dependsOn('DcRouterDb')
                .withStart(async () => {
                this.dnsManager = new DnsManager(this.options);
                await this.dnsManager.start();
            })
                .withStop(async () => {
                if (this.dnsManager) {
                    await this.dnsManager.stop();
                    this.dnsManager = undefined;
                }
            })
                .withRetry({ maxRetries: 1, baseDelayMs: 500 }));
        }
        // AcmeConfigManager: optional, depends on DcRouterDb — owns the singleton
        // ACME configuration (accountEmail, useProduction, etc.). Must run before
        // SmartProxy so setupSmartProxy() can read the ACME config from the DB.
        // On first boot, seeds from legacy `tls.contactEmail` / `smartProxyConfig.acme`.
        if (this.options.dbConfig?.enabled !== false) {
            this.serviceManager.addService(new plugins.taskbuffer.Service('AcmeConfigManager')
                .optional()
                .dependsOn('DcRouterDb')
                .withStart(async () => {
                this.acmeConfigManager = new AcmeConfigManager(this.options);
                await this.acmeConfigManager.start();
            })
                .withStop(async () => {
                if (this.acmeConfigManager) {
                    await this.acmeConfigManager.stop();
                    this.acmeConfigManager = undefined;
                }
            })
                .withRetry({ maxRetries: 1, baseDelayMs: 500 }));
        }
        // Email Domain Manager: optional, depends on DcRouterDb
        if (this.options.dbConfig?.enabled !== false) {
            this.serviceManager.addService(new plugins.taskbuffer.Service('EmailDomainManager')
                .optional()
                .dependsOn('DcRouterDb')
                .withStart(async () => {
                this.emailDomainManager = new EmailDomainManager(this);
                await this.emailDomainManager.start();
            })
                .withStop(async () => {
                if (this.emailDomainManager) {
                    await this.emailDomainManager.stop();
                    this.emailDomainManager = undefined;
                }
            }));
        }
        // SecurityPolicyManager: optional, depends on DcRouterDb — owns IP intelligence
        // and compiles the global block policy for SmartProxy and remote ingress edges.
        if (this.options.dbConfig?.enabled !== false) {
            this.serviceManager.addService(new plugins.taskbuffer.Service('SecurityPolicyManager')
                .optional()
                .dependsOn('DcRouterDb')
                .withStart(async () => {
                this.securityPolicyManager = new SecurityPolicyManager({
                    onPolicyChanged: () => this.applySecurityPolicy(),
                });
                await this.securityPolicyManager.start();
            })
                .withStop(async () => {
                if (this.securityPolicyManager) {
                    await this.securityPolicyManager.stop();
                    this.securityPolicyManager = undefined;
                }
            })
                .withRetry({ maxRetries: 1, baseDelayMs: 500 }));
        }
        // SmartProxy: critical, depends on DcRouterDb + DnsManager + AcmeConfigManager (if enabled)
        const smartProxyDeps = [];
        if (this.options.dbConfig?.enabled !== false) {
            smartProxyDeps.push('DcRouterDb');
            smartProxyDeps.push('DnsManager');
            smartProxyDeps.push('AcmeConfigManager');
            smartProxyDeps.push('SecurityPolicyManager');
        }
        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.
        // Always registered when the DB is enabled; setupSmartProxy() decides whether
        // to actually instantiate SmartAcme based on whether any DnsProviderDoc exists.
        // If `this.smartAcme` is unset by the time this service starts, withStart is a no-op.
        if (this.options.dbConfig?.enabled !== false) {
            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.routeConfigManager) {
                        // Go through RouteConfigManager to get the full merged route set
                        // and serialize via the route-update mutex (prevents stale overwrites)
                        logger.log('info', 'Re-triggering certificate provisioning via RouteConfigManager');
                        this.routeConfigManager.applyRoutes().catch((err) => {
                            logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
                        });
                    }
                    else if (this.smartProxy) {
                        // No RouteConfigManager (DB disabled) — re-send current routes to trigger cert provisioning
                        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) => {
                            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();
                // Initialize target profile manager
                this.targetProfileManager = new TargetProfileManager(() => this.routeConfigManager?.getRoutes() || new Map());
                await this.targetProfileManager.initialize();
                this.routeConfigManager = new RouteConfigManager(() => this.smartProxy, () => this.options.http3, this.createVpnClientAccessResolver(), this.referenceResolver, 
                // Sync routes to RemoteIngressManager whenever routes change,
                // then push updated derived ports to the Rust hub binary
                async (routes) => {
                    if (this.remoteIngressManager) {
                        this.remoteIngressManager.setRoutes(routes);
                    }
                    if (this.tunnelManager) {
                        try {
                            await this.tunnelManager.syncAllowedEdges();
                        }
                        catch (err) {
                            logger.log('error', `Failed to sync Remote Ingress allowed edges: ${err.message}`);
                        }
                    }
                }, undefined, (storedRoute) => this.hydrateStoredRouteForRuntime(storedRoute));
                this.apiTokenManager = new ApiTokenManager();
                await this.apiTokenManager.initialize();
                this.gatewayClientManager = new GatewayClientManager();
                await this.gatewayClientManager.initialize();
                await this.routeConfigManager.initialize(this.seedConfigRoutes, this.seedEmailRoutes, this.seedDnsRoutes);
                await this.targetProfileManager.normalizeAllRouteRefs();
                // 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.gatewayClientManager = undefined;
                this.referenceResolver = undefined;
                this.targetProfileManager = undefined;
            })
                .withRetry({ maxRetries: 2, baseDelayMs: 1000 }));
        }
        // Email Server: optional, depends on SmartProxy
        if (this.options.emailConfig) {
            const emailServiceDeps = ['SmartProxy', 'MetricsManager'];
            if (this.options.dbConfig?.enabled !== false) {
                emailServiceDeps.push('EmailDomainManager');
            }
            this.serviceManager.addService(new plugins.taskbuffer.Service('EmailServer')
                .optional()
                .dependsOn(...emailServiceDeps)
                .withStart(async () => {
                await this.setupUnifiedEmailHandling();
            })
                .withStop(async () => {
                if (this.emailServer) {
                    this.clearEmailEventSubscriptions();
                    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', ...(this.options.emailConfig ? ['EmailServer'] : []))
                .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) {
            const vpnServiceDeps = ['SmartProxy'];
            if (this.options.dbConfig?.enabled !== false) {
                vpnServiceDeps.push('ConfigManagers');
            }
            this.serviceManager.addService(new plugins.taskbuffer.Service('VpnServer')
                .optional()
                .dependsOn(...vpnServiceDeps)
                .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, `Service '${event.serviceName}': ${event.type}`, {
                state: event.state,
                ...(event.error ? { error: event.error } : {}),
                ...(event.attempt ? { attempt: event.attempt } : {}),
            });
        });
    }
    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.
     */
    async checkSystemLimits() {
        try {
            const fs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
            const limitsContent = await fs.file('/proc/self/limits').encoding('utf8').read();
            const nofileLine = limitsContent.split('\n').find((line) => 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
     */
    logStartupSummary() {
        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)
     */
    async setupDcRouterDb() {
        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();
        // Run any pending data migrations before anything else reads from the DB.
        // This must complete before ConfigManagers loads profiles.
        const migration = await createMigrationRunner(this.dcRouterDb.getDb(), commitinfo.version);
        const migrationResult = await migration.run();
        if (migrationResult.stepsApplied.length > 0) {
            logger.log('info', `smartmigration: ${migrationResult.currentVersionBefore ?? 'fresh'} → ${migrationResult.currentVersionAfter} ` +
                `(${migrationResult.stepsApplied.length} step(s) applied in ${migrationResult.totalDurationMs}ms)`);
        }
        else if (migrationResult.wasFreshInstall) {
            logger.log('info', `smartmigration: fresh install stamped to ${migrationResult.currentVersionAfter}`);
        }
        // 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
     */
    async setupSmartProxy() {
        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;
        }
        // Assemble serializable seed routes from constructor config — these will be seeded into DB
        // by RouteConfigManager.initialize() when the ConfigManagers service starts.
        this.seedConfigRoutes = (this.options.smartProxyConfig?.routes || []);
        logger.log('info', `Found ${this.seedConfigRoutes.length} routes in config`);
        this.seedEmailRoutes = [];
        if (this.options.emailConfig) {
            this.seedEmailRoutes = this.generateEmailRoutes(this.options.emailConfig);
            logger.log('debug', 'Email routes generated', { routes: JSON.stringify(this.seedEmailRoutes) });
        }
        this.seedDnsRoutes = [];
        this.runtimeDnsRoutes = [];
        if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
            this.seedDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: false });
            this.runtimeDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: true });
            logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.seedDnsRoutes) });
        }
        // Combined routes for SmartProxy bootstrap (before DB routes are loaded)
        let routes = [
            ...this.seedConfigRoutes,
            ...this.seedEmailRoutes,
            ...this.runtimeDnsRoutes,
        ];
        // Build the ACME options for SmartProxy from the DB-backed AcmeConfigManager.
        // If no config exists or it's disabled, SmartProxy's own ACME is turned off
        // and dcrouter's SmartAcme / certProvisionFunction are not wired.
        const dbAcme = this.acmeConfigManager?.getConfig();
        const acmeConfig = dbAcme && dbAcme.enabled
            ? {
                accountEmail: dbAcme.accountEmail,
                enabled: true,
                useProduction: dbAcme.useProduction,
                autoRenew: dbAcme.autoRenew,
                renewThresholdDays: dbAcme.renewThresholdDays,
            }
            : undefined;
        if (acmeConfig) {
            logger.log('info', `ACME config: accountEmail=${acmeConfig.accountEmail}, useProduction=${acmeConfig.useProduction}, autoRenew=${acmeConfig.autoRenew}`);
        }
        else {
            logger.log('info', 'ACME config: disabled or not yet configured in DB');
        }
        // Configure DNS-01 challenge if any DnsProviderDoc exists in the DB AND
        // ACME is enabled. The DnsManager dispatches each challenge through the
        // unified createRecord()/deleteRecord() path — works for both dcrouter-hosted
        // zones and provider-managed zones. Only domains under management get certs.
        let challengeHandlers = [];
        if (acmeConfig &&
            this.dnsManager &&
            (await this.dnsManager.hasAnyManagedDomain())) {
            logger.log('info', 'Configuring DNS-01 challenge for ACME via DnsManager (managed domains)');
            const convenientDnsProvider = this.dnsManager.buildAcmeConvenientDnsProvider();
            const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(convenientDnsProvider);
            challengeHandlers.push(dns01Handler);
        }
        // HTTP/3 augmentation (enabled by default unless explicitly disabled)
        if (this.options.http3?.enabled !== false) {
            const http3Config = { enabled: true, ...this.options.http3 };
            routes = augmentRoutesWithHttp3(routes, http3Config);
            logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
        }
        const compiledSecurityPolicy = await this.securityPolicyManager?.compileSmartProxyPolicy();
        const mergedSecurityPolicy = this.mergeSecurityPolicies(this.options.smartProxyConfig?.securityPolicy, compiledSecurityPolicy);
        // 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 = [];
            // Create SmartProxy configuration with sensible gateway defaults.
            // User's smartProxyConfig overrides these defaults via spread.
            const smartProxyConfig = {
                // --- 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,
                ...(mergedSecurityPolicy ? { securityPolicy: mergedSecurityPolicy } : {}),
                certStore: {
                    loadAll: async () => {
                        const docs = await ProxyCertDoc.findAll();
                        const certs = [];
                        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, publicKey, privateKey, ca) => {
                        let validUntil;
                        let validFrom;
                        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) => {
                        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) }));
                }
                // Safe non-null: challengeHandlers.length > 0 implies both dnsManager
                // and acmeConfig exist (enforced above).
                this.smartAcme = new plugins.smartacme.SmartAcme({
                    accountEmail: dbAcme.accountEmail,
                    certManager: new StorageBackedCertManager(),
                    environment: dbAcme.useProduction ? 'production' : 'integration',
                    challengeHandlers: challengeHandlers,
                    challengePriority: ['dns-01'],
                });
                const scheduler = this.certProvisionScheduler;
                smartProxyConfig.certProvisionFallbackToAcme = false;
                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) {
                        const message = `DNS-01 failed for ${domain}: ${err.message}`;
                        await scheduler.recordFailure(domain, message);
                        eventComms.warn(message);
                        throw new Error(message);
                    }
                };
            }
            // 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;
                if (!smartProxyConfig.proxyIPs) {
                    smartProxyConfig.proxyIPs = [];
                }
                if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
                    smartProxyConfig.proxyIPs.push('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) => {
                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,
                });
            });
            // Note: smartproxy v27.5.0 emits only 'certificate-issued' and 'certificate-failed'.
            // Renewals come through 'certificate-issued' (with optional isRenewal? in the payload).
            // The vestigial 'certificate-renewed' event from common-types.ts is never emitted.
            this.smartProxy.on('certificate-failed', (event) => {
                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;
                    let issuedAt;
                    // 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 domParts = cleanDomain.split('.');
                            const baseDomain = domParts.length > 2 ? domParts.slice(-2).join('.') : cleanDomain;
                            const certDoc = await AcmeCertDoc.findByDomain(baseDomain)
                                || (baseDomain !== cleanDomain ? await AcmeCertDoc.findByDomain(cleanDomain) : null);
                            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`);
        }
    }
    async applySecurityPolicy() {
        if (!this.securityPolicyManager) {
            return;
        }
        const compiledSmartProxyPolicy = await this.securityPolicyManager.compileSmartProxyPolicy();
        const mergedSecurityPolicy = this.mergeSecurityPolicies(this.options.smartProxyConfig?.securityPolicy, compiledSmartProxyPolicy);
        if (this.smartProxy && mergedSecurityPolicy) {
            const smartProxyWithPolicyApi = this.smartProxy;
            if (typeof smartProxyWithPolicyApi.updateSecurityPolicy === 'function') {
                await smartProxyWithPolicyApi.updateSecurityPolicy(mergedSecurityPolicy);
            }
        }
        const firewallConfig = await this.securityPolicyManager.compileRemoteIngressFirewall();
        if (this.remoteIngressManager) {
            this.remoteIngressManager.setFirewallConfig?.(firewallConfig);
        }
        if (this.tunnelManager) {
            await this.tunnelManager.syncAllowedEdges();
        }
    }
    mergeSecurityPolicies(...policies) {
        const blockedIps = new Set();
        const blockedCidrs = new Set();
        for (const policy of policies) {
            for (const ip of policy?.blockedIps || []) {
                if (ip)
                    blockedIps.add(ip);
            }
            for (const cidr of policy?.blockedCidrs || []) {
                if (cidr)
                    blockedCidrs.add(cidr);
            }
        }
        if (blockedIps.size === 0 && blockedCidrs.size === 0) {
            return undefined;
        }
        return {
            blockedIps: [...blockedIps].sort(),
            blockedCidrs: [...blockedCidrs].sort(),
        };
    }
    /**
     * Generate SmartProxy routes for email configuration
     */
    generateEmailRoutes(emailConfig) {
        const emailRoutes = [];
        // 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 = {
                25: 10025, // SMTP
                587: 10587, // Submission
                465: 10465 // SMTPS
            };
            const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping;
            const internalPort = portMapping[port] || port + 10000;
            let action = {
                type: 'forward',
                targets: [{
                        host: 'localhost', // Forward to internal email server
                        port: internalPort
                    }],
                tls: {
                    mode: tlsMode
                }
            };
            // For TLS terminate mode, add certificate info
            if (tlsMode === 'terminate' && action.tls) {
                action.tls.certificate = 'auto';
            }
            // Create the route configuration
            const routeConfig = {
                name: routeName,
                match: {
                    ports: [port]
                },
                action: action
            };
            // Add the route to our list
            emailRoutes.push(routeConfig);
        }
        return emailRoutes;
    }
    /**
     * Generate SmartProxy routes for DNS configuration
     */
    generateDnsRoutes(options) {
        if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
            return [];
        }
        const includeSocketHandler = options?.includeSocketHandler !== false;
        const dnsRoutes = [];
        // 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 = {
                name: `dns-over-https-${path.replace('/', '')}`,
                match: {
                    ports: [443], // HTTPS port for DoH
                    domains: [primaryNameserver],
                    path: path
                },
                action: includeSocketHandler
                    ? {
                        type: 'socket-handler',
                        socketHandler: this.createDnsSocketHandler()
                    }
                    : {
                        type: 'socket-handler',
                    }
            };
            dnsRoutes.push(dohRoute);
        }
        return dnsRoutes;
    }
    hydrateStoredRouteForRuntime(storedRoute) {
        const routeName = storedRoute.route.name || '';
        const isDohRoute = storedRoute.origin === 'dns'
            && storedRoute.route.action?.type === 'socket-handler'
            && routeName.startsWith('dns-over-https-');
        if (!isDohRoute) {
            return undefined;
        }
        return {
            ...storedRoute.route,
            action: {
                ...storedRoute.route.action,
                type: 'socket-handler',
                socketHandler: this.createDnsSocketHandler(),
            },
        };
    }
    /**
     * 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
     */
    isDomainMatch(domain, pattern) {
        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
     */
    findRouteNamesForDomain(domain) {
        if (!this.smartProxy)
            return [];
        const names = [];
        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;
    }
    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
     */
    async updateSmartProxyConfig(config) {
        // Stop existing SmartProxy if running
        if (this.smartProxy) {
            this.smartProxy.removeAllListeners();
            await this.smartProxy.stop();
            this.smartProxy = undefined;
        }
        // Update configuration
        this.options.smartProxyConfig = config;
        // Start new SmartProxy with updated configuration (rebuilds seed routes)
        await this.setupSmartProxy();
        // Re-seed and re-apply all routes after SmartProxy restart
        if (this.routeConfigManager) {
            await this.routeConfigManager.initialize(this.seedConfigRoutes, this.seedEmailRoutes);
        }
        logger.log('info', 'SmartProxy configuration updated');
    }
    /**
     * Set up unified email handling with pattern-based routing
     * This implements the consolidated emailConfig approach
     */
    async setupUnifiedEmailHandling() {
        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.map((domain) => ({
                    domain,
                    dnsMode: 'external-dns',
                    dkim: {
                        selector: 'default',
                        keySize: 2048,
                        rotateKeys: false,
                        rotationInterval: 90
                    }
                }));
            }
        }
        // Create config with mapped ports
        const emailConfig = await this.workAppMailManager.applyStoredIdentitiesToEmailConfig({
            ...this.options.emailConfig,
            domains: transformedDomains,
            ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
            persistRoutes: this.options.emailConfig.persistRoutes ?? false,
            queue: {
                storageType: 'disk',
                persistentPath: plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-queue'),
                ...this.options.emailConfig.queue,
            },
        });
        // Create unified email server
        this.emailServer = new UnifiedEmailServer(this, emailConfig);
        this.clearEmailEventSubscriptions();
        // Set up error handling
        this.addEmailEventSubscription(this.emailServer, 'error', (err) => {
            logger.log('error', `UnifiedEmailServer error: ${err.message}`);
        });
        // Start the server
        await this.emailServer.start();
        // Wire delivery events to MetricsManager and logger using smartmta's public queue APIs.
        if (this.metricsManager && this.emailServer) {
            const getEnvelope = (item) => {
                const emailLike = item?.processingResult;
                const from = emailLike?.from || emailLike?.email?.from || '';
                const recipients = Array.isArray(emailLike?.to)
                    ? emailLike.to
                    : Array.isArray(emailLike?.email?.to)
                        ? emailLike.email.to
                        : [];
                return {
                    from,
                    recipients: recipients.filter(Boolean),
                };
            };
            const updateQueueSize = () => {
                this.metricsManager.updateQueueSize(this.emailServer.getQueueStats().queueSize);
            };
            this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemEnqueued', (item) => {
                const envelope = getEnvelope(item);
                this.metricsManager.trackEmailReceived(envelope.from);
                updateQueueSize();
                logger.log('info', `Email queued: ${envelope.from} → ${envelope.recipients.join(', ') || 'unknown'}`, { zone: 'email' });
            });
            this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemDelivered', (item) => {
                const envelope = getEnvelope(item);
                this.metricsManager.trackEmailSent(envelope.recipients[0]);
                updateQueueSize();
                logger.log('info', `Email delivered to ${envelope.recipients.join(', ') || 'unknown'}`, { zone: 'email' });
            });
            this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemFailed', (item) => {
                const envelope = getEnvelope(item);
                this.metricsManager.trackEmailFailed(envelope.recipients[0], item?.lastError);
                updateQueueSize();
                logger.log('warn', `Email delivery failed to ${envelope.recipients.join(', ') || 'unknown'}: ${item?.lastError || 'unknown error'}`, { zone: 'email' });
            });
            this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemDeferred', () => {
                updateQueueSize();
            });
            this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemRemoved', () => {
                updateQueueSize();
            });
            this.addEmailEventSubscription(this.emailServer, 'bounceProcessed', () => {
                this.metricsManager.trackEmailBounced();
                logger.log('warn', 'Email bounce processed', { zone: 'email' });
            });
            updateQueueSize();
        }
        logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
    }
    /**
     * Update the unified email configuration
     * @param config New email configuration
     */
    async updateEmailConfig(config) {
        // 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
     */
    async stopUnifiedEmailComponents() {
        try {
            // Stop the unified email server which contains all components
            if (this.emailServer) {
                this.clearEmailEventSubscriptions();
                await this.emailServer.stop();
                logger.log('info', 'Unified email server stopped');
                this.emailServer = undefined;
            }
            logger.log('info', 'All unified email components stopped');
        }
        catch (error) {
            logger.log('error', `Error stopping unified email components: ${error.message}`);
            throw error;
        }
    }
    /**
     * Update domain rules for email routing
     * @param rules New domain rules to apply
     */
    async updateEmailRoutes(routes) {
        // 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
     */
    getStats() {
        const stats = {
            emailServer: this.emailServer?.getStats()
        };
        return stats;
    }
    /**
     * Register DNS records with the DNS server
     * @param records Array of DNS records to register
     */
    registerDnsRecords(records) {
        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
     */
    parseDnsRecordData(type, value) {
        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
     */
    async setupDnsWithSocketHandler() {
        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 = this.options.dnsBindInterface || '0.0.0.0'; // Default to all interfaces
        // Try to find the VM's internal IP address when no explicit bind address is configured.
        if (!this.options.dnsBindInterface) {
            interfaceLoop: 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 interfaceLoop;
                        }
                    }
                }
            }
        }
        // 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) => {
                // 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();
        // Ensure DKIM keys exist for internal-dns domains before generating records.
        await this.initializeDkimForEmailDomains();
        // Generate DKIM records directly from smartmta instead of scanning legacy JSON files.
        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)`);
        }
        // Hand the DnsServer to DnsManager so DB-backed local records on
        // dcrouter-hosted domains get registered too.
        if (this.dnsManager && this.dnsServer) {
            await this.dnsManager.attachDnsServer(this.dnsServer);
        }
    }
    /**
     * Create DNS socket handler for DoH
     */
    createDnsSocketHandler() {
        return async (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.handleHttpsSocket(socket);
            }
            catch (error) {
                logger.log('error', `DNS socket handler error: ${error.message}`);
                if (!socket.destroyed) {
                    socket.destroy();
                }
            }
        };
    }
    /**
     * Validate DNS configuration
     */
    async validateDnsConfiguration() {
        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
     */
    async generateEmailDnsRecords() {
        const records = [];
        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 requiredRecords = buildEmailDnsRecords({
                domain,
                hostname: this.options.emailConfig.hostname,
                mxPriority: domainConfig.dns?.internal?.mxPriority,
            }).filter((record) => !record.name.includes('._domainkey.'));
            for (const record of requiredRecords) {
                records.push({
                    name: record.name,
                    type: record.type,
                    value: record.value,
                    ttl,
                });
            }
        }
        logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`);
        return records;
    }
    /**
     * Generate DKIM DNS records for internal-dns domains from smartmta's selector-aware DKIM state.
     */
    async loadDkimRecords() {
        const records = [];
        if (!this.options.emailConfig?.domains || !this.emailServer?.dkimCreator) {
            return records;
        }
        for (const domainConfig of this.options.emailConfig.domains) {
            if (domainConfig.dnsMode !== 'internal-dns') {
                continue;
            }
            const selector = domainConfig.dkim?.selector || 'default';
            try {
                const dkimRecord = await this.emailServer.dkimCreator.getDNSRecordForDomain(domainConfig.domain, selector);
                records.push({
                    name: dkimRecord.name,
                    type: 'TXT',
                    value: dkimRecord.value,
                    ttl: domainConfig.dns?.internal?.ttl || 3600,
                });
            }
            catch (error) {
                logger.log('error', `Failed to generate DKIM record for ${domainConfig.domain}: ${error.message}`);
            }
        }
        return records;
    }
    /**
     * Initialize DKIM keys for all configured email domains
     * This ensures DKIM records are available immediately at startup
     */
    async initializeDkimForEmailDomains() {
        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 internal-dns email domain using the configured selector.
        for (const domainConfig of this.options.emailConfig.domains) {
            if (domainConfig.dnsMode !== 'internal-dns') {
                continue;
            }
            try {
                await dkimCreator.handleDKIMKeysForSelector(domainConfig.domain, domainConfig.dkim?.selector || 'default', domainConfig.dkim?.keySize || 2048);
                logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`);
            }
            catch (error) {
                logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${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
     */
    async generateAuthoritativeRecords() {
        const records = [];
        if (!this.options.dnsNsDomains || !this.options.dnsScopes) {
            return records;
        }
        // Determine the public IP for nameserver A records
        let publicIp = 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) {
                logger.log('error', `Failed to auto-discover public IP: ${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
     */
    extractDomain(recordName) {
        // Handle wildcards
        if (recordName.startsWith('*.')) {
            recordName = recordName.substring(2);
        }
        return recordName;
    }
    /**
     * Apply proxy IP replacement logic to DNS records
     */
    async applyProxyIpReplacement(records) {
        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++;
            }
        }
    }
    addEmailEventSubscription(emitter, eventName, listener) {
        emitter.on(eventName, listener);
        this.emailEventSubscriptions.push({ emitter, eventName, listener });
    }
    clearEmailEventSubscriptions() {
        for (const subscription of this.emailEventSubscriptions) {
            subscription.emitter.off(subscription.eventName, subscription.listener);
        }
        this.emailEventSubscriptions = [];
    }
    /**
     * Detect the server's public IP address
     */
    async detectServerPublicIp() {
        try {
            const smartNetwork = new plugins.smartnetwork.SmartNetwork();
            const publicIps = await smartNetwork.getPublicIps();
            if (publicIps.v4) {
                return publicIps.v4;
            }
            return null;
        }
        catch (error) {
            logger.log('warn', `Failed to detect public IP: ${error.message}`);
            return null;
        }
    }
    /**
     * Set up Remote Ingress hub for edge tunnel connections
     */
    async setupRemoteIngress() {
        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();
        this.remoteIngressManager.setFirewallConfig(await this.securityPolicyManager?.compileRemoteIngressFirewall());
        // Pass current bootstrap routes so the manager can derive edge ports initially.
        // Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
        // will push the complete merged routes here.
        const bootstrapRoutes = [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
        this.remoteIngressManager.setRoutes(bootstrapRoutes);
        // If ConfigManagers finished before us, re-apply routes
        // so the callback delivers the full DB set 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;
        // 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) {
                logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${err.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,
            performance: riCfg.performance,
        });
        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.
     */
    createVpnClientAccessResolver() {
        if (!this.options.vpnConfig?.enabled) {
            return undefined;
        }
        return (route, routeId) => {
            if (!this.vpnManager || !this.targetProfileManager) {
                // VPN not ready yet — deny all until re-apply after VPN starts.
                return [];
            }
            return this.targetProfileManager.getMatchingVpnClients(route, routeId, this.vpnManager.listClients(), this.routeConfigManager?.getRoutes() || new Map());
        };
    }
    async setupVpnServer() {
        if (!this.options.vpnConfig?.enabled) {
            return;
        }
        if (this.options.dbConfig?.enabled === false) {
            throw new Error('VPN requires dbConfig.enabled because clients, keys, routes, and target profiles are persisted in DcRouterDb');
        }
        if (!this.routeConfigManager || !this.targetProfileManager) {
            throw new Error('VPN requires initialized route and target profile managers');
        }
        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 profile-based VPN client grants get updated
                // (serialized by RouteConfigManager's mutex — safe as fire-and-forget)
                this.routeConfigManager?.applyRoutes().catch((err) => {
                    logger.log('warn', `Failed to re-apply routes after VPN client change: ${err?.message || err}`);
                });
            },
            onClientSourceIpsChanged: () => {
                // SmartProxy now receives the real source IP per connection via PROXY v2.
                // Source-IP changes are reflected in status/UI only; route config is static.
            },
            getClientDirectTargets: (targetProfileIds) => {
                if (!this.targetProfileManager)
                    return [];
                return this.targetProfileManager.getDirectTargetIps(targetProfileIds);
            },
            getClientAllowedIPs: async (targetProfileIds, clientId, _sourceIp) => {
                const subnet = this.options.vpnConfig?.subnet || '10.8.0.0/24';
                const ips = new Set([subnet]);
                if (!this.targetProfileManager)
                    return [...ips];
                const allRoutes = this.routeConfigManager?.getRoutes() || new Map();
                const { domains, targetIps } = this.targetProfileManager.getClientAccessSpec(targetProfileIds, allRoutes);
                // Add target IPs directly
                for (const ip of targetIps) {
                    ips.add(`${ip}/32`);
                }
                // Resolve DNS A records for matched domains (with caching)
                for (const domain of domains) {
                    if (this.isWildcardVpnDomain(domain)) {
                        this.logSkippedWildcardAllowedIp(domain);
                        continue;
                    }
                    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 vpnOnly routes
        // get correct profile-based VPN client grants.
        await this.routeConfigManager?.applyRoutes();
    }
    /** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
    vpnDomainIpCache = new Map();
    /** Deduplicate wildcard-resolution warnings for WireGuard AllowedIPs generation. */
    warnedWildcardVpnDomains = new Set();
    /**
     * Resolve a domain's A record(s) for VPN AllowedIPs, with a 5-minute cache.
     */
    async resolveVpnDomainIPs(domain) {
        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 });
            // Evict oldest entries if cache exceeds 1000 entries
            if (this.vpnDomainIpCache.size > 1000) {
                const firstKey = this.vpnDomainIpCache.keys().next().value;
                if (firstKey)
                    this.vpnDomainIpCache.delete(firstKey);
            }
            return ips;
        }
        catch (err) {
            logger.log('warn', `VPN: Failed to resolve ${domain} for AllowedIPs: ${err.message}`);
            return cached?.ips || []; // Return stale cache on failure, or empty
        }
    }
    isWildcardVpnDomain(domain) {
        return domain.includes('*');
    }
    logSkippedWildcardAllowedIp(domain) {
        if (this.warnedWildcardVpnDomains.has(domain))
            return;
        this.warnedWildcardVpnDomains.add(domain);
        logger.log('warn', `VPN: Skipping wildcard domain '${domain}' for WireGuard AllowedIPs; wildcard patterns must be resolved to concrete hostnames by matching routes.`);
    }
    // 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
     */
    async setupRadiusServer() {
        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
     */
    async updateRadiusConfig(config) {
        // 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');
    }
    /**
     * Update VPN configuration at runtime.
     */
    async updateVpnConfig(config) {
        if (this.vpnManager) {
            await this.vpnManager.stop();
            this.vpnManager = undefined;
        }
        this.options.vpnConfig = config;
        this.vpnDomainIpCache.clear();
        this.warnedWildcardVpnDomains.clear();
        this.routeConfigManager?.setVpnClientAccessResolver(this.createVpnClientAccessResolver());
        if (this.options.vpnConfig?.enabled) {
            await this.setupVpnServer();
        }
        else {
            await this.routeConfigManager?.applyRoutes();
        }
        logger.log('info', 'VPN configuration updated');
    }
}
export default DcRouter;
//# sourceMappingURL=data:application/json;base64, 