/**
 * Proxy Log Receiver for Onebox
 *
 * TCP server that receives reverse proxy access logs and broadcasts them to WebSocket clients.
 * Supports per-client filtering by domain and adaptive sampling at high volume.
 */

import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';

/**
 * Filter applied to a WebSocket client's log stream
 */
export interface ILogFilter {
  domain?: string;
  service?: string;
  sampleRate?: number; // 0.01 to 1.0, default 1.0
}

/**
 * Reverse proxy access log entry structure.
 */
export interface IProxyAccessLog {
  ts: number;
  level?: string;
  logger?: string;
  msg?: string;
  request: {
    remote_ip: string;
    remote_port?: string;
    client_ip?: string;
    proto: string;
    method: string;
    host: string;
    uri: string;
    headers?: Record<string, string[]>;
    tls?: {
      resumed: boolean;
      version: number;
      cipher_suite: number;
      proto: string;
      server_name: string;
    };
  };
  bytes_read?: number;
  user_id?: string;
  duration: number;
  size: number;
  status: number;
  resp_headers?: Record<string, string[]>;
}

/**
 * WebSocket client with filter
 */
interface ILogClient {
  id: string;
  ws: WebSocket;
  filter: ILogFilter;
}

/**
 * ProxyLogReceiver - TCP server for reverse proxy access logs
 */
export class ProxyLogReceiver {
  private server: Deno.TcpListener | null = null;
  private clients: Map<string, ILogClient> = new Map();
  private port: number;
  private running = false;
  private connections: Set<Deno.TcpConn> = new Set();
  private connectionReaders: Map<Deno.TcpConn, ReadableStreamDefaultReader<Uint8Array>> = new Map();
  private connectionHandlers: Set<Promise<void>> = new Set();
  private acceptTask: Promise<void> | null = null;

  // Adaptive sampling state
  private logCountWindow: number[] = []; // timestamps of recent logs
  private windowSize = 1000; // track last 1 second
  private currentSampleRate = 1.0;
  private logCounter = 0;

  // Ring buffer for recent logs (for late-joining clients)
  private recentLogs: IProxyAccessLog[] = [];
  private maxRecentLogs = 100;

  // Traffic stats aggregation (hourly rolling window)
  private trafficStats: {
    timestamp: number;
    requestCount: number;
    errorCount: number; // 4xx + 5xx
    totalDuration: number; // microseconds
    totalSize: number; // bytes
    statusCounts: Record<string, number>; // "2xx", "3xx", "4xx", "5xx"
  }[] = [];
  private maxStatsAge = 3600 * 1000; // 1 hour in ms
  private statsInterval = 60 * 1000; // 1 minute buckets

  constructor(port = 9999) {
    this.port = port;
    // Initialize first stats bucket
    this.trafficStats.push(this.createStatsBucket());
  }

  /**
   * Create a new stats bucket
   */
  private createStatsBucket(): typeof this.trafficStats[0] {
    return {
      timestamp: Math.floor(Date.now() / this.statsInterval) * this.statsInterval,
      requestCount: 0,
      errorCount: 0,
      totalDuration: 0,
      totalSize: 0,
      statusCounts: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
    };
  }

  /**
   * Get current stats bucket, creating new one if needed
   */
  private getCurrentStatsBucket(): typeof this.trafficStats[0] {
    const now = Date.now();
    const currentBucketTime = Math.floor(now / this.statsInterval) * this.statsInterval;

    // Get or create current bucket
    let bucket = this.trafficStats[this.trafficStats.length - 1];
    if (!bucket || bucket.timestamp !== currentBucketTime) {
      bucket = this.createStatsBucket();
      this.trafficStats.push(bucket);

      // Clean up old buckets
      const cutoff = now - this.maxStatsAge;
      while (this.trafficStats.length > 0 && this.trafficStats[0].timestamp < cutoff) {
        this.trafficStats.shift();
      }
    }

    return bucket;
  }

  /**
   * Record a request in traffic stats
   */
  private recordTrafficStats(log: IProxyAccessLog): void {
    const bucket = this.getCurrentStatsBucket();

    bucket.requestCount++;
    bucket.totalDuration += log.duration;
    bucket.totalSize += log.size || 0;

    // Categorize status code
    const statusCategory = Math.floor(log.status / 100);
    if (statusCategory === 2) {
      bucket.statusCounts['2xx']++;
    } else if (statusCategory === 3) {
      bucket.statusCounts['3xx']++;
    } else if (statusCategory === 4) {
      bucket.statusCounts['4xx']++;
      bucket.errorCount++;
    } else if (statusCategory === 5) {
      bucket.statusCounts['5xx']++;
      bucket.errorCount++;
    }
  }

  /**
   * Start the TCP server
   */
  async start(): Promise<void> {
    if (this.running) {
      logger.warn('ProxyLogReceiver is already running');
      return;
    }

    try {
      this.server = Deno.listen({ port: this.port, transport: 'tcp' });
      this.running = true;
      logger.success(`ProxyLogReceiver started on TCP port ${this.port}`);

      // Start accepting connections in background
      this.acceptTask = this.acceptConnections();
    } catch (error) {
      logger.error(`Failed to start ProxyLogReceiver: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Accept incoming TCP connections from the reverse proxy
   */
  private async acceptConnections(): Promise<void> {
    if (!this.server) return;

    try {
      for await (const conn of this.server) {
        this.connections.add(conn);
        const handlerTask = this.handleConnection(conn);
        this.connectionHandlers.add(handlerTask);
        handlerTask.finally(() => this.connectionHandlers.delete(handlerTask));
      }
    } catch (error) {
      if (this.running) {
        logger.error(`ProxyLogReceiver accept error: ${getErrorMessage(error)}`);
      }
    }
  }

  /**
   * Handle a single TCP connection from the reverse proxy
   */
  private async handleConnection(conn: Deno.TcpConn): Promise<void> {
    const remoteAddr = conn.remoteAddr as Deno.NetAddr;
    logger.debug(`ProxyLogReceiver: Connection from ${remoteAddr.hostname}:${remoteAddr.port}`);

    const reader = conn.readable.getReader();
    this.connectionReaders.set(conn, reader);
    const decoder = new TextDecoder();
    let buffer = '';

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });

        // Process complete newline-delimited JSON log lines.
        const lines = buffer.split('\n');
        buffer = lines.pop() || ''; // Keep incomplete line in buffer

        for (const line of lines) {
          if (line.trim()) {
            this.processLogLine(line);
          }
        }
      }
    } catch (error) {
      if (this.running) {
        logger.debug(`ProxyLogReceiver connection closed: ${getErrorMessage(error)}`);
      }
    } finally {
      this.connectionReaders.delete(conn);
      this.connections.delete(conn);
      try {
        reader.releaseLock();
      } catch {
        // Reader may already be released after cancellation during shutdown.
      }
      try {
        conn.close();
      } catch {
        // Already closed
      }
    }
  }

  /**
   * Process a single access log line
   */
  private processLogLine(line: string): void {
    try {
      const log = JSON.parse(line) as IProxyAccessLog;

      // Only process access logs (check for http.log.access or just access, or any log with request/status)
      const isAccessLog = log.logger === 'http.log.access' ||
                          log.logger === 'access' ||
                          (log.request && typeof log.status === 'number');
      if (!isAccessLog) {
        logger.debug(`ProxyLogReceiver: Skipping non-access log: ${log.logger || 'unknown'}`);
        return;
      }

      // Always record traffic stats (before sampling) for accurate aggregation
      this.recordTrafficStats(log);

      // Update adaptive sampling
      this.updateSampling();

      // Apply global sampling (skip if randomly filtered out)
      if (this.currentSampleRate < 1.0 && Math.random() > this.currentSampleRate) {
        return;
      }

      logger.debug(`ProxyLogReceiver: Access log received - ${log.request?.method} ${log.request?.host}${log.request?.uri} (status: ${log.status})`);

      // Add to recent logs buffer
      this.recentLogs.push(log);
      if (this.recentLogs.length > this.maxRecentLogs) {
        this.recentLogs.shift();
      }

      // Broadcast to WebSocket clients (log how many clients)
      logger.debug(`ProxyLogReceiver: Broadcasting to ${this.clients.size} clients`);
      this.broadcast(log);
    } catch (error) {
      logger.debug(`Failed to parse proxy log line: ${getErrorMessage(error)}`);
    }
  }

  /**
   * Update adaptive sampling rate based on log volume
   */
  private updateSampling(): void {
    const now = Date.now();

    // Add current timestamp
    this.logCountWindow.push(now);

    // Remove timestamps older than 1 second
    const cutoff = now - this.windowSize;
    while (this.logCountWindow.length > 0 && this.logCountWindow[0] < cutoff) {
      this.logCountWindow.shift();
    }

    // Calculate logs per second
    const logsPerSecond = this.logCountWindow.length;

    // Adjust sample rate based on volume
    if (logsPerSecond > 1000) {
      this.currentSampleRate = 0.01; // 1%
    } else if (logsPerSecond > 500) {
      this.currentSampleRate = 0.1; // 10%
    } else if (logsPerSecond > 100) {
      this.currentSampleRate = 0.5; // 50%
    } else {
      this.currentSampleRate = 1.0; // 100%
    }
  }

  /**
   * Broadcast a log entry to all connected WebSocket clients
   */
  private broadcast(log: IProxyAccessLog): void {
    const message = JSON.stringify({
      type: 'access_log',
      data: {
        ts: log.ts,
        request: {
          remote_ip: log.request.remote_ip,
          method: log.request.method,
          host: log.request.host,
          uri: log.request.uri,
          proto: log.request.proto,
        },
        status: log.status,
        duration: log.duration,
        size: log.size,
      },
      timestamp: Date.now(),
    });

    for (const [clientId, client] of this.clients) {
      try {
        // Apply client-specific filter
        if (!this.matchesFilter(log, client.filter)) {
          continue;
        }

        // Apply client-specific sample rate
        if (client.filter.sampleRate && client.filter.sampleRate < 1.0) {
          if (Math.random() > client.filter.sampleRate) {
            continue;
          }
        }

        if (client.ws.readyState === WebSocket.OPEN) {
          client.ws.send(message);
        } else {
          // Remove dead clients
          this.clients.delete(clientId);
        }
      } catch {
        this.clients.delete(clientId);
      }
    }
  }

  /**
   * Check if a log entry matches a client's filter
   */
  private matchesFilter(log: IProxyAccessLog, filter: ILogFilter): boolean {
    // Domain filter
    if (filter.domain) {
      const logHost = log.request.host.toLowerCase();
      const filterDomain = filter.domain.toLowerCase();

      // Match exact domain or subdomain
      if (logHost !== filterDomain && !logHost.endsWith(`.${filterDomain}`)) {
        return false;
      }
    }

    return true;
  }

  /**
   * Add a WebSocket client to receive logs
   */
  addClient(clientId: string, ws: WebSocket, filter: ILogFilter = {}): void {
    this.clients.set(clientId, { id: clientId, ws, filter });
    logger.debug(`ProxyLogReceiver: Added client ${clientId} (${this.clients.size} total)`);

    // Send recent logs to new client
    for (const log of this.recentLogs) {
      if (this.matchesFilter(log, filter)) {
        try {
          ws.send(
            JSON.stringify({
              type: 'access_log',
              data: {
                ts: log.ts,
                request: {
                  remote_ip: log.request.remote_ip,
                  method: log.request.method,
                  host: log.request.host,
                  uri: log.request.uri,
                  proto: log.request.proto,
                },
                status: log.status,
                duration: log.duration,
                size: log.size,
              },
              timestamp: Date.now(),
            }),
          );
        } catch {
          // Client disconnected
        }
      }
    }
  }

  /**
   * Remove a WebSocket client
   */
  removeClient(clientId: string): void {
    if (this.clients.delete(clientId)) {
      logger.debug(`ProxyLogReceiver: Removed client ${clientId} (${this.clients.size} remaining)`);
    }
  }

  /**
   * Update a client's filter
   */
  updateClientFilter(clientId: string, filter: ILogFilter): void {
    const client = this.clients.get(clientId);
    if (client) {
      client.filter = filter;
      logger.debug(`ProxyLogReceiver: Updated filter for client ${clientId}`);
    }
  }

  /**
   * Stop the TCP server
   */
  async stop(): Promise<void> {
    if (!this.running) {
      return;
    }

    this.running = false;

    // Cancel pending reads before closing sockets so background handlers can finish.
    await Promise.allSettled(
      Array.from(this.connectionReaders.values()).map((reader) => reader.cancel()),
    );

    // Close all connections
    for (const conn of this.connections) {
      try {
        conn.close();
      } catch {
        // Already closed
      }
    }
    this.connections.clear();

    // Close server
    if (this.server) {
      try {
        this.server.close();
      } catch {
        // Already closed
      }
      this.server = null;
    }

    if (this.acceptTask) {
      await this.acceptTask.catch(() => {});
      this.acceptTask = null;
    }

    await Promise.allSettled(this.connectionHandlers);
    this.connectionHandlers.clear();
    this.connectionReaders.clear();

    // Clear clients
    this.clients.clear();

    logger.info('ProxyLogReceiver stopped');
  }

  /**
   * Get current stats
   */
  getStats(): {
    running: boolean;
    port: number;
    clients: number;
    connections: number;
    sampleRate: number;
    recentLogsCount: number;
  } {
    return {
      running: this.running,
      port: this.port,
      clients: this.clients.size,
      connections: this.connections.size,
      sampleRate: this.currentSampleRate,
      recentLogsCount: this.recentLogs.length,
    };
  }

  /**
   * Get aggregated traffic stats for the specified time range
   * @param minutes Number of minutes to aggregate (default: 60)
   */
  getTrafficStats(minutes = 60): {
    requestCount: number;
    errorCount: number;
    avgResponseTime: number; // in milliseconds
    totalBytes: number;
    statusCounts: Record<string, number>;
    requestsPerMinute: number;
    errorRate: number; // percentage
  } {
    const now = Date.now();
    const cutoff = now - (minutes * 60 * 1000);

    // Aggregate all buckets within the time range
    let requestCount = 0;
    let errorCount = 0;
    let totalDuration = 0;
    let totalBytes = 0;
    const statusCounts: Record<string, number> = { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 };

    for (const bucket of this.trafficStats) {
      if (bucket.timestamp >= cutoff) {
        requestCount += bucket.requestCount;
        errorCount += bucket.errorCount;
        totalDuration += bucket.totalDuration;
        totalBytes += bucket.totalSize;
        for (const [status, count] of Object.entries(bucket.statusCounts)) {
          statusCounts[status] = (statusCounts[status] || 0) + count;
        }
      }
    }

    // Calculate averages
    const avgResponseTime = requestCount > 0
      ? (totalDuration / requestCount) / 1000 // Convert from microseconds to milliseconds
      : 0;
    const requestsPerMinute = requestCount / Math.max(minutes, 1);
    const errorRate = requestCount > 0 ? (errorCount / requestCount) * 100 : 0;

    return {
      requestCount,
      errorCount,
      avgResponseTime: Math.round(avgResponseTime * 100) / 100, // Round to 2 decimal places
      totalBytes,
      statusCounts,
      requestsPerMinute: Math.round(requestsPerMinute * 100) / 100,
      errorRate: Math.round(errorRate * 100) / 100,
    };
  }
}
