// Copyright 2026 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/legacy/components/data_grid/data_grid.js';
import '../../../ui/kit/kit.js';
import '../../../ui/components/tooltips/tooltips.js';

import * as Common from '../../../core/common/common.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 type * as Protocol from '../../../generated/protocol.js';
import * as Components from '../../../ui/legacy/components/utils/utils.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Lit from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';

import adScriptsTableStyles from './adScriptsTable.css.js';
import adsViewStyles from './adsView.css.js';

const {html} = Lit;
const {repeat} = Lit.Directives;
const {bindToSetting} = UI.UIUtils;

const UIStrings = {
  /**
   * @description Title for the metrics table.
   */
  metrics: 'Metrics',
  /**
   * @description Title for a metric showing the percentage of the viewport covered by ads.
   */
  viewportAdDensity: 'Viewport ad density',
  /**
   * @description Title for a metric showing the number of ads in the viewport.
   */
  viewportAdCount: 'Viewport ad count',
  /**
   * @description Title for a metric showing the total CPU usage by ads.
   */
  totalCpuUsage: 'Total CPU usage by ads',
  /**
   * @description Title for a metric showing the total network usage by ads.
   */
  totalNetworkUsage: 'Total network usage by ads',
  /**
   * @description Subtext showing the average value of a metric.
   * @example {5.00%} PH1
   */
  average: '(Average: {PH1})',
  /**
   * @description Title for the ad iframes table.
   * @example {3} PH1
   */
  adIframesTitle: 'Ad iframes (total {PH1})',
  /**
   * @description Text to display when a value is not available.
   */
  notAvailable: 'N/A',
  /**
   * @description Text to display when a frame has no name/id.
   */
  unnamed: '<unnamed>',
  /**
   * @description Title for the Element Id column in the ad iframes table.
   */
  elementId: 'Element ID',
  /**
   * @description Title for the Initial origin column in the ad iframes table.
   */
  initialOrigin: 'Initial origin',
  /**
   * @description Title for the CPU column in the ad iframes table.
   */
  cpu: 'CPU',
  /**
   * @description Title for the Network column in the ad iframes table.
   */
  network: 'Network',
  /**
   * @description Accessible name for the ad iframes table.
   */
  adIframes: 'Ad iframes',
  /**
   * @description Title for the ad scripts table.
   * @example {3} PH1
   */
  adScriptsTitle: 'Ad scripts (total {PH1})',
  /**
   * @description Title for the URL column in the ad scripts table.
   */
  url: 'URL',
  /**
   * @description Accessible name for the ad scripts table.
   */
  adScripts: 'Ad scripts',
  /**
   * @description Title for the ad provenance column in the ad scripts table.
   */
  adProvenance: 'Ad provenance',
  /**
   * @description Text to display when a script has no provenance.
   */
  noProvenance: '<no provenance>',
  /**
   * @description Text to display in the tooltip when a script has no provenance.
   */
  noProvenanceTooltip: 'No provenance data is available',
  /**
   * @description Title for the filter list rule in the ad provenance tooltip.
   */
  filterListRule: 'Filter list rule',
  /**
   * @description Title for the root script filter list rule in the ad provenance tooltip.
   */
  rootScriptFilterListRule: 'Root script filter list rule',
  /**
   * @description Title for the creator ad script ancestry in the ad provenance tooltip.
   */
  creatorAdScriptAncestry: 'Creator ad script ancestry',
  /**
   * @description Title for the settings section.
   */
  settings: 'Settings',
  /**
   * @description The name of a checkbox setting. This setting highlights the
   * rendering elements for ads that are found on the page.
   */
  highlightAds: 'Highlight ads',
  /**
   * @description Explanation text for the 'Highlight ads' setting.
   */
  highlightsElementsRedDetectedToBe: 'Highlights elements (red) detected to be ads.',
  /**
   * @description Text explaining that ad detection is not perfect.
   */
  adDetectionMistakes: 'Chrome’s ad detection can make mistakes.',
  /**
   * @description Link text for learning more about ad detection in Chrome.
   */
  learnMore: 'Learn more',
} as const;

const str_ = i18n.i18n.registerUIStrings('panels/application/components/AdsView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

/**
 * @description Data for a single row in the ad iframes table.
 */
interface AdFrameNodeData {
  elementId: string;
  initialOrigin: string;
  networkBytes: string;
  rawNetworkBytes: number;
  cpuTime: string;
  rawCpuTime: number;
  revealFrame: (e: Event) => void;
}

interface AdScriptNodeData {
  url: Platform.DevToolsPath.UrlString;
  parsedProvenance: Protocol.Network.AdProvenance|null;
  scriptId: Protocol.Runtime.ScriptId;
}

export interface ViewInput {
  metrics: Protocol.Ads.AdMetrics;
  adFrames: AdFrameNodeData[];
  adScripts: AdScriptNodeData[];
  target: SDK.Target.Target|null;
  getLinkElement: (url: string) => HTMLElement;
}

export type View = (input: ViewInput, output: undefined, target: HTMLElement|DocumentFragment) => void;

const formatMetric = (val: number|undefined, formatter: (val: number) => string): string => {
  if (val === undefined || val === -1) {
    return i18nString(UIStrings.notAvailable);
  }
  return formatter(val);
};

const formatCpu = (val: number|undefined): string => {
  return formatMetric(val, (v: number): string => i18n.TimeUtilities.millisToString(v));
};

const formatNetwork = (val: number|undefined): string => {
  return formatMetric(val, (v: number): string => i18n.ByteUtilities.bytesToString(v));
};

const SCRIPT_LINK_OPTIONS = {
  jslogContext: 'ad-script',
};
const stopPropagation = (e: Event): void => e.stopPropagation();

const DEFAULT_VIEW: View = (input, output, target) => {
  const metrics = input.metrics;

  const formatValue = (val: number, isPercentage: boolean): string => {
    return formatMetric(val, (v: number): string => {
      if (isPercentage) {
        return new Intl
            .NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
              style: 'percent',
              maximumFractionDigits: 0,
            })
            .format(v / 100);
      }
      return new Intl.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale).format(v);
    });
  };

  const formatAverage = (val: number|undefined, isPercentage: boolean): string => {
    return formatMetric(val, (v: number): string => {
      if (isPercentage) {
        return new Intl
            .NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
              style: 'percent',
              minimumFractionDigits: 2,
              maximumFractionDigits: 2,
            })
            .format(v / 100);
      }
      return new Intl
          .NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
            minimumFractionDigits: 2,
            maximumFractionDigits: 2,
          })
          .format(v);
    });
  };

  // clang-format off
  Lit.render(html`
    <style>${adsViewStyles}</style>
    <div class="ads-view-container" jslog=${VisualLogging.pane('ads')}>
      <div class="metrics-title">${i18nString(UIStrings.metrics)}</div>
      <dl class="metrics-container">
        <div class="metric-box">
          <dt class="metric-title">${i18nString(UIStrings.viewportAdDensity)}</dt>
          <dd class="metric-value">
            <span>${formatValue(metrics.viewportAdDensityByArea, true)}</span>
            <span class="metric-average">${i18nString(UIStrings.average, {
                PH1: formatAverage(metrics.averageViewportAdDensityByArea, true),
              })}</span>
          </dd>
        </div>
        <div class="metric-box">
          <dt class="metric-title">${i18nString(UIStrings.viewportAdCount)}</dt>
          <dd class="metric-value">
            <span>${formatValue(metrics.viewportAdCount, false)}</span>
            <span class="metric-average">${i18nString(UIStrings.average, {
                PH1: formatAverage(metrics.averageViewportAdCount, false),
              })}</span>
          </dd>
        </div>
        <div class="metric-box">
          <dt class="metric-title">${i18nString(UIStrings.totalCpuUsage)}</dt>
          <dd class="metric-value">
            <span>${formatCpu(metrics.totalAdCpuTime)}</span>
          </dd>
        </div>
        <div class="metric-box">
          <dt class="metric-title">${i18nString(UIStrings.totalNetworkUsage)}</dt>
          <dd class="metric-value">
            <span>${formatNetwork(metrics.totalAdNetworkBytes)}</span>
          </dd>
        </div>
      </dl>
      <hr class="divider">
      <div class="ad-frames-title">${i18nString(UIStrings.adIframesTitle, {PH1: input.adFrames.length})}</div>
      <div class="ad-frames-container">
        <devtools-data-grid striped resize="last" class="ad-frames-data-grid" name=${i18nString(UIStrings.adIframes)}>
          <table>
            <tr>
              <th id="elementId" weight="1" sortable>${i18nString(UIStrings.elementId)}</th>
              <th id="initialOrigin" weight="2" sortable>${i18nString(UIStrings.initialOrigin)}</th>
              <th id="cpuTime" weight="1" sortable type="numeric">${i18nString(UIStrings.cpu)}</th>
              <th id="networkBytes" weight="1" sortable type="numeric">${i18nString(UIStrings.network)}</th>
            </tr>
            ${input.adFrames.map(frame => html`
              <tr>
                <td title=${frame.elementId}>
                  ${frame.elementId
                    ? html`
                        <button class="text-button link-style devtools-link" @click=${frame.revealFrame}>
                          ${frame.elementId}
                        </button>
                      `
                    : Lit.nothing}
                </td>
                <td title=${frame.initialOrigin}>${frame.initialOrigin}</td>
                <td title=${frame.cpuTime} data-value=${frame.rawCpuTime}>${frame.cpuTime}</td>
                <td title=${frame.networkBytes} data-value=${frame.rawNetworkBytes}>${frame.networkBytes}</td>
              </tr>
            `)}
          </table>
        </devtools-data-grid>
      </div>
      <hr class="divider">
      <div class="ad-scripts-title">${i18nString(UIStrings.adScriptsTitle, {PH1: input.adScripts.length})}</div>
      <div class="ad-scripts-container">
        <devtools-data-grid striped resize="last" class="ad-scripts-data-grid" name=${i18nString(UIStrings.adScripts)}>
          <table>
            ${Lit.Directives.unsafeHTML(`<style>${adScriptsTableStyles}</style>`)}
            <tr>
              <th id="url" weight="1" sortable>${i18nString(UIStrings.url)}</th>
              <th id="provenance" weight="1" sortable>${i18nString(UIStrings.adProvenance)}</th>
            </tr>
            ${repeat(input.adScripts, script => script.url, script => html`
              <tr>
                <td title=${script.url}>
                  ${input.getLinkElement(script.url)}
                </td>
                <td>
                  <devtools-tooltip id=${`ad-tooltip-${script.scriptId}`} variant=rich @copy=${stopPropagation}>
                    <div class="ad-provenance-tooltip">
                      ${script.parsedProvenance?.filterlistRule ? html`
                        <div class="ad-provenance-tooltip-title">${i18nString(UIStrings.filterListRule)}</div>
                        <div class="ad-provenance-tooltip-content">${script.parsedProvenance.filterlistRule}</div>
                      ` : Lit.nothing}
                      ${script.parsedProvenance?.adScriptAncestry ? html`
                        <div class="ad-provenance-tooltip-title">${i18nString(UIStrings.creatorAdScriptAncestry)}</div>
                        <div class="ad-provenance-tooltip-content">
                          ${input.target ? script.parsedProvenance.adScriptAncestry.ancestryChain.map(ancestor => html`
                            <div>
                              ${UI.Widget.widget(Components.Linkifier.ScriptLocationLink, {
                                target: input.target ?? undefined,
                                scriptId: ancestor.scriptId,
                                options: SCRIPT_LINK_OPTIONS,
                              })}
                            </div>
                          `) : Lit.nothing}
                        </div>
                        ${script.parsedProvenance.adScriptAncestry.rootScriptFilterlistRule ? html`
                          <div class="ad-provenance-tooltip-title">${i18nString(UIStrings.rootScriptFilterListRule)}</div>
                          <div class="ad-provenance-tooltip-content">${script.parsedProvenance.adScriptAncestry.rootScriptFilterlistRule}</div>
                        ` : Lit.nothing}
                      ` : Lit.nothing}
                      ${!script.parsedProvenance?.adScriptAncestry && !script.parsedProvenance?.filterlistRule ? i18nString(UIStrings.noProvenanceTooltip) : Lit.nothing}
                    </div>
                  </devtools-tooltip>
                  <div aria-details=${`ad-tooltip-${script.scriptId}`}>
                    ${script.parsedProvenance?.filterlistRule ? html`<span>${script.parsedProvenance.filterlistRule}</span>` : Lit.nothing}
                    ${script.parsedProvenance?.filterlistRule && script.parsedProvenance?.adScriptAncestry && input.target ? html`<span>, </span>` : Lit.nothing}
                    ${script.parsedProvenance?.adScriptAncestry && input.target ?
                      UI.Widget.widget(Components.Linkifier.ScriptLocationLink, {
                        target: input.target ?? undefined,
                        scriptId: script.parsedProvenance.adScriptAncestry.ancestryChain[0].scriptId,
                        options: SCRIPT_LINK_OPTIONS,
                      }) : Lit.nothing}
                    ${!script.parsedProvenance?.adScriptAncestry && !script.parsedProvenance?.filterlistRule ? i18nString(UIStrings.noProvenance) : Lit.nothing}
                  </div>
                </td>
              </tr>
            `)}
          </table>
        </devtools-data-grid>
      </div>
      <hr class="divider">
      <div class="settings-title">${i18nString(UIStrings.settings)}</div>
      <devtools-checkbox class="setting-container small"
          ${bindToSetting(Common.Settings.Settings.instance().resolve(SDK.SDKSettings.showAdHighlightsSettingDescriptor))}>
        <div class="setting-text-container">
          <div class="setting-label">${i18nString(UIStrings.highlightAds)}</div>
          <div class="setting-explanation">${i18nString(UIStrings.highlightsElementsRedDetectedToBe)}</div>
        </div>
      </devtools-checkbox>
      <hr class="divider">
      <div class="footer-text">
        <devtools-icon class="inline-icon" name="info"></devtools-icon>
        &#32;
        <span>
          ${i18nString(UIStrings.adDetectionMistakes)}
          &#32;
          <devtools-link class="link devtools-link" href="https://chromium.googlesource.com/chromium/src/+/main/docs/ad_tagging.md" jslogcontext="learn-more">
            ${i18nString(UIStrings.learnMore)}
          </devtools-link>
        </span>
      </div>
    </div>
  `, target);
  // clang-format on
};

export class AdsView extends UI.Widget.Widget {
  #currentMetrics: Protocol.Ads.AdMetrics;
  #pollTimer?: number;
  #isPolling = false;
  #pollSessionId = 0;
  #view: View;

  readonly #adFrames = new Map<Protocol.Page.FrameId, Protocol.Ads.AdFrameData>();
  readonly #adIframeElementIds = new Map<Protocol.Page.FrameId, string|null>();
  readonly #fetchingElementIds = new Set<Protocol.Page.FrameId>();
  readonly #unresolvedScriptIds = new Set<Protocol.Runtime.ScriptId>();
  readonly #adScriptNodeData: AdScriptNodeData[] = [];
  readonly #urlToLinkElement = new Map<string, HTMLElement>();
  readonly #reconstructedProvenance = new Map<Protocol.Runtime.ScriptId, Protocol.Network.AdProvenance>();

  constructor(view: View = DEFAULT_VIEW) {
    super({useShadowDom: true});
    this.#view = view;
    this.#currentMetrics = {
      viewportAdDensityByArea: 0,
      averageViewportAdDensityByArea: 0,
      viewportAdCount: 0,
      averageViewportAdCount: 0,
      totalAdCpuTime: 0,
      totalAdNetworkBytes: 0,
      updateAdFrames: [],
      removeAdFrames: [],
    };
    this.requestUpdate();
  }

  override wasShown(): void {
    super.wasShown();
    this.#startPolling();
    SDK.TargetManager.TargetManager.instance().addModelListener(SDK.ResourceTreeModel.ResourceTreeModel,
                                                                SDK.ResourceTreeModel.Events.PrimaryPageChanged,
                                                                this.#onPrimaryPageChanged, this);
  }

  override willHide(): void {
    this.#stopPolling();
    SDK.TargetManager.TargetManager.instance().removeModelListener(SDK.ResourceTreeModel.ResourceTreeModel,
                                                                   SDK.ResourceTreeModel.Events.PrimaryPageChanged,
                                                                   this.#onPrimaryPageChanged, this);
    super.willHide();
  }

  #startPolling(): void {
    if (this.#isPolling) {
      return;
    }
    this.#isPolling = true;
    this.#pollSessionId++;
    void this.#pollMetrics(this.#pollSessionId);
  }

  #stopPolling(): void {
    this.#isPolling = false;
    if (this.#pollTimer !== undefined) {
      window.clearTimeout(this.#pollTimer);
      this.#pollTimer = undefined;
    }
  }

  async #pollMetrics(sessionId: number): Promise<void> {
    if (!this.#isPolling || this.#pollSessionId !== sessionId) {
      return;
    }
    const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
    if (target) {
      const adsAgent = target.adsAgent();
      if (adsAgent) {
        const [metricsResponse, scriptsResponse] = await Promise.all([
          adsAgent.invoke_getAdMetrics(),
          adsAgent.invoke_getAdScripts(),
        ]);
        if (!this.#isPolling || this.#pollSessionId !== sessionId) {
          return;
        }
        let needsUpdate = false;
        if (!metricsResponse.getError()) {
          this.#currentMetrics = metricsResponse.metrics;
          this.#processAdFrames(metricsResponse.metrics);
          needsUpdate = true;
        }
        if (!scriptsResponse.getError()) {
          this.#processAdScripts(scriptsResponse.newScripts || []);
          needsUpdate = true;
        }
        if (needsUpdate) {
          this.requestUpdate();
        }
      }
    }
    if (this.#isPolling && this.#pollSessionId === sessionId) {
      this.#pollTimer = window.setTimeout(() => this.#pollMetrics(sessionId), 500);
    }
  }

  #processAdFrames(metrics: Protocol.Ads.AdMetrics): void {
    // Drop removed frames from the local cache.
    for (const frameId of metrics.removeAdFrames || []) {
      this.#adFrames.delete(frameId);
      this.#adIframeElementIds.delete(frameId);
    }

    // Merge partial updates into the local cache.
    for (const frame of metrics.updateAdFrames || []) {
      const frameId = frame.frameId;
      const existingFrame = this.#adFrames.get(frameId) || {} as Protocol.Ads.AdFrameData;

      // Object Spread / Protocol Undefined Behavior
      // To reduce the payload size, the C++ backend only sends the 'initialOrigin' field
      // when it changes since the last sent message for the same frame. Because
      // the parsed JSON frame won't have the 'initialOrigin' key if it hasn't changed,
      // spreading it over existingFrame won't overwrite existingFrame.initialOrigin with
      // undefined.
      const newFrame = {...existingFrame, ...frame} as Protocol.Ads.AdFrameData;
      this.#adFrames.set(frameId, newFrame);
    }

    // Asynchronously fetch Element IDs for newly tracked frames.
    // Duplicate requests are prevented by #fetchingElementIds.
    for (const frameId of this.#adFrames.keys()) {
      if (!this.#adIframeElementIds.has(frameId) && !this.#fetchingElementIds.has(frameId)) {
        this.#fetchingElementIds.add(frameId);
        void this.#fetchIframeElementId(frameId)
            .then(elementId => {
              if (this.#adFrames.has(frameId) && elementId !== undefined) {
                this.#adIframeElementIds.set(frameId, elementId);
              }
            })
            .catch(() => {})
            .finally(() => {
              this.#fetchingElementIds.delete(frameId);
              this.requestUpdate();
            });
      }
    }
  }

  // Lazily reconstructs the full script ancestry chain for a given script.
  // The backend guarantees that scripts across different batches are ordered
  // correctly (i.e., an ancestor script will always be sent in the same or an
  // earlier batch than its descendants). However, scripts arriving within the
  // same batch may be out of order. This recursive, topological approach ensures
  // we can resolve those in-batch ordering issues while maintaining O(N)
  // complexity overall via memoization in #reconstructedProvenance.
  #reconstructProvenance(scriptId: Protocol.Runtime.ScriptId,
                         newScriptsMap: Map<Protocol.Runtime.ScriptId, Protocol.Ads.AdScript>):
      Protocol.Network.AdProvenance|null {
    if (this.#reconstructedProvenance.has(scriptId)) {
      return this.#reconstructedProvenance.get(scriptId) ?? null;
    }
    const script = newScriptsMap.get(scriptId);
    if (!script || !script.provenance) {
      return null;
    }

    let fullProvenance: Protocol.Network.AdProvenance = script.provenance;
    if (script.provenance.adScriptAncestry) {
      const immediateAncestor = script.provenance.adScriptAncestry.ancestryChain[0];
      const ancestorProvenance =
          immediateAncestor ? this.#reconstructProvenance(immediateAncestor.scriptId, newScriptsMap) : null;
      if (ancestorProvenance) {
        const newChain = [immediateAncestor];
        if (ancestorProvenance.adScriptAncestry) {
          newChain.push(...ancestorProvenance.adScriptAncestry.ancestryChain);
        }
        const rootScriptFilterlistRule =
            ancestorProvenance.filterlistRule || ancestorProvenance.adScriptAncestry?.rootScriptFilterlistRule;
        fullProvenance = {
          ...script.provenance,
          adScriptAncestry: {
            ancestryChain: newChain,
            ...(rootScriptFilterlistRule ? {rootScriptFilterlistRule} : {}),
          },
        };
      }
    }
    this.#reconstructedProvenance.set(scriptId, fullProvenance);
    return fullProvenance;
  }

  #processAdScripts(newScripts: Protocol.Ads.AdScript[]): void {
    const newScriptsMap = new Map(newScripts.map(s => [s.scriptId, s]));
    for (const script of newScripts) {
      this.#unresolvedScriptIds.add(script.scriptId);
      this.#reconstructProvenance(script.scriptId, newScriptsMap);
    }
  }

  async #fetchIframeElementId(frameId: Protocol.Page.FrameId): Promise<string|null|undefined> {
    const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId);
    if (!frame) {
      return undefined;
    }
    const deferredNode = await frame.getOwnerDeferredDOMNode();
    if (deferredNode) {
      const node = await deferredNode.resolvePromise();
      return node?.getAttribute('id') || null;
    }
    return null;
  }

  #onPrimaryPageChanged(): void {
    this.#currentMetrics = {
      viewportAdDensityByArea: 0,
      averageViewportAdDensityByArea: 0,
      viewportAdCount: 0,
      averageViewportAdCount: 0,
      totalAdCpuTime: 0,
      totalAdNetworkBytes: 0,
      updateAdFrames: [],
      removeAdFrames: [],
    };
    this.#adFrames.clear();
    this.#adIframeElementIds.clear();
    this.#fetchingElementIds.clear();
    this.#unresolvedScriptIds.clear();
    this.#adScriptNodeData.length = 0;
    this.#urlToLinkElement.clear();
    this.#reconstructedProvenance.clear();
    this.requestUpdate();
  }

  override performUpdate(): void {
    const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
    const debuggerModel = target?.model(SDK.DebuggerModel.DebuggerModel);

    const adFramesArray: AdFrameNodeData[] = [];
    for (const [frameId, frame] of this.#adFrames) {
      // The table displays the resolved ID, or an <unnamed> placeholder if the
      // element lacks an ID attribute, or an empty string while pending.
      const elementIdText = this.#adIframeElementIds.has(frameId) ?
          (this.#adIframeElementIds.get(frameId) || i18nString(UIStrings.unnamed)) :
          '';

      const revealFrame = (e: Event): void => {
        e.preventDefault();
        e.stopPropagation();
        const frameToReveal = SDK.FrameManager.FrameManager.instance().getFrame(frameId);
        if (frameToReveal) {
          void Common.Revealer.reveal(frameToReveal);
        }
      };

      adFramesArray.push({
        elementId: elementIdText,
        initialOrigin: frame.initialOrigin || '',
        cpuTime: formatCpu(frame.cpuTime),
        rawCpuTime: frame.cpuTime ?? -1,
        networkBytes: formatNetwork(frame.networkBytes),
        rawNetworkBytes: frame.networkBytes ?? -1,
        revealFrame,
      });
    }

    for (const scriptId of this.#unresolvedScriptIds) {
      const sdkScript = debuggerModel?.scriptForId(scriptId);

      // Filter out inline <script> tags and anonymous eval/dynamic scripts.
      // We only want to display named external resources or explicitly named
      // scripts to reduce noise in the UI.
      if (!sdkScript) {
        continue;
      }
      this.#unresolvedScriptIds.delete(scriptId);

      if (sdkScript.isInlineScript() || sdkScript.isContentScript() || !sdkScript.sourceURL) {
        continue;
      }

      const url = sdkScript.sourceURL;

      // De-duplicate scripts by URL. V8 frequently generates multiple
      // ScriptIds for the same URL (e.g., when the same external script
      // is loaded into multiple iframes).
      if (this.#urlToLinkElement.has(url)) {
        continue;
      }
      this.#urlToLinkElement.set(url, Components.Linkifier.Linkifier.linkifyURL(url, {text: url}));

      const parsedProvenance = this.#reconstructedProvenance.get(scriptId) ?? null;
      this.#adScriptNodeData.push({
        url,
        parsedProvenance,
        scriptId,
      });
    }

    const viewInput: ViewInput = {
      metrics: this.#currentMetrics,
      adFrames: adFramesArray,
      adScripts: this.#adScriptNodeData,
      target: target || null,
      getLinkElement: (url: string) => this.#urlToLinkElement.get(url) as HTMLElement,
    };
    this.#view(viewInput, undefined, this.contentElement);
  }
}
