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

/* eslint-disable @devtools/no-lit-render-outside-of-view */

import '../../../ui/components/spinners/spinners.js';

import * as Host from '../../../core/host/host.js';
import * as i18n from '../../../core/i18n/i18n.js';
import type * as Platform from '../../../core/platform/platform.js';
import type * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import type {MarkdownLitRenderer} from '../../../ui/components/markdown_view/MarkdownView.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Lit from '../../../ui/lit/lit.js';
import {PatchWidget} from '../PatchWidget.js';

import {ChatInput} from './ChatInput.js';
import chatViewStyles from './chatView.css.js';
import {type ChatMessage, type ModelChatMessage, UserActionRow} from './UserActionRow.js';

export {ChatInput, type ImageInputData} from './ChatInput.js';

const {html, Directives: {ref, repeat, createRef}} = Lit;

/*
* Strings that don't need to be translated at this time.
*/
const UIStringsNotTranslate = {
  /**
   * @description Text for the empty state of the AI assistance panel.
   */
  emptyStateText: 'How can I help you?',
} as const;

const lockedString = i18n.i18n.lockedString;

const SCROLL_ROUNDING_OFFSET = 1;

export interface Props {
  onTextSubmit:
      (text: string, imageInput?: Host.AidaClient.Part,
       multimodalInputType?: AiAssistanceModel.AiAgent.MultimodalInputType) => void;
  onInspectElementClick: () => void;
  onFeedbackSubmit: (rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void;
  onCancelClick: () => void;
  onContextClick: () => void;
  onNewConversation: () => void;
  onCopyResponseClick: (message: ModelChatMessage) => void;
  changeManager: AiAssistanceModel.ChangeManager.ChangeManager;
  inspectElementToggled: boolean;
  messages: ChatMessage[];
  selectedContext: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null;
  isLoading: boolean;
  canShowFeedbackForm: boolean;
  userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'>;
  conversationType: AiAssistanceModel.AiHistoryStorage.ConversationType;
  isReadOnly: boolean;
  blockedByCrossOrigin: boolean;
  changeSummary?: string;
  multimodalInputEnabled?: boolean;
  isTextInputDisabled: boolean;
  emptyStateSuggestions: AiAssistanceModel.AiAgent.ConversationSuggestion[];
  inputPlaceholder: Platform.UIString.LocalizedString;
  disclaimerText: Platform.UIString.LocalizedString;
  isArtifactsSidebarOpen: boolean;
  uploadImageInputEnabled?: boolean;
  markdownRenderer: MarkdownLitRenderer;
  additionalFloatyContext: UI.Floaty.FloatyContextSelection[];
}

export class ChatView extends HTMLElement {
  readonly #shadow = this.attachShadow({mode: 'open'});
  #scrollTop?: number;
  #props: Props;
  #messagesContainerElement?: Element;
  #mainElementRef = createRef<HTMLElement>();
  #messagesContainerResizeObserver = new ResizeObserver(() => this.#handleMessagesContainerResize());
  /**
   * Indicates whether the chat scroll position should be pinned to the bottom.
   *
   * This is true when:
   *   - The scroll is at the very bottom, allowing new messages to push the scroll down automatically.
   *   - The panel is initially rendered and the user hasn't scrolled yet.
   *
   * It is set to false when the user scrolls up to view previous messages.
   */
  #pinScrollToBottom = true;
  /**
   * Indicates whether the scroll event originated from code
   * or a user action. When set to `true`, `handleScroll` will ignore the event,
   * allowing it to only handle user-driven scrolls and correctly decide
   * whether to pin the content to the bottom.
   */
  #isProgrammaticScroll = false;
  #inputRef = createRef<UI.Widget.WidgetElement<ChatInput>>();

  constructor(props: Props) {
    super();
    this.#props = props;
  }

  set props(props: Props) {
    this.#props = props;
    this.#render();
  }

  connectedCallback(): void {
    this.#render();

    if (this.#messagesContainerElement) {
      this.#messagesContainerResizeObserver.observe(this.#messagesContainerElement);
    }
  }

  disconnectedCallback(): void {
    this.#messagesContainerResizeObserver.disconnect();
  }

  focusTextInput(): void {
    const textArea = this.#shadow.querySelector('.chat-input') as HTMLTextAreaElement;
    if (!textArea) {
      return;
    }

    textArea.focus();
  }

  restoreScrollPosition(): void {
    if (this.#scrollTop === undefined) {
      return;
    }

    if (!this.#mainElementRef?.value) {
      return;
    }

    this.#setMainElementScrollTop(this.#scrollTop);
  }

  scrollToBottom(): void {
    if (!this.#mainElementRef?.value) {
      return;
    }

    this.#setMainElementScrollTop(this.#mainElementRef.value.scrollHeight);
  }

  #handleMessagesContainerResize(): void {
    if (!this.#pinScrollToBottom) {
      return;
    }

    if (!this.#mainElementRef?.value) {
      return;
    }

    if (this.#pinScrollToBottom) {
      this.#setMainElementScrollTop(this.#mainElementRef.value.scrollHeight);
    }
  }

  #setMainElementScrollTop(scrollTop: number): void {
    if (!this.#mainElementRef?.value) {
      return;
    }

    this.#scrollTop = scrollTop;
    this.#isProgrammaticScroll = true;
    this.#mainElementRef.value.scrollTop = scrollTop;
  }

  #handleMessageContainerRef(el: Element|undefined): void {
    this.#messagesContainerElement = el;

    if (el) {
      this.#messagesContainerResizeObserver.observe(el);
    } else {
      this.#pinScrollToBottom = true;
      this.#messagesContainerResizeObserver.disconnect();
    }
  }

  #handleScroll = (ev: Event): void => {
    if (!ev.target || !(ev.target instanceof HTMLElement)) {
      return;
    }

    // Do not handle scroll events caused by programmatically
    // updating the scroll position. We want to know whether user
    // did scroll the container from the user interface.
    if (this.#isProgrammaticScroll) {
      this.#isProgrammaticScroll = false;
      return;
    }

    this.#scrollTop = ev.target.scrollTop;
    this.#pinScrollToBottom =
        ev.target.scrollTop + ev.target.clientHeight + SCROLL_ROUNDING_OFFSET > ev.target.scrollHeight;
  };

  #handleSuggestionClick = (suggestion: string): void => {
    this.#inputRef.value?.getWidget()?.setInputValue(suggestion);
    this.#render();
    this.focusTextInput();
    Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceDynamicSuggestionClicked);
  };

  #render(): void {

    const inputWidgetClasses = Lit.Directives.classMap({
      'chat-input-widget': true,
      sticky: !this.#props.isReadOnly,
    });

    // clang-format off
    Lit.render(html`
      <style>${chatViewStyles}</style>
      <div class="chat-ui">
        <main @scroll=${this.#handleScroll} ${ref(this.#mainElementRef)}>
          ${renderMainContents({
            messages: this.#props.messages,
            isLoading: this.#props.isLoading,
            isReadOnly: this.#props.isReadOnly,
            canShowFeedbackForm: this.#props.canShowFeedbackForm,
            isTextInputDisabled: this.#props.isTextInputDisabled,
            suggestions: this.#props.emptyStateSuggestions,
            userInfo: this.#props.userInfo,
            markdownRenderer: this.#props.markdownRenderer,
            changeSummary: this.#props.changeSummary,
            changeManager: this.#props.changeManager,
            onSuggestionClick: this.#handleSuggestionClick,
            onFeedbackSubmit: this.#props.onFeedbackSubmit,
            onMessageContainerRef: this.#handleMessageContainerRef,
            onCopyResponseClick: this.#props.onCopyResponseClick,
          })}
          <devtools-widget class=${inputWidgetClasses} .widgetConfig=${UI.Widget.widgetConfig(ChatInput, {
            isLoading: this.#props.isLoading,
            blockedByCrossOrigin: this.#props.blockedByCrossOrigin,
            isTextInputDisabled: this.#props.isTextInputDisabled,
            inputPlaceholder: this.#props.inputPlaceholder,
            disclaimerText: this.#props.disclaimerText,
            selectedContext: this.#props.selectedContext,
            inspectElementToggled: this.#props.inspectElementToggled,
            multimodalInputEnabled: this.#props.multimodalInputEnabled ?? false,
            conversationType: this.#props.conversationType,
            uploadImageInputEnabled: this.#props.uploadImageInputEnabled ?? false,
            isReadOnly: this.#props.isReadOnly,
            additionalFloatyContext: this.#props.additionalFloatyContext,
            onContextClick: this.#props.onContextClick,
            onInspectElementClick: this.#props.onInspectElementClick,
            onTextSubmit: (
                text: string, imageInput?: Host.AidaClient.Part,
                multimodalInputType?: AiAssistanceModel.AiAgent.MultimodalInputType) => {
              this.#props.onTextSubmit(text, imageInput, multimodalInputType);
              this.#render();
            },
            onCancelClick: this.#props.onCancelClick,
            onNewConversation: this.#props.onNewConversation,
          })} ${ref(this.#inputRef)}></devtools-widget>
        </main>
      </div>
    `, this.#shadow, {host: this});
    // clang-format on
  }
}

function renderMainContents({
  messages,
  isLoading,
  isReadOnly,
  canShowFeedbackForm,
  isTextInputDisabled,
  suggestions,
  userInfo,
  markdownRenderer,
  changeSummary,
  changeManager,
  onSuggestionClick,
  onFeedbackSubmit,
  onCopyResponseClick,
  onMessageContainerRef,
}: {
  messages: ChatMessage[],
  isLoading: boolean,
  isReadOnly: boolean,
  canShowFeedbackForm: boolean,
  isTextInputDisabled: boolean,
  suggestions: AiAssistanceModel.AiAgent.ConversationSuggestion[],
  userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'>,
  markdownRenderer: MarkdownLitRenderer,
  changeManager: AiAssistanceModel.ChangeManager.ChangeManager,
  onSuggestionClick: (suggestion: string) => void,
  onFeedbackSubmit: (rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void,
  onCopyResponseClick: (message: ModelChatMessage) => void,
  onMessageContainerRef: (el: Element|undefined) => void,
  changeSummary?: string,
}): Lit.LitTemplate {
  if (messages.length > 0) {
    return renderMessages({
      messages,
      isLoading,
      isReadOnly,
      canShowFeedbackForm,
      userInfo,
      markdownRenderer,
      changeSummary,
      changeManager,
      onSuggestionClick,
      onFeedbackSubmit,
      onMessageContainerRef,
      onCopyResponseClick
    });
  }

  return renderEmptyState({isTextInputDisabled, suggestions, onSuggestionClick});
}

function renderMessages({
  messages,
  isLoading,
  isReadOnly,
  canShowFeedbackForm,
  userInfo,
  markdownRenderer,
  changeSummary,
  changeManager,
  onSuggestionClick,
  onFeedbackSubmit,
  onCopyResponseClick,
  onMessageContainerRef,
}: {
  messages: ChatMessage[],
  isLoading: boolean,
  isReadOnly: boolean,
  canShowFeedbackForm: boolean,
  userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'>,
  markdownRenderer: MarkdownLitRenderer,
  onSuggestionClick: (suggestion: string) => void,
  onFeedbackSubmit: (rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void,
  onCopyResponseClick: (message: ModelChatMessage) => void,
  onMessageContainerRef: (el: Element|undefined) => void,
  changeSummary?: string,
  changeManager?: AiAssistanceModel.ChangeManager.ChangeManager,
}): Lit.TemplateResult {
  function renderPatchWidget(): Lit.LitTemplate {
    if (isLoading) {
      return Lit.nothing;
    }

    // clang-format off
    return html`<devtools-widget
      .widgetConfig=${UI.Widget.widgetConfig(PatchWidget, {
        changeSummary: changeSummary ?? '',
        changeManager,
      })}
    ></devtools-widget>`;
    // clang-format on
  }

  // clang-format off
  return html`
    <div class="messages-container" ${ref(onMessageContainerRef)}>
      ${repeat(messages, message =>
        html`<devtools-widget .widgetConfig=${UI.Widget.widgetConfig(UserActionRow, {
          message,
          isLoading,
          isReadOnly,
          canShowFeedbackForm,
          userInfo,
          markdownRenderer,
          isLastMessage: messages.at(-1) === message,
          onSuggestionClick,
          onFeedbackSubmit,
          onCopyResponseClick,
        })}></devtools-widget>`
      )}
      ${renderPatchWidget()}
    </div>
  `;
  // clang-format on
}

function renderEmptyState({isTextInputDisabled, suggestions, onSuggestionClick}: {
  isTextInputDisabled: boolean,
  suggestions: AiAssistanceModel.AiAgent.ConversationSuggestion[],
  onSuggestionClick: (suggestion: string) => void,
}): Lit.TemplateResult {
  // clang-format off
  return html`<div class="empty-state-container">
    <div class="header">
      <div class="icon">
        <devtools-icon
          name="smart-assistant"
        ></devtools-icon>
      </div>
      <h1>${lockedString(UIStringsNotTranslate.emptyStateText)}</h1>
    </div>
    <div class="empty-state-content">
      ${suggestions.map(({title, jslogContext}) => {
        return html`<devtools-button
          class="suggestion"
          @click=${() => onSuggestionClick(title)}
          .data=${
            {
              variant: Buttons.Button.Variant.OUTLINED,
              size: Buttons.Button.Size.REGULAR,
              title,
              jslogContext: jslogContext ?? 'suggestion',
              disabled: isTextInputDisabled,
            } as Buttons.Button.ButtonData
          }
        >${title}</devtools-button>`;
      })}
    </div>
  </div>`;
  // clang-format on
}

declare global {
  interface HTMLElementTagNameMap {
    'devtools-ai-chat-view': ChatView;
  }
}

customElements.define('devtools-ai-chat-view', ChatView);
