// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../../../ui/components/request_link_icon/request_link_icon.js';

import * as i18n from '../../../core/i18n/i18n.js';
import type * as Platform from '../../../core/platform/platform.js';
import * as SDK from '../../../core/sdk/sdk.js';
import * as Helpers from '../../../models/trace/helpers/helpers.js';
import * as Trace from '../../../models/trace/trace.js';
import * as LegacyComponents from '../../../ui/legacy/components/utils/utils.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Lit from '../../../ui/lit/lit.js';

import networkRequestDetailsStyles from './networkRequestDetails.css.js';
import networkRequestTooltipStyles from './networkRequestTooltip.css.js';
import {NetworkRequestTooltip} from './NetworkRequestTooltip.js';
import {colorForNetworkRequest} from './Utils.js';

const {html, render} = Lit;

const MAX_URL_LENGTH = 100;

const UIStrings = {
  /**
   * @description Text that refers to the network request method
   */
  requestMethod: 'Request method',
  /**
   * @description Text that refers to the network request protocol
   */
  protocol: 'Protocol',
  /**
   * @description Text to show the priority of an item
   */
  priority: 'Priority',
  /**
   * @description Text used when referring to the data sent in a network request that is encoded as a particular file format.
   */
  encodedData: 'Encoded data',
  /**
   * @description Text used to refer to the data sent in a network request that has been decoded.
   */
  decodedBody: 'Decoded body',
  /**
   * @description Text in Timeline indicating that input has happened recently
   */
  yes: 'Yes',
  /**
   * @description Text in Timeline indicating that input has not happened recently
   */
  no: 'No',
  /**
   * @description Text to indicate to the user they are viewing an event representing a network request.
   */
  networkRequest: 'Network request',
  /**
   * @description Text for the data source of a network request.
   */
  fromCache: 'From cache',
  /**
   * @description Text used to show the mime-type of the data transferred with a network request (e.g. "application/json").
   */
  mimeType: 'MIME type',
  /**
   * @description Text used to show the user that a request was served from the browser's in-memory cache.
   */
  FromMemoryCache: ' (from memory cache)',
  /**
   * @description Text used to show the user that a request was served from the browser's file cache.
   */
  FromCache: ' (from cache)',
  /**
   * @description Label for a network request indicating that it was a HTTP2 server push instead of a regular network request, in the Performance panel
   */
  FromPush: ' (from push)',
  /**
   * @description Text used to show a user that a request was served from an installed, active service worker.
   */
  FromServiceWorker: ' (from `service worker`)',
  /**
   * @description Text for the event initiated by another one
   */
  initiatedBy: 'Initiated by',
  /**
   * @description Text that refers to if the network request is blocking
   */
  blocking: 'Blocking',
  /**
   * @description Text that refers to if the network request is in-body parser render-blocking
   */
  inBodyParserBlocking: 'In-body parser blocking',
  /**
   * @description Text that refers to if the network request is render-blocking
   */
  renderBlocking: 'Render-blocking',
  /**
   * @description Text to refer to a 3rd Party entity.
   */
  entity: '3rd party',
  /**
   * @description Label for a column containing the names of timings (performance metric) taken in the server side application.
   */
  serverTiming: 'Server timing',
  /**
   * @description Label for a column containing the values of timings (performance metric) taken in the server side application.
   */
  time: 'Time',
  /**
   * @description Label for a column containing the description of timings (performance metric) taken in the server side application.
   */
  description: 'Description',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/NetworkRequestDetails.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class NetworkRequestDetails extends UI.Widget.Widget {
  #view: typeof DEFAULT_VIEW;
  #request: Trace.Types.Events.SyntheticNetworkRequest|null = null;
  #requestPreviewElements = new WeakMap<Trace.Types.Events.SyntheticNetworkRequest, HTMLElement>();
  #entityMapper: Trace.EntityMapper.EntityMapper|null = null;
  #target: SDK.Target.Target|null = null;
  #linkifier: LegacyComponents.Linkifier.Linkifier|null = null;
  #serverTimings: SDK.ServerTiming.ServerTiming[]|null = null;
  #parsedTrace: Trace.TraceModel.ParsedTrace|null = null;

  constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
    super(element);
    this.#view = view;
    this.requestUpdate();
  }

  set linkifier(linkifier: LegacyComponents.Linkifier.Linkifier|null) {
    this.#linkifier = linkifier;
    this.requestUpdate();
  }

  set parsedTrace(parsedTrace: Trace.TraceModel.ParsedTrace|null) {
    this.#parsedTrace = parsedTrace;
    this.requestUpdate();
  }

  set target(maybeTarget: SDK.Target.Target|null) {
    this.#target = maybeTarget;
    this.requestUpdate();
  }

  set request(event: Trace.Types.Events.SyntheticNetworkRequest) {
    this.#request = event;

    for (const header of event.args.data.responseHeaders ?? []) {
      const headerName = header.name.toLocaleLowerCase();
      // Some popular hosting providers like vercel or render get rid of
      // Server-Timing headers added by users, so as a workaround we
      // also support server timing headers with the `-test` suffix
      // while this feature is experimental, to enable easier trials.
      if (headerName === 'server-timing' || headerName === 'server-timing-test') {
        header.name = 'server-timing';
        this.#serverTimings = SDK.ServerTiming.ServerTiming.parseHeaders([header]);
        break;
      }
    }
    this.requestUpdate();
  }

  set entityMapper(mapper: Trace.EntityMapper.EntityMapper|null) {
    this.#entityMapper = mapper;
    this.requestUpdate();
  }

  override performUpdate(): Promise<void>|void {
    this.#view(
        {
          request: this.#request,
          previewElementsCache: this.#requestPreviewElements,
          target: this.#target,
          entityMapper: this.#entityMapper,
          serverTimings: this.#serverTimings,
          linkifier: this.#linkifier,
          parsedTrace: this.#parsedTrace,
        },
        {}, this.contentElement);
  }
}

export interface ViewInput {
  request: Trace.Types.Events.SyntheticNetworkRequest|null;
  target: SDK.Target.Target|null;
  previewElementsCache: WeakMap<Trace.Types.Events.SyntheticNetworkRequest, HTMLElement>;
  entityMapper: Trace.EntityMapper.EntityMapper|null;
  serverTimings: SDK.ServerTiming.ServerTiming[]|null;
  linkifier: LegacyComponents.Linkifier.Linkifier|null;
  parsedTrace: Trace.TraceModel.ParsedTrace|null;
}

export const DEFAULT_VIEW: (
    input: ViewInput, output: object, target: HTMLElement) => void = (input, _output, target) => {
  if (!input.request) {
    render(Lit.nothing, target);
    return;
  }
  const {request} = input;
  const {data} = request.args;

  const redirectsHtml = NetworkRequestTooltip.renderRedirects(request);
  // clang-format off
      render(html`
        <style>${networkRequestDetailsStyles}</style>
        <style>${networkRequestTooltipStyles}</style>

        <div class="network-request-details-content">
          ${renderTitle(input.request)}
          ${renderURL(input.request)}
          <div class="network-request-details-cols">
            ${Lit.Directives.until(renderPreviewElement(
              input.request,
              input.target,
              input.previewElementsCache,
            ))}
            <div class="network-request-details-col">
              ${renderRow(i18nString(UIStrings.requestMethod), data.requestMethod)}
              ${renderRow(i18nString(UIStrings.protocol), data.protocol)}
              ${renderRow(i18nString(UIStrings.priority), NetworkRequestTooltip.renderPriorityValue(request))}
              ${renderRow(i18nString(UIStrings.mimeType), data.mimeType)}
              ${renderEncodedDataLength(request)}
              ${renderRow(i18nString(UIStrings.decodedBody), i18n.ByteUtilities.bytesToString(request.args.data.decodedBodyLength))}
              ${renderBlockingRow(request)}
              ${renderFromCache(request)}
              ${renderThirdPartyEntity(request, input.entityMapper)}
            </div>
            <div class="column-divider"></div>
            <div class="network-request-details-col">
              <div class="timing-rows">
                ${NetworkRequestTooltip.renderTimings(request)}
              </div>
            </div>
            ${renderServerTimings(input.serverTimings)}
            ${redirectsHtml ? html `
              <div class="column-divider"></div>
              <div class="network-request-details-col redirect-details">
                ${redirectsHtml}
              </div>
            ` : Lit.nothing}
            </div>
            ${renderInitiatedBy(request, input.parsedTrace, input.target, input.linkifier)}
          </div>
        </div>
     `, target);
  // clang-format on
};

function renderTitle(request: Trace.Types.Events.SyntheticNetworkRequest): Lit.TemplateResult {
  const style = {
    backgroundColor: `${colorForNetworkRequest(request)}`,
  };

  return html`
    <div class="network-request-details-title">
      <div style=${Lit.Directives.styleMap(style)}></div>
      ${i18nString(UIStrings.networkRequest)}
    </div>
  `;
}

function renderURL(request: Trace.Types.Events.SyntheticNetworkRequest): Lit.TemplateResult {
  const options: LegacyComponents.Linkifier.LinkifyURLOptions = {
    tabStop: true,
    showColumnNumber: false,
    inlineFrameIndex: 0,
    maxLength: MAX_URL_LENGTH,
  };
  const linkifiedURL = LegacyComponents.Linkifier.Linkifier.linkifyURL(
      request.args.data.url as Platform.DevToolsPath.UrlString, options);

  // Potentially link to request within Network Panel
  const networkRequest = SDK.TraceObject.RevealableNetworkRequest.create(request);
  if (networkRequest) {
    linkifiedURL.addEventListener('contextmenu', (event: MouseEvent) => {
      const contextMenu = new UI.ContextMenu.ContextMenu(event);
      contextMenu.appendApplicableItems(networkRequest);
      void contextMenu.show();
    });

    // clang-format off
      const urlElement = html`
        ${linkifiedURL}
        <devtools-request-link-icon .data=${{request: networkRequest.networkRequest}}>
        </devtools-request-link-icon>
      `;
    // clang-format on
    return html`<div class="network-request-details-item">${urlElement}</div>`;
  }

  return html`<div class="network-request-details-item">${linkifiedURL}</div>`;
}

async function renderPreviewElement(
    request: Trace.Types.Events.SyntheticNetworkRequest, target: SDK.Target.Target|null,
    previewElementsCache: WeakMap<Trace.Types.Events.SyntheticNetworkRequest, HTMLElement>): Promise<Lit.LitTemplate> {
  if (!request.args.data.url || !target) {
    return Lit.nothing;
  }

  const url = request.args.data.url as Platform.DevToolsPath.UrlString;

  if (!previewElementsCache.get(request)) {
    const previewOpts = {
      imageAltText:
          LegacyComponents.ImagePreview.ImagePreview.defaultAltTextForImageURL(url as Platform.DevToolsPath.UrlString),
      align: LegacyComponents.ImagePreview.Align.START,
      hideFileData: true,
    };

    const previewElement = await LegacyComponents.ImagePreview.ImagePreview.build(
        url as Platform.DevToolsPath.UrlString, false, previewOpts);
    if (previewElement) {
      previewElementsCache.set(request, previewElement);
    }
  }

  const requestPreviewElement = previewElementsCache.get(request);
  if (requestPreviewElement) {
    // clang-format off
    return html`
      <div class="network-request-details-col">${requestPreviewElement}</div>
      <div class="column-divider"></div>`;
    // clang-format on
  }
  return Lit.nothing;
}

function renderRow(title: string, value?: string|Node|Lit.TemplateResult): Lit.LitTemplate {
  if (!value) {
    return Lit.nothing;
  }
  // clang-format off
    return html`
      <div class="network-request-details-row">
        <div class="title">${title}</div>
        <div class="value">${value}</div>
      </div>`;
  // clang-format on
}

function renderEncodedDataLength(request: Trace.Types.Events.SyntheticNetworkRequest): Lit.LitTemplate {
  let lengthText = '';
  if (request.args.data.syntheticData.isMemoryCached) {
    lengthText += i18nString(UIStrings.FromMemoryCache);
  } else if (request.args.data.syntheticData.isDiskCached) {
    lengthText += i18nString(UIStrings.FromCache);
  } else if (request.args.data.timing?.pushStart) {
    lengthText += i18nString(UIStrings.FromPush);
  }
  if (request.args.data.fromServiceWorker) {
    lengthText += i18nString(UIStrings.FromServiceWorker);
  }
  if (request.args.data.encodedDataLength || !lengthText) {
    lengthText = `${i18n.ByteUtilities.bytesToString(request.args.data.encodedDataLength)}${lengthText}`;
  }
  return renderRow(i18nString(UIStrings.encodedData), lengthText);
}

function renderBlockingRow(request: Trace.Types.Events.SyntheticNetworkRequest): Lit.LitTemplate {
  if (!Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(request)) {
    return Lit.nothing;
  }

  let renderBlockingText;
  switch (request.args.data.renderBlocking) {
    case 'blocking':
      renderBlockingText = UIStrings.renderBlocking;
      break;
    case 'in_body_parser_blocking':
      renderBlockingText = UIStrings.inBodyParserBlocking;
      break;
    default:
      // Shouldn't fall to this block, if so, this network request is not
      // render-blocking, so return null.
      return Lit.nothing;
  }
  return renderRow(i18nString(UIStrings.blocking), renderBlockingText);
}

function renderFromCache(
    request: Trace.Types.Events.SyntheticNetworkRequest,
    ): Lit.LitTemplate {
  const cached = request.args.data.syntheticData.isMemoryCached || request.args.data.syntheticData.isDiskCached;
  return renderRow(i18nString(UIStrings.fromCache), cached ? i18nString(UIStrings.yes) : i18nString(UIStrings.no));
}

function renderThirdPartyEntity(
    request: Trace.Types.Events.SyntheticNetworkRequest,
    entityMapper: Trace.EntityMapper.EntityMapper|null): Lit.LitTemplate {
  if (!entityMapper) {
    return Lit.nothing;
  }
  const entity = entityMapper.entityForEvent(request);
  if (!entity) {
    return Lit.nothing;
  }
  return renderRow(i18nString(UIStrings.entity), entity.name);
}

function renderServerTimings(timings: SDK.ServerTiming.ServerTiming[]|null): Lit.LitTemplate[]|Lit.LitTemplate {
  if (!timings || timings.length === 0) {
    return Lit.nothing;
  }
  // clang-format off
  return html`
    <div class="column-divider"></div>
    <div class="network-request-details-col server-timings">
      <div class="server-timing-column-header">${i18nString(UIStrings.serverTiming)}</div>
      <div class="server-timing-column-header">${i18nString(UIStrings.description)}</div>
      <div class="server-timing-column-header">${i18nString(UIStrings.time)}</div>
      ${timings.map(timing => {
        const classes = timing.metric.startsWith('(c') ? 'synthetic value' : 'value';
        return html`
          <div class=${classes}>${timing.metric || '-'}</div>
          <div class=${classes}>${timing.description || '-'}</div>
          <div class=${classes}>${timing.value || '-'}</div>
        `;
      })}
    </div>`;
  // clang-format on
}
function renderInitiatedBy(
    request: Trace.Types.Events.SyntheticNetworkRequest,
    parsedTrace: Trace.TraceModel.ParsedTrace|null,
    target: SDK.Target.Target|null,
    linkifier: LegacyComponents.Linkifier.Linkifier|null,
    ): Lit.LitTemplate {
  if (!linkifier) {
    return Lit.nothing;
  }

  const hasStackTrace = Trace.Helpers.Trace.stackTraceInEvent(request) !== null;
  let link: HTMLElement|null = null;
  const options: LegacyComponents.Linkifier.LinkifyOptions = {
    tabStop: true,
    showColumnNumber: true,
    inlineFrameIndex: 0,
  };
  // If we have a stack trace, that is the most reliable way to get the initiator data and display a link to the source.
  if (hasStackTrace) {
    const topFrame = Trace.Helpers.Trace.getStackTraceTopCallFrameInEventPayload(request) ?? null;
    if (topFrame) {
      link = linkifier.maybeLinkifyConsoleCallFrame(target, topFrame, options);
    }
  }
  // If we do not, we can see if the network handler found an initiator and try
  // to link by URL
  const initiator = parsedTrace ? Trace.Extras.Initiators.getNetworkInitiator(parsedTrace.data, request) : undefined;
  // Initiator will always be a synthetic network request but TS doesn't know that.
  if (initiator && Trace.Types.Events.isSyntheticNetworkRequest(initiator)) {
    link = linkifier.maybeLinkifyScriptLocation(
        target,
        null,  // this would be the scriptId, but we don't have one. The linkifier will fallback to using the URL.
        initiator.args.data.url as Platform.DevToolsPath.UrlString,
        undefined,  // line number
        options);
  }

  if (!link) {
    return Lit.nothing;
  }

  // clang-format off
    return html`
      <div class="network-request-details-item">
        <div class="title">${i18nString(UIStrings.initiatedBy)}</div>
        <div class="value focusable-outline">${link}</div>
      </div>`;
  // clang-format on
}
