/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 * */

import {isUtf8} from 'node:buffer';

import {
  DevTools,
  type HTTPRequest,
  type HTTPResponse,
} from '../third_party/index.js';

const BODY_CONTEXT_SIZE_LIMIT = 10000;

export interface NetworkFormatterOptions {
  requestId?: number | string;
  selectedInDevToolsUI?: boolean;
  requestIdResolver?: (request: HTTPRequest) => number | string;
  fetchData?: boolean;
  requestFilePath?: string;
  responseFilePath?: string;
  saveFile?: (
    data: Uint8Array<ArrayBufferLike>,
    filename: string,
    extension: '.network-request' | '.network-response',
  ) => Promise<{filename: string}>;
  redactNetworkHeaders: boolean;
}

interface NetworkRequestConcise {
  requestId?: number | string;
  method: string;
  url: string;
  status: string;
  selectedInDevToolsUI?: boolean;
}

interface NetworkRequestDetailed extends NetworkRequestConcise {
  requestHeaders: Record<string, string>;
  requestBody?: string;
  requestBodyFilePath?: string;
  responseHeaders?: Record<string, string>;
  responseBody?: string;
  responseBodyFilePath?: string;
  failure?: string;
  redirectChain?: NetworkRequestConcise[];
}

export class NetworkFormatter {
  #request: HTTPRequest;
  #options: NetworkFormatterOptions;
  #requestBody?: string;
  #responseBody?: string;
  #requestBodyFilePath?: string;
  #responseBodyFilePath?: string;

  private constructor(request: HTTPRequest, options: NetworkFormatterOptions) {
    this.#request = request;
    this.#options = options;
  }

  static async from(
    request: HTTPRequest,
    options: NetworkFormatterOptions,
  ): Promise<NetworkFormatter> {
    const instance = new NetworkFormatter(request, options);
    if (options.fetchData) {
      await instance.#loadDetailedData();
    }
    return instance;
  }

  async #loadDetailedData(): Promise<void> {
    // Load Request Body
    if (this.#request.hasPostData()) {
      let data;
      try {
        data =
          this.#request.postData() ?? (await this.#request.fetchPostData());
      } catch {
        // Ignore parsing errors
      }
      const requestBodyNotAvailableMessage =
        '<Request body not available anymore>';
      if (this.#options.requestFilePath) {
        if (!this.#options.saveFile) {
          throw new Error('saveFile is not provided');
        }
        if (data) {
          const result = await this.#options.saveFile(
            Buffer.from(data),
            this.#options.requestFilePath,
            '.network-request',
          );
          this.#requestBodyFilePath = result.filename;
        } else {
          this.#requestBody = requestBodyNotAvailableMessage;
        }
      } else {
        if (data) {
          this.#requestBody = getSizeLimitedString(
            data,
            BODY_CONTEXT_SIZE_LIMIT,
          );
        } else {
          this.#requestBody = requestBodyNotAvailableMessage;
        }
      }
    }

    // Load Response Body
    const response = this.#request.response();
    if (response) {
      const responseBodyNotAvailableMessage =
        '<Response body not available anymore>';
      if (this.#options.responseFilePath) {
        try {
          const buffer = await response.buffer();
          if (!this.#options.saveFile) {
            throw new Error('saveFile is not provided');
          }
          const result = await this.#options.saveFile(
            buffer,
            this.#options.responseFilePath,
            '.network-response',
          );
          this.#responseBodyFilePath = result.filename;
        } catch {
          // Flatten error handling for buffer() failure and save failure
        }

        if (!this.#responseBodyFilePath) {
          this.#responseBody = responseBodyNotAvailableMessage;
        }
      } else {
        this.#responseBody = await this.#getFormattedResponseBody(
          response,
          BODY_CONTEXT_SIZE_LIMIT,
        );
      }
    }
  }

  toString(): string {
    return convertNetworkRequestConciseToString(this.toJSON());
  }

  toStringDetailed(): string {
    return converNetworkRequestDetailedToStringDetailed(this.toJSONDetailed());
  }

  toJSON(): NetworkRequestConcise {
    return {
      requestId: this.#options.requestId,
      method: this.#request.method(),
      url: this.#request.url(),
      status: this.#getStatusFromRequest(this.#request),
      selectedInDevToolsUI: this.#options.selectedInDevToolsUI,
    };
  }

  #redactNetworkHeaders(
    headers: Record<string, string>,
  ): Record<string, string> {
    const headersList = Object.entries(headers).map(item => {
      return {name: item[0], value: item[1]};
    });
    const redacted =
      DevTools.NetworkRequestFormatter.sanitizeHeaders(headersList);
    return redacted.reduce<Record<string, string>>((acc, item) => {
      acc[item.name] = item.value;
      return acc;
    }, {});
  }

  toJSONDetailed(): NetworkRequestDetailed {
    const redirectChain = this.#request.redirectChain();
    const formattedRedirectChain = redirectChain.reverse().map(request => {
      const id = this.#options.requestIdResolver
        ? this.#options.requestIdResolver(request)
        : undefined;
      const formatter = new NetworkFormatter(request, {
        requestId: id,
        saveFile: this.#options.saveFile,
        redactNetworkHeaders: this.#options.redactNetworkHeaders,
      });
      return formatter.toJSON();
    });

    const responseHeaders = this.#request.response()?.headers();

    return {
      ...this.toJSON(),
      requestHeaders: this.#options.redactNetworkHeaders
        ? this.#redactNetworkHeaders(this.#request.headers())
        : this.#request.headers(),
      requestBody: this.#requestBody,
      requestBodyFilePath: this.#requestBodyFilePath,
      responseHeaders:
        this.#options.redactNetworkHeaders && responseHeaders
          ? this.#redactNetworkHeaders(responseHeaders)
          : this.#request.response()?.headers(),
      responseBody: this.#responseBody,
      responseBodyFilePath: this.#responseBodyFilePath,
      failure: this.#request.failure()?.errorText,
      redirectChain: formattedRedirectChain.length
        ? formattedRedirectChain
        : undefined,
    };
  }

  #getStatusFromRequest(request: HTTPRequest): string {
    const httpResponse = request.response();
    const failure = request.failure();
    let status: string;
    if (httpResponse) {
      status = httpResponse.status().toString();
    } else if (failure) {
      status = failure.errorText;
    } else {
      status = 'pending';
    }
    return status;
  }

  async #getFormattedResponseBody(
    httpResponse: HTTPResponse,
    sizeLimit = BODY_CONTEXT_SIZE_LIMIT,
  ): Promise<string | undefined> {
    try {
      const responseBuffer = await httpResponse.buffer();

      if (isUtf8(responseBuffer)) {
        const responseAsTest = responseBuffer.toString('utf-8');

        if (responseAsTest.length === 0) {
          return '<empty response>';
        }

        return getSizeLimitedString(responseAsTest, sizeLimit);
      }

      return '<binary data>';
    } catch {
      return '<not available anymore>';
    }
  }
}

function getSizeLimitedString(text: string, sizeLimit: number) {
  if (text.length > sizeLimit) {
    return text.substring(0, sizeLimit) + '... <truncated>';
  }
  return text;
}

function convertNetworkRequestConciseToString(
  data: NetworkRequestConcise,
): string {
  // TODO truncate the URL
  return `reqid=${data.requestId} ${data.method} ${data.url} [${data.status}]${data.selectedInDevToolsUI ? ` [selected in the DevTools Network panel]` : ''}`;
}

function formatHeadlers(headers: Record<string, string>): string[] {
  const response: string[] = [];
  for (const [name, value] of Object.entries(headers)) {
    response.push(`- ${name}:${value}`);
  }
  return response;
}

function converNetworkRequestDetailedToStringDetailed(
  data: NetworkRequestDetailed,
): string {
  const response: string[] = [];
  response.push(`## Request ${data.url}`);
  response.push(`Status: ${data.status}`);
  response.push(`### Request Headers`);
  for (const line of formatHeadlers(data.requestHeaders)) {
    response.push(line);
  }

  if (data.requestBody) {
    response.push(`### Request Body`);
    response.push(data.requestBody);
  } else if (data.requestBodyFilePath) {
    response.push(`### Request Body`);
    response.push(`Saved to ${data.requestBodyFilePath}.`);
  }

  if (data.responseHeaders) {
    response.push(`### Response Headers`);
    for (const line of formatHeadlers(data.responseHeaders)) {
      response.push(line);
    }
  }

  if (data.responseBody) {
    response.push(`### Response Body`);
    response.push(data.responseBody);
  } else if (data.responseBodyFilePath) {
    response.push(`### Response Body`);
    response.push(`Saved to ${data.responseBodyFilePath}.`);
  }

  if (data.failure) {
    response.push(`### Request failed with`);
    response.push(data.failure);
  }

  const redirectChain = data.redirectChain;
  if (redirectChain?.length) {
    response.push(`### Redirect chain`);
    let indent = 0;
    // `redirectChain` is already ordered by toJSONDetailed(); don't reverse it
    // again here or the text output contradicts structuredContent (the JSON).
    for (const request of redirectChain) {
      response.push(
        `${'  '.repeat(indent)}${convertNetworkRequestConciseToString(request)}`,
      );
      indent++;
    }
  }
  return response.join('\n');
}
