import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import { buildEmailDnsRecords } from '../email/index.js';
import type { DcRouter } from '../classes.dcrouter.js';

type TDnsRecordSeed = { name: string; type: string; value: string; ttl?: number; useIngressProxy?: boolean };

/**
 * Sets up and feeds the embedded authoritative smartdns server: validates the
 * DNS configuration, generates authoritative/email/DKIM records, applies
 * proxy-IP replacement, registers record handlers, wires rate-limited query
 * logging/metrics, and provides the DoH socket handler for SmartProxy routes.
 */
export class DnsServerRuntime {
  // Adaptive query-log rate limiting state
  private logWindowSecond = 0; // epoch second of current window
  private logWindowCount = 0; // queries logged this second
  private batchCount = 0;
  private batchTimer: ReturnType<typeof setTimeout> | null = null;

  constructor(private dcRouterRef: DcRouter) {}

  /**
   * Create the DNS server, start it on UDP, wire metrics/logging, and
   * register all generated records.
   */
  public async setup(): Promise<void> {
    const options = this.dcRouterRef.options;
    if (!options.dnsNsDomains || options.dnsNsDomains.length === 0) {
      throw new Error('dnsNsDomains is required for DNS server setup');
    }

    if (!options.dnsScopes || options.dnsScopes.length === 0) {
      throw new Error('dnsScopes is required for DNS server setup');
    }

    const primaryNameserver = options.dnsNsDomains[0];
    logger.log('info', `Setting up DNS server with primary nameserver: ${primaryNameserver}`);

    // Get VM IP address for UDP binding
    const networkInterfaces = plugins.os.networkInterfaces() as Record<
      string,
      Array<{ internal: boolean; family: string; address: string }> | undefined
    >;
    let vmIpAddress = options.dnsBindInterface || '0.0.0.0'; // Default to all interfaces

    // Try to find the VM's internal IP address when no explicit bind address is configured.
    if (!options.dnsBindInterface) {
      interfaceLoop: for (const [_name, interfaces] of Object.entries(networkInterfaces)) {
        if (interfaces) {
          for (const iface of interfaces) {
            if (!iface.internal && iface.family === 'IPv4') {
              vmIpAddress = iface.address;
              break interfaceLoop;
            }
          }
        }
      }
    }

    // Create DNS server instance with manual HTTPS mode
    const dnsServer = new plugins.smartdns.dnsServerMod.DnsServer({
      udpPort: 53,
      udpBindInterface: vmIpAddress,
      httpsPort: 443, // Required but won't bind due to manual mode
      manualHttpsMode: true, // Enable manual HTTPS socket handling
      dnssecZone: primaryNameserver,
      primaryNameserver: primaryNameserver, // Automatically generates correct SOA records
      // For now, use self-signed cert until we integrate with Let's Encrypt
      httpsKey: '',
      httpsCert: ''
    });
    this.dcRouterRef.dnsServer = dnsServer;

    // Start the DNS server (UDP only)
    await dnsServer.start();
    logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);

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

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

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

        if (this.logWindowCount < 2) {
          this.logWindowCount++;
          const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
          logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
        } else {
          this.batchCount++;
          if (!this.batchTimer) {
            this.batchTimer = setTimeout(flushDnsBatch, 5000);
          }
        }
      });
    }

    // Validate DNS configuration
    await this.validateConfiguration();

    // Generate and register authoritative records
    const authoritativeRecords = await this.generateAuthoritativeRecords();

    // Generate email DNS records
    const emailDnsRecords = await this.generateEmailDnsRecords();

    // Ensure DKIM keys exist for internal-dns domains before generating records.
    await this.initializeDkimForEmailDomains();

    // Generate DKIM records directly from smartmta.
    const dkimRecords = await this.loadDkimRecords();

    // Combine all records: authoritative, email, DKIM, and user-defined
    const allRecords: TDnsRecordSeed[] = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
    if (options.dnsRecords && options.dnsRecords.length > 0) {
      allRecords.push(...options.dnsRecords);
    }

    // Apply proxy IP replacement if configured
    await this.applyProxyIpReplacement(allRecords);

    // Register all DNS records
    if (allRecords.length > 0) {
      this.registerRecords(allRecords);
      logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${options.dnsRecords?.length || 0} user-defined)`);
    }

    // Hand the DnsServer to DnsManager so DB-backed local records on
    // dcrouter-hosted domains get registered too.
    if (this.dcRouterRef.dnsManager) {
      await this.dcRouterRef.dnsManager.attachDnsServer(dnsServer);
    }
  }

  /**
   * Create the DoH socket handler SmartProxy routes hand TLS sockets to.
   */
  public createSocketHandler(): (socket: plugins.net.Socket) => Promise<void> {
    return async (socket: plugins.net.Socket) => {
      if (!this.dcRouterRef.dnsServer) {
        logger.log('error', 'DNS socket handler called but DNS server not initialized');
        socket.end();
        return;
      }

      // Prevent uncaught exception from socket 'error' events
      socket.on('error', (err) => {
        logger.log('error', `DNS socket error: ${err.message}`);
        if (!socket.destroyed) {
          socket.destroy();
        }
      });

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

      try {
        // Use the built-in socket handler from smartdns
        // This handles HTTP/2, DoH protocol, etc.
        await (this.dcRouterRef.dnsServer as any).handleHttpsSocket(socket);
      } catch (error: unknown) {
        logger.log('error', `DNS socket handler error: ${(error as Error).message}`);
        if (!socket.destroyed) {
          socket.destroy();
        }
      }
    };
  }

  /** Flush the pending rate-limited query-log batch and reset logging state. */
  public flushQueryLogBatch(): void {
    if (this.batchTimer) {
      clearTimeout(this.batchTimer);
      if (this.batchCount > 0) {
        logger.log('info', `DNS: ${this.batchCount} queries processed (final flush)`, { zone: 'dns' });
      }
      this.batchTimer = null;
      this.batchCount = 0;
      this.logWindowSecond = 0;
      this.logWindowCount = 0;
    }
  }

  private registerRecords(records: TDnsRecordSeed[]): void {
    const dnsServer = this.dcRouterRef.dnsServer;
    if (!dnsServer) return;

    // Register a separate handler for each record
    // This ensures multiple records of the same type (like NS records) are all served
    for (const record of records) {
      // Register handler for this specific record
      dnsServer.registerHandler(record.name, [record.type], (question) => {
        // Check if this handler matches the question
        if (question.name === record.name && question.type === record.type) {
          return {
            name: record.name,
            type: record.type,
            class: 'IN',
            ttl: record.ttl || 300,
            data: this.parseRecordData(record.type, record.value)
          };
        }

        return null;
      });
    }

    logger.log('info', `Registered ${records.length} DNS handlers (one per record)`);
  }

  private parseRecordData(type: string, value: string): any {
    switch (type) {
      case 'A':
        return value; // IP address as string
      case 'MX':
        const [priority, exchange] = value.split(' ');
        return { priority: parseInt(priority), exchange };
      case 'TXT':
        return value;
      case 'NS':
        return value;
      case 'SOA':
        // SOA format: primary-ns admin-email serial refresh retry expire minimum
        const parts = value.split(' ');
        return {
          mname: parts[0],
          rname: parts[1],
          serial: parseInt(parts[2]),
          refresh: parseInt(parts[3]),
          retry: parseInt(parts[4]),
          expire: parseInt(parts[5]),
          minimum: parseInt(parts[6])
        };
      default:
        return value;
    }
  }

  private async validateConfiguration(): Promise<void> {
    const options = this.dcRouterRef.options;
    if (!options.dnsNsDomains || !options.dnsScopes) {
      return;
    }

    logger.log('info', 'Validating DNS configuration...');

    // Check if email domains with internal-dns are in dnsScopes
    if (options.emailConfig?.domains) {
      for (const domainConfig of options.emailConfig.domains) {
        if (domainConfig.dnsMode === 'internal-dns' &&
            !options.dnsScopes.includes(domainConfig.domain)) {
          logger.log('warn', `Email domain '${domainConfig.domain}' with internal-dns mode is not in dnsScopes. It should be added to dnsScopes.`);
        }
      }
    }

    // Validate user-provided DNS records are within scopes
    if (options.dnsRecords) {
      for (const record of options.dnsRecords) {
        const recordDomain = this.extractDomain(record.name);
        const isInScope = options.dnsScopes.some(scope =>
          recordDomain === scope || recordDomain.endsWith(`.${scope}`)
        );

        if (!isInScope) {
          logger.log('warn', `DNS record for '${record.name}' is outside defined scopes [${options.dnsScopes.join(', ')}]`);
        }
      }
    }
  }

  private async generateEmailDnsRecords(): Promise<TDnsRecordSeed[]> {
    const options = this.dcRouterRef.options;
    const records: TDnsRecordSeed[] = [];

    if (!options.emailConfig?.domains) {
      return records;
    }

    // Filter domains with internal-dns mode
    const internalDnsDomains = options.emailConfig.domains.filter(
      domain => domain.dnsMode === 'internal-dns'
    );

    for (const domainConfig of internalDnsDomains) {
      const domain = domainConfig.domain;
      const ttl = domainConfig.dns?.internal?.ttl || 3600;
      const requiredRecords = buildEmailDnsRecords({
        domain,
        hostname: options.emailConfig.hostname,
        mxPriority: domainConfig.dns?.internal?.mxPriority,
      }).filter((record) => !record.name.includes('._domainkey.'));

      for (const record of requiredRecords) {
        records.push({
          name: record.name,
          type: record.type,
          value: record.value,
          ttl,
        });
      }
    }

    logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`);
    return records;
  }

  private async loadDkimRecords(): Promise<TDnsRecordSeed[]> {
    const options = this.dcRouterRef.options;
    const records: TDnsRecordSeed[] = [];
    if (!options.emailConfig?.domains || !this.dcRouterRef.emailServer?.dkimCreator) {
      return records;
    }

    for (const domainConfig of options.emailConfig.domains) {
      if (domainConfig.dnsMode !== 'internal-dns') {
        continue;
      }
      const selector = domainConfig.dkim?.selector || 'default';
      try {
        const dkimRecord = await this.dcRouterRef.emailServer.dkimCreator.getDNSRecordForDomain(domainConfig.domain, selector);
        records.push({
          name: dkimRecord.name,
          type: 'TXT',
          value: dkimRecord.value,
          ttl: domainConfig.dns?.internal?.ttl || 3600,
        });
      } catch (error: unknown) {
        logger.log('error', `Failed to generate DKIM record for ${domainConfig.domain}: ${(error as Error).message}`);
      }
    }

    return records;
  }

  private async initializeDkimForEmailDomains(): Promise<void> {
    const options = this.dcRouterRef.options;
    if (!options.emailConfig?.domains || !this.dcRouterRef.emailServer) {
      return;
    }

    logger.log('info', 'Initializing DKIM keys for email domains...');

    // Get DKIMCreator instance from email server (public in smartmta)
    const dkimCreator = this.dcRouterRef.emailServer.dkimCreator;
    if (!dkimCreator) {
      logger.log('warn', 'DKIMCreator not available, skipping DKIM initialization');
      return;
    }

    // Ensure necessary directories exist
    paths.ensureDataDirectories(this.dcRouterRef.resolvedPaths);

    // Generate DKIM keys for each internal-dns email domain using the configured selector.
    for (const domainConfig of options.emailConfig.domains) {
      if (domainConfig.dnsMode !== 'internal-dns') {
        continue;
      }
      try {
        await dkimCreator.handleDKIMKeysForSelector(
          domainConfig.domain,
          domainConfig.dkim?.selector || 'default',
          domainConfig.dkim?.keySize || 2048,
        );
        logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`);
      } catch (error: unknown) {
        logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${(error as Error).message}`);
      }
    }

    logger.log('info', 'DKIM initialization complete');
  }

  private async generateAuthoritativeRecords(): Promise<TDnsRecordSeed[]> {
    const options = this.dcRouterRef.options;
    const records: TDnsRecordSeed[] = [];

    if (!options.dnsNsDomains || !options.dnsScopes) {
      return records;
    }

    // Determine the public IP for nameserver A records
    let publicIp: string | null = null;

    // Use proxy IPs if configured (these should be public IPs)
    if (options.proxyIps && options.proxyIps.length > 0) {
      publicIp = options.proxyIps[0]; // Use first proxy IP
      logger.log('info', `Using proxy IP for nameserver A records: ${publicIp}`);
    } else if (options.publicIp) {
      // Use explicitly configured public IP
      publicIp = options.publicIp;
      this.dcRouterRef.detectedPublicIp = publicIp;
      logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`);
    } else {
      // Auto-discover public IP using smartnetwork
      try {
        logger.log('info', 'Auto-discovering public IP address...');
        const smartNetwork = new plugins.smartnetwork.SmartNetwork();
        const publicIps = await smartNetwork.getPublicIps();

        if (publicIps.v4) {
          publicIp = publicIps.v4;
          this.dcRouterRef.detectedPublicIp = publicIp;
          logger.log('info', `Auto-discovered public IPv4: ${publicIp}`);
        } else {
          logger.log('warn', 'Could not auto-discover public IPv4 address');
        }
      } catch (error: unknown) {
        logger.log('error', `Failed to auto-discover public IP: ${(error as Error).message}`);
      }

      if (!publicIp) {
        logger.log('warn', 'No public IP available. Nameserver A records require either proxyIps, publicIp, or successful auto-discovery.');
      }
    }

    // Generate A records for nameservers if we have a public IP
    if (publicIp) {
      for (const nsDomain of options.dnsNsDomains) {
        records.push({
          name: nsDomain,
          type: 'A',
          value: publicIp,
          ttl: 3600
        });
      }
      logger.log('info', `Generated A records for ${options.dnsNsDomains.length} nameservers`);
    }

    // Generate NS records for each domain in scopes
    for (const domain of options.dnsScopes) {
      // Add NS records for all nameservers
      for (const nsDomain of options.dnsNsDomains) {
        records.push({
          name: domain,
          type: 'NS',
          value: nsDomain,
          ttl: 3600
        });
      }

      // SOA records are now automatically generated by smartdns DnsServer
      // with the primaryNameserver configuration option
    }

    logger.log('info', `Generated ${records.length} total records (A + NS) for ${options.dnsScopes.length} domains`);
    return records;
  }

  private extractDomain(recordName: string): string {
    // Handle wildcards
    if (recordName.startsWith('*.')) {
      recordName = recordName.substring(2);
    }
    return recordName;
  }

  private async applyProxyIpReplacement(records: TDnsRecordSeed[]): Promise<void> {
    const options = this.dcRouterRef.options;
    if (!options.proxyIps || options.proxyIps.length === 0) {
      return; // No proxy IPs configured, skip replacement
    }

    // Get server's public IP
    const serverIp = await this.detectServerPublicIp();
    if (!serverIp) {
      logger.log('warn', 'Could not detect server public IP, skipping proxy IP replacement');
      return;
    }

    logger.log('info', `Applying proxy IP replacement. Server IP: ${serverIp}, Proxy IPs: ${options.proxyIps.join(', ')}`);

    let proxyIndex = 0;
    for (const record of records) {
      if (record.type === 'A' &&
          record.value === serverIp &&
          record.useIngressProxy !== false) {
        // Round-robin through proxy IPs
        const proxyIp = options.proxyIps[proxyIndex % options.proxyIps.length];
        logger.log('info', `Replacing A record for ${record.name}: ${record.value} → ${proxyIp}`);
        record.value = proxyIp;
        proxyIndex++;
      }
    }
  }

  private async detectServerPublicIp(): Promise<string | null> {
    try {
      const smartNetwork = new plugins.smartnetwork.SmartNetwork();
      const publicIps = await smartNetwork.getPublicIps();

      if (publicIps.v4) {
        return publicIps.v4;
      }

      return null;
    } catch (error: unknown) {
      logger.log('warn', `Failed to detect public IP: ${(error as Error).message}`);
      return null;
    }
  }
}
