// 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 * as Platform from '../../core/platform/platform.js';
import type * as SDK from '../../core/sdk/sdk.js';
import type * as ChangeTracker from '../change_tracker/change_tracker.js';
import type * as CommentManager from '../comment_manager/comment_manager.js';

type Tracker = ChangeTracker.ChangeTracker.ChangeTracker|undefined;

/**
 * Maximum number of characters of page controlled data (tag names, attribute names and values,
 * text node values and HTML) that a change description embeds. The inspected page fully controls
 * these strings and `Changed HTML from ... to ...` in particular can otherwise hold on to the
 * entire subtree of an element.
 */
export const MAX_VALUE_LENGTH = 100;

/** Shortens page controlled data so that both its start and its end stay recognizable. */
function trimValue(value: string): string {
  return Platform.StringUtilities.trimMiddle(value, MAX_VALUE_LENGTH);
}

function buildAnchor(node: SDK.DOMModel.DOMNode,
                     selector = 'element'): CommentManager.CommentManager.CommentAnchorSignature {
  return {
    vePath: 'Panel: elements > Tree: elements > TreeItem',
    textSignature: selector,
    node: {
      backendNodeId: node.backendNodeId(),
      targetId: node.domModel().target().id(),
    },
  };
}

/**
 * Parses a raw attribute string (e.g. 'class="btn"', 'data-id=123', or 'disabled')
 * into its attribute name and unquoted value for single-attribute inline edits.
 */
function parseAttributeText(text: string): {name: string, value: string} {
  const trimmed = text.trim();
  const eqIdx = trimmed.indexOf('=');
  if (eqIdx === -1) {
    return {name: trimmed, value: ''};
  }
  const name = trimmed.slice(0, eqIdx).trim();
  let value = trimmed.slice(eqIdx + 1).trim();
  if (value.length >= 2 &&
      ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\'')))) {
    value = value.slice(1, -1);
  }
  return {name, value};
}

function describeAddAttribute(attributeName: string, value?: string): string {
  if (value !== undefined && value !== '') {
    return `Added attribute ${trimValue(attributeName)}="${trimValue(value)}"`;
  }
  return `Added attribute "${trimValue(attributeName)}"`;
}

function describeRemoveAttribute(attributeName: string): string {
  return `Removed attribute "${trimValue(attributeName)}"`;
}

/**
 * Describes a modification of an existing attribute, which covers renaming the attribute, changing
 * its value, or both at once. `oldValue` is `undefined` if the previous value is unknown.
 */
function describeAttributeChange(
    oldAttributeName: string,
    oldValue: string|undefined,
    newAttributeName: string,
    newValue: string,
    ): string {
  if (oldAttributeName !== newAttributeName) {
    if (oldValue !== undefined && oldValue !== newValue) {
      return `Renamed attribute "${trimValue(oldAttributeName)}"="${trimValue(oldValue)}" to "${
          trimValue(newAttributeName)}"="${trimValue(newValue)}"`;
    }
    return `Renamed attribute "${trimValue(oldAttributeName)}" to "${trimValue(newAttributeName)}"`;
  }
  if (oldValue !== undefined) {
    return `Changed attribute "${trimValue(newAttributeName)}" from "${trimValue(oldValue)}" to "${
        trimValue(newValue)}"`;
  }
  return `Changed attribute "${trimValue(newAttributeName)}" to "${trimValue(newValue)}"`;
}

export interface AttributeEdit {
  /** Name of the edited attribute, or an empty string when a new attribute is added. */
  attributeName: string;
  /** Raw attribute text before the edit, if there was any. */
  oldText: string|null;
  /** Raw attribute text the user committed. Empty when the attribute is removed. */
  newText: string;
}

function describeAttributeEdit({attributeName, oldText, newText}: AttributeEdit): string {
  const parsedNew = parseAttributeText(newText);
  const parsedOld = oldText !== null ? parseAttributeText(oldText) : undefined;

  if (!newText.trim()) {
    return describeRemoveAttribute(attributeName.trim());
  }
  if (!attributeName.trim()) {
    return describeAddAttribute(parsedNew.name, parsedNew.value);
  }
  return describeAttributeChange(
      attributeName.trim(),
      parsedOld?.value,
      parsedNew.name || attributeName.trim(),
      parsedNew.value,
  );
}

/**
 * These strings are for the consumption by AI agents, they don't need to be translated.
 */
function describeTagNameEdit(oldTagName: string, newTagName: string): string {
  return `Renamed tag from <${trimValue(oldTagName)}> to <${trimValue(newTagName)}>`;
}

function describeTextNodeEdit(oldText: string, newText: string): string {
  return `Changed text from "${trimValue(oldText)}" to "${trimValue(newText)}"`;
}

function describeNodeRemoval(tagName: string): string {
  return `Removed node <${trimValue(tagName)}>`;
}

function describeHTMLEdit(oldValue?: string, newValue?: string): string {
  if (oldValue !== undefined && newValue !== undefined) {
    return `Changed HTML from "${trimValue(oldValue)}" to "${trimValue(newValue)}"`;
  }
  if (newValue !== undefined) {
    return `Changed HTML to "${trimValue(newValue)}"`;
  }
  return 'Edited HTML';
}

function describeVisibilityToggle(tagName: string, hidden: boolean): string {
  return hidden ? `Hid element <${trimValue(tagName)}>` : `Unhid element <${trimValue(tagName)}>`;
}

function describeNodeDuplication(tagName: string): string {
  return `Duplicated node <${trimValue(tagName)}>`;
}

function describeNodeMove(tagName: string, directionUp: boolean): string {
  return `Moved node <${trimValue(tagName)}> ${directionUp ? 'up' : 'down'}`;
}

function describeNodeDrop(tagName: string): string {
  return `Moved node <${trimValue(tagName)}> via drag and drop`;
}

function describeNodePaste(tagName: string, isCut: boolean): string {
  return isCut ? `Pasted (moved) node <${trimValue(tagName)}>` : `Pasted node <${trimValue(tagName)}>`;
}

export function trackAttributeEdit(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined,
                                   edit: AttributeEdit): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeAttributeEdit(edit), buildAnchor(node, selector));
}

export function trackTagNameEdit(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined,
                                 oldTagName: string, newTagName: string): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeTagNameEdit(oldTagName, newTagName), buildAnchor(node, selector));
}

export function trackTextNodeEdit(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined,
                                  oldText: string, newText: string): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeTextNodeEdit(oldText, newText), buildAnchor(node, selector));
}

export function trackNodeRemoval(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeNodeRemoval(node.nodeName().toLowerCase()), buildAnchor(node, selector));
}

export function trackHTMLEdit(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined,
                              oldValue: string, newValue: string): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeHTMLEdit(oldValue, newValue), buildAnchor(node, selector));
}

export function trackVisibilityToggle(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined,
                                      hidden: boolean): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeVisibilityToggle(node.nodeName().toLowerCase(), hidden), buildAnchor(node, selector));
}

export function trackNodeDuplication(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeNodeDuplication(node.nodeName().toLowerCase()), buildAnchor(node, selector));
}

export function trackNodeMove(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined,
                              directionUp: boolean): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeNodeMove(node.nodeName().toLowerCase(), directionUp), buildAnchor(node, selector));
}

export function trackNodeDrop(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeNodeDrop(node.nodeName().toLowerCase()), buildAnchor(node, selector));
}

export function trackNodePaste(tracker: Tracker, node: SDK.DOMModel.DOMNode, selector: string|undefined,
                               isCut: boolean): void {
  if (!tracker?.isTracking) {
    return;
  }
  tracker.trackChange(describeNodePaste(node.nodeName().toLowerCase(), isCut), buildAnchor(node, selector));
}
