// 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 type * as Common from '../../core/common/common.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as CommentManager from '../../models/comment_manager/comment_manager.js';
import * as Comments from '../../ui/comments/comments.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Lit from '../../ui/lit/lit.js';

import commentsOverlayStyles from './commentsOverlay.css.js';
import {CommentThreadWidget, type Title} from './CommentThreadWidget.js';

const {
  html,
  render,
  nothing,
  Directives: {repeat, styleMap},
} = Lit;

export interface ViewInput {
  pins: Comments.CommentOverlayManager.PinPositionData[];
  highlights: Comments.CommentOverlayManager.HighlightRectData[];
  hoverHighlight: Comments.CommentOverlayManager.HoverHighlightData|null;
  commentMode: boolean;
  onPinClick: (threadId: string) => void;
  activeThread: CommentManager.CommentManager.CommentThread|null;
  activePin: Comments.CommentOverlayManager.PinPositionData|null;
  title: Title;
  onAddComment: (text: string) => void;
}

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

const POPUP_MARGIN = 8;
const PIN_HEIGHT = 30;
const POPUP_WIDTH = 288;
const POPUP_HEIGHT = 220;
const AUTO_CLOSE_DELAY_MS = 2000;

const DEFAULT_VIEW: View = (input: ViewInput, _output: undefined, target: HTMLElement): void => {
  // clang-format off
  render(html`
    <style>${commentsOverlayStyles}</style>
    <div class="comments-overlay-container">
      ${input.hoverHighlight && input.hoverHighlight.visible ? html`
        <div
          class="comment-hover-highlight"
          style=${styleMap({
            top: `${input.hoverHighlight.top}px`,
            left: `${input.hoverHighlight.left}px`,
            width: `${input.hoverHighlight.width}px`,
            height: `${input.hoverHighlight.height}px`,
          })}>
        </div>
      ` : nothing}
      ${input.highlights.map(h => h.visible ? html`
        <div
          class="comment-anchor-highlight"
          style=${styleMap({
            top: `${h.top}px`,
            left: `${h.left}px`,
            width: `${h.width}px`,
            height: `${h.height}px`,
          })}>
        </div>
      ` : nothing)}
      ${input.pins.map(p => p.visible ? html`
        <div
          class="comment-pin"
          style=${styleMap({
            top: `${p.top}px`,
            left: `${p.left}px`,
          })}
          @click=${() => input.onPinClick(p.id)}>
          <div class="comment-cursor">${p.index}</div>
        </div>
      ` : nothing)}
      ${input.activePin && input.activeThread ? repeat(
        [{pin: input.activePin, thread: input.activeThread}],
        item => item.thread.id,
        item => html`
          <div
            class="comment-popup-widget"
            style=${styleMap({
              top: `${Math.min(
                Math.max(POPUP_MARGIN, item.pin.top + PIN_HEIGHT),
                Math.max(POPUP_MARGIN, target.clientHeight - POPUP_HEIGHT),
              )}px`,
              left: `${Math.min(
                Math.max(POPUP_MARGIN, item.pin.left),
                Math.max(POPUP_MARGIN, target.clientWidth - POPUP_WIDTH - POPUP_MARGIN),
              )}px`,
            })}>
            ${UI.Widget.widget(CommentThreadWidget, {
              title: input.title,
              comments: [...item.thread.comments],
              onAddComment: input.onAddComment,
            })}
          </div>
        `,
      ) : nothing}
    </div>
  `, target);
  // clang-format on
};

export class CommentsOverlayWidget extends UI.Widget.Widget {
  static override readonly INJECT: readonly[typeof CommentManager.CommentManager.CommentManager] =
      [CommentManager.CommentManager.CommentManager] as const;

  readonly #view: View;
  readonly #commentManager: CommentManager.CommentManager.CommentManager;
  #commentOverlayManager: Comments.CommentOverlayManager.CommentOverlayManager;
  #activeThreadId: string|null = null;
  #closeTimeoutId: number|null = null;
  #cachedTitle: Title = {text: ''};
  #cachedTitleAnchor: CommentManager.CommentManager.CommentAnchorSignature|null = null;

  constructor(
      element: HTMLElement|undefined,
      [commentManager]: UI.Widget.WidgetDependencies<typeof CommentsOverlayWidget>,
      view: View = DEFAULT_VIEW,
  ) {
    super(element, {useShadowDom: false});
    this.#view = view;
    this.#commentManager = commentManager;
    this.#commentOverlayManager = new Comments.CommentOverlayManager.CommentOverlayManager(
        this.#commentManager,
    );
  }

  setOverlayManagerForTest(overlayManager: Comments.CommentOverlayManager.CommentOverlayManager): void {
    this.#commentOverlayManager = overlayManager;
  }

  override wasShown(): void {
    super.wasShown();
    this.#commentOverlayManager.start();
    this.#commentOverlayManager.addEventListener(
        Comments.CommentOverlayManager.Events.POSITIONS_UPDATED,
        this.#onStateChanged,
        this,
    );
    this.#commentOverlayManager.addEventListener(
        Comments.CommentOverlayManager.Events.HOVER_HIGHLIGHT_CHANGED,
        this.#onStateChanged,
        this,
    );

    this.#commentManager.addEventListener(
        CommentManager.CommentManager.Events.COMMENT_THREADS_CHANGED,
        this.#onStateChanged,
        this,
    );
    this.#commentManager.addEventListener(
        CommentManager.CommentManager.Events.COMMENT_MODE_CHANGED,
        this.#onCommentModeChanged,
        this,
    );
    this.#commentManager.addEventListener(
        CommentManager.CommentManager.Events.AGENT_ATTACHED_CHANGED,
        this.#onAgentAttachedChanged,
        this,
    );

    this.requestUpdate();
  }

  override willHide(): void {
    this.#clearCloseTimeout();
    this.#commentOverlayManager.stop();
    this.#commentOverlayManager.removeEventListener(
        Comments.CommentOverlayManager.Events.POSITIONS_UPDATED,
        this.#onStateChanged,
        this,
    );
    this.#commentOverlayManager.removeEventListener(
        Comments.CommentOverlayManager.Events.HOVER_HIGHLIGHT_CHANGED,
        this.#onStateChanged,
        this,
    );

    this.#commentManager.removeEventListener(
        CommentManager.CommentManager.Events.COMMENT_THREADS_CHANGED,
        this.#onStateChanged,
        this,
    );
    this.#commentManager.removeEventListener(
        CommentManager.CommentManager.Events.COMMENT_MODE_CHANGED,
        this.#onCommentModeChanged,
        this,
    );
    this.#commentManager.removeEventListener(
        CommentManager.CommentManager.Events.AGENT_ATTACHED_CHANGED,
        this.#onAgentAttachedChanged,
        this,
    );

    super.willHide();
  }

  #clearCloseTimeout(): void {
    if (this.#closeTimeoutId !== null) {
      window.clearTimeout(this.#closeTimeoutId);
      this.#closeTimeoutId = null;
    }
  }

  #onAgentAttachedChanged(
      event: Common.EventTarget.EventTargetEvent<
          CommentManager.CommentManager.EventTypes[CommentManager.CommentManager.Events.AGENT_ATTACHED_CHANGED]>,
      ): void {
    if (!event.data) {
      this.#activeThreadId = null;
    }
    this.requestUpdate();
  }

  #onCommentModeChanged(
      event: Common.EventTarget.EventTargetEvent<
          CommentManager.CommentManager.EventTypes[CommentManager.CommentManager.Events.COMMENT_MODE_CHANGED]>,
      ): void {
    const isModeActive = event.data;
    if (!isModeActive) {
      this.#activeThreadId = null;
    }
    const action = UI.ActionRegistry.ActionRegistry.instance().getAction(
        'comments.toggle-comment-mode',
    );
    action?.setToggled(isModeActive);
    this.requestUpdate();
  }

  #onStateChanged(): void {
    const draftThread = this.#commentManager.getCommentThreads().find(t => t.status === 'DRAFT');
    if (draftThread) {
      this.#activeThreadId = draftThread.id;
    } else if (this.#activeThreadId && !this.#commentManager.getCommentThread(this.#activeThreadId)) {
      this.#activeThreadId = null;
    }
    this.requestUpdate();
  }

  async #getOrComputeTitle(anchor: CommentManager.CommentManager.CommentAnchorSignature|null): Promise<Title> {
    if (anchor === this.#cachedTitleAnchor) {
      return this.#cachedTitle;
    }
    const title = anchor ? await this.#computeTitle(anchor) : {text: ''};
    this.#cachedTitleAnchor = anchor;
    this.#cachedTitle = title;
    return title;
  }

  async #computeTitle(anchor: CommentManager.CommentManager.CommentAnchorSignature): Promise<Title> {
    if (anchor.node) {
      const target = SDK.TargetManager.TargetManager.instance().targetById(anchor.node.targetId);
      if (target) {
        const deferredNode = new SDK.DOMModel.DeferredDOMNode(
            target,
            anchor.node.backendNodeId as Protocol.DOM.BackendNodeId,
        );
        const node = await deferredNode.resolvePromise();
        if (node) {
          return {node};
        }
      }
      return {text: ''};
    }

    if (anchor.networkRequestId) {
      const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
      const request = target?.model(SDK.NetworkManager.NetworkManager)?.requestForId(anchor.networkRequestId);
      if (request) {
        return {text: request.name()};
      }
    }

    return {text: anchor.textSignature || ''};
  }

  #handlePinClick = (threadId: string): void => {
    const thread = this.#commentManager.getCommentThread(threadId);
    if (this.#activeThreadId === threadId) {
      if (thread?.status === 'DRAFT') {
        this.#commentOverlayManager.clearDraftThreads();
      }
      this.#activeThreadId = null;
    } else {
      this.#commentOverlayManager.clearDraftThreads();
      this.#activeThreadId = threadId;
    }
    this.requestUpdate();
  };

  override async performUpdate(signal?: AbortSignal): Promise<void> {
    if (!this.#commentManager.isAgentAttached()) {
      this.#view(
          {
            pins: [],
            highlights: [],
            hoverHighlight: null,
            commentMode: false,
            onPinClick: this.#handlePinClick,
            activeThread: null,
            activePin: null,
            title: {text: ''},
            onAddComment: () => {},
          },
          undefined,
          this.contentElement,
      );
      return;
    }

    const activeThread =
        this.#activeThreadId ? this.#commentManager.getCommentThread(this.#activeThreadId) ?? null : null;
    const title = await this.#getOrComputeTitle(activeThread?.anchor ?? null);
    signal?.throwIfAborted();

    const pins = this.#commentOverlayManager.getPinPositions();
    const highlights = this.#commentOverlayManager.getHighlightRects();
    const activePin = this.#activeThreadId ? pins.find(p => p.id === this.#activeThreadId) ?? null : null;

    const viewInput: ViewInput = {
      pins,
      highlights,
      hoverHighlight: this.#commentOverlayManager.getHoverHighlight(),
      commentMode: this.#commentManager.isCommentMode(),
      onPinClick: this.#handlePinClick,
      activeThread,
      activePin,
      title,
      onAddComment: (text: string) => {
        if (!activeThread) {
          return;
        }
        activeThread.save(text);
        const threadId = activeThread.id;
        this.#closeTimeoutId = window.setTimeout(() => {
          this.#closeTimeoutId = null;
          if (this.#activeThreadId === threadId) {
            this.#activeThreadId = null;
            this.requestUpdate();
          }
        }, AUTO_CLOSE_DELAY_MS);
      },
    };
    this.#view(viewInput, undefined, this.contentElement);
  }
}

let widgetInstance: CommentsOverlayWidget|null = null;

export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
  readonly #commentManager: CommentManager.CommentManager.CommentManager;

  constructor(commentManager?: CommentManager.CommentManager.CommentManager) {
    this.#commentManager = commentManager ??
        Root.DevToolsContext.globalInstance().get(
            CommentManager.CommentManager.CommentManager,
        );
  }

  handleAction(_context: UI.Context.Context, actionId: string): boolean {
    if (actionId === 'comments.toggle-comment-mode') {
      if (!this.#commentManager.isAgentAttached()) {
        return false;
      }
      if (!widgetInstance) {
        widgetInstance = new CommentsOverlayWidget(
            undefined,
            [this.#commentManager],
        );
        widgetInstance.markAsRoot();
        widgetInstance.show(document.body);
      }
      this.#commentManager.setCommentMode(
          !this.#commentManager.isCommentMode(),
      );
      return true;
    }
    return false;
  }

  static resetForTest(): void {
    if (widgetInstance) {
      widgetInstance.detach();
      widgetInstance = null;
    }
  }
}

export class ButtonProvider implements UI.Toolbar.Provider {
  readonly #button: UI.Toolbar.ToolbarButton;
  readonly #commentManager: CommentManager.CommentManager.CommentManager;

  constructor(commentManager?: CommentManager.CommentManager.CommentManager) {
    this.#commentManager = commentManager ??
        Root.DevToolsContext.globalInstance().get(
            CommentManager.CommentManager.CommentManager,
        );
    const action = UI.ActionRegistry.ActionRegistry.instance().getAction('comments.toggle-comment-mode');
    action.setEnabled(this.#commentManager.isAgentAttached());
    this.#button = UI.Toolbar.Toolbar.createActionButton(action);
    this.#button.setVisible(this.#commentManager.isAgentAttached());
    this.#commentManager.addEventListener(
        CommentManager.CommentManager.Events.AGENT_ATTACHED_CHANGED,
        event => {
          action.setEnabled(event.data);
          this.#button.setVisible(event.data);
        },
    );
  }

  item(): UI.Toolbar.ToolbarItem|null {
    return this.#button;
  }
}
