// 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/legacy.js';

import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import type * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import * as ApplicationComponents from './components/components.js';

const UIStrings = {
  /**
   * @description Placeholder text when no context is detected.
   */
  noContext: 'No context entries detected across frames.',
  /**
   * @description Fallback label when a frame has no URL.
   */
  unknownFrame: 'Unknown Frame',
  /**
   * @description Placeholder for a search field in a toolbar
   */
  filterByText: 'Filter by key or value',
  /**
   * @description Text to refresh the page
   */
  refresh: 'Refresh',
} as const;

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

export interface FrameContextData {
  url: string;
  frameId: string;
  displayName: string;
  entries: Protocol.CrashReportContext.CrashReportContextEntry[];
}

interface ViewInput {
  frames: FrameContextData[];
  selectedKey: string|null;
  onRowSelected: (key: string) => void;
  onRefresh: () => void;
  onFilterChanged: (e: CustomEvent<string>) => void;
  filters: TextUtils.TextUtils.ParsedFilter[];
}

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

export const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLElement): void => {
  const {widget} = UI.Widget;
  // clang-format off
  render(
      html`
    <style>${UI.inspectorCommonStyles}</style>
    <style>
      .crash-report-context-view {
        padding-top: 5px;
        overflow: auto;
      }

      .frame-section {
        margin-top: var(--sys-size-8);
      }

      .frame-section:first-child {
        margin-top: 0;
      }

      .frame-header {
        display: flex;
        align-items: center;
        padding: var(--sys-size-4) var(--sys-size-6);
        gap: var(--sys-size-6);
        background-color: var(--sys-color-surface2);
        border-bottom: 1px solid var(--sys-color-divider);
      }

      .frame-url {
        font-weight: var(--ref-typeface-weight-bold);
        flex: 1;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-family: var(--default-font-family);
      }


      .toolbar-container {
        border-bottom: 1px solid var(--sys-color-divider);
        background-color: var(--sys-color-cdt-base-container);
      }
    </style>
    <div class="vbox flex-auto" jslog=${VisualLogging.pane('crash-report-context')}>
      <devtools-toolbar class="crash-report-context-toolbar" role="toolbar" jslog=${VisualLogging.toolbar()}>
        <devtools-button title=${i18nString(UIStrings.refresh)}
                         @click=${input.onRefresh}
                         .iconName=${'refresh'}
                         .variant=${Buttons.Button.Variant.TOOLBAR}
                         jslog=${VisualLogging.action('refresh').track({
                           click: true
                         })}>
        </devtools-button>
        <devtools-toolbar-input type="filter" placeholder=${i18nString(UIStrings.filterByText)}
            @change=${(e: CustomEvent<string>) => input.onFilterChanged(e)} class="flex-auto">
        </devtools-toolbar-input>
      </devtools-toolbar>
      ${input.frames.length > 0 ? html`
        <div class="crash-report-context-view flex-auto">
          ${input.frames.map(frame => html`
            <div class="frame-section">
              <div class="frame-header">
                <span class="frame-url" title="URL: ${frame.url}\nFrame ID: ${frame.frameId}">${frame.displayName}</span>
              </div>
              <div class="grid-container">
                <devtools-widget
                  ${widget(ApplicationComponents.CrashReportContextGrid.CrashReportContextGrid, {
                    data: {
                      entries: frame.entries.map(e => ({key: e.key, value: e.value})),
                      selectedKey: input.selectedKey || undefined,
                      filters: input.filters
                    }
                  })}
                  @select=${(e: CustomEvent<string>) => input.onRowSelected(e.detail)}>
                </devtools-widget>
              </div>
            </div>
          `)}
        </div>
      ` : html`
        ${widget(UI.EmptyWidget.EmptyWidget, {
          header: i18nString(UIStrings.noContext),
        })}
      `}
    </div>
  `,
      target);
  // clang-format on
};

export class CrashReportContextView extends UI.Widget.VBox {
  private selectedKey: string|null = null;
  readonly #view: View;
  #filters: TextUtils.TextUtils.ParsedFilter[] = [];

  constructor(view: View = DEFAULT_VIEW) {
    super();
    this.#view = view;
    this.requestUpdate();
  }

  override async performUpdate(): Promise<void> {
    const models =
        SDK.TargetManager.TargetManager.instance().models(SDK.CrashReportContextModel.CrashReportContextModel);
    const allEntries =
        (await Promise.all(models.map(model => model.getEntries())))
            .flat()
            .filter((entry): entry is Protocol.CrashReportContext.CrashReportContextEntry => entry !== null);

    const frameData = this.#processFrameData(allEntries);

    this.#view(
        {
          frames: frameData,
          selectedKey: this.selectedKey,
          filters: this.#filters,
          onRowSelected: (key: string) => {
            this.selectedKey = key;
            this.requestUpdate();
          },
          onRefresh: () => {
            this.requestUpdate();
          },
          onFilterChanged: (e: CustomEvent<string>) => {
            const text = e.detail;
            const textFilterRegExp = text ? Platform.StringUtilities.createPlainTextSearchRegex(text, 'i') : null;
            if (textFilterRegExp) {
              this.#filters = [
                {key: 'key,value', regex: textFilterRegExp, negative: false},
              ];
            } else {
              this.#filters = [];
            }
            this.requestUpdate();
          },
        },
        undefined, this.contentElement);
  }

  #processFrameData(allEntries: Protocol.CrashReportContext.CrashReportContextEntry[]): FrameContextData[] {
    if (allEntries.length === 0) {
      return [];
    }

    const entriesByFrame = Map.groupBy(allEntries, entry => entry.frameId);

    return [...entriesByFrame.entries()]
        .map(([frameId, frameEntries]) => {
          const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId as Protocol.Page.FrameId);
          const url = frame?.url || i18nString(UIStrings.unknownFrame);
          const displayName = frame?.displayName() || url;
          return {
            url,
            frameId,
            displayName,
            isMain: frame?.isMainFrame() ?? false,
            origin: frame?.securityOrigin || '',
            entries: frameEntries,
          };
        })
        // Ensure the main (outermost) frame is always listed first at the top of the View
        .sort((a, b) => {
          if (a.isMain && !b.isMain) {
            return -1;
          }
          if (!a.isMain && b.isMain) {
            return 1;
          }
          return 0;
        })
        .map(data => ({
               url: data.url,
               frameId: data.frameId,
               displayName: data.displayName,
               entries: data.entries,
             }));
  }
}
