/**
 * Network interceptor — capture HTTP requests/responses
 *
 * @module network
 */

import type { NetworkRequest } from './types.js';

// DOM type declarations for ESLint
type RequestInfo = string | Request | URL;
type HeadersInit = Headers | Record<string, string> | string[][];

export interface NetworkInterceptorOptions {
  /** URL patterns to include (default: all) */
  includePatterns?: RegExp[];
  /** URL patterns to exclude */
  excludePatterns?: RegExp[];
  /** Max request body size to capture (default: 100KB) */
  maxBodySize?: number;
  /** Whether to capture request headers (default: true) */
  captureRequestHeaders?: boolean;
  /** Whether to capture response headers (default: true) */
  captureResponseHeaders?: boolean;
  /** Sanitize header values matching these patterns */
  sensitiveHeaderPatterns?: RegExp[];
}

/**
 * Interceptor for capturing network requests
 */
export class NetworkInterceptor {
  private options: Required<NetworkInterceptorOptions>;
  private originalFetch: typeof fetch;
  private isActive = false;
  private pendingRequests = new Map<string, NetworkRequest>();
  private onRequest: (request: NetworkRequest) => void;

  constructor(
    onRequest: (request: NetworkRequest) => void,
    options: NetworkInterceptorOptions = {}
  ) {
    this.onRequest = onRequest;
    this.options = {
      includePatterns: options.includePatterns ?? [],
      excludePatterns: options.excludePatterns ?? [],
      maxBodySize: options.maxBodySize ?? 100 * 1024,
      captureRequestHeaders: options.captureRequestHeaders ?? true,
      captureResponseHeaders: options.captureResponseHeaders ?? true,
      sensitiveHeaderPatterns: options.sensitiveHeaderPatterns ?? [
        /authorization/i,
        /cookie/i,
        /token/i,
        /api-key/i,
      ],
    };
    this.originalFetch = globalThis.fetch.bind(globalThis);
  }

  /**
   * Start intercepting fetch calls
   */
  start(): void {
    if (this.isActive) return;
    this.isActive = true;

    globalThis.fetch = this.interceptedFetch.bind(this);
  }

  /**
   * Stop intercepting fetch calls
   */
  stop(): void {
    if (!this.isActive) return;
    this.isActive = false;

    globalThis.fetch = this.originalFetch;
  }

  /**
   * Check if URL should be captured
   */
  private shouldCapture(url: string): boolean {
    // Check excludes first
    for (const pattern of this.options.excludePatterns) {
      if (pattern.test(url)) return false;
    }

    // If includes specified, must match one
    if (this.options.includePatterns.length > 0) {
      for (const pattern of this.options.includePatterns) {
        if (pattern.test(url)) return true;
      }
      return false;
    }

    return true;
  }

  /**
   * Sanitize headers
   */
  private sanitizeHeaders(
    headers: HeadersInit | undefined
  ): Record<string, string> {
    const sanitized: Record<string, string> = {};
    const headerEntries = headers instanceof Headers
      ? Array.from(headers.entries())
      : typeof headers === 'object' && headers !== null
        ? Object.entries(headers)
        : [];

    for (const [key, value] of headerEntries) {
      const isSensitive = this.options.sensitiveHeaderPatterns.some(p =>
        p.test(key)
      );
      sanitized[key] = isSensitive ? '[REDACTED]' : value;
    }

    return sanitized;
  }

  /**
   * Generate request ID
   */
  private generateId(): string {
    return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
  }

  /**
   * Intercepted fetch implementation
   */
  private async interceptedFetch(
    input: RequestInfo | URL,
    init?: RequestInit
  ): Promise<Response> {
    const url = input.toString();
    const shouldCapture = this.shouldCapture(url);

    const requestId = this.generateId();
    const startTime = new Date().toISOString();

    // Create request record if capturing
    if (shouldCapture) {
      const request: NetworkRequest = {
        id: requestId,
        url: url.slice(0, 2048), // Limit URL length
        method: (init?.method ?? 'GET').toUpperCase(),
        requestHeaders: this.options.captureRequestHeaders
          ? this.sanitizeHeaders(init?.headers)
          : {},
        startTime,
      };

      // Capture request body if present and not too large
      if (init?.body && typeof init.body === 'string') {
        if (init.body.length <= this.options.maxBodySize) {
          request.requestBody = init.body.slice(0, this.options.maxBodySize);
        }
      }

      this.pendingRequests.set(requestId, request);
    }

    try {
      const response = await this.originalFetch(input, init);

      // Update with response info if capturing
      if (shouldCapture) {
        const request = this.pendingRequests.get(requestId);
        if (request) {
          request.status = response.status;
          request.endTime = new Date().toISOString();
          request.durationMs = new Date(request.endTime).getTime() - new Date(startTime).getTime();

          if (this.options.captureResponseHeaders) {
            request.responseHeaders = this.sanitizeHeaders(
              Object.fromEntries(response.headers.entries())
            );
          }

          // Don't capture response body (too large/complex)
          // Just record that we received a response

          this.pendingRequests.delete(requestId);
          this.onRequest(request);
        }
      }

      return response;
    } catch (error) {
      // Record error if capturing
      if (shouldCapture) {
        const request = this.pendingRequests.get(requestId);
        if (request) {
          request.endTime = new Date().toISOString();
          request.durationMs = new Date(request.endTime).getTime() - new Date(startTime).getTime();
          request.error = error instanceof Error ? error.message : String(error);

          this.pendingRequests.delete(requestId);
          this.onRequest(request);
        }
      }

      throw error;
    }
  }

  /**
   * Check if interceptor is active
   */
  isRunning(): boolean {
    return this.isActive;
  }
}
