import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { RouteDoc } from '../db/index.js';
import { routePathClasses } from '../../ts_interfaces/data/route-management.js';
import type {
  IHttpRedirectInfo,
  IRoute,
  IMergedRoute,
  IRouteWarning,
  IRouteMetadata,
  IRoutePathPolicyBinding,
  IRouteSourceBinding,
  IRouteSecurity,
} from '../../ts_interfaces/data/route-management.js';
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
import type { ReferenceResolver } from './classes.reference-resolver.js';
import { SourcePolicyCompiler } from './classes.source-policy-compiler.js';
import { deriveHttpRedirects } from './helpers.http-redirects.js';

export type TVpnClientAllowEntry = string | { clientId: string; domains: string[] };

export interface IRouteMutationResult {
  success: boolean;
  message?: string;
}

/**
 * Simple async mutex — serializes concurrent applyRoutes() calls so the Rust engine
 * never receives rapid overlapping route updates that can churn UDP/QUIC listeners.
 */
class RouteUpdateMutex {
  private locked = false;
  private queue: Array<() => void> = [];

  async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
    await new Promise<void>((resolve) => {
      if (!this.locked) {
        this.locked = true;
        resolve();
      } else {
        this.queue.push(resolve);
      }
    });
    try {
      return await fn();
    } finally {
      this.locked = false;
      const next = this.queue.shift();
      if (next) {
        this.locked = true;
        next();
      }
    }
  }
}

export class RouteConfigManager {
  private routes = new Map<string, IRoute>();
  private warnings: IRouteWarning[] = [];
  private routeUpdateMutex = new RouteUpdateMutex();

  constructor(
    private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
    private getHttp3Config?: () => IHttp3Config | undefined,
    private getVpnClientAccessForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TVpnClientAllowEntry[],
    private referenceResolver?: ReferenceResolver,
    private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void | Promise<void>,
    private getRuntimeRoutes?: (preparedRoutes?: plugins.smartproxy.IRouteConfig[]) => plugins.smartproxy.IRouteConfig[],
    private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined,
    private applyInboundProxyPolicies?: (routes: plugins.smartproxy.IRouteConfig[]) => plugins.smartproxy.IRouteConfig[],
  ) {}

  /** Expose routes map for reference resolution lookups. */
  public getRoutes(): Map<string, IRoute> {
    return this.routes;
  }

  public getRoute(id: string): IRoute | undefined {
    return this.routes.get(id);
  }

  public setVpnClientAccessResolver(
    resolver?: (route: IDcRouterRouteConfig, routeId?: string) => TVpnClientAllowEntry[],
  ): void {
    this.getVpnClientAccessForRoute = resolver;
  }

  public async runExclusiveRouteUpdate<T>(fn: () => Promise<T>): Promise<T> {
    return await this.routeUpdateMutex.runExclusive(fn);
  }

  /**
   * Load persisted routes, seed serializable config/email/dns routes,
   * compute warnings, and apply the combined DB-backed + runtime route set to SmartProxy.
   */
  public async initialize(
    configRoutes: IDcRouterRouteConfig[] = [],
    emailRoutes: IDcRouterRouteConfig[] = [],
    dnsRoutes: IDcRouterRouteConfig[] = [],
  ): Promise<void> {
    await this.loadRoutes();
    await this.seedRoutes(configRoutes, 'config');
    await this.seedRoutes(emailRoutes, 'email');
    await this.seedRoutes(dnsRoutes, 'dns');
    this.computeWarnings();
    this.logWarnings();
    await this.applyRoutes();
  }

  // =========================================================================
  // Route listing
  // =========================================================================

  public getMergedRoutes(): { routes: IMergedRoute[]; warnings: IRouteWarning[] } {
    const merged: IMergedRoute[] = [];

    for (const route of this.routes.values()) {
      merged.push({
        route: route.route,
        id: route.id,
        enabled: route.enabled,
        origin: route.origin,
        systemKey: route.systemKey,
        createdAt: route.createdAt,
        updatedAt: route.updatedAt,
        metadata: route.metadata,
      });
    }

    return { routes: merged, warnings: [...this.warnings] };
  }

  public getHttpRedirects(): IHttpRedirectInfo[] {
    return deriveHttpRedirects(this.getPreparedEnabledRoutesForApply());
  }

  // =========================================================================
  // Route CRUD
  // =========================================================================

  public async createRoute(
    route: IDcRouterRouteConfig,
    createdBy: string,
    enabled = true,
    metadata?: IRouteMetadata,
  ): Promise<string> {
    const id = plugins.uuid.v4();
    const now = Date.now();
    const sourceBindingsPayloadError = SourcePolicyCompiler.validateSourceBindingsPayload(metadata?.sourceBindings);
    if (sourceBindingsPayloadError) {
      throw new Error(sourceBindingsPayloadError);
    }

    // Ensure route has a name
    if (!route.name) {
      route.name = `route-${id.slice(0, 8)}`;
    }

    // Resolve references if metadata has refs and resolver is available
    let resolvedMetadata = this.normalizeRouteMetadata(metadata);
    if (resolvedMetadata && this.referenceResolver) {
      const resolved = this.referenceResolver.resolveRoute(route, resolvedMetadata);
      route = resolved.route;
      resolvedMetadata = this.normalizeRouteMetadata(resolved.metadata);
    }
    const sourceBindingsValidationError = this.validateSourceBindings(resolvedMetadata?.sourceBindings, route);
    if (sourceBindingsValidationError) {
      throw new Error(sourceBindingsValidationError);
    }

    const stored: IRoute = {
      id,
      route,
      enabled,
      createdAt: now,
      updatedAt: now,
      createdBy,
      origin: 'api',
      metadata: resolvedMetadata,
    };

    this.routes.set(id, stored);
    await this.persistRoute(stored);
    await this.applyRoutes();
    return id;
  }

  public async updateRoute(
    id: string,
    patch: {
      route?: Partial<IDcRouterRouteConfig>;
      enabled?: boolean;
      metadata?: Partial<IRouteMetadata>;
    },
  ): Promise<IRouteMutationResult> {
    const stored = this.routes.get(id);
    if (!stored) {
      return { success: false, message: 'Route not found' };
    }
    const sourceBindingsPayloadError = SourcePolicyCompiler.validateSourceBindingsPayload(patch.metadata?.sourceBindings);
    if (sourceBindingsPayloadError) {
      return { success: false, message: sourceBindingsPayloadError };
    }

    const previousRoute = structuredClone(stored.route);
    const previousMetadata = structuredClone(stored.metadata);
    const previousEnabled = stored.enabled;

    const isToggleOnlyPatch = patch.enabled !== undefined
      && patch.route === undefined
      && patch.metadata === undefined;
    if (stored.origin !== 'api' && !isToggleOnlyPatch) {
      return {
        success: false,
        message: 'System routes are managed by the system and can only be toggled',
      };
    }

    if (patch.route) {
      const mergedAction = patch.route.action
        ? { ...stored.route.action, ...patch.route.action }
        : stored.route.action;
      // Handle explicit null to remove nested action properties (e.g., tls: null)
      if (patch.route.action) {
        for (const [key, val] of Object.entries(patch.route.action)) {
          if (val === null) {
            delete (mergedAction as any)[key];
          }
        }
      }
      const mergedRoute = { ...stored.route, ...patch.route, action: mergedAction } as IDcRouterRouteConfig;

      // Handle explicit null to remove optional top-level route properties (e.g., remoteIngress: null)
      for (const [key, val] of Object.entries(patch.route)) {
        if (val === null && key !== 'action' && key !== 'match') {
          delete (mergedRoute as any)[key];
        }
      }

      stored.route = mergedRoute;
    }
    if (patch.enabled !== undefined) {
      stored.enabled = patch.enabled;
    }
    if (patch.metadata !== undefined) {
      stored.metadata = this.normalizeRouteMetadata({
        ...stored.metadata,
        ...patch.metadata,
      });
    }

    // Re-resolve if metadata refs exist and resolver is available
    if (stored.metadata && this.referenceResolver) {
      const resolved = this.referenceResolver.resolveRoute(stored.route, stored.metadata);
      stored.route = resolved.route;
      stored.metadata = this.normalizeRouteMetadata(resolved.metadata);
    }

    const sourceBindingsValidationError = this.validateSourceBindings(stored.metadata?.sourceBindings, stored.route);
    if (sourceBindingsValidationError) {
      stored.route = previousRoute;
      stored.metadata = previousMetadata;
      stored.enabled = previousEnabled;
      return { success: false, message: sourceBindingsValidationError };
    }

    stored.updatedAt = Date.now();

    await this.persistRoute(stored);
    await this.applyRoutes();
    return { success: true };
  }

  public async deleteRoute(id: string): Promise<IRouteMutationResult> {
    const stored = this.routes.get(id);
    if (!stored) {
      return { success: false, message: 'Route not found' };
    }
    if (stored.origin !== 'api') {
      return {
        success: false,
        message: 'System routes are managed by the system and cannot be deleted',
      };
    }

    this.routes.delete(id);
    const doc = await RouteDoc.findById(id);
    if (doc) await doc.delete();
    await this.applyRoutes();
    return { success: true };
  }

  public async toggleRoute(id: string, enabled: boolean): Promise<IRouteMutationResult> {
    return this.updateRoute(id, { enabled });
  }

  public findApiRouteByExternalKey(externalKey: string): IRoute | undefined {
    for (const route of this.routes.values()) {
      if (route.origin === 'api' && route.metadata?.externalKey === externalKey) {
        return route;
      }
    }
    return undefined;
  }

  // =========================================================================
  // Private: seed routes from constructor config
  // =========================================================================

  /**
   * Upsert seed routes by name+origin. Preserves user's `enabled` state.
   * Deletes stale DB routes whose origin matches but name is not in the seed set.
   */
  private async seedRoutes(
    seedRoutes: IDcRouterRouteConfig[],
    origin: 'config' | 'email' | 'dns',
  ): Promise<void> {
    const seedSystemKeys = new Set<string>();
    const seedNames = new Set<string>();
    let seeded = 0;
    let updated = 0;

    for (const route of seedRoutes) {
      const name = route.name || '';
      if (name) {
        seedNames.add(name);
      }
      const systemKey = this.buildSystemRouteKey(origin, route);
      if (systemKey) {
        seedSystemKeys.add(systemKey);
      }

      const existingId = this.findExistingSeedRouteId(origin, route, systemKey);

      if (existingId) {
        // Update route config but preserve enabled state
        const existing = this.routes.get(existingId)!;
        existing.route = route;
        existing.systemKey = systemKey;
        existing.updatedAt = Date.now();
        await this.persistRoute(existing);
        updated++;
      } else {
        // Insert new seed route
        const id = plugins.uuid.v4();
        const now = Date.now();
        const newRoute: IRoute = {
          id,
          route,
          enabled: true,
          createdAt: now,
          updatedAt: now,
          createdBy: 'system',
          origin,
          systemKey,
        };
        this.routes.set(id, newRoute);
        await this.persistRoute(newRoute);
        seeded++;
      }
    }

    // Delete stale routes: same origin but name not in current seed set
    const staleIds: string[] = [];
    for (const [id, r] of this.routes) {
      if (r.origin !== origin) continue;

      const routeName = r.route.name || '';
      const matchesSeedSystemKey = r.systemKey ? seedSystemKeys.has(r.systemKey) : false;
      const matchesSeedName = routeName ? seedNames.has(routeName) : false;
      if (!matchesSeedSystemKey && !matchesSeedName) {
        staleIds.push(id);
      }
    }
    for (const id of staleIds) {
      this.routes.delete(id);
      const doc = await RouteDoc.findById(id);
      if (doc) await doc.delete();
    }

    if (seeded > 0 || updated > 0 || staleIds.length > 0) {
      logger.log('info', `Seed routes (${origin}): ${seeded} new, ${updated} updated, ${staleIds.length} stale removed`);
    }
  }

  // =========================================================================
  // Private: persistence
  // =========================================================================

  private buildSystemRouteKey(
    origin: 'config' | 'email' | 'dns',
    route: IDcRouterRouteConfig,
  ): string | undefined {
    const name = route.name?.trim();
    if (!name) return undefined;
    return `${origin}:${name}`;
  }

  private findExistingSeedRouteId(
    origin: 'config' | 'email' | 'dns',
    route: IDcRouterRouteConfig,
    systemKey?: string,
  ): string | undefined {
    const routeName = route.name || '';

    for (const [id, storedRoute] of this.routes) {
      if (storedRoute.origin !== origin) continue;

      if (systemKey && storedRoute.systemKey === systemKey) {
        return id;
      }

      if (storedRoute.route.name === routeName) {
        return id;
      }
    }

    return undefined;
  }

  private async loadRoutes(): Promise<void> {
    const docs = await RouteDoc.findAll();

    for (const doc of docs) {
      if (!doc.id) continue;

      const storedRoute: IRoute = {
        id: doc.id,
        route: doc.route,
        enabled: doc.enabled,
        createdAt: doc.createdAt,
        updatedAt: doc.updatedAt,
        createdBy: doc.createdBy,
        origin: doc.origin || 'api',
        systemKey: doc.systemKey,
        metadata: this.normalizeRouteMetadata(doc.metadata),
      };

      this.routes.set(doc.id, storedRoute);
    }
    if (this.routes.size > 0) {
      logger.log('info', `Loaded ${this.routes.size} route(s) from database`);
    }
  }

  private async persistRoute(stored: IRoute): Promise<void> {
    const existingDoc = await RouteDoc.findById(stored.id);
    if (existingDoc) {
      existingDoc.route = stored.route;
      existingDoc.enabled = stored.enabled;
      existingDoc.updatedAt = stored.updatedAt;
      existingDoc.createdBy = stored.createdBy;
      existingDoc.origin = stored.origin;
      existingDoc.systemKey = stored.systemKey;
      existingDoc.metadata = stored.metadata;
      await existingDoc.save();
    } else {
      const doc = new RouteDoc();
      doc.id = stored.id;
      doc.route = stored.route;
      doc.enabled = stored.enabled;
      doc.createdAt = stored.createdAt;
      doc.updatedAt = stored.updatedAt;
      doc.createdBy = stored.createdBy;
      doc.origin = stored.origin;
      doc.systemKey = stored.systemKey;
      doc.metadata = stored.metadata;
      await doc.save();
    }
  }

  private normalizeRouteMetadata(metadata?: Partial<IRouteMetadata>): IRouteMetadata | undefined {
    if (!metadata) {
      return undefined;
    }

    const normalizeString = (value?: string): string | undefined => {
      if (typeof value !== 'string') {
        return undefined;
      }
      const trimmed = value.trim();
      return trimmed.length > 0 ? trimmed : undefined;
    };

    const normalized: IRouteMetadata = {
      sourceBindings: this.normalizeSourceBindings(metadata.sourceBindings),
      networkTargetRef: normalizeString(metadata.networkTargetRef),
      networkTargetName: normalizeString(metadata.networkTargetName),
      lastResolvedAt: typeof metadata.lastResolvedAt === 'number' && Number.isFinite(metadata.lastResolvedAt)
        ? metadata.lastResolvedAt
        : undefined,
      ownerType: metadata.ownerType === 'gatewayClient' || metadata.ownerType === 'workhoster' || metadata.ownerType === 'operator' || metadata.ownerType === 'system'
        ? metadata.ownerType
        : undefined,
      gatewayClientType: metadata.gatewayClientType === 'onebox' || metadata.gatewayClientType === 'cloudly' || metadata.gatewayClientType === 'custom'
        ? metadata.gatewayClientType
        : metadata.workHosterType,
      gatewayClientId: normalizeString(metadata.gatewayClientId || metadata.workHosterId),
      gatewayClientAppId: normalizeString(metadata.gatewayClientAppId || metadata.workAppId),
      workHosterType: metadata.workHosterType === 'onebox' || metadata.workHosterType === 'cloudly' || metadata.workHosterType === 'custom'
        ? metadata.workHosterType
        : metadata.gatewayClientType,
      workHosterId: normalizeString(metadata.workHosterId || metadata.gatewayClientId),
      workAppId: normalizeString(metadata.workAppId || metadata.gatewayClientAppId),
      externalKey: normalizeString(metadata.externalKey),
    };

    if (!normalized.networkTargetRef) {
      normalized.networkTargetName = undefined;
    }
    if (!normalized.sourceBindings && !normalized.networkTargetRef) {
      normalized.lastResolvedAt = undefined;
    }
    if (normalized.ownerType !== 'gatewayClient' && normalized.ownerType !== 'workhoster') {
      normalized.gatewayClientType = undefined;
      normalized.gatewayClientId = undefined;
      normalized.gatewayClientAppId = undefined;
      normalized.workHosterType = undefined;
      normalized.workHosterId = undefined;
      normalized.workAppId = undefined;
      normalized.externalKey = undefined;
    } else {
      normalized.ownerType = 'gatewayClient';
      normalized.workHosterType = normalized.gatewayClientType;
      normalized.workHosterId = normalized.gatewayClientId;
      normalized.workAppId = normalized.gatewayClientAppId;
    }

    if (Object.values(normalized).every((value) => value === undefined)) {
      return undefined;
    }

    return normalized;
  }

  private normalizeSourceBindings(sourceBindings?: Partial<IRouteSourceBinding>[]): IRouteSourceBinding[] | undefined {
    if (!Array.isArray(sourceBindings)) {
      return undefined;
    }

    const normalizedBindings: IRouteSourceBinding[] = [];
    for (const binding of sourceBindings) {
      const sourceProfileRef = typeof binding.sourceProfileRef === 'string'
        ? binding.sourceProfileRef.trim()
        : '';
      if (!sourceProfileRef) {
        continue;
      }
      const normalizedRateLimit = this.normalizeRateLimit(binding.rateLimit);
      const normalizedPathPolicies = this.normalizePathPolicies(binding.pathPolicies);

      normalizedBindings.push({
        ...(typeof binding.id === 'string' && binding.id.trim() ? { id: binding.id.trim() } : {}),
        sourceProfileRef,
        ...(typeof binding.sourceProfileName === 'string' && binding.sourceProfileName.trim()
          ? { sourceProfileName: binding.sourceProfileName.trim() }
          : {}),
        ...(normalizedRateLimit ? { rateLimit: normalizedRateLimit } : {}),
        ...(typeof binding.maxConnections === 'number' && Number.isFinite(binding.maxConnections) && binding.maxConnections >= 0
          ? { maxConnections: binding.maxConnections }
          : {}),
        ...(binding.onExceeded?.type === '429'
          ? {
              onExceeded: {
                type: '429' as const,
                ...(typeof binding.onExceeded.errorMessage === 'string' && binding.onExceeded.errorMessage.trim()
                  ? { errorMessage: binding.onExceeded.errorMessage.trim() }
                  : {}),
              },
            }
          : {}),
        ...(normalizedPathPolicies ? { pathPolicies: normalizedPathPolicies } : {}),
      });
    }

    return normalizedBindings.length > 0 ? normalizedBindings : undefined;
  }

  private normalizePathPolicies(
    pathPolicies?: IRoutePathPolicyBinding[],
  ): IRoutePathPolicyBinding[] | undefined {
    if (!Array.isArray(pathPolicies)) {
      return undefined;
    }

    const validClasses = new Set<string>(routePathClasses);
    const normalizedPathPolicies: IRoutePathPolicyBinding[] = [];
    for (const pathPolicy of pathPolicies) {
      if (!validClasses.has(pathPolicy.pathClass)) {
        continue;
      }

      const normalizedRateLimit = this.normalizeRateLimit(pathPolicy.rateLimit);
      const pathPatterns = Array.isArray(pathPolicy.pathPatterns)
        ? [...new Set(pathPolicy.pathPatterns
            .map((pattern) => typeof pattern === 'string' ? pattern.trim() : '')
            .filter(Boolean))]
        : undefined;

      normalizedPathPolicies.push({
        ...(typeof pathPolicy.id === 'string' && pathPolicy.id.trim() ? { id: pathPolicy.id.trim() } : {}),
        pathClass: pathPolicy.pathClass,
        ...(pathPatterns?.length ? { pathPatterns } : {}),
        ...(normalizedRateLimit ? { rateLimit: normalizedRateLimit } : {}),
        ...(typeof pathPolicy.maxConnections === 'number' && Number.isFinite(pathPolicy.maxConnections) && pathPolicy.maxConnections >= 0
          ? { maxConnections: pathPolicy.maxConnections }
          : {}),
        ...(pathPolicy.onExceeded?.type === '429'
          ? {
              onExceeded: {
                type: '429' as const,
                ...(typeof pathPolicy.onExceeded.errorMessage === 'string' && pathPolicy.onExceeded.errorMessage.trim()
                  ? { errorMessage: pathPolicy.onExceeded.errorMessage.trim() }
                  : {}),
              },
            }
          : {}),
      });
    }

    return normalizedPathPolicies.length > 0 ? normalizedPathPolicies : undefined;
  }

  private validateSourceBindings(
    sourceBindings: IRouteSourceBinding[] | undefined,
    route: IDcRouterRouteConfig,
  ): string | undefined {
    const shapeError = SourcePolicyCompiler.validateSourceBindingsShape(sourceBindings, route);
    if (shapeError) {
      return shapeError;
    }
    return SourcePolicyCompiler.validateResolvedSourceBindings(sourceBindings, this.referenceResolver);
  }

  private normalizeRateLimit(rateLimit?: IRouteSecurity['rateLimit']): IRouteSecurity['rateLimit'] | undefined {
    if (!rateLimit || typeof rateLimit !== 'object') {
      return undefined;
    }

    const maxRequests = Number(rateLimit.maxRequests);
    const window = Number(rateLimit.window);
    if (!Number.isFinite(maxRequests) || maxRequests < 0 || !Number.isFinite(window) || window < 0) {
      return undefined;
    }

    return {
      enabled: rateLimit.enabled !== false,
      maxRequests,
      window,
      keyBy: 'ip',
      ...(typeof rateLimit.errorMessage === 'string' && rateLimit.errorMessage.trim()
        ? { errorMessage: rateLimit.errorMessage.trim() }
        : {}),
    };
  }

  // =========================================================================
  // Private: warnings
  // =========================================================================

  private computeWarnings(): void {
    this.warnings = [];

    for (const route of this.routes.values()) {
      if (!route.enabled) {
        const name = route.route.name || route.id;
        this.warnings.push({
          type: 'disabled-route',
          routeName: name,
          message: `Route '${name}' (id: ${route.id}) is disabled`,
        });
      }
    }
  }

  private logWarnings(): void {
    for (const w of this.warnings) {
      logger.log('warn', w.message);
    }
  }

  // =========================================================================
  // Re-resolve routes after profile/target changes
  // =========================================================================

  /**
   * Re-resolve specific routes by ID (after a profile or target is updated).
   * Persists each route and calls applyRoutes() once at the end.
   */
  public async reResolveRoutes(routeIds: string[]): Promise<void> {
    if (!this.referenceResolver || routeIds.length === 0) return;

    for (const routeId of routeIds) {
      const stored = this.routes.get(routeId);
      if (!stored?.metadata) continue;

      const resolved = this.referenceResolver.resolveRoute(stored.route, stored.metadata);
      stored.route = resolved.route;
      stored.metadata = this.normalizeRouteMetadata(resolved.metadata);
      stored.updatedAt = Date.now();
      await this.persistRoute(stored);
    }

    await this.applyRoutes();
    logger.log('info', `Re-resolved ${routeIds.length} route(s) after profile/target change`);
  }

  // =========================================================================
  // Apply routes to SmartProxy
  // =========================================================================

  public async applyRoutes(): Promise<void> {
    await this.routeUpdateMutex.runExclusive(async () => {
      const smartProxy = this.getSmartProxy();
      if (!smartProxy) return;

      let enabledRoutes = this.getPreparedEnabledRoutesForApply();

      const runtimeRoutes = this.getRuntimeRoutes?.(enabledRoutes) || [];
      for (const route of runtimeRoutes) {
        enabledRoutes.push(this.prepareRouteForApply(route));
      }
      if (this.applyInboundProxyPolicies) {
        enabledRoutes = this.applyInboundProxyPolicies(enabledRoutes);
      }

      await smartProxy.updateRoutes(enabledRoutes);

      // Notify listeners (e.g. RemoteIngressManager) of the route set
      if (this.onRoutesApplied) {
        await this.onRoutesApplied(enabledRoutes);
      }

      logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.routes.size} total)`);
    });
  }

  private getPreparedEnabledRoutesForApply(): plugins.smartproxy.IRouteConfig[] {
    const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];

    // Add all enabled routes with HTTP/3, VPN, and source-policy augmentation
    for (const route of this.routes.values()) {
      if (route.enabled) {
        enabledRoutes.push(...this.prepareStoredRoutesForApply(route));
      }
    }

    return enabledRoutes;
  }

  private prepareStoredRoutesForApply(storedRoute: IRoute): plugins.smartproxy.IRouteConfig[] {
    if (this.isManagedAccessRoute(storedRoute) && !storedRoute.metadata?.sourceBindings?.length) {
      return [];
    }
    const hydratedRoute = this.hydrateStoredRoute?.(storedRoute);
    const sourceBoundRoutes = SourcePolicyCompiler.compileRoute(
      hydratedRoute || storedRoute.route,
      storedRoute.metadata,
      this.referenceResolver,
      storedRoute.id,
    );
    return sourceBoundRoutes.map((route) => this.prepareRouteForApply(route, storedRoute.id));
  }

  private isManagedAccessRoute(storedRoute: IRoute): boolean {
    const metadata = storedRoute.metadata;
    if (storedRoute.origin !== 'api' || !metadata) {
      return false;
    }
    return metadata.ownerType === 'gatewayClient'
      || metadata.ownerType === 'workhoster'
      || Boolean(metadata.gatewayClientId)
      || Boolean(metadata.workHosterId)
      || Boolean(metadata.externalKey);
  }

  private prepareRouteForApply(
    route: plugins.smartproxy.IRouteConfig,
    routeId?: string,
  ): plugins.smartproxy.IRouteConfig {
    let preparedRoute = route;
    const http3Config = this.getHttp3Config?.();

    if (http3Config?.enabled !== false) {
      preparedRoute = augmentRouteWithHttp3(preparedRoute, { enabled: true, ...http3Config });
    }

    return this.injectVpnSecurity(preparedRoute, routeId);
  }

  private injectVpnSecurity(
    route: plugins.smartproxy.IRouteConfig,
    routeId?: string,
  ): plugins.smartproxy.IRouteConfig {
    const dcRoute = route as IDcRouterRouteConfig;
    const vpnEntries = this.getVpnClientAccessForRoute?.(dcRoute, routeId) || [];

    if (!dcRoute.vpnOnly && vpnEntries.length === 0) {
      return route;
    }

    const existingVpnSecurity = route.security?.vpn || {};
    const mergedAllowedClients = this.mergeVpnClientAllowEntries(
      existingVpnSecurity.allowedClients || [],
      vpnEntries,
    );

    return {
      ...route,
      security: {
        ...route.security,
        vpn: {
          ...existingVpnSecurity,
          required: dcRoute.vpnOnly ? true : existingVpnSecurity.required,
          allowedClients: mergedAllowedClients,
        },
      },
    };
  }

  private mergeVpnClientAllowEntries(
    existingEntries: TVpnClientAllowEntry[],
    vpnEntries: TVpnClientAllowEntry[],
  ): TVpnClientAllowEntry[] {
    const merged: TVpnClientAllowEntry[] = [];
    const seen = new Set<string>();

    for (const entry of [...existingEntries, ...vpnEntries]) {
      const key = typeof entry === 'string'
        ? `client:${entry}`
        : `domain:${entry.clientId}:${[...entry.domains].sort().join(',')}`;
      if (seen.has(key)) continue;
      seen.add(key);
      merged.push(entry);
    }

    return merged;
  }
}
