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, CachedEmail } 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 { RemoteIngressHubLifecycle, RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
import { VpnAccessResolver, VpnManager } from './vpn/index.js';
import { RouteConfigManager, ApiTokenManager, GatewayClientManager, ReferenceResolver, DbSeeder, TargetProfileManager, buildHttpRedirectRuntimeRoutes } from './config/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyManager, RoutePolicyAugmenter } from './security/index.js';
import { augmentRoutesWithHttp3 } from './http3/index.js';
import { DnsManager } from './dns/manager.dns.js';
import { DnsServerRuntime } from './dns/classes.dns-server-runtime.js';
import { AcmeConfigManager } from './acme/manager.acme-config.js';
import { SmartAcmeLifecycle } from './acme/classes.smartacme-lifecycle.js';
import { AcceptedEmailSpool, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, SmartMtaStorageManager, WorkAppMailManager } 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;
    smartProxyLifecycleChain = Promise.resolve();
    emailLifecycleChain = Promise.resolve();
    // 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;
    emailSettingsManager;
    emailDomainManager;
    workAppMailManager;
    acceptedEmailSpool;
    securityPolicyManager;
    // Auto-discovered public IP (populated by generateAuthoritativeRecords)
    detectedPublicIp = null;
    // DNS query logging rate limiter state
    // 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;
    smartAcmeLifecycle;
    vpnAccessResolver;
    remoteIngressHubLifecycle;
    dnsServerRuntime;
    emailRouteBuilder;
    routePolicyAugmenter;
    // 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);
        this.acceptedEmailSpool = new AcceptedEmailSpool(this);
        this.smartAcmeLifecycle = new SmartAcmeLifecycle(this);
        this.vpnAccessResolver = new VpnAccessResolver(this);
        this.remoteIngressHubLifecycle = new RemoteIngressHubLifecycle(this);
        this.dnsServerRuntime = new DnsServerRuntime(this);
        this.emailRouteBuilder = new EmailRouteBuilder(this);
        this.routePolicyAugmenter = new RoutePolicyAugmenter(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.
        if (this.options.dbConfig?.enabled !== false) {
            this.serviceManager.addService(new plugins.taskbuffer.Service('AcmeConfigManager')
                .optional()
                .dependsOn('DcRouterDb')
                .withStart(async () => {
                this.acmeConfigManager = new AcmeConfigManager();
                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', 'EmailSettingsManager')
                .withStart(async () => {
                this.emailDomainManager = new EmailDomainManager(this);
                await this.emailDomainManager.start();
            })
                .withStop(async () => {
                if (this.emailDomainManager) {
                    await this.emailDomainManager.stop();
                    this.emailDomainManager = undefined;
                }
            }));
        }
        // EmailSettingsManager: optional, depends on DcRouterDb — owns the DB-backed
        // singleton email server config and projects it into runtime options before
        // SmartProxy and EmailDomainManager read email settings.
        if (this.options.dbConfig?.enabled !== false) {
            this.serviceManager.addService(new plugins.taskbuffer.Service('EmailSettingsManager')
                .optional()
                .dependsOn('DcRouterDb')
                .withStart(async () => {
                this.emailSettingsManager = new EmailSettingsManager(this.options);
                await this.emailSettingsManager.start();
            })
                .withStop(async () => {
                if (this.emailSettingsManager) {
                    await this.emailSettingsManager.stop();
                    this.emailSettingsManager = undefined;
                }
            })
                .withRetry({ maxRetries: 1, baseDelayMs: 500 }));
        }
        // 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 }));
        }
        // RemoteIngressManager: optional, depends on DcRouterDb — owns DB-backed
        // hub settings and edge registrations. It starts before SmartProxy so
        // SmartProxy can use the DB-backed enabled flag for PROXY protocol setup.
        if (this.options.dbConfig?.enabled !== false) {
            this.serviceManager.addService(new plugins.taskbuffer.Service('RemoteIngressManager')
                .optional()
                .dependsOn('DcRouterDb')
                .withStart(async () => {
                this.remoteIngressManager = new RemoteIngressManager();
                await this.remoteIngressManager.initialize();
            })
                .withStop(async () => {
                this.remoteIngressManager = 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('EmailSettingsManager');
            smartProxyDeps.push('SecurityPolicyManager');
            smartProxyDeps.push('RemoteIngressManager');
        }
        this.serviceManager.addService(new plugins.taskbuffer.Service('SmartProxy')
            .critical()
            .dependsOn(...smartProxyDeps)
            .withStart(async () => {
            await this.setupSmartProxy();
        })
            .withStop(async () => {
            await this.queueSmartProxyLifecycleTask(async () => {
                try {
                    if (this.smartProxy) {
                        const existingSmartProxy = this.smartProxy;
                        existingSmartProxy.removeAllListeners();
                        await existingSmartProxy.stop();
                        if (this.smartProxy === existingSmartProxy) {
                            this.smartProxy = undefined;
                        }
                    }
                }
                finally {
                    await this.smartAcmeLifecycle.stop();
                }
            });
        })
            .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 () => {
                this.smartAcmeLifecycle.serviceStarted = true;
                this.smartAcmeLifecycle.startInBackground();
            })
                .withStop(async () => {
                this.smartAcmeLifecycle.serviceStarted = false;
                await this.smartAcmeLifecycle.stop();
            })
                .withRetry({ maxRetries: 0 }));
        }
        // 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.vpnAccessResolver.createRouteAllowResolver(), this.referenceResolver, 
                // Sync routes to RemoteIngressManager whenever routes change,
                // then push updated derived ports to the Rust hub binary
                async (routes) => {
                    try {
                        await this.remoteIngressHubLifecycle.updateRoutes(routes);
                    }
                    catch (err) {
                        logger.log('error', `Failed to sync Remote Ingress allowed edges: ${err.message}`);
                    }
                }, (preparedRoutes) => buildHttpRedirectRuntimeRoutes(preparedRoutes || []), (storedRoute) => this.emailRouteBuilder.hydrateStoredRouteForRuntime(storedRoute), (routes) => this.routePolicyAugmenter.applyInboundProxyProtocolPolicies(routes));
                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 and durable DB persistence.
        if (this.options.dbConfig?.enabled !== false) {
            const emailServiceDeps = ['SmartProxy', 'MetricsManager'];
            emailServiceDeps.push('EmailDomainManager');
            this.serviceManager.addService(new plugins.taskbuffer.Service('EmailServer')
                .optional()
                .dependsOn(...emailServiceDeps)
                .withStart(async () => {
                await this.queueEmailLifecycleTask(async () => {
                    if (!this.options.emailConfig) {
                        logger.log('info', 'EmailServer: no email settings configured, skipping startup');
                        return;
                    }
                    await this.setupUnifiedEmailHandling();
                });
            })
                .withStop(async () => {
                await this.queueEmailLifecycleTask(async () => {
                    await this.stopUnifiedEmailComponents();
                });
            })
                .withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }));
        }
        else if (this.options.emailConfig) {
            logger.log('warn', 'EmailServer: dbConfig.enabled=false, skipping SMTP startup because accepted email requires durable DB persistence');
        }
        // 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.dbConfig?.enabled !== false) ? ['EmailServer'] : []))
                .withStart(async () => {
                await this.dnsServerRuntime.setup();
            })
                .withStop(async () => {
                // Flush pending DNS batch log
                this.dnsServerRuntime.flushQueryLogBatch();
                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 and DB-backed settings.
        // The service starts as a no-op when the DB setting is disabled, so the UI
        // can still manage edge registrations and hub settings.
        if (this.options.dbConfig?.enabled !== false) {
            this.serviceManager.addService(new plugins.taskbuffer.Service('RemoteIngress')
                .optional()
                .dependsOn('SmartProxy', 'RemoteIngressManager')
                .withStart(async () => {
                await this.remoteIngressHubLifecycle.setup();
            })
                .withStop(async () => {
                await this.remoteIngressHubLifecycle.stop();
            })
                .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 } : {}),
            });
        });
    }
    isRemoteIngressHubEnabled() {
        return this.remoteIngressManager?.getHubSettings().enabled
            ?? this.options.remoteIngressConfig?.enabled
            ?? false;
    }
    getRemoteIngressHubSettingsMigrationSeed() {
        const remoteIngressConfig = this.options.remoteIngressConfig;
        const seed = {};
        if (remoteIngressConfig?.enabled !== undefined) {
            seed.enabled = remoteIngressConfig.enabled;
        }
        if (remoteIngressConfig?.tunnelPort !== undefined) {
            seed.tunnelPort = remoteIngressConfig.tunnelPort;
        }
        if (remoteIngressConfig?.hubDomain !== undefined) {
            seed.hubDomain = remoteIngressConfig.hubDomain;
        }
        if (remoteIngressConfig?.performance !== undefined) {
            seed.performance = remoteIngressConfig.performance;
        }
        return seed;
    }
    getEmailSettingsMigrationSeed() {
        const seed = {};
        if (this.options.emailConfig) {
            seed.enabled = true;
            seed.emailConfig = JSON.parse(JSON.stringify(this.options.emailConfig));
        }
        if (this.options.emailPortConfig) {
            seed.emailPortConfig = JSON.parse(JSON.stringify(this.options.emailPortConfig));
        }
        return seed;
    }
    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
        const remoteIngressHubSettings = this.remoteIngressManager?.getHubSettings();
        if (this.tunnelManager && remoteIngressHubSettings?.enabled) {
            const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0;
            const connectedCount = this.tunnelManager.getConnectedCount();
            logger.log('info', `Remote Ingress: tunnel port=${remoteIngressHubSettings.tunnelPort}, 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, {
            remoteIngressHubSettings: this.getRemoteIngressHubSettingsMigrationSeed(),
            emailServerSettings: this.getEmailSettingsMigrationSeed(),
        });
        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) {
            const existingSmartProxy = this.smartProxy;
            try {
                existingSmartProxy.removeAllListeners();
                await existingSmartProxy.stop();
                if (this.smartProxy === existingSmartProxy) {
                    this.smartProxy = undefined;
                }
            }
            finally {
                await this.smartAcmeLifecycle.stop();
            }
        }
        // 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.options.dbConfig?.enabled !== false) {
            this.seedEmailRoutes = this.emailRouteBuilder.generateEmailRoutes(this.options.emailConfig);
            logger.log('debug', 'Email routes generated', { routes: JSON.stringify(this.seedEmailRoutes) });
        }
        else if (this.options.emailConfig) {
            logger.log('warn', 'Email routes skipped because dbConfig.enabled=false and SMTP acceptance requires durable DB persistence');
        }
        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.emailRouteBuilder.getRuntimeEmailRoutes(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');
        }
        routes = this.routePolicyAugmenter.applyInboundProxyProtocolPolicies(routes);
        const compiledSecurityPolicy = await this.securityPolicyManager?.compileSmartProxyPolicy();
        const mergedSecurityPolicy = this.routePolicyAugmenter.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.publicKey === publicKey &&
                            doc.privateKey === privateKey &&
                            doc.ca === (ca || '')) {
                            // Unchanged cert — skip the write. Re-saving identical PEMs on
                            // every provisioning sweep is what ballooned the DB WAL/oplog.
                            return;
                        }
                        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.
            // SmartAcme starts in the background because ACME account setup can be slow or rate-limited,
            // and must not block dcrouter's global startup timeout.
            if (this.smartAcme) {
                await this.smartAcmeLifecycle.stop();
            }
            if (challengeHandlers.length > 0) {
                // 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'],
                });
                if (this.smartAcmeLifecycle.serviceStarted) {
                    this.smartAcmeLifecycle.startInBackground();
                }
                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.smartAcmeLifecycle.ready) {
                        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);
                    }
                };
            }
            // RemoteIngress and VPN forward through localhost with PROXY protocol.
            // SmartProxy only uses this as a trust list; routes still opt in per listener.
            if (this.isRemoteIngressHubEnabled() || this.options.vpnConfig?.enabled) {
                const trustedProxyIPs = new Set(smartProxyConfig.trustedProxyIPs || []);
                trustedProxyIPs.add('127.0.0.1');
                smartProxyConfig.trustedProxyIPs = [...trustedProxyIPs];
            }
            // Create SmartProxy instance
            logger.log('info', `Creating SmartProxy instance: routes=${smartProxyConfig.routes?.length}, acme=${smartProxyConfig.acme?.enabled}, certProvisionFunction=${!!smartProxyConfig.certProvisionFunction}`);
            const smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
            smartProxy.registerChallengeProvider('smartchallenge', new plugins.smartchallenge.SmartChallengeProvider({
                challengeTypes: [new plugins.smartchallenge.WaitChallengeType()],
            }));
            this.smartProxy = smartProxy;
            // Set up event listeners
            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
            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.
            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...');
            try {
                await smartProxy.start();
            }
            catch (err) {
                smartProxy.removeAllListeners();
                if (this.smartProxy === smartProxy) {
                    this.smartProxy = undefined;
                }
                await this.smartAcmeLifecycle.stop();
                if (this.certProvisionScheduler) {
                    this.certProvisionScheduler.clear();
                    this.certProvisionScheduler = undefined;
                }
                await smartProxy.stop().catch((stopErr) => {
                    logger.log('warn', `Failed to clean up SmartProxy after startup failure: ${stopErr.message}`);
                });
                throw err;
            }
            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.routePolicyAugmenter.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();
        await this.remoteIngressHubLifecycle.applyFirewallConfig(firewallConfig);
    }
    /**
     * 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.dnsServerRuntime.createSocketHandler()
                    }
                    : {
                        type: 'socket-handler',
                    }
            };
            dnsRoutes.push(dohRoute);
        }
        return dnsRoutes;
    }
    /**
     * Check if a domain matches a pattern (including wildcard support)
     * @param domain The domain to check
     * @param pattern The pattern to match against (e.g., "*.example.com")
     * @returns Whether the domain matches the pattern
     */
    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');
        }
        if (!this.dcRouterDb?.isReady()) {
            throw new Error('DcRouterDb is required for email acceptance');
        }
        // Apply port mapping if behind SmartProxy
        const portMapping = this.options.emailPortConfig?.portMapping || {
            25: 10025, // SMTP
            587: 10587, // Submission
            465: 10465 // SMTPS
        };
        // Create config with mapped ports
        const baseEmailConfig = this.options.emailConfig;
        const configuredMessageDataHook = baseEmailConfig.hooks?.onMessageData;
        const mappedEmailPorts = baseEmailConfig.ports.map(port => portMapping[port] || port + 10000);
        const configuredSecurePort = baseEmailConfig.smtp?.securePort;
        const mappedSecurePort = configuredSecurePort === undefined
            ? undefined
            : mappedEmailPorts.includes(configuredSecurePort) && !baseEmailConfig.ports.includes(configuredSecurePort)
                ? configuredSecurePort
                : portMapping[configuredSecurePort] || configuredSecurePort + 10000;
        const emailConfig = await this.workAppMailManager.applyStoredIdentitiesToEmailConfig({
            ...this.options.emailConfig,
            ports: mappedEmailPorts,
            persistRoutes: this.options.emailConfig.persistRoutes ?? false,
            queue: {
                ...this.options.emailConfig.queue,
                storageType: 'memory',
            },
            smtp: {
                ...baseEmailConfig.smtp,
                ...(mappedSecurePort !== undefined
                    ? { securePort: mappedSecurePort }
                    : {}),
                recipientValidation: true,
                proxyProtocol: {
                    ...baseEmailConfig.smtp?.proxyProtocol,
                    required: true,
                    trustedIps: ['127.0.0.1', '::1'],
                },
            },
            hooks: {
                ...baseEmailConfig.hooks,
                onMessageData: async (context) => {
                    const configuredDecision = configuredMessageDataHook
                        ? await configuredMessageDataHook(context)
                        : undefined;
                    if (configuredDecision && !configuredDecision.accepted) {
                        return configuredDecision;
                    }
                    const dcrouterDecision = await this.acceptedEmailSpool.acceptMessage(context, configuredDecision ? configuredDecision.continueProcessing === true : true);
                    return {
                        ...dcrouterDecision,
                        smtpCode: configuredDecision?.smtpCode ?? dcrouterDecision.smtpCode,
                        smtpMessage: configuredDecision?.smtpMessage ?? dcrouterDecision.smtpMessage,
                    };
                },
            },
        });
        // Create unified email server
        const emailServer = new UnifiedEmailServer(this, emailConfig);
        this.emailServer = emailServer;
        this.clearEmailEventSubscriptions();
        // Set up error handling
        this.addEmailEventSubscription(emailServer, 'error', (err) => {
            logger.log('error', `UnifiedEmailServer error: ${err.message}`);
        });
        // Start the server
        try {
            await emailServer.start();
        }
        catch (error) {
            this.clearEmailEventSubscriptions();
            try {
                await emailServer.stop();
            }
            catch (stopError) {
                logger.log('warn', `Error cleaning up failed UnifiedEmailServer start: ${stopError.message}`);
            }
            if (this.emailServer === emailServer) {
                this.emailServer = undefined;
            }
            throw error;
        }
        try {
            this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemEnqueued', (item) => {
                this.acceptedEmailSpool.trackQueueUpdate(item, 'queued', 'Unable to update accepted email after queue enqueue');
            });
            this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemDelivered', (item) => {
                this.acceptedEmailSpool.trackQueueUpdate(item, 'delivered', 'Unable to mark accepted email delivered');
            });
            this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemDeferred', (item) => {
                this.acceptedEmailSpool.trackQueueUpdate(item, 'queued', 'Unable to defer accepted email');
            });
            this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemFailed', (item) => {
                this.acceptedEmailSpool.trackQueueUpdate(item, 'failed', 'Unable to mark accepted email failed');
            });
            // Wire delivery events to MetricsManager and logger using smartmta's public queue APIs.
            if (this.metricsManager) {
                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(emailServer.getQueueStats().queueSize);
                };
                this.addEmailEventSubscription(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(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(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(emailServer.deliveryQueue, 'itemDeferred', () => {
                    updateQueueSize();
                });
                this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemRemoved', () => {
                    updateQueueSize();
                });
                this.addEmailEventSubscription(emailServer, 'bounceProcessed', () => {
                    this.metricsManager.trackEmailBounced();
                    logger.log('warn', 'Email bounce processed', { zone: 'email' });
                });
                updateQueueSize();
            }
            await this.acceptedEmailSpool.recoverQueuedEmails();
            this.acceptedEmailSpool.start();
        }
        catch (error) {
            this.acceptedEmailSpool.beginStop();
            try {
                await emailServer.stop();
            }
            catch (stopError) {
                logger.log('warn', `Error cleaning up failed UnifiedEmailServer setup: ${stopError.message}`);
            }
            await this.acceptedEmailSpool.stop();
            await this.acceptedEmailSpool.drainQueueUpdates();
            this.clearEmailEventSubscriptions();
            if (this.emailServer === emailServer) {
                this.emailServer = undefined;
            }
            throw error;
        }
        logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
    }
    /**
     * Update the unified email configuration
     * @param config New email configuration
     */
    async updateEmailConfig(config) {
        await this.queueEmailLifecycleTask(async () => {
            // Stop existing email components
            await this.stopUnifiedEmailComponents();
            // Update configuration
            this.options.emailConfig = config;
            this.emailDomainManager?.setBaseEmailDomains(config.domains);
            await this.emailDomainManager?.syncManagedDomainsToRuntime();
            // Start email handling with new configuration
            await this.setupUnifiedEmailHandling();
            logger.log('info', 'Unified email configuration updated');
        });
    }
    async updateEmailServerSettings(settings, updatedBy = 'system') {
        return await this.queueEmailLifecycleTask(async () => {
            if (!this.emailSettingsManager) {
                throw new Error('EmailSettingsManager is not initialized');
            }
            const updatedSettings = await this.emailSettingsManager.updateSettings(settings, updatedBy);
            this.emailDomainManager?.setBaseEmailDomains(this.options.emailConfig?.domains);
            await this.emailDomainManager?.syncManagedDomainsToRuntime();
            this.seedEmailRoutes = this.options.emailConfig
                ? this.emailRouteBuilder.generateEmailRoutes(this.options.emailConfig)
                : [];
            if (this.routeConfigManager) {
                await this.routeConfigManager.initialize(this.seedConfigRoutes, this.seedEmailRoutes, this.seedDnsRoutes);
            }
            if (this.options.emailConfig) {
                if (this.emailServer) {
                    await this.stopUnifiedEmailComponents();
                }
                await this.setupUnifiedEmailHandling();
            }
            else if (this.emailServer) {
                await this.stopUnifiedEmailComponents();
            }
            return updatedSettings;
        });
    }
    /**
     * Stop all unified email components
     */
    async stopUnifiedEmailComponents() {
        try {
            // Stop the unified email server which contains all components
            if (this.emailServer) {
                const emailServer = this.emailServer;
                this.emailServer = undefined;
                this.acceptedEmailSpool.beginStop();
                try {
                    await emailServer.stop();
                }
                finally {
                    await this.acceptedEmailSpool.stop();
                    await this.acceptedEmailSpool.drainQueueUpdates();
                    this.clearEmailEventSubscriptions();
                }
                logger.log('info', 'Unified email server stopped');
            }
            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
     */
    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 = [];
    }
    /**
     * Set up Remote Ingress hub for edge tunnel connections
     */
    queueSmartProxyLifecycleTask(task) {
        const run = this.smartProxyLifecycleChain.then(task);
        this.smartProxyLifecycleChain = run.then(() => undefined, () => undefined);
        return run;
    }
    queueEmailLifecycleTask(task) {
        const run = this.emailLifecycleChain.then(task);
        this.emailLifecycleChain = run.then(() => undefined, () => undefined);
        return run;
    }
    /** Serialized edge mutation on the RemoteIngress hub (delegates to the hub lifecycle). */
    async mutateRemoteIngressEdges(mutation, syncAllowedEdges = true) {
        return await this.remoteIngressHubLifecycle.mutateEdges(mutation, syncAllowedEdges);
    }
    async updateRemoteIngressHubSettings(updates, updatedBy) {
        return await this.remoteIngressHubLifecycle.updateHubSettings(updates, updatedBy);
    }
    /** Restart SmartProxy after RemoteIngress hub settings changed listener wiring. Called by RemoteIngressHubLifecycle. */
    async restartSmartProxyForRemoteIngressSettings() {
        await this.queueSmartProxyLifecycleTask(async () => {
            const restartSmartProxy = async () => {
                try {
                    if (this.smartProxy) {
                        const existingSmartProxy = this.smartProxy;
                        existingSmartProxy.removeAllListeners();
                        await existingSmartProxy.stop();
                        if (this.smartProxy === existingSmartProxy) {
                            this.smartProxy = undefined;
                        }
                    }
                }
                finally {
                    await this.smartAcmeLifecycle.stop();
                }
                await this.setupSmartProxy();
            };
            if (this.routeConfigManager) {
                await this.routeConfigManager.runExclusiveRouteUpdate(restartSmartProxy);
            }
            else {
                await restartSmartProxy();
            }
            if (!this.routeConfigManager) {
                return;
            }
            await this.routeConfigManager.initialize(this.seedConfigRoutes, this.seedEmailRoutes, this.seedDnsRoutes);
        });
    }
    /** Bootstrap routes the RemoteIngress hub uses to derive edge ports before the DB route set is applied. */
    getRemoteIngressBootstrapRoutes() {
        return [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
    }
    /**
     * Set up VPN server for VPN-based route access control.
     */
    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) => await this.vpnAccessResolver.getClientAllowedIPs(targetProfileIds),
        });
        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();
    }
    // 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.vpnAccessResolver.reset();
        this.routeConfigManager?.setVpnClientAccessResolver(this.vpnAccessResolver.createRouteAllowResolver());
        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, 