// 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/components/icon_button/icon_button.js';
import '../../ui/components/lists/lists.js';
import '../../ui/components/node_text/node_text.js';
import '../../ui/legacy/components/data_grid/data_grid.js';
import '../../ui/legacy/legacy.js';

import type {JSONSchema7, JSONSchema7Definition} from 'json-schema';

import * as Common from '../../core/common/common.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 * as Protocol from '../../generated/protocol.js';
import type * as StackTrace from '../../models/stack_trace/stack_trace.js';
import * as WebMCP from '../../models/web_mcp/web_mcp.js';
import * as Adorners from '../../ui/components/adorners/adorners.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import type * as IconButton from '../../ui/components/icon_button/icon_button.js';
import type * as NodeText from '../../ui/components/node_text/node_text.js';
import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {
  Directives,
  html,
  nothing,
  render,
  type TemplateResult,
} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as ProtocolMonitor from '../protocol_monitor/protocol_monitor.js';

import webMCPViewStyles from './webMCPView.css.js';

const UIStrings = {
  /**
   * @description Text for the header of the tool registry section
   */
  toolRegistry: 'Available Tools',
  /**
   * @description Title of text to display when no tools are registered
   */
  noToolsPlaceholderTitle: 'Available `WebMCP` Tools',
  /**
   * @description Text to display when no tools are registered
   */
  noToolsPlaceholder:
      'Registered `WebMCP` tools for this page will appear here. No tools have been registered or detected yet.',
  /**
   * @description Title of text to display when no calls have been made
   */
  noCallsPlaceholderTitle: 'Tool Activity',
  /**
   * @description Text to display when no calls have been made
   */
  noCallsPlaceholder: 'Start interacting with your `WebMCP` agent to see real-time tool calls and executions here.',
  /**
   * @description Text for the header of the tool details section
   */
  toolDetails: 'Details',
  /**
   * @description Text for the link to reveal the tool's DOM node in the Elements panel
   */
  viewInElementsPanel: 'View in Elements panel',
  /**
   * @description Text for the frame of a tool
   */
  frame: 'Frame',
  /**
   * @description Text for the name of a tool call
   */
  name: 'Name',
  /**
   * @description Text for the status of a tool call
   */
  status: 'Status',
  /**
   * @description Text for the input of a tool call
   */
  input: 'Input',
  /**
   * @description Text for the output of a tool call
   */
  output: 'Output',
  /**
   * @description Text for the status of a tool call that is in progress
   */
  inProgress: 'In Progress',
  /**
   * @description Tooltip for the clear log button
   */
  clearLog: 'Clear log',
  /**
   * @description Text to close something
   */
  close: 'Close',
  /**
   * @description Placeholder for the filter input
   */
  filter: 'Filter',
  /**
   * @description Tooltip for the tool types dropdown
   */
  toolTypes: 'Tool types',
  /**
   * @description Tooltip for the status types dropdown
   */
  statusTypes: 'Status types',
  /**
   * @description Tooltip for the clear filters button
   */
  clearFilters: 'Clear filters',
  /**
   * @description Filter option for imperative tools
   */
  imperative: 'Imperative',
  /**
   * @description Filter option for declarative tools
   */
  declarative: 'Declarative',
  /**
   * @description Text for the status of a tool call that has failed
   */
  error: 'Error',
  /**
   * @description Text for the status of a tool call that was canceled
   */
  canceled: 'Canceled',
  /**
   * @description Text for the status of a tool call that succeeded
   */
  completed: 'Completed',
  /**
   * @description Text for the status of a tool call that has failed
   */
  pending: 'In Progress',
  /**
   * @description Text for the total number of tool calls
   * @example {2} PH1
   */
  totalCalls: '{PH1} Total calls',
  /**
   * @description Text for the number of failed tool calls
   * @example {1} PH1
   */
  failed: '{PH1} Failed',
  /**
   * @description Text for the number of canceled tool calls
   * @example {1} PH1
   */
  canceledCount: '{PH1} Canceled',
  /**
   * @description Text for the number of in progress tool calls
   * @example {1} PH1
   */
  inProgressCount: '{PH1} In Progress',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/WebMCPView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {widget} = UI.Widget;

export interface FilterState {
  text: string;
  toolTypes?: {
    imperative?: boolean,
    declarative?: boolean,
  };
  statusTypes?: {
    completed?: boolean,
    error?: boolean,
    pending?: boolean,
  };
}

export interface FilterMenuButton {
  button: UI.Toolbar.ToolbarMenuButton;
  setCount: (count: number) => void;
}

export interface FilterMenuButtons {
  toolTypes: FilterMenuButton;
  statusTypes: FilterMenuButton;
}
export interface ViewInput {
  tools: WebMCP.WebMCPModel.Tool[];
  selectedTool: WebMCP.WebMCPModel.Tool|null;
  onToolSelect: (tool: WebMCP.WebMCPModel.Tool|null) => void;
  selectedCall: WebMCP.WebMCPModel.Call|null;
  onCallSelect: (call: WebMCP.WebMCPModel.Call|null) => void;
  filters: FilterState;
  filterButtons: FilterMenuButtons;
  onClearLogClick: () => void;
  onFilterChange: (filters: FilterState) => void;
  toolCalls: WebMCP.WebMCPModel.Call[];
}

export function filterToolCalls(
    toolCalls: WebMCP.WebMCPModel.Call[], filterState: FilterState): WebMCP.WebMCPModel.Call[] {
  let filtered = [...toolCalls];

  const statusTypes = filterState.statusTypes;
  if (statusTypes) {
    filtered = filtered.filter(call => {
      const {completed, error, pending} = statusTypes;
      if (completed && call.result?.status === Protocol.WebMCP.InvocationStatus.Completed) {
        return true;
      }
      if (error && call.result?.status === Protocol.WebMCP.InvocationStatus.Error) {
        return true;
      }
      if (pending && call.result === undefined) {
        return true;
      }
      return false;
    });
  }

  const toolTypes = filterState.toolTypes;
  if (toolTypes) {
    filtered = filtered.filter(call => {
      const {imperative, declarative} = toolTypes;
      if (imperative && !call.tool.isDeclarative) {
        return true;
      }
      if (declarative && call.tool.isDeclarative) {
        return true;
      }
      return false;
    });
  }

  if (filterState.text) {
    const regex = Platform.StringUtilities.createPlainTextSearchRegex(filterState.text, 'i');
    filtered = filtered.filter(call => {
      return regex.test(call.tool.name) || regex.test(call.input) ||
          (call.result?.output && regex.test(JSON.stringify(call.result.output))) ||
          (call.result?.errorText && regex.test(call.result.errorText));
    });
  }

  return filtered;
}
export type View = (input: ViewInput, output: object, target: HTMLElement) => void;
function calculateToolStats(calls: WebMCP.WebMCPModel.Call[]):
    {total: number, completed: number, failed: number, canceled: number, inProgress: number} {
  let total = 0, completed = 0, failed = 0, canceled = 0, inProgress = 0;
  for (const call of calls) {
    total++;
    if (call.result?.status === Protocol.WebMCP.InvocationStatus.Error) {
      failed++;
    } else if (call.result?.status === Protocol.WebMCP.InvocationStatus.Canceled) {
      canceled++;
    } else if (call.result?.status === Protocol.WebMCP.InvocationStatus.Completed) {
      completed++;
    } else if (call.result === undefined) {
      inProgress++;
    }
  }
  return {total, completed, failed, canceled, inProgress};
}

function getIconGroupsFromStats(toolStats: ReturnType<typeof calculateToolStats>):
    IconButton.IconButton.IconWithTextData[] {
  const groups = [];
  if (toolStats.completed > 0) {
    groups.push({
      iconName: 'check-circle',
      iconColor: 'var(--sys-color-green)',
      iconWidth: '16px',
      iconHeight: '16px',
      text: String(toolStats.completed),
    });
  }
  if (toolStats.failed > 0) {
    groups.push({
      iconName: 'cross-circle-filled',
      iconColor: 'var(--sys-color-error)',
      iconWidth: '16px',
      iconHeight: '16px',
      text: String(toolStats.failed),
    });
  }
  if (toolStats.canceled > 0) {
    groups.push({
      iconName: 'record-stop',
      iconColor: 'var(--sys-color-on-surface-light)',
      iconWidth: '16px',
      iconHeight: '16px',
      text: String(toolStats.canceled),
    });
  }
  if (toolStats.inProgress > 0) {
    groups.push({
      iconName: 'dots-circle',
      iconWidth: '16px',
      iconHeight: '16px',
      text: String(toolStats.inProgress),
    });
  }
  return groups;
}

export function parsePayload(payload?: unknown): {
  valueObject: unknown,
  valueString: string|undefined,
} {
  if (payload === undefined) {
    return {valueObject: undefined, valueString: undefined};
  }
  if (typeof payload === 'string') {
    try {
      return {valueObject: JSON.parse(payload), valueString: undefined};
    } catch {
      return {valueObject: undefined, valueString: payload};
    }
  }
  return {valueObject: payload, valueString: undefined};
}

export const DEFAULT_VIEW: View = (input, output, target) => {
  const tools = input.tools;
  const stats = calculateToolStats(input.toolCalls);
  const isFilterActive =
      Boolean(input.filters.text) || Boolean(input.filters.toolTypes) || Boolean(input.filters.statusTypes);
  const iconName = (call: WebMCP.WebMCPModel.Call): string => {
    switch (call.result?.status) {
      case Protocol.WebMCP.InvocationStatus.Error:
        return 'cross-circle-filled';
      case Protocol.WebMCP.InvocationStatus.Canceled:
        return 'record-stop';
      case undefined:
        return 'dots-circle';
      default:
        return '';
    }
  };
  const statusString = (call: WebMCP.WebMCPModel.Call): string => {
    switch (call.result?.status) {
      case Protocol.WebMCP.InvocationStatus.Error:
        return i18nString(UIStrings.error);
      case Protocol.WebMCP.InvocationStatus.Canceled:
        return i18nString(UIStrings.canceled);
      case Protocol.WebMCP.InvocationStatus.Completed:
        return i18nString(UIStrings.completed);
      default:
        return i18nString(UIStrings.inProgress);
    }
  };
  // clang-format off
  render(html`
    <style>${webMCPViewStyles}</style>
    <style>${UI.FilterBar.filterStyles}</style>
    <devtools-split-view class="webmcp-view" direction="row" sidebar-position="second" name="webmcp-split-view">
      <div slot="main" class="call-log">
        <div class="webmcp-toolbar-container" role="toolbar" jslog=${VisualLogging.toolbar()}>
          <devtools-toolbar class="webmcp-toolbar" role="presentation" wrappable>
            <devtools-button title=${i18nString(UIStrings.clearLog)}
                             .iconName=${'clear'}
                             .variant=${Buttons.Button.Variant.TOOLBAR}
                             @click=${input.onClearLogClick}></devtools-button>
            <div class="toolbar-divider"></div>
            <devtools-toolbar-input type="filter"
                                    placeholder=${i18nString(UIStrings.filter)}
                                    .value=${input.filters.text}
                                    @change=${(e: CustomEvent<string>) =>
                                      input.onFilterChange({...input.filters, text: e.detail})}>
            </devtools-toolbar-input>
            <div class="toolbar-divider"></div>
            ${input.filterButtons.toolTypes.button.element}
            <div class="toolbar-divider"></div>
            ${input.filterButtons.statusTypes.button.element}
            <div class="toolbar-spacer"></div>
            <devtools-button title=${i18nString(UIStrings.clearFilters)}
                             .iconName=${'filter-clear'}
                             .variant=${Buttons.Button.Variant.TOOLBAR}
                             @click=${() => input.onFilterChange({text: ''})}
                             ?hidden=${!isFilterActive}></devtools-button>
          </devtools-toolbar>
        </div>
        ${input.toolCalls.length > 0 ? html`
          <devtools-split-view name="webmcp-call-split-view"
                               direction="column"
                               sidebar-position="second"
                               sidebar-visibility=${input.selectedCall ? 'show' : 'hidden'}>
            <div slot="main" style="display: flex; flex-direction: column; overflow: hidden; height: 100%;">
              <devtools-data-grid striped .template=${html`
                <table>
                  <style>${webMCPViewStyles}</style>
                  <tr>
                    <th id="name" weight="20">
                      ${i18nString(UIStrings.name)}
                    </th>
                    <th id="status" weight="20">${i18nString(UIStrings.status)}</th>
                            ${!input.selectedCall ? html`
                    <th id="input" weight="30">${i18nString(UIStrings.input)}</th>
                    <th id="output" weight="30">${i18nString(UIStrings.output)}</th>
                            ` : nothing}
                  </tr>
                      ${Directives.repeat(input.toolCalls, call => call.invocationId + '-' + (call.result?.status ?? ''),
                                          call => html`
                    <tr class=${Directives.classMap({
                      'status-error': call.result?.status === Protocol.WebMCP.InvocationStatus.Error,
                      'status-cancelled': call.result?.status === Protocol.WebMCP.InvocationStatus.Canceled,
                      selected: call === input.selectedCall,
                    })} @click=${() => input.onCallSelect(call)}>
                      <td>${call.tool.name}</td>
                      <td>
                        <div class="status-cell">
                          ${iconName(call) ? html`<devtools-icon class="small" name=${iconName(call)}></devtools-icon>`
                                           : ''}
                          <span>${statusString(call)}</span>
                        </div>
                      </td>
                      ${!input.selectedCall ? html`
                        <td>${call.input}</td>
                        <td>${call.result?.output ? JSON.stringify(call.result.output)
                                                    : call.result?.errorText ?? ''}</td>
                        ` : nothing}
                    </tr>
                  `)}
                  </table>`}>
              </devtools-data-grid>
            </div>
            <div slot="sidebar" style="height: 100%; display: flex; flex-direction: column; overflow: hidden;">
              <devtools-tabbed-pane class="call-details-tabbed-pane">
                <devtools-button
                  slot="left"
                  .iconName=${'cross'}
                  .size=${Buttons.Button.Size.SMALL}
                  .variant=${Buttons.Button.Variant.ICON}
                  title=${i18nString(UIStrings.close)}
                  @click=${() => input.onCallSelect(null)}
                ></devtools-button>
                <devtools-widget
                  id="webmcp.tool-details"
                  title=${i18nString(UIStrings.toolDetails)}
                  ${widget(ToolDetailsWidget, {tool: input.selectedCall?.tool})}>
                </devtools-widget>
                <devtools-widget
                  id="webmcp.call-inputs"
                  title=${i18nString(UIStrings.input)}
                  ${widget(PayloadWidget, parsePayload(input.selectedCall?.input))}>
                </devtools-widget>
                <devtools-widget
                  id="webmcp.call-outputs"
                  title=${i18nString(UIStrings.output)}
                  ${widget(PayloadWidget, {
                          valueObject: input.selectedCall?.result?.output,
                          errorText: input.selectedCall?.result?.errorText,
                          exceptionDetails: input.selectedCall?.result?.exceptionDetails,
                  })}>
                </devtools-widget>
              </devtools-tabbed-pane>
            </div>
          </devtools-split-view>
          <div class="webmcp-toolbar-container" role="toolbar">
            <devtools-toolbar class="webmcp-toolbar" role="presentation" wrappable>
              <span class="toolbar-text">${i18nString(UIStrings.totalCalls, {PH1: stats.total})}</span>
              <div class="toolbar-divider"></div>
              <span class="toolbar-text status-error-text">${i18nString(UIStrings.failed, {PH1: stats.failed})}</span>
              <div class="toolbar-divider"></div>
              <span class="toolbar-text status-cancelled-text">${
                  i18nString(UIStrings.canceledCount, {PH1: stats.canceled})}</span>
              <div class="toolbar-divider"></div>
              <span class="toolbar-text">${i18nString(UIStrings.inProgressCount, {PH1: stats.inProgress})}</span>
            </devtools-toolbar>
          </div>
        ` : html`
        ${UI.Widget.widget(UI.EmptyWidget.EmptyWidget, {header: i18nString(UIStrings.noCallsPlaceholderTitle),
                                                          text: i18nString(UIStrings.noCallsPlaceholder)})}
        `}
      </div>
      <devtools-split-view slot="sidebar"
                           direction="column"
                           sidebar-position="second"
                           name="webmcp-details-split-view"
                           sidebar-visibility=${input.selectedTool ? 'show' : 'hidden'}>
        <div slot="main" class="tool-list">
          <div class="section-title">${i18nString(UIStrings.toolRegistry)}</div>
          ${tools.length === 0 ? html`
          ${UI.Widget.widget(UI.EmptyWidget.EmptyWidget, {header: i18nString(UIStrings.noToolsPlaceholderTitle),
                                                          text: i18nString(UIStrings.noToolsPlaceholder)})}
          ` : html`
            <devtools-list>
              ${tools.map(tool => {
                const toolStats = calculateToolStats(input.toolCalls.filter(c => c.tool === tool));
                const groups = getIconGroupsFromStats(toolStats);
                return html`
                    <div class=${Directives.classMap({'tool-item': true, selected: tool === input.selectedTool})}
                         @click=${() => input.onToolSelect(tool)}>
                    <div class="tool-name-container">
                      <div class="tool-name source-code">${tool.name}</div>
                      ${groups.length > 0 ? html`<icon-button .data=${
                          {groups, compact: false} as IconButton.IconButton.IconButtonData}></icon-button>` : ''}
                    </div>
                    <div class="tool-description">${tool.description}</div>
                  </div>
                `;
              })}
            </devtools-list>
          `}
        </div>
        <div slot="sidebar" class="tool-details">
          <div class="section-title">
            <devtools-button
              .iconName=${'cross'}
              .size=${Buttons.Button.Size.SMALL}
              .variant=${Buttons.Button.Variant.ICON}
              title=${i18nString(UIStrings.close)}
              @click=${() => input.onToolSelect(null)}
            ></devtools-button>
            <span>${i18nString(UIStrings.toolDetails)}</span>
          </div>
          ${widget(ToolDetailsWidget, {tool: input.selectedTool})}
        </div>
      </devtools-split-view>
    </devtools-split-view>
  `, target);
  // clang-format on
};

export class WebMCPView extends UI.Widget.VBox {
  readonly #view: View;
  #selectedTool: WebMCP.WebMCPModel.Tool|null = null;
  #selectedCall: WebMCP.WebMCPModel.Call|null = null;

  #filterState: FilterState = {
    text: '',
  };

  #filterButtons: FilterMenuButtons;

  static createFilterButtons(
      onToolTypesClick: (contextMenu: UI.ContextMenu.ContextMenu) => void,
      onStatusTypesClick: (contextMenu: UI.ContextMenu.ContextMenu) => void): FilterMenuButtons {
    const createButton =
        (label: string, onContextMenu: (contextMenu: UI.ContextMenu.ContextMenu) => void, jsLogContext: string):
            FilterMenuButton => {
              const button = new UI.Toolbar.ToolbarMenuButton(
                  onContextMenu,
                  /* isIconDropdown=*/ false, /* useSoftMenu=*/ true, jsLogContext,
                  /* iconName=*/ undefined,
                  /* keepOpen=*/ true);
              button.setText(label);

              /* eslint-disable-next-line @devtools/no-imperative-dom-api */
              const adorner = new Adorners.Adorner.Adorner();
              adorner.name = 'countWrapper';
              const countElement = document.createElement('span');
              adorner.append(countElement);
              adorner.classList.add('active-filters-count');
              adorner.classList.add('hidden');
              button.setAdorner(adorner);

              const setCount = (count: number): void => {
                countElement.textContent = `${count}`;
                count === 0 ? adorner.hide() : adorner.show();
              };

              return {button, setCount};
            };

    return {
      toolTypes: createButton(i18nString(UIStrings.toolTypes), onToolTypesClick, 'webmcp.tool-types'),
      statusTypes: createButton(i18nString(UIStrings.statusTypes), onStatusTypesClick, 'webmcp.status-types'),
    };
  }
  constructor(target?: HTMLElement, view: View = DEFAULT_VIEW) {
    super(target);
    this.#view = view;
    this.#filterButtons = WebMCPView.createFilterButtons(
        this.#showToolTypesContextMenu.bind(this),
        this.#showStatusTypesContextMenu.bind(this),
    );
    SDK.TargetManager.TargetManager.instance().observeModels(WebMCP.WebMCPModel.WebMCPModel, {
      modelAdded: (model: WebMCP.WebMCPModel.WebMCPModel) => this.#webMCPModelAdded(model),
      modelRemoved: (model: WebMCP.WebMCPModel.WebMCPModel) => this.#webMCPModelRemoved(model),
    });
    this.requestUpdate();
  }

  #showToolTypesContextMenu(contextMenu: UI.ContextMenu.ContextMenu): void {
    const toggle = (key: 'imperative'|'declarative'): void => {
      const current = this.#filterState.toolTypes ?? {};
      const next = {...current, [key]: !current[key]};
      let toolTypesToPass: FilterState['toolTypes'] = next;
      if (!next.imperative && !next.declarative) {
        toolTypesToPass = undefined;
      }
      this.#handleFilterChange({...this.#filterState, toolTypes: toolTypesToPass});
    };

    contextMenu.defaultSection().appendCheckboxItem(
        i18nString(UIStrings.imperative), () => toggle('imperative'),
        {checked: this.#filterState.toolTypes?.imperative ?? false, jslogContext: 'webmcp.imperative'});
    contextMenu.defaultSection().appendCheckboxItem(
        i18nString(UIStrings.declarative), () => toggle('declarative'),
        {checked: this.#filterState.toolTypes?.declarative ?? false, jslogContext: 'webmcp.declarative'});
  }

  #showStatusTypesContextMenu(contextMenu: UI.ContextMenu.ContextMenu): void {
    const toggle = (key: 'completed'|'error'|'pending'): void => {
      const current = this.#filterState.statusTypes ?? {};
      const next = {...current, [key]: !current[key]};
      let statusTypesToPass: FilterState['statusTypes'] = next;
      if (!next.completed && !next.error && !next.pending) {
        statusTypesToPass = undefined;
      }
      this.#handleFilterChange({...this.#filterState, statusTypes: statusTypesToPass});
    };

    contextMenu.defaultSection().appendCheckboxItem(
        i18nString(UIStrings.completed), () => toggle('completed'),
        {checked: this.#filterState.statusTypes?.['completed'] ?? false, jslogContext: 'webmcp.completed'});
    contextMenu.defaultSection().appendCheckboxItem(
        i18nString(UIStrings.error), () => toggle('error'),
        {checked: this.#filterState.statusTypes?.['error'] ?? false, jslogContext: 'webmcp.error'});
    contextMenu.defaultSection().appendCheckboxItem(
        i18nString(UIStrings.pending), () => toggle('pending'),
        {checked: this.#filterState.statusTypes?.['pending'] ?? false, jslogContext: 'webmcp.pending'});
  }
  #webMCPModelAdded(model: WebMCP.WebMCPModel.WebMCPModel): void {
    model.addEventListener(WebMCP.WebMCPModel.Events.TOOLS_ADDED, this.requestUpdate, this);
    model.addEventListener(WebMCP.WebMCPModel.Events.TOOLS_REMOVED, this.requestUpdate, this);
    model.addEventListener(WebMCP.WebMCPModel.Events.TOOL_INVOKED, this.requestUpdate, this);
    model.addEventListener(WebMCP.WebMCPModel.Events.TOOL_RESPONDED, this.requestUpdate, this);
  }

  #webMCPModelRemoved(model: WebMCP.WebMCPModel.WebMCPModel): void {
    model.removeEventListener(WebMCP.WebMCPModel.Events.TOOLS_ADDED, this.requestUpdate, this);
    model.removeEventListener(WebMCP.WebMCPModel.Events.TOOLS_REMOVED, this.requestUpdate, this);
    model.removeEventListener(WebMCP.WebMCPModel.Events.TOOL_INVOKED, this.requestUpdate, this);
    model.removeEventListener(WebMCP.WebMCPModel.Events.TOOL_RESPONDED, this.requestUpdate, this);
  }

  #handleClearLogClick = (): void => {
    const models = SDK.TargetManager.TargetManager.instance().models(WebMCP.WebMCPModel.WebMCPModel);
    for (const model of models) {
      model.clearCalls();
    }
    this.requestUpdate();
  };

  #handleFilterChange = (filters: FilterState): void => {
    this.#filterState = filters;

    const toolTypesCount =
        this.#filterState.toolTypes ? Object.values(this.#filterState.toolTypes).filter(Boolean).length : 0;
    this.#filterButtons.toolTypes.setCount(toolTypesCount);

    const statusTypesCount =
        this.#filterState.statusTypes ? Object.values(this.#filterState.statusTypes).filter(Boolean).length : 0;
    this.#filterButtons.statusTypes.setCount(statusTypesCount);

    this.requestUpdate();
  };

  #getTools(): WebMCP.WebMCPModel.Tool[] {
    const models = SDK.TargetManager.TargetManager.instance().models(WebMCP.WebMCPModel.WebMCPModel);
    const tools = models.flatMap(model => model.tools.toArray());
    return tools.sort((a, b) => a.name.localeCompare(b.name));
  }
  override performUpdate(): void {
    const models = SDK.TargetManager.TargetManager.instance().models(WebMCP.WebMCPModel.WebMCPModel);
    const toolCalls = models.flatMap(model => model.toolCalls);
    const filteredCalls = filterToolCalls(toolCalls, this.#filterState);
    const input: ViewInput = {
      tools: this.#getTools(),
      selectedTool: this.#selectedTool,
      onToolSelect: tool => {
        this.#selectedTool = tool;
        this.requestUpdate();
      },
      selectedCall: this.#selectedCall,
      onCallSelect: call => {
        this.#selectedCall = call;
        this.requestUpdate();
      },
      toolCalls: filteredCalls,
      filters: this.#filterState,
      filterButtons: this.#filterButtons,
      onClearLogClick: this.#handleClearLogClick,
      onFilterChange: this.#handleFilterChange,
    };
    this.#view(input, {}, this.contentElement);
  }
}
export interface PayloadViewInput {
  valueObject?: unknown;
  valueString?: string;
  errorText?: string;
  exceptionDetails?: WebMCP.WebMCPModel.ExceptionDetails;
}

export const PAYLOAD_DEFAULT_VIEW = (input: PayloadViewInput, output: object, target: HTMLElement): void => {
  if (!input.valueObject && !input.valueString && !input.errorText && !input.exceptionDetails) {
    render(nothing, target);
    return;
  }
  const isParsable = input.valueObject !== undefined;

  const createPayload = (parsedInput: unknown): TemplateResult => {
    const object = new SDK.RemoteObject.LocalJSONObject(parsedInput);
    const section =
        new ObjectUI.ObjectPropertiesSection.RootElement(new ObjectUI.ObjectPropertiesSection.ObjectTree(object, {
          readOnly: true,
          propertiesMode: ObjectUI.ObjectPropertiesSection.ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED,
        }));
    section.title = document.createTextNode(object.description);
    section.listItemElement.classList.add('source-code', 'object-properties-section');
    section.childrenListElement.classList.add('source-code', 'object-properties-section');
    section.expand();
    return html`<devtools-tree .template=${html`
          <style>${ObjectUI.ObjectPropertiesSection.objectValueStyles}</style>
          <style>${ObjectUI.ObjectPropertiesSection.objectPropertiesSectionStyles}</style>
          <ul role="tree">
            <devtools-tree-wrapper .treeElement=${section}></devtools-tree-wrapper>
          </ul>
        `}></devtools-tree>`;
  };

  const createSourceText = (text: string): TemplateResult => html`<div class="payload-value source-code">${text}</div>`;
  const createErrorText = (text: string): TemplateResult =>
      html`<div class="payload-value source-code error-text">${text}</div>`;

  const createException = (
      details: WebMCP.WebMCPModel.ExceptionDetails,
      linkifier: Components.Linkifier.Linkifier = new Components.Linkifier.Linkifier(),
      ): TemplateResult => {
    const renderFrame = (
        frame: StackTrace.ErrorStackParser.ParsedErrorFrame,
        index: number,
        array: StackTrace.ErrorStackParser.ParsedErrorFrame[],
        ): TemplateResult => {
      const newline = index < array.length - 1 ? '\n' : '';
      const {line, link, isCallFrame} = frame;

      if (!isCallFrame) {
        return html`<span>${line}${newline}</span>`;
      }

      if (!link) {
        return html`<span class="formatted-builtin-stack-frame">${line}${newline}</span>`;
      }

      const scriptLocationLink = linkifier.linkifyScriptLocation(
          details.error.runtimeModel().target(),
          link.scriptId || null,
          link.url,
          link.lineNumber,
          {
            columnNumber: link.columnNumber,
            inlineFrameIndex: 0,
            showColumnNumber: true,
          },
      );
      scriptLocationLink.tabIndex = -1;

      return html`<span class="formatted-stack-frame">${link.prefix}${scriptLocationLink}${link.suffix}${
          newline}</span>`;
    };

    return html`
      <div class="payload-value source-code error-text">
        ${details.frames.length === 0 && details.description ? html`<span>${details.description}\n</span>` : nothing}
        <div>${details.frames.map(renderFrame)}</div>
        ${details.cause ? html`\nCaused by:\n${createException(details.cause, linkifier)}` : nothing}</div>`;
  };

  render(
      html`
    <style>${webMCPViewStyles}</style>
    <div class="call-payload-view">
      <div class="call-payload-content">
            ${
          isParsable ? createPayload(input.valueObject) :
                       (input.valueString !== undefined ?
                            createSourceText(input.valueString) :
                            (input.exceptionDetails ? createException(input.exceptionDetails) :
                                                      (input.errorText ? createErrorText(input.errorText) : nothing)))}
      </div>
    </div>
  `,
      target);
};

export class PayloadWidget extends UI.Widget.Widget {
  #valueObject?: unknown;
  #valueString?: string;
  #errorText?: string;
  #exceptionDetailsPromise?: Promise<WebMCP.WebMCPModel.ExceptionDetails|undefined>;
  #exceptionDetails?: WebMCP.WebMCPModel.ExceptionDetails;
  #view: typeof PAYLOAD_DEFAULT_VIEW;

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

  set valueObject(valueObject: unknown) {
    this.#valueObject = valueObject;
    this.requestUpdate();
  }

  get valueObject(): unknown {
    return this.#valueObject;
  }

  set valueString(valueString: string|undefined) {
    this.#valueString = valueString;
    this.requestUpdate();
  }

  get valueString(): string|undefined {
    return this.#valueString;
  }

  set errorText(errorText: string|undefined) {
    this.#errorText = errorText;
    this.requestUpdate();
  }

  get errorText(): string|undefined {
    return this.#errorText;
  }

  async #updateExceptionDetails(
      exceptionDetailsPromise: Promise<WebMCP.WebMCPModel.ExceptionDetails|undefined>|undefined): Promise<void> {
    if (this.#exceptionDetailsPromise === exceptionDetailsPromise) {
      return;
    }
    this.#exceptionDetailsPromise = exceptionDetailsPromise;
    this.#exceptionDetails = undefined;
    this.requestUpdate();
    const exceptionDetails = await exceptionDetailsPromise;
    if (this.#exceptionDetailsPromise === exceptionDetailsPromise) {
      this.#exceptionDetails = exceptionDetails;
      this.requestUpdate();
    }
  }

  set exceptionDetails(exceptionDetailsPromise: Promise<WebMCP.WebMCPModel.ExceptionDetails|undefined>|undefined) {
    void this.#updateExceptionDetails(exceptionDetailsPromise);
  }

  get exceptionDetails(): Promise<WebMCP.WebMCPModel.ExceptionDetails|undefined>|undefined {
    return this.#exceptionDetailsPromise;
  }
  override wasShown(): void {
    super.wasShown();
    this.requestUpdate();
  }

  override performUpdate(): void {
    const input: PayloadViewInput = {
      valueObject: this.#valueObject,
      valueString: this.#valueString,
      errorText: this.#errorText,
      exceptionDetails: this.#exceptionDetails,
    };
    this.#view(input, {}, this.contentElement);
  }
}

export interface ToolDetailsViewInput {
  tool: WebMCP.WebMCPModel.Tool|null|undefined;
  origin: SDK.DOMModel.DOMNode|StackTrace.StackTrace.StackTrace|undefined;
  highlightNode: (node: SDK.DOMModel.DOMNode) => void;
  clearHighlight: () => void;
  revealNode: (node: SDK.DOMModel.DOMNode) => void;
}

// clang-format off
const TOOL_DETAILS_VIEW = (input: ToolDetailsViewInput, output: undefined, target: HTMLElement): void => {
  if (!input.tool) {
    render(nothing, target);
    return;
  }
  const tool = input.tool;
  const origin = input.origin;
  render(html`
    <style>${webMCPViewStyles}</style>
    <div class="tool-details-grid">
      <div class="label">Name</div>
      <div class="value source-code">${tool.name}</div>
      <div class="label">Description</div>
      <div class="value">${tool.description}</div>
      ${tool.frame ? html`
      <div class="label">${i18nString(UIStrings.frame)}</div>
      <div class="value">${Components.Linkifier.Linkifier.linkifyRevealable(tool.frame, tool.frame.displayName())}</div>
      ` : nothing}
      ${origin instanceof SDK.DOMModel.DOMNode ? html`
      <div class="label">Origin</div>
      <div class="value tool-origin-container">
        <span
            class="node-text-container source-code tool-origin-node"
            data-label="true"
            @mouseenter=${() => input.highlightNode(origin)}
            @mouseleave=${input.clearHighlight}>
          <devtools-node-text .data=${{
              nodeId: origin.getAttribute('id') || undefined,
              nodeTitle: origin.nodeNameInCorrectCase(),
              nodeClasses: origin.getAttribute('class')?.split(/\s+/).filter(s => Boolean(s))
            } as NodeText.NodeText.NodeTextData}>
          </devtools-node-text>
        </span>
        <devtools-button class="show-element"
           .title=${i18nString(UIStrings.viewInElementsPanel)}
           aria-label=${i18nString(UIStrings.viewInElementsPanel)}
           .iconName=${'select-element'}
           .jslogContext=${'elements.select-element'}
           .size=${Buttons.Button.Size.SMALL}
           .variant=${Buttons.Button.Variant.ICON}
           @click=${() => input.revealNode(origin)}
           ></devtools-button>
      </div>` : origin ? html`
      <div class="label">Origin</div>
      <div class="value">
        ${widget(Components.JSPresentationUtils.StackTracePreviewContent,
                 {stackTrace: origin, options: { expandable: true}})}
      </div>` : nothing}
    </div>
  `, target);
};
// clang-format on

export class ToolDetailsWidget extends UI.Widget.Widget {
  #tool: WebMCP.WebMCPModel.Tool|null|undefined = null;
  #origin: SDK.DOMModel.DOMNode|StackTrace.StackTrace.StackTrace|undefined;

  #view: typeof TOOL_DETAILS_VIEW;

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

  set tool(tool: WebMCP.WebMCPModel.Tool|null|undefined) {
    if (this.#tool === tool) {
      return;
    }
    this.#tool = tool;
    this.#origin = undefined;

    if (this.#tool) {
      void this.#setToolOrigin(this.#tool);
    }
    this.requestUpdate();
  }

  async #setToolOrigin(tool: WebMCP.WebMCPModel.Tool): Promise<void> {
    const origin = await (tool.node ? tool.node.resolvePromise() : tool.stackTrace);
    if (this.#tool === tool && origin) {
      this.#origin = origin;
      this.requestUpdate();
    }
  }

  get tool(): WebMCP.WebMCPModel.Tool|null|undefined {
    return this.#tool;
  }

  #highlightNode = (node: SDK.DOMModel.DOMNode): void => {
    node.highlight();
  };

  #clearHighlight = (): void => {
    SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
  };

  #revealNode = (node: SDK.DOMModel.DOMNode): void => {
    void Common.Revealer.reveal(node);
    void node.scrollIntoView();
  };

  override performUpdate(): void {
    const viewInput = {
      tool: this.#tool,
      origin: this.#origin,
      highlightNode: this.#highlightNode,
      clearHighlight: this.#clearHighlight,
      revealNode: this.#revealNode,
    };
    this.#view(viewInput, undefined, this.contentElement);
  }

  override wasShown(): void {
    super.wasShown();
    this.requestUpdate();
  }
}

export interface ParsedToolSchema {
  parameters: ProtocolMonitor.JSONEditor.Parameter[];
  typesByName: Map<string, ProtocolMonitor.JSONEditor.Parameter[]>;
  enumsByName: Map<string, Record<string, string>>;
}

const parsedSchemaCache = new WeakMap<object, ParsedToolSchema>();

export function parseToolSchema(schema: JSONSchema7): ParsedToolSchema {
  if (typeof schema === 'object' && schema !== null) {
    const cached = parsedSchemaCache.get(schema);
    if (cached) {
      return cached;
    }
  }

  const typesByName = new Map<string, ProtocolMonitor.JSONEditor.Parameter[]>();
  const enumsByName = new Map<string, Record<string, string>>();
  const simpleTypesByName = new Map<string, ProtocolMonitor.JSONEditor.ParameterType>();
  let typeCount = 0;

  function createEnumRecord(values: unknown[]): Record<string, string> {
    const enumRecord: Record<string, string> = {};
    for (const val of values) {
      enumRecord[String(val)] = String(val);
    }
    return enumRecord;
  }

  function preScanDefinition(name: string, def: JSONSchema7Definition): void {
    if (typeof def === 'boolean') {
      return;
    }
    if (def.type === 'string' && def.enum) {
      enumsByName.set(name, createEnumRecord(def.enum));
    } else if (def.type && typeof def.type === 'string' && def.type !== 'object' && def.type !== 'array') {
      let paramType = ProtocolMonitor.JSONEditor.ParameterType.STRING;
      switch (def.type) {
        case 'number':
        case 'integer':
          paramType = ProtocolMonitor.JSONEditor.ParameterType.NUMBER;
          break;
        case 'boolean':
          paramType = ProtocolMonitor.JSONEditor.ParameterType.BOOLEAN;
          break;
      }
      simpleTypesByName.set(name, paramType);
    }
  }

  function parseDefinition(name: string, def: JSONSchema7Definition): void {
    if (typeof def === 'boolean') {
      return;
    }
    if (def.type === 'object' && def.properties) {
      const nestedParams: ProtocolMonitor.JSONEditor.Parameter[] = [];
      for (const [key, value] of Object.entries(def.properties)) {
        const isOpt = !(def.required || []).includes(key);
        nestedParams.push(parseProperty(key, value, isOpt));
      }
      typesByName.set(name, nestedParams);
    }
  }

  // First pass: populate enums and simple types
  if (schema.definitions) {
    for (const [name, def] of Object.entries(schema.definitions)) {
      preScanDefinition(name, def);
    }
  }
  if (schema.$defs) {
    for (const [name, def] of Object.entries(schema.$defs)) {
      preScanDefinition(name, def);
    }
  }

  // Second pass: parse objects
  if (schema.definitions) {
    for (const [name, def] of Object.entries(schema.definitions)) {
      parseDefinition(name, def);
    }
  }
  if (schema.$defs) {
    for (const [name, def] of Object.entries(schema.$defs)) {
      parseDefinition(name, def);
    }
  }

  function parseProperty(
      name: string, propDef: JSONSchema7Definition, optional: boolean): ProtocolMonitor.JSONEditor.Parameter {
    if (typeof propDef === 'boolean') {
      return {
        name,
        optional,
        description: '',
        type: ProtocolMonitor.JSONEditor.ParameterType.STRING,
        isCorrectType: true,
      };
    }
    const prop = propDef;
    if (prop.$ref) {
      const typeRef = prop.$ref.split('/').pop() || '';
      let paramType = ProtocolMonitor.JSONEditor.ParameterType.OBJECT;
      if (enumsByName.has(typeRef)) {
        paramType = ProtocolMonitor.JSONEditor.ParameterType.STRING;
      } else {
        const simpleType = simpleTypesByName.get(typeRef);
        if (simpleType !== undefined) {
          paramType = simpleType;
        }
      }
      return {
        name,
        optional,
        description: prop.description || '',
        type: paramType,
        typeRef,
        isCorrectType: true,
      };
    }

    const typeStr = Array.isArray(prop.type) ? prop.type[0] : prop.type;
    let type: string|undefined = typeStr === 'integer' ? 'number' : typeStr;
    if (!typeStr) {
      if (prop.properties) {
        type = 'object';
      } else if (prop.items) {
        type = 'array';
      } else {
        type = 'unknown';
      }
    }
    const description = prop.description || '';

    let paramType = ProtocolMonitor.JSONEditor.ParameterType.UNKNOWN;
    switch (type) {
      case 'string':
        paramType = ProtocolMonitor.JSONEditor.ParameterType.STRING;
        break;
      case 'number':
        paramType = ProtocolMonitor.JSONEditor.ParameterType.NUMBER;
        break;
      case 'boolean':
        paramType = ProtocolMonitor.JSONEditor.ParameterType.BOOLEAN;
        break;
      case 'object':
        paramType = ProtocolMonitor.JSONEditor.ParameterType.OBJECT;
        break;
      case 'array':
        paramType = ProtocolMonitor.JSONEditor.ParameterType.ARRAY;
        break;
    }

    const base: ProtocolMonitor.JSONEditor.Parameter = {
      name,
      optional,
      description,
      type: paramType,
      isCorrectType: true,
    };

    if (type === 'object') {
      if (prop.properties) {
        const typeRef = `Object_${++typeCount}`;
        const nestedParams: ProtocolMonitor.JSONEditor.Parameter[] = [];
        for (const [key, value] of Object.entries(prop.properties)) {
          const isOpt = !(prop.required || []).includes(key);
          nestedParams.push(parseProperty(key, value, isOpt));
        }
        typesByName.set(typeRef, nestedParams);
        base.typeRef = typeRef;
      } else {
        base.isKeyEditable = true;
      }
    } else if (type === 'array') {
      const items =
          prop.items && !Array.isArray(prop.items) && typeof prop.items !== 'boolean' ? prop.items : undefined;
      if (items) {
        const itemTypeStr = Array.isArray(items.type) ? items.type[0] : items.type;
        if (items.$ref) {
          base.typeRef = items.$ref.split('/').pop() || '';
        } else if (itemTypeStr === 'object' && items.properties) {
          const typeRef = `Object_${++typeCount}`;
          const nestedParams: ProtocolMonitor.JSONEditor.Parameter[] = [];
          for (const [key, value] of Object.entries(items.properties)) {
            const isOpt = !(items.required || []).includes(key);
            nestedParams.push(parseProperty(key, value, isOpt));
          }
          typesByName.set(typeRef, nestedParams);
          base.typeRef = typeRef;
        } else if (itemTypeStr) {
          const itemType = itemTypeStr === 'integer' ? 'number' : itemTypeStr;
          if (itemType === 'string' && items.enum) {
            const typeRef = `Enum_${++typeCount}`;
            enumsByName.set(typeRef, createEnumRecord(items.enum));
            base.typeRef = typeRef;
          } else {
            base.typeRef = itemType as string;
          }
        } else {
          base.typeRef = 'string';
        }
      } else {
        base.typeRef = 'string';
      }
    } else if (type === 'string' && prop.enum) {
      const typeRef = `Enum_${++typeCount}`;
      enumsByName.set(typeRef, createEnumRecord(prop.enum));
      base.typeRef = typeRef;
    }

    return base;
  }

  const parameters: ProtocolMonitor.JSONEditor.Parameter[] = [];
  if ((schema.type === 'object' || !schema.type) && schema.properties) {
    for (const [key, value] of Object.entries(schema.properties)) {
      const isOpt = !(schema.required || []).includes(key);
      parameters.push(parseProperty(key, value, isOpt));
    }
  }

  const result = {parameters, typesByName, enumsByName};
  if (typeof schema === 'object' && schema !== null) {
    parsedSchemaCache.set(schema, result);
  }
  return result;
}
