import { EVENTS } from "@bitwarden/common/autofill/constants";

import AutofillPageDetails from "../models/autofill-page-details";
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service";
import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service";
import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
import { DomElementVisibilityService } from "../services/abstractions/dom-element-visibility.service";
import { DomQueryService } from "../services/abstractions/dom-query.service";
import { CollectAutofillContentService } from "../services/collect-autofill-content.service";
import InsertAutofillContentService from "../services/insert-autofill-content.service";
import { AutofillTriageResponse } from "../types/autofill-triage";
import { sendExtensionMessage } from "../utils";
import { EventSecurity } from "../utils/event-security";

import {
  AutofillExtensionMessage,
  AutofillExtensionMessageHandlers,
  AutofillInit as AutofillInitInterface,
} from "./abstractions/autofill-init";

class AutofillInit implements AutofillInitInterface {
  private readonly sendExtensionMessage = sendExtensionMessage;
  private readonly collectAutofillContentService: CollectAutofillContentService;
  private readonly insertAutofillContentService: InsertAutofillContentService;
  private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined;
  private lastContextMenuClickedElement: HTMLElement | null = null;
  private isMonitoring = false;
  private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = {
    collectPageDetails: ({ message }) =>
      this.isMonitoring ? this.collectPageDetails(message) : undefined,
    collectPageDetailsImmediately: ({ message }) =>
      this.isMonitoring ? this.collectPageDetails(message, true) : undefined,
    collectAutofillTriage: () =>
      this.isMonitoring ? this.collectPageDetailsForContextMenu() : undefined,
    fillForm: ({ message }) => (this.isMonitoring ? this.fillForm(message) : undefined),
    applyTargetedFields: ({ message }) =>
      this.isMonitoring ? this.applyTargetedFields(message) : undefined,
    clearTargetingRulesCache: () => this.handleClearTargetingRulesCache(),
    startAutofillMonitors: () => this.startMonitoring(),
    stopAutofillMonitors: () => this.stopMonitoring(),
  };

  /**
   * AutofillInit constructor. Initializes the DomElementVisibilityService,
   * CollectAutofillContentService and InsertAutofillContentService classes.
   *
   * @param domQueryService - Service used to handle DOM queries.
   * @param domElementVisibilityService - Used to check if an element is viewable.
   * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
   * @param autofillInlineMenuContentService - The inline menu content service, potentially undefined.
   * @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined.
   */
  constructor(
    domQueryService: DomQueryService,
    domElementVisibilityService: DomElementVisibilityService,
    private autofillOverlayContentService?: AutofillOverlayContentService,
    private autofillInlineMenuContentService?: AutofillInlineMenuContentService,
    private overlayNotificationsContentService?: OverlayNotificationsContentService,
  ) {
    this.collectAutofillContentService = new CollectAutofillContentService(
      domElementVisibilityService,
      domQueryService,
      this.autofillOverlayContentService,
    );
    this.insertAutofillContentService = new InsertAutofillContentService(
      domElementVisibilityService,
      this.collectAutofillContentService,
    );
  }

  /**
   * Initializes the autofill content script, setting up
   * the extension message listeners. This method should
   * be called once when the content script is loaded.
   */
  init() {
    this.setupExtensionMessageListeners();
  }

  /**
   * Attaches monitoring-scoped listeners (contextmenu, LOAD) and fans
   * out to each sub-monitor. Idempotent.
   */
  startMonitoring(): void {
    if (this.isMonitoring) {
      return;
    }
    this.isMonitoring = true;

    // Start sub-monitors first so any page-details collection triggered
    // by this controller below sees a fully wired-up service graph.
    this.collectAutofillContentService.startMonitoring();
    this.autofillOverlayContentService?.startMonitoring();
    this.autofillInlineMenuContentService?.startMonitoring();
    this.collectPageDetailsOnLoad();
  }

  /**
   * Detaches monitoring-scoped listeners, cancels the LOAD timeout,
   * and fans out to each sub-monitor. Idempotent.
   */
  stopMonitoring(): void {
    this.isMonitoring = false;
    this.clearCollectPageDetailsOnLoadTimeout();
    globalThis.removeEventListener(EVENTS.LOAD, this.sendCollectDetailsMessage);
    this.collectAutofillContentService.stopMonitoring();
    this.autofillOverlayContentService?.stopMonitoring();
    this.autofillInlineMenuContentService?.stopMonitoring();
  }

  /**
   * Triggers a collection of the page details from the
   * background script, ensuring that autofill is ready
   * to act on the page.
   */
  private collectPageDetailsOnLoad() {
    if (globalThis.document.readyState === "complete") {
      this.sendCollectDetailsMessage();
    }

    globalThis.addEventListener(EVENTS.LOAD, this.sendCollectDetailsMessage);
  }

  /**
   * Sends a message to collect page details after a short delay.
   */
  private sendCollectDetailsMessage = () => {
    this.clearCollectPageDetailsOnLoadTimeout();
    this.collectPageDetailsOnLoadTimeout = setTimeout(
      () => this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }),
      750,
    );
  };

  /**
   * Collects the page details and sends them to the
   * extension background script. If the `sendDetailsInResponse`
   * parameter is set to true, the page details will be
   * returned to facilitate sending the details in the
   * response to the extension message.
   *
   * @param message - The extension message.
   * @param sendDetailsInResponse - Determines whether to send the details in the response.
   */
  private async collectPageDetails(
    message: AutofillExtensionMessage,
    sendDetailsInResponse = false,
  ): Promise<AutofillPageDetails | void> {
    const pageDetails: AutofillPageDetails =
      await this.collectAutofillContentService.getPageDetails();
    if (sendDetailsInResponse) {
      return pageDetails;
    }

    void this.sendExtensionMessage("collectPageDetailsResponse", {
      tab: message.tab,
      details: pageDetails,
      sender: message.sender,
    });
  }

  /**
   * Collects page details and returns them directly in the response for autofill triage.
   */
  private async collectPageDetailsForContextMenu(): Promise<AutofillTriageResponse> {
    const pageDetails = await this.collectAutofillContentService.getPageDetails();

    let targetFieldRef: string | undefined;
    const el = this.lastContextMenuClickedElement;
    if (el) {
      const htmlId = el.id;
      const htmlName = el instanceof HTMLInputElement ? el.name : undefined;
      const match = pageDetails.fields.find(
        (f) => (htmlId && f.htmlID === htmlId) || (htmlName && f.htmlName === htmlName),
      );
      targetFieldRef = match?.htmlID ?? match?.htmlName ?? undefined;
    }

    return { pageDetails, targetFieldRef };
  }

  /**
   * Fills the form with the given fill script.
   *
   * @param {AutofillExtensionMessage} message
   */
  private async fillForm({ fillScript, pageDetailsUrl, showAnimations }: AutofillExtensionMessage) {
    if ((document.defaultView || window).location.href !== pageDetailsUrl || !fillScript) {
      return;
    }

    this.blurFocusedFieldAndCloseInlineMenu();
    await this.sendExtensionMessage("updateIsFieldCurrentlyFilling", {
      isFieldCurrentlyFilling: true,
    });
    await this.insertAutofillContentService.fillForm(fillScript, showAnimations ?? true);

    setTimeout(
      () =>
        this.sendExtensionMessage("updateIsFieldCurrentlyFilling", {
          isFieldCurrentlyFilling: false,
        }),
      250,
    );
  }

  /**
   * Applies targeted fields dispatched from the background for this frame.
   * Called when the top-level frame has detected that a targeting rule crosses
   * into this iframe and has routed the inner selectors here.
   *
   * @param message - The extension message containing iframe targeted fields.
   */
  private applyTargetedFields(message: AutofillExtensionMessage): Promise<void> {
    return this.collectAutofillContentService.applyExternalTargetedFields(
      message.iframeTargetedFields ?? [],
    );
  }

  /**
   * Drops cached targeting rules in this frame and re-collects page details so
   * the background's `pageDetailsForTab` is repopulated with the new strategy.
   */
  private handleClearTargetingRulesCache(): void {
    this.collectAutofillContentService.clearCachedTargetingRules();
    void this.collectPageDetails({ command: "collectPageDetails", sender: "autofillInit" });
  }

  /**
   * Blurs the most recently focused field and removes the inline menu. Used
   * in cases where the background unlock or vault item reprompt popout
   * is opened.
   */
  private blurFocusedFieldAndCloseInlineMenu() {
    this.autofillOverlayContentService?.blurMostRecentlyFocusedField(true);
  }

  /**
   * Clears the send collect details message timeout.
   */
  private clearCollectPageDetailsOnLoadTimeout() {
    if (this.collectPageDetailsOnLoadTimeout) {
      clearTimeout(this.collectPageDetailsOnLoadTimeout);
    }
  }

  /**
   * Sets up the extension message listeners for the content script.
   */
  private setupExtensionMessageListeners() {
    chrome.runtime.onMessage.addListener(this.handleExtensionMessage);
    globalThis.document.addEventListener("contextmenu", this.handleContextMenuClick);
  }

  /**
   * Saves a local copy of the last element that was clicked to create the context menu.
   * @param event - The mouse click event.
   */
  private readonly handleContextMenuClick = (event: MouseEvent) => {
    if (EventSecurity.isEventTrusted(event)) {
      this.lastContextMenuClickedElement = event.target as HTMLElement;
    }
  };

  /**
   * Handles the extension messages sent to the content script.
   *
   * @param message - The extension message.
   * @param sender - The message sender.
   * @param sendResponse - The send response callback.
   */
  private handleExtensionMessage = (
    message: AutofillExtensionMessage,
    sender: chrome.runtime.MessageSender,
    sendResponse: (response?: any) => void,
  ): boolean | null => {
    const command: string = message.command;
    const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command);
    if (!handler) {
      return null;
    }

    const messageResponse = handler({ message, sender });
    if (typeof messageResponse === "undefined") {
      return null;
    }

    void Promise.resolve(messageResponse).then((response) => sendResponse(response));
    return true;
  };

  /**
   * Gets the extension message handler for the given command.
   *
   * @param command - The extension message command.
   */
  private getExtensionMessageHandler(command: string): CallableFunction | undefined {
    if (this.autofillOverlayContentService?.messageHandlers?.[command]) {
      return this.autofillOverlayContentService.messageHandlers[command];
    }

    if (this.autofillInlineMenuContentService?.messageHandlers?.[command]) {
      return this.autofillInlineMenuContentService.messageHandlers[command];
    }

    if (this.overlayNotificationsContentService?.messageHandlers?.[command]) {
      return this.overlayNotificationsContentService.messageHandlers[command];
    }

    return this.extensionMessageHandlers[command];
  }

  /**
   * Handles destroying the autofill init content script. Removes all
   * listeners, timeouts, and object instances to prevent memory leaks.
   */
  destroy() {
    this.stopMonitoring();
    globalThis.document.removeEventListener("contextmenu", this.handleContextMenuClick);
    chrome.runtime.onMessage.removeListener(this.handleExtensionMessage);
    this.lastContextMenuClickedElement = null;
    this.autofillOverlayContentService?.destroy();
    this.autofillInlineMenuContentService?.destroy();
    this.overlayNotificationsContentService?.destroy();
  }
}

export default AutofillInit;
