/**
 * Network interception — capture Facebook GraphQL API responses via CDP.
 */

import { EventEmitter } from 'node:events';
import type { BrowserContext } from './cdp.js';
import { config } from './config.js';

// ── Types ──────────────────────────────────────────────────────────────────

export interface GraphQLResponse {
  requestId: string;
  url: string;
  operationName?: string;
  body: Record<string, unknown>;
  timestamp: number;
}

export interface NetworkResponse {
  url: string;
  status: number;
  headers: Record<string, string>;
  requestId: string;
}

// ── Event map for typed emitter ────────────────────────────────────────────

export interface NetworkInterceptorEvents {
  graphql: [response: GraphQLResponse];
  response: [response: NetworkResponse];
  error: [error: Error];
}

// ── Interceptor ────────────────────────────────────────────────────────────

export class NetworkInterceptor extends EventEmitter<NetworkInterceptorEvents> {
  private ctx: BrowserContext | null = null;
  private requestMap = new Map<string, { url: string; postBody?: string }>();
  private unsubscribers: Array<() => void> = [];

  /**
   * Start intercepting network traffic on the given CDP session.
   */
  async start(ctx: BrowserContext): Promise<void> {
    this.ctx = ctx;

    // Track outgoing requests so we can correlate requestId → URL / POST body
    const offRequest = ctx.client.on(
      'Network.requestWillBeSent',
      (params) => {
        this.requestMap.set(params.requestId, {
          url: params.request.url,
          postBody: params.request.postData,
        });
      },
    );
    this.unsubscribers.push(offRequest as unknown as () => void);

    // Handle completed responses
    const offResponse = ctx.client.on(
      'Network.responseReceived',
      (params) => {
        this.handleResponse(params).catch((err) => {
          this.emit('error', err instanceof Error ? err : new Error(String(err)));
        });
      },
    );
    this.unsubscribers.push(offResponse as unknown as () => void);
  }

  /**
   * Stop intercepting.
   */
  async stop(): Promise<void> {
    for (const unsub of this.unsubscribers) {
      try { unsub(); } catch { /* already removed */ }
    }
    this.unsubscribers = [];
    this.requestMap.clear();
    this.ctx = null;
  }

  // ── Internal ───────────────────────────────────────────────────────────

  private async handleResponse(
    params: { requestId: string; response: { url: string; status: number; headers: Record<string, string> } },
  ): Promise<void> {
    const { requestId, response } = params;
    const url = response.url;

    // Emit generic response event
    this.emit('response', {
      url,
      status: response.status,
      headers: response.headers as Record<string, string>,
      requestId,
    });

    // Only process GraphQL responses
    if (!config.network.graphqlUrlPattern.test(url)) return;

    const body = await this.fetchBody(requestId);
    if (!body) return;

    const operationName = this.extractOperationName(requestId, body);

    this.emit('graphql', {
      requestId,
      url,
      operationName,
      body,
      timestamp: Date.now(),
    });
  }

  /**
   * Fetch the response body for a given request ID.
   * Returns null if the body is unavailable (e.g., too large, evicted).
   */
  private async fetchBody(
    requestId: string,
  ): Promise<Record<string, unknown> | null> {
    if (!this.ctx) return null;
    try {
      const { body, base64Encoded } = await this.ctx.Network.getResponseBody({
        requestId,
      });
      const raw = base64Encoded
        ? Buffer.from(body, 'base64').toString('utf-8')
        : body;
      return JSON.parse(raw) as Record<string, unknown>;
    } catch {
      // Body unavailable — evicted, streaming, too large, etc.
      return null;
    }
  }

  /**
   * Try to determine the GraphQL operation name from either the
   * original POST body or the parsed response payload.
   */
  private extractOperationName(
    requestId: string,
    body: Record<string, unknown>,
  ): string | undefined {
    // 1. From the request POST data (fb_api_req_friendly_name or doc_id style)
    const req = this.requestMap.get(requestId);
    if (req?.postBody) {
      try {
        const params = new URLSearchParams(req.postBody);
        const friendly = params.get('fb_api_req_friendly_name');
        if (friendly) return friendly;
      } catch { /* not URL-encoded, ignore */ }

      try {
        const json = JSON.parse(req.postBody) as Record<string, unknown>;
        if (typeof json.operationName === 'string') return json.operationName;
      } catch { /* not JSON, ignore */ }
    }

    // 2. From the response body
    if (typeof body.operationName === 'string') return body.operationName;

    // 3. Some FB responses nest under data.{operationName}
    if (body.data && typeof body.data === 'object') {
      const keys = Object.keys(body.data as object);
      if (keys.length === 1) return keys[0];
    }

    return undefined;
  }
}
