import { logger } from '../logger.js';
import type { TVpnClientAllowEntry } from '../config/classes.route-config-manager.js';
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
import type { DcRouter } from '../classes.dcrouter.js';

/**
 * Resolves which VPN clients may access which routes and which IPs belong in
 * a client's WireGuard AllowedIPs, including cached DNS resolution of
 * VPN-gated route domains.
 */
export class VpnAccessResolver {
  /** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
  private domainIpCache = new Map<string, { ips: string[]; expiresAt: number }>();
  /** Deduplicate wildcard-resolution warnings for WireGuard AllowedIPs generation. */
  private warnedWildcardDomains = new Set<string>();

  constructor(private dcRouterRef: DcRouter) {}

  /** Clear DNS and warning caches, e.g. after a VPN config change. */
  public reset(): void {
    this.domainIpCache.clear();
    this.warnedWildcardDomains.clear();
  }

  /**
   * Build the per-route VPN client allow resolver handed to RouteConfigManager,
   * or undefined when VPN is disabled.
   */
  public createRouteAllowResolver(): ((
    route: IDcRouterRouteConfig,
    routeId?: string,
  ) => TVpnClientAllowEntry[]) | undefined {
    if (!this.dcRouterRef.options.vpnConfig?.enabled) {
      return undefined;
    }

    return (route: IDcRouterRouteConfig, routeId?: string) => {
      if (!this.dcRouterRef.vpnManager || !this.dcRouterRef.targetProfileManager) {
        // VPN not ready yet — deny all until re-apply after VPN starts.
        return [];
      }

      return this.dcRouterRef.targetProfileManager.getMatchingVpnClients(
        route,
        routeId,
        this.dcRouterRef.vpnManager.listClients(),
        this.dcRouterRef.routeConfigManager?.getRoutes() || new Map(),
      );
    };
  }

  /**
   * Compute the WireGuard AllowedIPs for a client from its target profiles:
   * the VPN subnet, direct target IPs, and DNS-resolved route domains.
   */
  public async getClientAllowedIPs(targetProfileIds: string[]): Promise<string[]> {
    const subnet = this.dcRouterRef.options.vpnConfig?.subnet || '10.8.0.0/24';
    const ips = new Set<string>([subnet]);

    const targetProfileManager = this.dcRouterRef.targetProfileManager;
    if (!targetProfileManager) return [...ips];

    const allRoutes = this.dcRouterRef.routeConfigManager?.getRoutes() || new Map();

    const { domains, targetIps } = targetProfileManager.getClientAccessSpec(
      targetProfileIds,
      allRoutes,
    );

    // Add target IPs directly
    for (const ip of targetIps) {
      ips.add(`${ip}/32`);
    }

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

    return [...ips];
  }

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

  private isWildcardDomain(domain: string): boolean {
    return domain.includes('*');
  }

  private logSkippedWildcardAllowedIp(domain: string): void {
    if (this.warnedWildcardDomains.has(domain)) return;
    this.warnedWildcardDomains.add(domain);
    logger.log(
      'warn',
      `VPN: Skipping wildcard domain '${domain}' for WireGuard AllowedIPs; wildcard patterns must be resolved to concrete hostnames by matching routes.`,
    );
  }
}
