import * as plugins from '../plugins.js';
import { DcRouter } from '../classes.dcrouter.js';
import { MetricsCache } from './classes.metricscache.js';
import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
import { logger } from '../logger.js';
import type { IAsnActivity } from '../../ts_interfaces/data/stats.js';

export class MetricsManager {
  private metricsLogger: plugins.smartlog.Smartlog;
  private smartMetrics: plugins.smartmetrics.SmartMetrics;
  private dcRouter: DcRouter;
  private resetInterval?: NodeJS.Timeout;
  private metricsCache: MetricsCache;
  
  // Constants
  private readonly MAX_TOP_DOMAINS = 1000; // Limit topDomains Map size
  
  // Track email-specific metrics
  private emailMetrics = {
    sentToday: 0,
    receivedToday: 0,
    failedToday: 0,
    bouncedToday: 0,
    queueSize: 0,
    lastResetDate: new Date().toDateString(),
    deliveryTimes: [] as number[], // Track delivery times in ms
    recipients: new Map<string, number>(), // Track email count by recipient
    recentActivity: [] as Array<{ timestamp: number; type: string; details: string }>,
  };
  
  // Track DNS-specific metrics
  private dnsMetrics = {
    totalQueries: 0,
    cacheHits: 0,
    cacheMisses: 0,
    queryTypes: {} as Record<string, number>,
    topDomains: new Map<string, number>(),
    lastResetDate: new Date().toDateString(),
    // Per-second query count ring buffer (300 entries = 5 minutes)
    queryRing: new Int32Array(300),
    queryRingLastSecond: 0, // last epoch second that was written
    responseTimes: [] as number[], // Track response times in ms
    recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>,
  };
  
  // Per-minute time-series buckets for charts
  private emailMinuteBuckets = new Map<number, { sent: number; received: number; failed: number }>();
  private dnsMinuteBuckets = new Map<number, { queries: number }>();

  // Track security-specific metrics
  private securityMetrics = {
    blockedIPs: 0,
    authFailures: 0,
    spamDetected: 0,
    malwareDetected: 0,
    phishingDetected: 0,
    lastResetDate: new Date().toDateString(),
    incidents: [] as Array<{ timestamp: number; type: string; severity: string; details: string }>,
  };
  
  constructor(dcRouter: DcRouter) {
    this.dcRouter = dcRouter;
    // Create a Smartlog instance for SmartMetrics (requires its own instance)
    this.metricsLogger = new plugins.smartlog.Smartlog({
      logContext: {
        environment: 'production',
        runtime: 'node',
        zone: 'dcrouter-metrics',
      }
    });
    this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.metricsLogger, 'dcrouter');
    // Initialize metrics cache with 500ms TTL
    this.metricsCache = new MetricsCache(500);
  }
  
  public async start(): Promise<void> {
    // Start SmartMetrics collection
    this.smartMetrics.start();
    
    // Reset daily counters at midnight
    this.resetInterval = setInterval(() => {
      const currentDate = new Date().toDateString();
      
      if (currentDate !== this.emailMetrics.lastResetDate) {
        this.emailMetrics.sentToday = 0;
        this.emailMetrics.receivedToday = 0;
        this.emailMetrics.failedToday = 0;
        this.emailMetrics.bouncedToday = 0;
        this.emailMetrics.deliveryTimes = [];
        this.emailMetrics.recipients.clear();
        this.emailMetrics.recentActivity = [];
        this.emailMetrics.lastResetDate = currentDate;
      }
      
      if (currentDate !== this.dnsMetrics.lastResetDate) {
        this.dnsMetrics.totalQueries = 0;
        this.dnsMetrics.cacheHits = 0;
        this.dnsMetrics.cacheMisses = 0;
        this.dnsMetrics.queryTypes = {};
        this.dnsMetrics.topDomains.clear();
        this.dnsMetrics.queryRing.fill(0);
        this.dnsMetrics.queryRingLastSecond = 0;
        this.dnsMetrics.responseTimes = [];
        this.dnsMetrics.recentQueries = [];
        this.dnsMetrics.lastResetDate = currentDate;
      }

      if (currentDate !== this.securityMetrics.lastResetDate) {
        this.securityMetrics.blockedIPs = 0;
        this.securityMetrics.authFailures = 0;
        this.securityMetrics.spamDetected = 0;
        this.securityMetrics.malwareDetected = 0;
        this.securityMetrics.phishingDetected = 0;
        this.securityMetrics.incidents = [];
        this.securityMetrics.lastResetDate = currentDate;
      }

      // Prune old time-series buckets every minute (don't wait for lazy query)
      this.pruneOldBuckets();
    }, 60000); // Check every minute
    
    logger.log('info', 'MetricsManager started');
  }

  public async stop(): Promise<void> {
    // Clear the reset interval
    if (this.resetInterval) {
      clearInterval(this.resetInterval);
      this.resetInterval = undefined;
    }

    this.smartMetrics.stop();

    // Clear caches and time-series buckets on shutdown
    this.metricsCache.clear();
    this.emailMinuteBuckets.clear();
    this.dnsMinuteBuckets.clear();

    logger.log('info', 'MetricsManager stopped');
  }
  
  // Get server metrics from SmartMetrics and SmartProxy
  public async getServerStats() {
    return this.metricsCache.get('serverStats', async () => {
      const smartMetricsData = await this.smartMetrics.getMetrics();
      const smartProxy = this.dcRouter.smartProxy;
      const proxyMetrics = smartProxy ? smartProxy.getMetrics() : null;
      const proxyStats = smartProxy ? await smartProxy.getStatistics() : null;
      const { heapUsed, heapTotal, external, rss } = process.memoryUsage();

      return {
        uptime: process.uptime(),
        startTime: Date.now() - (process.uptime() * 1000),
        memoryUsage: {
          heapUsed,
          heapTotal,
          external,
          rss,
          maxMemoryMB: this.smartMetrics.maxMemoryMB,
          actualUsageBytes: smartMetricsData.memoryUsageBytes,
          actualUsagePercentage: smartMetricsData.memoryPercentage,
        },
        cpuUsage: {
          user: smartMetricsData.cpuPercentage,
          system: 0,
        },
        activeConnections: proxyStats ? proxyStats.activeConnections : 0,
        totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0,
        requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0,
        throughput: proxyMetrics ? {
          bytesIn: proxyMetrics.totals.bytesIn(),
          bytesOut: proxyMetrics.totals.bytesOut(),
          bytesInPerSecond: proxyMetrics.throughput.instant().in,
          bytesOutPerSecond: proxyMetrics.throughput.instant().out,
        } : { bytesIn: 0, bytesOut: 0, bytesInPerSecond: 0, bytesOutPerSecond: 0 },
      };
    });
  }
  
  // Get email metrics
  public async getEmailStats() {
    return this.metricsCache.get('emailStats', () => {
      // Calculate average delivery time
      const avgDeliveryTime = this.emailMetrics.deliveryTimes.length > 0
        ? this.emailMetrics.deliveryTimes.reduce((a, b) => a + b, 0) / this.emailMetrics.deliveryTimes.length
        : 0;
      
      // Get top recipients
      const topRecipients = Array.from(this.emailMetrics.recipients.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, 10)
        .map(([email, count]) => ({ email, count }));
      
      // Get recent activity (last 50 entries)
      const recentActivity = this.emailMetrics.recentActivity.slice(-50);
      
      return {
        sentToday: this.emailMetrics.sentToday,
        receivedToday: this.emailMetrics.receivedToday,
        failedToday: this.emailMetrics.failedToday,
        bounceRate: this.emailMetrics.bouncedToday > 0 
          ? (this.emailMetrics.bouncedToday / this.emailMetrics.sentToday) * 100 
          : 0,
        deliveryRate: this.emailMetrics.sentToday > 0 
          ? ((this.emailMetrics.sentToday - this.emailMetrics.failedToday) / this.emailMetrics.sentToday) * 100 
          : 100,
        queueSize: this.emailMetrics.queueSize,
        averageDeliveryTime: Math.round(avgDeliveryTime),
        topRecipients,
        recentActivity,
      };
    });
  }
  
  // Get DNS metrics
  public async getDnsStats() {
    return this.metricsCache.get('dnsStats', () => {
      const cacheHitRate = this.dnsMetrics.totalQueries > 0 
        ? (this.dnsMetrics.cacheHits / this.dnsMetrics.totalQueries) * 100 
        : 0;
      
      const topDomains = Array.from(this.dnsMetrics.topDomains.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, 10)
        .map(([domain, count]) => ({ domain, count }));
      
      // Calculate queries per second from ring buffer (sum last 60 seconds)
      const queriesPerSecond = this.getQueryRingSum(60) / 60;
      
      // Calculate average response time
      const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
        ? this.dnsMetrics.responseTimes.reduce((a, b) => a + b, 0) / this.dnsMetrics.responseTimes.length
        : 0;
      
      return {
        queriesPerSecond: Math.round(queriesPerSecond * 10) / 10,
        totalQueries: this.dnsMetrics.totalQueries,
        cacheHits: this.dnsMetrics.cacheHits,
        cacheMisses: this.dnsMetrics.cacheMisses,
        cacheHitRate: cacheHitRate,
        topDomains: topDomains,
        queryTypes: this.dnsMetrics.queryTypes,
        averageResponseTime: Math.round(avgResponseTime),
        activeDomains: this.dnsMetrics.topDomains.size,
        recentQueries: this.dnsMetrics.recentQueries.slice(),
      };
    });
  }
  
  /**
   * Sync security metrics from the SecurityLogger singleton (last 24h).
   * Called before returning security stats so counters reflect real events.
   */
  private syncFromSecurityLogger(): void {
    try {
      const securityLogger = SecurityLogger.getInstance();
      const summary = securityLogger.getEventsSummary(86400000); // last 24h

      this.securityMetrics.spamDetected = summary.byType[SecurityEventType.SPAM] || 0;
      this.securityMetrics.malwareDetected = summary.byType[SecurityEventType.MALWARE] || 0;
      this.securityMetrics.phishingDetected = summary.byType[SecurityEventType.DMARC] || 0; // phishing via DMARC
      this.securityMetrics.authFailures =
        summary.byType[SecurityEventType.AUTHENTICATION] || 0;
      this.securityMetrics.blockedIPs =
        (summary.byType[SecurityEventType.IP_REPUTATION] || 0) +
        (summary.byType[SecurityEventType.REJECTED_CONNECTION] || 0);
    } catch {
      // SecurityLogger may not be initialized yet — ignore
    }
  }

  // Get security metrics
  public async getSecurityStats() {
    return this.metricsCache.get('securityStats', () => {
      // Sync counters from the real SecurityLogger events
      this.syncFromSecurityLogger();

      // Get recent incidents (last 20)
      const recentIncidents = this.securityMetrics.incidents.slice(-20);

      return {
        blockedIPs: this.securityMetrics.blockedIPs,
        authFailures: this.securityMetrics.authFailures,
        spamDetected: this.securityMetrics.spamDetected,
        malwareDetected: this.securityMetrics.malwareDetected,
        phishingDetected: this.securityMetrics.phishingDetected,
        totalThreatsBlocked: this.securityMetrics.spamDetected +
          this.securityMetrics.malwareDetected +
          this.securityMetrics.phishingDetected,
        recentIncidents,
      };
    });
  }
  
  public async getActiveConnectionSnapshots(
    options: plugins.smartproxy.IActiveConnectionSnapshotOptions = {},
  ): Promise<plugins.smartproxy.IActiveConnectionSnapshot[]> {
    const cacheKey = `activeConnectionSnapshots:${options.limit ?? 1000}:${options.routeId ?? ''}`;
    return await this.metricsCache.get<plugins.smartproxy.IActiveConnectionSnapshot[]>(cacheKey, async () => {
      if (!this.dcRouter.smartProxy) {
        return [];
      }
      return this.dcRouter.smartProxy.getActiveConnectionSnapshots(options);
    }, 500);
  }

  // Get connection info from SmartProxy
  public async getConnectionInfo() {
    return this.metricsCache.get('connectionInfo', async () => {
      const snapshots = await this.getActiveConnectionSnapshots({ limit: 10000 });
      const connectionsByRoute = new Map<string, { count: number; lastActivity: Date }>();

      for (const snapshot of snapshots) {
        const source = snapshot.routeId || snapshot.domain || `${snapshot.protocol || 'connection'}:${snapshot.localPort}`;
        const existing = connectionsByRoute.get(source) || { count: 0, lastActivity: new Date(snapshot.startedAtMs) };
        existing.count++;
        if (snapshot.startedAtMs > existing.lastActivity.getTime()) {
          existing.lastActivity = new Date(snapshot.startedAtMs);
        }
        connectionsByRoute.set(source, existing);
      }

      const connectionInfo: Array<{ type: string; count: number; source: string; lastActivity: Date }> = [];
      for (const [source, info] of connectionsByRoute) {
        connectionInfo.push({
          type: 'https',
          count: info.count,
          source,
          lastActivity: info.lastActivity,
        });
      }

      return connectionInfo;
    });
  }
  
  // Email event tracking methods
  public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
    this.emailMetrics.sentToday++;
    this.incrementEmailBucket('sent');
    
    if (recipient) {
      const count = this.emailMetrics.recipients.get(recipient) || 0;
      this.emailMetrics.recipients.set(recipient, count + 1);

      // Cap recipients map to prevent unbounded growth within a day
      if (this.emailMetrics.recipients.size > this.MAX_TOP_DOMAINS) {
        const sorted = Array.from(this.emailMetrics.recipients.entries())
          .sort((a, b) => b[1] - a[1])
          .slice(0, Math.floor(this.MAX_TOP_DOMAINS * 0.8));
        this.emailMetrics.recipients = new Map(sorted);
      }
    }
    
    if (deliveryTimeMs) {
      this.emailMetrics.deliveryTimes.push(deliveryTimeMs);
      // Keep only last 1000 delivery times
      if (this.emailMetrics.deliveryTimes.length > 1000) {
        this.emailMetrics.deliveryTimes.shift();
      }
    }
    
    this.emailMetrics.recentActivity.push({
      timestamp: Date.now(),
      type: 'sent',
      details: recipient || 'unknown',
    });
    
    // Keep only last 1000 activities
    if (this.emailMetrics.recentActivity.length > 1000) {
      this.emailMetrics.recentActivity.shift();
    }
  }
  
  public trackEmailReceived(sender?: string): void {
    this.emailMetrics.receivedToday++;
    this.incrementEmailBucket('received');
    
    this.emailMetrics.recentActivity.push({
      timestamp: Date.now(),
      type: 'received',
      details: sender || 'unknown',
    });
    
    // Keep only last 1000 activities
    if (this.emailMetrics.recentActivity.length > 1000) {
      this.emailMetrics.recentActivity.shift();
    }
  }
  
  public trackEmailFailed(recipient?: string, reason?: string): void {
    this.emailMetrics.failedToday++;
    this.incrementEmailBucket('failed');
    
    this.emailMetrics.recentActivity.push({
      timestamp: Date.now(),
      type: 'failed',
      details: `${recipient || 'unknown'}: ${reason || 'unknown error'}`,
    });
    
    // Keep only last 1000 activities
    if (this.emailMetrics.recentActivity.length > 1000) {
      this.emailMetrics.recentActivity.shift();
    }
  }
  
  public trackEmailBounced(recipient?: string): void {
    this.emailMetrics.bouncedToday++;
    
    this.emailMetrics.recentActivity.push({
      timestamp: Date.now(),
      type: 'bounced',
      details: recipient || 'unknown',
    });
    
    // Keep only last 1000 activities
    if (this.emailMetrics.recentActivity.length > 1000) {
      this.emailMetrics.recentActivity.shift();
    }
  }
  
  public updateQueueSize(size: number): void {
    this.emailMetrics.queueSize = size;
  }
  
  // DNS event tracking methods
  public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number, answered?: boolean): void {
    this.dnsMetrics.totalQueries++;
    this.incrementDnsBucket();

    // Store recent query entry
    this.dnsMetrics.recentQueries.push({
      timestamp: Date.now(),
      domain,
      type: queryType,
      answered: answered ?? true,
      responseTimeMs: responseTimeMs ?? 0,
    });
    if (this.dnsMetrics.recentQueries.length > 100) {
      this.dnsMetrics.recentQueries.shift();
    }
    
    if (cacheHit) {
      this.dnsMetrics.cacheHits++;
    } else {
      this.dnsMetrics.cacheMisses++;
    }
    
    // Increment per-second query counter in ring buffer
    this.incrementQueryRing();
    
    // Track response time if provided
    if (responseTimeMs) {
      this.dnsMetrics.responseTimes.push(responseTimeMs);
      // Keep only last 1000 response times
      if (this.dnsMetrics.responseTimes.length > 1000) {
        this.dnsMetrics.responseTimes.shift();
      }
    }
    
    // Track query types
    this.dnsMetrics.queryTypes[queryType] = (this.dnsMetrics.queryTypes[queryType] || 0) + 1;
    
    // Track top domains with size limit
    const currentCount = this.dnsMetrics.topDomains.get(domain) || 0;
    this.dnsMetrics.topDomains.set(domain, currentCount + 1);
    
    // If we've exceeded the limit, remove the least accessed domains
    if (this.dnsMetrics.topDomains.size > this.MAX_TOP_DOMAINS) {
      // Convert to array, sort by count, and keep only top domains
      const sortedDomains = Array.from(this.dnsMetrics.topDomains.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, Math.floor(this.MAX_TOP_DOMAINS * 0.8)); // Keep 80% to avoid frequent cleanup
      
      // Clear and repopulate with top domains
      this.dnsMetrics.topDomains.clear();
      sortedDomains.forEach(([domain, count]) => {
        this.dnsMetrics.topDomains.set(domain, count);
      });
    }
  }
  
  // Security event tracking methods
  public trackBlockedIP(ip?: string, reason?: string): void {
    this.securityMetrics.blockedIPs++;
    
    this.securityMetrics.incidents.push({
      timestamp: Date.now(),
      type: 'ip_blocked',
      severity: 'medium',
      details: `IP ${ip || 'unknown'} blocked: ${reason || 'security policy'}`,
    });
    
    // Keep only last 1000 incidents
    if (this.securityMetrics.incidents.length > 1000) {
      this.securityMetrics.incidents.shift();
    }
  }
  
  public trackAuthFailure(username?: string, ip?: string): void {
    this.securityMetrics.authFailures++;
    
    this.securityMetrics.incidents.push({
      timestamp: Date.now(),
      type: 'auth_failure',
      severity: 'low',
      details: `Authentication failed for ${username || 'unknown'} from ${ip || 'unknown'}`,
    });
    
    // Keep only last 1000 incidents
    if (this.securityMetrics.incidents.length > 1000) {
      this.securityMetrics.incidents.shift();
    }
  }
  
  public trackSpamDetected(sender?: string): void {
    this.securityMetrics.spamDetected++;
    
    this.securityMetrics.incidents.push({
      timestamp: Date.now(),
      type: 'spam_detected',
      severity: 'low',
      details: `Spam detected from ${sender || 'unknown'}`,
    });
    
    // Keep only last 1000 incidents
    if (this.securityMetrics.incidents.length > 1000) {
      this.securityMetrics.incidents.shift();
    }
  }
  
  public trackMalwareDetected(source?: string): void {
    this.securityMetrics.malwareDetected++;
    
    this.securityMetrics.incidents.push({
      timestamp: Date.now(),
      type: 'malware_detected',
      severity: 'high',
      details: `Malware detected from ${source || 'unknown'}`,
    });
    
    // Keep only last 1000 incidents
    if (this.securityMetrics.incidents.length > 1000) {
      this.securityMetrics.incidents.shift();
    }
  }
  
  public trackPhishingDetected(source?: string): void {
    this.securityMetrics.phishingDetected++;
    
    this.securityMetrics.incidents.push({
      timestamp: Date.now(),
      type: 'phishing_detected',
      severity: 'high',
      details: `Phishing attempt from ${source || 'unknown'}`,
    });
    
    // Keep only last 1000 incidents
    if (this.securityMetrics.incidents.length > 1000) {
      this.securityMetrics.incidents.shift();
    }
  }
  
  // Get network metrics from SmartProxy
  public async getNetworkStats() {
    // Use shorter cache TTL for network stats to ensure real-time updates
    return this.metricsCache.get('networkStats', async () => {
      const smartProxy = this.dcRouter.smartProxy;
      const proxyMetrics = smartProxy ? smartProxy.getMetrics() : null;

      if (!proxyMetrics) {
        return {
          connectionsByIP: new Map<string, number>(),
          throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
          topIPs: [] as Array<{ ip: string; count: number }>,
          topIPsByBandwidth: [] as Array<{ ip: string; count: number; bwIn: number; bwOut: number }>,
          topASNs: [] as IAsnActivity[],
          totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
          throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
          throughputByIP: new Map<string, { in: number; out: number }>(),
          requestsPerSecond: 0,
          requestsTotal: 0,
          backends: [] as Array<any>,
          domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number; requestCount: number; requestsPerSecond?: number; requestsLastMinute?: number }>,
          frontendProtocols: null,
          backendProtocols: null,
        };
      }

      const activeConnectionSnapshots = await this.getActiveConnectionSnapshots({ limit: 10000 });

      const connectionsByIP = new Map<string, number>();
      const connectionsByRoute = new Map<string, number>();
      const activeConnectionsByDomain = new Map<string, number>();

      for (const snapshot of activeConnectionSnapshots) {
        connectionsByIP.set(snapshot.sourceIp, (connectionsByIP.get(snapshot.sourceIp) || 0) + 1);
        if (snapshot.routeId) {
          connectionsByRoute.set(snapshot.routeId, (connectionsByRoute.get(snapshot.routeId) || 0) + 1);
        }
        if (snapshot.domain) {
          activeConnectionsByDomain.set(snapshot.domain, (activeConnectionsByDomain.get(snapshot.domain) || 0) + 1);
        }
      }

      const instantThroughput = proxyMetrics.throughput.instant();

      // Get throughput rate
      const throughputRate = {
        bytesInPerSecond: instantThroughput.in,
        bytesOutPerSecond: instantThroughput.out
      };

      // Get top IPs by active connection count
      const topIPs = Array.from(connectionsByIP.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, 10)
        .map(([ip, count]) => ({ ip, count }));

      // Get total data transferred
      const totalDataTransferred = {
        bytesIn: proxyMetrics.totals.bytesIn(),
        bytesOut: proxyMetrics.totals.bytesOut()
      };

      // Get throughput history from Rust engine (up to 300 seconds)
      const throughputHistory = proxyMetrics.throughput.history(300);

      // Get per-IP throughput
      const throughputByIP = proxyMetrics.throughput.byIP();

      // Get HTTP request rates
      const requestsPerSecond = proxyMetrics.requests.perSecond();
      const requestsTotal = proxyMetrics.requests.total();
      const domainRequestRates = proxyMetrics.requests.byDomain();

      // Get frontend/backend protocol distribution
      const frontendProtocols = proxyMetrics.connections.frontendProtocols() ?? null;
      const backendProtocols = proxyMetrics.connections.backendProtocols() ?? null;

      // Collect backend protocol data
      const backendMetrics = proxyMetrics.backends.byBackend();
      const protocolCache = proxyMetrics.backends.detectedProtocols();

      // Group protocol cache entries by host:port so we can match them to backend metrics.
      // The protocol cache is keyed by (host, port, domain) in Rust, so the same host:port
      // can have multiple entries for different domains.
      const cacheByBackend = new Map<string, (typeof protocolCache)[number][]>();
      for (const entry of protocolCache) {
        const backendKey = `${entry.host}:${entry.port}`;
        let entries = cacheByBackend.get(backendKey);
        if (!entries) {
          entries = [];
          cacheByBackend.set(backendKey, entries);
        }
        entries.push(entry);
      }

      const backends: Array<any> = [];
      const seenCacheKeys = new Set<string>();

      for (const [key, bm] of backendMetrics) {
        backends.push({
          id: `backend:${key}`,
          backend: key,
          domain: null,
          protocol: bm.protocol,
          activeConnections: bm.activeConnections,
          totalConnections: bm.totalConnections,
          connectErrors: bm.connectErrors,
          handshakeErrors: bm.handshakeErrors,
          requestErrors: bm.requestErrors,
          avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
          poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
          h2Failures: bm.h2Failures,
          h2Suppressed: false,
          h3Suppressed: false,
          h2CooldownRemainingSecs: null,
          h3CooldownRemainingSecs: null,
          h2ConsecutiveFailures: null,
          h3ConsecutiveFailures: null,
          h3Port: null,
          cacheAgeSecs: null,
        });

        const cacheEntries = cacheByBackend.get(key);
        if (cacheEntries && cacheEntries.length > 0) {
          // Protocol cache rows are domain-scoped metadata, not live backend connections.
          for (const cache of cacheEntries) {
            const compositeKey = `${cache.host}:${cache.port}:${cache.domain ?? ''}`;
            seenCacheKeys.add(compositeKey);
            backends.push({
              id: `cache:${compositeKey}`,
              backend: key,
              domain: cache.domain ?? null,
              protocol: cache.protocol ?? bm.protocol,
              activeConnections: 0,
              totalConnections: 0,
              connectErrors: 0,
              handshakeErrors: 0,
              requestErrors: 0,
              avgConnectTimeMs: 0,
              poolHitRate: 0,
              h2Failures: 0,
              h2Suppressed: cache.h2Suppressed,
              h3Suppressed: cache.h3Suppressed,
              h2CooldownRemainingSecs: cache.h2CooldownRemainingSecs,
              h3CooldownRemainingSecs: cache.h3CooldownRemainingSecs,
              h2ConsecutiveFailures: cache.h2ConsecutiveFailures,
              h3ConsecutiveFailures: cache.h3ConsecutiveFailures,
              h3Port: cache.h3Port,
              cacheAgeSecs: cache.ageSecs,
            });
          }
        }
      }

      // Include protocol cache entries with no matching backend metric
      for (const entry of protocolCache) {
        const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`;
        if (!seenCacheKeys.has(compositeKey)) {
          backends.push({
            id: `cache:${compositeKey}`,
            backend: `${entry.host}:${entry.port}`,
            domain: entry.domain,
            protocol: entry.protocol,
            activeConnections: 0,
            totalConnections: 0,
            connectErrors: 0,
            handshakeErrors: 0,
            requestErrors: 0,
            avgConnectTimeMs: 0,
            poolHitRate: 0,
            h2Failures: 0,
            h2Suppressed: entry.h2Suppressed,
            h3Suppressed: entry.h3Suppressed,
            h2CooldownRemainingSecs: entry.h2CooldownRemainingSecs,
            h3CooldownRemainingSecs: entry.h3CooldownRemainingSecs,
            h2ConsecutiveFailures: entry.h2ConsecutiveFailures,
            h3ConsecutiveFailures: entry.h3ConsecutiveFailures,
            h3Port: entry.h3Port,
            cacheAgeSecs: entry.ageSecs,
          });
        }
      }

      // Build top 10 IPs by bandwidth (sorted by total throughput desc)
      const allIPData = new Map<string, { count: number; bwIn: number; bwOut: number }>();
      for (const [ip, count] of connectionsByIP) {
        allIPData.set(ip, { count, bwIn: 0, bwOut: 0 });
      }
      for (const [ip, tp] of throughputByIP) {
        const existing = allIPData.get(ip);
        if (existing) {
          existing.bwIn = tp.in;
          existing.bwOut = tp.out;
        } else {
          allIPData.set(ip, { count: 0, bwIn: tp.in, bwOut: tp.out });
        }
      }
      const topIPsByBandwidth = Array.from(allIPData.entries())
        .sort((a, b) => (b[1].bwIn + b[1].bwOut) - (a[1].bwIn + a[1].bwOut))
        .slice(0, 10)
        .map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));

      const observedIps = [...new Set([
        ...connectionsByIP.keys(),
        ...throughputByIP.keys(),
        ...topIPs.map((item) => item.ip),
        ...topIPsByBandwidth.map((item) => item.ip),
      ])];
      this.dcRouter.securityPolicyManager?.queueObservedIps(observedIps);

      const topASNs = await this.buildTopASNs(observedIps, allIPData);

      // Build domain activity using per-IP domain request counts from Rust engine
      const throughputByRoute = proxyMetrics.throughput.byRoute();

      // Aggregate per-IP domain request counts into per-domain totals
      const domainRequestTotals = new Map<string, number>();
      const domainRequestsByIP = proxyMetrics.connections.domainRequestsByIP();
      for (const [, domainMap] of domainRequestsByIP) {
        for (const [domain, count] of domainMap) {
          domainRequestTotals.set(domain, (domainRequestTotals.get(domain) || 0) + count);
        }
      }

      // Map canonical route key → domains from route config
      const routeDomains = new Map<string, string[]>();
      if (this.dcRouter.smartProxy) {
        for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) {
          const routeKey = route.name || route.id;
          if (!routeKey || !route.match.domains) continue;
          const domains = Array.isArray(route.match.domains)
            ? route.match.domains
            : [route.match.domains];
          if (domains.length > 0) {
            routeDomains.set(routeKey, domains);
          }
        }
      }

      // Resolve wildcards using domains seen in request metrics
      const allKnownDomains = new Set<string>(domainRequestTotals.keys());
      for (const domain of domainRequestRates.keys()) {
        allKnownDomains.add(domain);
      }
      for (const entry of protocolCache) {
        if (entry.domain) allKnownDomains.add(entry.domain);
      }
      for (const snapshot of activeConnectionSnapshots) {
        if (snapshot.domain) allKnownDomains.add(snapshot.domain);
      }

      // Build reverse map: concrete domain → canonical route key(s)
      const domainToRoutes = new Map<string, string[]>();
      for (const [routeKey, domains] of routeDomains) {
        for (const pattern of domains) {
          if (pattern.includes('*')) {
            const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '[^.]+') + '$');
            for (const knownDomain of allKnownDomains) {
              if (regex.test(knownDomain)) {
                const existing = domainToRoutes.get(knownDomain);
                if (existing) { existing.push(routeKey); }
                else { domainToRoutes.set(knownDomain, [routeKey]); }
              }
            }
          } else {
            const existing = domainToRoutes.get(pattern);
            if (existing) { existing.push(routeKey); }
            else { domainToRoutes.set(pattern, [routeKey]); }
          }
        }
      }

      const hasLiveDomainRates = domainRequestRates.size > 0;
      const getDomainWeight = (domain: string): number => {
        const liveRate = domainRequestRates.get(domain);
        return hasLiveDomainRates
          ? (liveRate?.lastMinute ?? 0)
          : (domainRequestTotals.get(domain) || 0);
      };

      // For each route, compute the total activity weight across all resolved domains
      // so we can distribute route-level throughput/connections. Prefer live domain
      // request rates from SmartProxy 27.8+, falling back to lifetime counters.
      const routeTotalRequests = new Map<string, number>();
      for (const [domain, routeKeys] of domainToRoutes) {
        const reqs = getDomainWeight(domain);
        for (const routeKey of routeKeys) {
          routeTotalRequests.set(routeKey, (routeTotalRequests.get(routeKey) || 0) + reqs);
        }
      }

      // Aggregate metrics per domain using request-count-proportional splitting
      const domainAgg = new Map<string, {
        activeConnections: number;
        bytesInPerSec: number;
        bytesOutPerSec: number;
        routeCount: number;
        requestCount: number;
        requestsPerSecond: number;
        requestsLastMinute: number;
      }>();

      for (const [domain, routeKeys] of domainToRoutes) {
        const domainReqs = getDomainWeight(domain);
        const requestRate = domainRequestRates.get(domain);
        let totalConns = 0;
        let totalIn = 0;
        let totalOut = 0;

        for (const routeKey of routeKeys) {
          const conns = connectionsByRoute.get(routeKey) || 0;
          const tp = throughputByRoute.get(routeKey) || { in: 0, out: 0 };
          const routeTotal = routeTotalRequests.get(routeKey) || 0;

          const share = routeTotal > 0 ? domainReqs / routeTotal : 0;
          totalConns += conns * share;
          totalIn += tp.in * share;
          totalOut += tp.out * share;
        }

        domainAgg.set(domain, {
          activeConnections: activeConnectionsByDomain.get(domain) ?? Math.round(totalConns),
          bytesInPerSec: totalIn,
          bytesOutPerSec: totalOut,
          routeCount: routeKeys.length,
          requestCount: domainRequestTotals.get(domain) || 0,
          requestsPerSecond: requestRate?.perSecond ?? 0,
          requestsLastMinute: requestRate?.lastMinute ?? 0,
        });
      }

      const domainActivity = Array.from(domainAgg.entries())
        .map(([domain, data]) => ({
          domain,
          bytesInPerSecond: data.bytesInPerSec,
          bytesOutPerSecond: data.bytesOutPerSec,
          activeConnections: data.activeConnections,
          routeCount: data.routeCount,
          requestCount: data.requestCount,
          requestsPerSecond: data.requestsPerSecond,
          requestsLastMinute: data.requestsLastMinute,
        }))
        .sort((a, b) => {
          if (hasLiveDomainRates) {
            return (b.requestsPerSecond - a.requestsPerSecond) ||
              (b.requestsLastMinute - a.requestsLastMinute) ||
              ((b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
          }
          return (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond);
        });

      return {
        connectionsByIP,
        throughputRate,
        topIPs,
        topIPsByBandwidth,
        topASNs,
        totalDataTransferred,
        throughputHistory,
        throughputByIP,
        requestsPerSecond,
        requestsTotal,
        backends,
        frontendProtocols,
        backendProtocols,
        domainActivity,
      };
    }, 1000); // 1s cache — matches typical dashboard poll interval
  }

  private async buildTopASNs(
    observedIps: string[],
    allIPData: Map<string, { count: number; bwIn: number; bwOut: number }>,
  ): Promise<IAsnActivity[]> {
    const manager = this.dcRouter.securityPolicyManager;
    if (!manager || observedIps.length === 0) {
      return [];
    }

    const intelligenceRecords = await manager.listIpIntelligence({
      ipAddresses: observedIps,
      limit: Math.max(100, observedIps.length),
    });
    const asnActivity = new Map<number, IAsnActivity>();

    for (const record of intelligenceRecords) {
      if (typeof record.asn !== 'number') continue;

      const ipData = allIPData.get(record.ipAddress);
      if (!ipData) continue;

      const existing = asnActivity.get(record.asn);
      const activity = existing || {
        asn: record.asn,
        organization: record.asnOrg || record.registrantOrg || `AS${record.asn}`,
        country: record.countryCode || record.country || record.registrantCountry || null,
        activeConnections: 0,
        ipCount: 0,
        bytesInPerSecond: 0,
        bytesOutPerSecond: 0,
        sampleIps: [],
      };

      activity.activeConnections += ipData.count;
      activity.bytesInPerSecond += ipData.bwIn;
      activity.bytesOutPerSecond += ipData.bwOut;
      activity.ipCount++;
      if (activity.sampleIps.length < 5) {
        activity.sampleIps.push(record.ipAddress);
      }
      asnActivity.set(record.asn, activity);
    }

    return [...asnActivity.values()]
      .sort((a, b) => {
        const connectionDiff = b.activeConnections - a.activeConnections;
        if (connectionDiff !== 0) return connectionDiff;
        const bandwidthA = a.bytesInPerSecond + a.bytesOutPerSecond;
        const bandwidthB = b.bytesInPerSecond + b.bytesOutPerSecond;
        return bandwidthB - bandwidthA;
      })
      .slice(0, 10);
  }

  // --- Time-series helpers ---

  private static minuteKey(ts: number = Date.now()): number {
    return Math.floor(ts / 60000) * 60000;
  }

  private incrementEmailBucket(field: 'sent' | 'received' | 'failed'): void {
    const key = MetricsManager.minuteKey();
    let bucket = this.emailMinuteBuckets.get(key);
    if (!bucket) {
      bucket = { sent: 0, received: 0, failed: 0 };
      this.emailMinuteBuckets.set(key, bucket);
    }
    bucket[field]++;
  }

  private incrementDnsBucket(): void {
    const key = MetricsManager.minuteKey();
    let bucket = this.dnsMinuteBuckets.get(key);
    if (!bucket) {
      bucket = { queries: 0 };
      this.dnsMinuteBuckets.set(key, bucket);
    }
    bucket.queries++;
  }

  /**
   * Increment the per-second query counter in the ring buffer.
   * Zeros any stale slots between the last write and the current second.
   */
  private incrementQueryRing(): void {
    const currentSecond = Math.floor(Date.now() / 1000);
    const ring = this.dnsMetrics.queryRing;
    const last = this.dnsMetrics.queryRingLastSecond;

    if (last === 0) {
      // First call — zero and anchor
      ring.fill(0);
      this.dnsMetrics.queryRingLastSecond = currentSecond;
      ring[currentSecond % ring.length] = 1;
      return;
    }

    const gap = currentSecond - last;
    if (gap >= ring.length) {
      // Entire ring is stale — clear all
      ring.fill(0);
    } else if (gap > 0) {
      // Zero slots from (last+1) to currentSecond (inclusive)
      for (let s = last + 1; s <= currentSecond; s++) {
        ring[s % ring.length] = 0;
      }
    }

    this.dnsMetrics.queryRingLastSecond = currentSecond;
    ring[currentSecond % ring.length]++;
  }

  /**
   * Sum query counts from the ring buffer for the last N seconds.
   */
  private getQueryRingSum(seconds: number): number {
    const currentSecond = Math.floor(Date.now() / 1000);
    const ring = this.dnsMetrics.queryRing;
    const last = this.dnsMetrics.queryRingLastSecond;

    if (last === 0) return 0;

    // First, zero stale slots so reads are accurate even without writes
    const gap = currentSecond - last;
    if (gap >= ring.length) return 0; // all data is stale

    let sum = 0;
    const limit = Math.min(seconds, ring.length);
    for (let i = 0; i < limit; i++) {
      const sec = currentSecond - i;
      if (sec < last - (ring.length - 1)) break; // slot is from older cycle
      if (sec > last) continue; // no writes yet for this second
      sum += ring[sec % ring.length];
    }
    return sum;
  }

  private pruneOldBuckets(): void {
    const cutoff = Date.now() - 86400000; // 24h
    for (const key of this.emailMinuteBuckets.keys()) {
      if (key < cutoff) this.emailMinuteBuckets.delete(key);
    }
    for (const key of this.dnsMinuteBuckets.keys()) {
      if (key < cutoff) this.dnsMinuteBuckets.delete(key);
    }
  }

  /**
   * Get email time-series data for the last N hours, aggregated per minute.
   */
  public getEmailTimeSeries(hours: number = 24): {
    sent: Array<{ timestamp: number; value: number }>;
    received: Array<{ timestamp: number; value: number }>;
    failed: Array<{ timestamp: number; value: number }>;
  } {
    this.pruneOldBuckets();
    const cutoff = Date.now() - hours * 3600000;
    const sent: Array<{ timestamp: number; value: number }> = [];
    const received: Array<{ timestamp: number; value: number }> = [];
    const failed: Array<{ timestamp: number; value: number }> = [];

    const sortedKeys = Array.from(this.emailMinuteBuckets.keys())
      .filter((k) => k >= cutoff)
      .sort((a, b) => a - b);

    for (const key of sortedKeys) {
      const bucket = this.emailMinuteBuckets.get(key)!;
      sent.push({ timestamp: key, value: bucket.sent });
      received.push({ timestamp: key, value: bucket.received });
      failed.push({ timestamp: key, value: bucket.failed });
    }

    return { sent, received, failed };
  }

  /**
   * Get DNS time-series data for the last N hours, aggregated per minute.
   */
  public getDnsTimeSeries(hours: number = 24): {
    queries: Array<{ timestamp: number; value: number }>;
  } {
    this.pruneOldBuckets();
    const cutoff = Date.now() - hours * 3600000;
    const queries: Array<{ timestamp: number; value: number }> = [];

    const sortedKeys = Array.from(this.dnsMinuteBuckets.keys())
      .filter((k) => k >= cutoff)
      .sort((a, b) => a - b);

    for (const key of sortedKeys) {
      const bucket = this.dnsMinuteBuckets.get(key)!;
      queries.push({ timestamp: key, value: bucket.queries });
    }

    return { queries };
  }
}
