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

import './CollapsibleAssistanceContentWidget.js';
import '../../../models/trace/insights/insights.js';
import '../../../panels/timeline/components/components.js';
import './PerformanceAgentFlameChart.js';

import * as Common from '../../../core/common/common.js';
import * as SDK from '../../../core/sdk/sdk.js';
import type * as Protocol from '../../../generated/protocol.js';
import * as GreenDev from '../../../models/greendev/greendev.js';
import * as Logs from '../../../models/logs/logs.js';
import * as NetworkTimeCalculator from '../../../models/network_time_calculator/network_time_calculator.js';
import * as Helpers from '../../../models/trace/helpers/helpers.js';
import * as Trace from '../../../models/trace/trace.js';
import type * as Marked from '../../../third_party/marked/marked.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Lit from '../../../ui/lit/lit.js';
import * as PanelsCommon from '../../common/common.js';
import * as Network from '../../network/network.js';
import * as TimelineComponents from '../../timeline/components/components.js';
import * as Insights from '../../timeline/components/insights/insights.js';

import {MarkdownRendererWithCodeBlock} from './MarkdownRendererWithCodeBlock.js';
import type * as PerformanceAgentFlameChart from './PerformanceAgentFlameChart.js';

const {html} = Lit.StaticHtml;
const {ref, createRef} = Lit.Directives;
const {widgetConfig} = UI.Widget;

export class PerformanceAgentMarkdownRenderer extends MarkdownRendererWithCodeBlock {
  #insightRenderer = new Insights.InsightRenderer.InsightRenderer();

  constructor(
      private mainFrameId = '',
      private lookupEvent: (key: Trace.Types.File.SerializableKey) => Trace.Types.Events.Event | null = () => null,
      private parsedTrace: Trace.TraceModel.ParsedTrace|null = null) {
    super();
  }

  override templateForToken(token: Marked.Marked.MarkedToken): Lit.LitTemplate|null {
    if (!this.parsedTrace) {
      return null;
    }

    // NOTE: The custom tag handling below (e.g., <ai-insight>, <network-request-widget>)
    // is part of a prototype for the GreenDev project and is only rendered when the GreenDev
    // feature is enabled.
    if (token.type === 'html' && GreenDev.Prototypes.instance().isEnabled('inlineWidgets')) {
      if (token.text.includes('<flame-chart-widget')) {
        const startMatch = token.text.match(/start="?(\d+)"?/);
        const endMatch = token.text.match(/end="?(\d+)"?/);
        const start = startMatch ? Number(startMatch[1]) : this.parsedTrace.data.Meta.traceBounds.min;
        const end = endMatch ? Number(endMatch[1]) : this.parsedTrace.data.Meta.traceBounds.max;
        return html`<devtools-performance-agent-flame-chart .data=${{
          parsedTrace: this.parsedTrace,
          start,
          end,
        } as PerformanceAgentFlameChart.PerformanceAgentFlameChartData}
          }></devtools-performance-agent-flame-chart>`;
      }

      // Flexible regex to match the tag name and a value a.
      // match[1]: tagName (e.g., 'ai-insight', 'network-request-widget')
      // match[2]: value (value needed to display the widget)
      const regex = /<([a-z-]+)\s+value="([^"]+)">/;
      const match = token.text.match(regex);

      if (!match) {
        return null;
      }

      const tagName = match[1];
      const value = match[2];

      if (tagName === 'ai-insight' && value) {
        const componentName = value;

        const insightSet = this.parsedTrace.insights?.values().next().value;
        const insightM = insightSet?.model[componentName as Trace.Insights.Types.InsightKeys];

        if (!insightM) {
          return null;
        }

        return html`<devtools-collapsible-assistance-content-widget  .data=${{
          headerText: `Insight - ${componentName}`
        }
        }>
        ${this.#insightRenderer.renderInsightToWidgetElement(this.parsedTrace, insightSet, insightM, componentName, {
          selected: true,
          isAIAssistanceContext: true
        })}
        </devtools-collapsible-assistance-content-widget>`;
      }
      if (tagName === 'network-request-widget' && value) {
        const rawTraceEvent =
            Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager().getRawTraceEvents().at(Number(value));

        // Rendering RequestTimingView widget only works for fresh traces where the network log is in sync.
        // If the trace is uploaded, we need to use the synthetic events and
        // render the network request tooltip that uses the synthetic events.
        if (rawTraceEvent && Trace.Types.Events.isSyntheticNetworkRequest(rawTraceEvent)) {
          const rawTraceEventId = rawTraceEvent?.args?.data?.requestId;
          const rawTraceEventUrl = rawTraceEvent?.args?.data?.url;
          const networkRequest = rawTraceEvent ? Logs.NetworkLog.NetworkLog.instance()
                                                     .requestsForId(rawTraceEventId)
                                                     .find(r => r.url() === rawTraceEventUrl) :
                                                 null;

          if (networkRequest) {
            const calculator = new NetworkTimeCalculator.NetworkTimeCalculator(true);
            return html`<devtools-collapsible-assistance-content-widget
            .data=${{
              headerText: `Network Request: ${
                  networkRequest.url().length > 80 ? networkRequest.url().slice(0, 80) + '...' : networkRequest.url()}`
            }
            }>
            <devtools-widget class="actions" .widgetConfig=${
                UI.Widget.widgetConfig(Network.RequestTimingView.RequestTimingView, {
                  request: networkRequest,
                  calculator,
                })}></devtools-widget>
            </devtools-collapsible-assistance-content-widget>`;
          }
        }

        const syntheticRequest =
            Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager().syntheticEventForRawEventIndex(
                Number(value));

        let networkTooltip = null;
        if (syntheticRequest && Trace.Types.Events.isSyntheticNetworkRequest(syntheticRequest)) {
          // clang-format off
          networkTooltip = html`<devtools-widget .widgetConfig=${widgetConfig(TimelineComponents.NetworkRequestTooltip.NetworkRequestTooltip, {
            networkRequest: syntheticRequest,
          })}></devtools-widget>`;
          // clang-format on
        }

        return html`<devtools-collapsible-assistance-content-widget
        .data=${{
          headerText: 'Network Request'
        }
        }>
          ${networkTooltip}
          </devtools-collapsible-assistance-content-widget>`;
      }

      return null;
    }

    if (token.type === 'link' && token.href.startsWith('#')) {
      if (token.href.startsWith('#node-')) {
        const nodeId = Number(token.href.replace('#node-', '')) as Protocol.DOM.BackendNodeId;

        const templateRef = createRef();
        void this.#linkifyNode(nodeId, token.text).then(node => {
          if (!templateRef.value || !node) {
            return;
          }

          templateRef.value.textContent = '';
          templateRef.value.append(node);
        });
        return html`<span ${ref(templateRef)}>${token.text}</span>`;
      }

      const event = this.lookupEvent(token.href.slice(1) as Trace.Types.File.SerializableKey);
      if (!event) {
        return html`${token.text}`;
      }

      let label = token.text;
      let title = '';
      if (Trace.Types.Events.isSyntheticNetworkRequest(event)) {
        title = event.args.data.url;
      } else {
        label += ` (${event.name})`;
      }

      // eslint-disable-next-line @devtools/no-a-tags-in-lit
      return html`<a href="#" draggable=false .title=${title} @click=${(e: Event) => {
        e.stopPropagation();
        void Common.Revealer.reveal(new SDK.TraceObject.RevealableEvent(event));
      }}>${label}</a>`;
    }

    return super.templateForToken(token);
  }

  // Taken from front_end/panels/timeline/components/insights/NodeLink.ts
  // Would be nice to move the above component to somewhere that allows the AI
  // Assistance panel to also use it.
  async #linkifyNode(backendNodeId: Protocol.DOM.BackendNodeId, label: string): Promise<Node|undefined> {
    if (backendNodeId === undefined) {
      return;
    }

    const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
    const domModel = target?.model(SDK.DOMModel.DOMModel);
    if (!domModel) {
      return undefined;
    }
    const domNodesMap = await domModel.pushNodesByBackendIdsToFrontend(new Set([backendNodeId]));
    const node = domNodesMap?.get(backendNodeId);
    if (!node) {
      return;
    }

    if (node.frameId() !== this.mainFrameId) {
      return;
    }

    const linkedNode = PanelsCommon.DOMLinkifier.Linkifier.instance().linkify(node, {textContent: label});
    return linkedNode;
  }
}
