import {
  InlineMenuElementPosition,
  InlineMenuPosition,
} from "../../../background/abstractions/overlay.background";
import { AutofillExtensionMessage } from "../../../content/abstractions/autofill-init";
import {
  AutofillOverlayElement,
  AutofillOverlayElementType,
} from "../../../enums/autofill-overlay.enum";
import {
  sendExtensionMessage,
  generateRandomCustomElementName,
  setElementStyles,
  requestIdleCallbackPolyfill,
} from "../../../utils";
import {
  InlineMenuExtensionMessageHandlers,
  AutofillInlineMenuContentService as AutofillInlineMenuContentServiceInterface,
} from "../abstractions/autofill-inline-menu-content.service";
import { AutofillInlineMenuButtonIframe } from "../iframe-content/autofill-inline-menu-button-iframe";
import { AutofillInlineMenuListIframe } from "../iframe-content/autofill-inline-menu-list-iframe";

const experienceValidationBackoffThresholds = {
  topLayer: {
    countLimit: 5,
    timeSpanLimit: 5000,
  },
  popoverAttribute: {
    countLimit: 10,
    timeSpanLimit: 5000,
  },
};

type BackoffCheckType = keyof typeof experienceValidationBackoffThresholds;

export class AutofillInlineMenuContentService implements AutofillInlineMenuContentServiceInterface {
  private readonly sendExtensionMessage = sendExtensionMessage;
  private readonly generateRandomCustomElementName = generateRandomCustomElementName;
  private readonly setElementStyles = setElementStyles;
  private isFirefoxBrowser =
    globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 ||
    globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
  private buttonElement?: HTMLElement;
  private buttonIframe?: AutofillInlineMenuButtonIframe;
  private listElement?: HTMLElement;
  private listIframe?: AutofillInlineMenuListIframe;
  private htmlMutationObserver: MutationObserver;
  private bodyMutationObserver: MutationObserver;
  private inlineMenuElementsMutationObserver: MutationObserver;
  private containerElementMutationObserver: MutationObserver;
  private refreshCountWithinTimeThreshold: { [key in BackoffCheckType]: number } = {
    topLayer: 0,
    popoverAttribute: 0,
  };
  private lastTrackedTimestamp = {
    topLayer: Date.now(),
    popoverAttribute: Date.now(),
  };
  /**
   * Distinct from preventing inline menu script injection, this is for cases
   * where the page is subsequently determined to be risky.
   */
  private inlineMenuEnabled = true;
  private mutationObserverIterations = 0;
  private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout | null = null;
  private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout | null = null;
  private lastElementOverrides: WeakMap<Element, number> = new WeakMap();
  private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = {
    all: "initial",
    position: "fixed",
    display: "block",
    zIndex: "2147483647",
  };
  private readonly extensionMessageHandlers: InlineMenuExtensionMessageHandlers = {
    closeAutofillInlineMenu: ({ message }) => this.closeInlineMenu(message),
    appendAutofillInlineMenuToDom: ({ message }) => this.appendInlineMenuElements(message),
  };

  private isMonitoring = false;

  constructor() {
    /**
     * Sets up mutation observers for the inline menu elements, the menu container, and
     * the document element. The mutation observers are used to remove any styles that
     * are added to the inline menu elements by the website. They are also used to ensure
     * that the inline menu elements are always present at the bottom of the menu container.
     */
    this.htmlMutationObserver = new MutationObserver(this.handlePageMutations);
    this.bodyMutationObserver = new MutationObserver(this.handlePageMutations);
    this.inlineMenuElementsMutationObserver = new MutationObserver(
      this.handleInlineMenuElementMutationObserverUpdate,
    );
    this.containerElementMutationObserver = new MutationObserver(
      this.handleContainerElementMutationObserverUpdate,
    );
  }

  /**
   * Attaches the page-attribute mutation observers that guard the inline menu
   * against website style interference. Idempotent.
   */
  startMonitoring(): void {
    if (this.isMonitoring) {
      return;
    }
    this.isMonitoring = true;
    this.observePageAttributes();
  }

  /**
   * Detaches the page-attribute observers, clears monitoring-scoped
   * iteration counters, and removes the inline-menu UI from the DOM so
   * a logged-out user does not see an orphaned iframe. The iframes
   * themselves are torn down in `destroy()`. Idempotent.
   */
  stopMonitoring(): void {
    this.isMonitoring = false;
    this.closeInlineMenu();
    this.unobservePageAttributes();
    this.unobserveCustomElements();
    this.unobserveContainerElement();
    this.clearPersistentLastChildOverrideTimeout();
    if (this.mutationObserverIterationsResetTimeout) {
      clearTimeout(this.mutationObserverIterationsResetTimeout);
      this.mutationObserverIterationsResetTimeout = null;
    }
    this.mutationObserverIterations = 0;
    this.refreshCountWithinTimeThreshold.topLayer = 0;
    this.refreshCountWithinTimeThreshold.popoverAttribute = 0;
  }

  /**
   * Returns the message handlers for the autofill inline menu content service.
   */
  get messageHandlers() {
    return this.extensionMessageHandlers;
  }

  /**
   * Identifies if the passed element corresponds to the inline menu button or list.
   *
   * @param element  - The element being checked
   */
  isElementInlineMenu(element: HTMLElement) {
    return element === this.buttonElement || element === this.listElement;
  }

  /**
   * Checks if the inline menu button is visible at the top frame.
   */
  private async isInlineMenuButtonVisible() {
    // `isConnected` check for extra defensiveness in cases where the host element is disconnected
    return (
      !!this.buttonElement?.isConnected &&
      (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true
    );
  }

  /**
   * Checks if the inline menu list if visible at the top frame.
   */
  private async isInlineMenuListVisible() {
    // `isConnected` check for extra defensiveness in cases where the host element is disconnected
    return (
      !!this.listElement?.isConnected &&
      (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true
    );
  }

  /**
   * Removes the autofill inline menu from the page. This will initially
   * unobserve the menu container to ensure the mutation observer no
   * longer triggers.
   */
  private closeInlineMenu = (message?: AutofillExtensionMessage) => {
    if (message?.overlayElement === AutofillOverlayElement.Button) {
      this.closeInlineMenuButton();
      return;
    }

    if (message?.overlayElement === AutofillOverlayElement.List) {
      this.closeInlineMenuList();
      return;
    }

    this.unobserveContainerElement();
    this.closeInlineMenuButton();
    this.closeInlineMenuList();
  };

  /**
   * Removes the inline menu button from the DOM if it is currently present. Will
   * also remove the inline menu reposition event listeners.
   */
  private closeInlineMenuButton() {
    if (this.buttonElement) {
      this.buttonElement.remove();
      void this.sendExtensionMessage("autofillOverlayElementClosed", {
        overlayElement: AutofillOverlayElement.Button,
      });
    }
  }

  /**
   * Removes the inline menu list from the DOM if it is currently present.
   */
  private closeInlineMenuList() {
    if (this.listElement) {
      this.listElement.remove();
      void this.sendExtensionMessage("autofillOverlayElementClosed", {
        overlayElement: AutofillOverlayElement.List,
      });
    }
  }

  /**
   * Updates the position of both the inline menu button and inline menu list.
   */
  private async appendInlineMenuElements({ overlayElement }: AutofillExtensionMessage) {
    if (!this.inlineMenuEnabled) {
      return;
    }

    if (overlayElement === AutofillOverlayElement.Button) {
      return this.appendButtonElement();
    }

    return this.appendListElement();
  }

  /**
   * Updates the position of the inline menu button.
   */
  private async appendButtonElement(): Promise<void> {
    if (!this.buttonElement) {
      this.buttonElement = this.createButtonElement();
      this.updateCustomElementDefaultStyles(this.buttonElement);
    }

    if (!(await this.isInlineMenuButtonVisible())) {
      this.appendInlineMenuElementToDom(this.buttonElement);
      this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.Button, true);
      this.buttonElement.showPopover();
    }
  }

  /**
   * Updates the position of the inline menu list.
   */
  private async appendListElement(): Promise<void> {
    if (!this.listElement) {
      this.listElement = this.createListElement();
      this.updateCustomElementDefaultStyles(this.listElement);
    }

    if (!(await this.isInlineMenuListVisible())) {
      this.appendInlineMenuElementToDom(this.listElement);
      this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.List, true);
      this.listElement.showPopover();
    }
  }

  /**
   * Updates the visibility status of the inline menu element within the background script.
   *
   * @param overlayElement - The inline menu element to update the visibility status for.
   * @param isVisible - The visibility status to update the inline menu element to.
   */
  private updateInlineMenuElementIsVisibleStatus(
    overlayElement: AutofillOverlayElementType,
    isVisible: boolean,
  ) {
    void this.sendExtensionMessage("updateAutofillInlineMenuElementIsVisibleStatus", {
      overlayElement,
      isVisible,
    });
  }

  /**
   * Appends the inline menu element to the menu container. This method will also
   * observe the menu container to ensure that the inline menu element is not
   * interfered with by any DOM changes.
   *
   * @param element - The inline menu element to append to the menu container.
   */
  private appendInlineMenuElementToDom(element: HTMLElement) {
    const containerElement = this.getInlineMenuContainerElement();
    this.observeContainerElement(containerElement);
    containerElement.appendChild(element);
  }

  /**
   * Determines which element the inline menu should be appended to. When the
   * focused field lives inside a modal, the menu must be appended within that
   * modal so it shares the modal's DOM subtree. Modals commonly run a focus trap
   * that forces focus back inside their subtree; a menu appended to
   * `document.body` sits outside it, so the trap steals focus the moment the user
   * interacts with the menu and both clicks and keyboard navigation fail.
   * Both native top-layer `<dialog>` modals and ARIA modals
   * (`role="dialog"`/`role="alertdialog"` with `aria-modal="true"`) are
   * handled; otherwise the menu is appended to `document.body`.
   */
  private getInlineMenuContainerElement(): HTMLElement {
    const activeElement = globalThis.document.activeElement;

    const parentDialogElement = activeElement?.closest("dialog");

    if (parentDialogElement?.open && parentDialogElement.matches(":modal")) {
      return parentDialogElement;
    }

    const parentAriaModalElement = activeElement?.closest<HTMLElement>(
      '[role="dialog"][aria-modal="true"], [role="alertdialog"][aria-modal="true"]',
    );

    if (parentAriaModalElement) {
      return parentAriaModalElement;
    }

    return globalThis.document.body;
  }

  /**
   * Creates the autofill inline menu button element. Will not attempt
   * to create the element if it already exists in the DOM.
   */
  private createButtonElement() {
    if (this.isFirefoxBrowser) {
      this.buttonElement = globalThis.document.createElement("div");
      this.buttonElement.setAttribute("popover", "manual");
      this.buttonIframe = new AutofillInlineMenuButtonIframe(this.buttonElement);

      return this.buttonElement;
    }

    const customElementName = this.generateRandomCustomElementName();
    const self = this;
    globalThis.customElements?.define(
      customElementName,
      class extends HTMLElement {
        constructor() {
          super();
          self.buttonIframe = new AutofillInlineMenuButtonIframe(this);
        }
      },
    );

    this.buttonElement = globalThis.document.createElement(customElementName);
    this.buttonElement.setAttribute("popover", "manual");
    return this.buttonElement;
  }

  /**
   * Creates the autofill inline menu list element. Will not attempt
   * to create the element if it already exists in the DOM.
   */
  private createListElement() {
    if (this.isFirefoxBrowser) {
      this.listElement = globalThis.document.createElement("div");
      this.listElement.setAttribute("popover", "manual");
      this.listIframe = new AutofillInlineMenuListIframe(this.listElement);

      return this.listElement;
    }

    const customElementName = this.generateRandomCustomElementName();
    const self = this;
    globalThis.customElements?.define(
      customElementName,
      class extends HTMLElement {
        constructor() {
          super();
          self.listIframe = new AutofillInlineMenuListIframe(this);
        }
      },
    );

    this.listElement = globalThis.document.createElement(customElementName);
    this.listElement.setAttribute("popover", "manual");
    return this.listElement;
  }

  /**
   * Updates the default styles for the custom element. This method will
   * remove any styles that are added to the custom element by other methods.
   *
   * @param element - The custom element to update the default styles for.
   */
  private updateCustomElementDefaultStyles(element: HTMLElement) {
    this.unobserveCustomElements();

    this.setElementStyles(element, this.customElementDefaultStyles, true);

    this.observeCustomElements();
  }

  /**
   * Sets up mutation observers to verify that the inline menu
   * elements are not modified by the website.
   */
  private observeCustomElements() {
    if (this.buttonElement) {
      this.inlineMenuElementsMutationObserver?.observe(this.buttonElement, {
        attributes: true,
      });
    }

    if (this.listElement) {
      this.inlineMenuElementsMutationObserver?.observe(this.listElement, { attributes: true });
    }
  }

  /**
   * Sets up mutation observers to verify that the page `html` and `body` attributes
   * are not altered in a way that would impact safe display of the inline menu.
   *
   * Only attributes that directly affect the computed opacity/visibility of the inline
   * menu container are observed. This minimizes calls to `getComputedStyle` (indirectly
   * via `checkPageRisks`), which blocks layout and freezes the browser.
   */
  private observePageAttributes() {
    // FIXME: find a more efficient means to monitor attribute changes so that indirect
    // uses of attributes can be monitored without impacting the layout hot path.
    const attributeFilter = ["style", "hidden", "popover", "width", "height"];

    if (document.documentElement) {
      this.htmlMutationObserver?.observe(document.documentElement, {
        attributes: true,
        attributeFilter,
      });
    }

    if (document.body) {
      this.bodyMutationObserver?.observe(document.body, {
        attributes: true,
        attributeFilter,
      });
    }
  }

  private unobservePageAttributes() {
    this.htmlMutationObserver?.disconnect();
    this.bodyMutationObserver?.disconnect();
  }

  /**
   * Disconnects the mutation observers that are used to verify that the inline menu
   * elements are not modified by the website.
   */
  private unobserveCustomElements() {
    this.inlineMenuElementsMutationObserver?.disconnect();
  }

  /**
   * Sets up a mutation observer for the element which contains the inline menu.
   */
  private observeContainerElement(element: HTMLElement) {
    this.containerElementMutationObserver?.observe(element, { childList: true });
  }

  /**
   * Disconnects the mutation observer for the element which contains the inline menu.
   */
  private unobserveContainerElement() {
    this.containerElementMutationObserver?.disconnect();
  }

  /**
   * Handles the mutation observer update for the inline menu elements. This method will
   * remove any attributes or styles that might be added to the inline menu elements by
   * a separate process within the website where this script is injected.
   *
   * @param mutationRecord - The mutation record that triggered the update.
   */
  private handleInlineMenuElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => {
    if (this.isTriggeringExcessiveMutationObserverIterations()) {
      return;
    }

    for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) {
      const record = mutationRecord[recordIndex];
      if (record.type !== "attributes") {
        continue;
      }

      const element = record.target as HTMLElement;
      if (record.attributeName === "popover" && this.inlineMenuEnabled) {
        const attributeValue = element.getAttribute(record.attributeName);
        if (attributeValue !== "manual") {
          this.refreshPopoverAttribute(element);
        }

        continue;
      }

      if (record.attributeName === "style") {
        element.removeAttribute("style");
        this.updateCustomElementDefaultStyles(element);

        continue;
      }

      this.removeModifiedElementAttributes(element);
    }
  };

  /**
   * Removes all elements from a passed inline menu
   * element except for the style attribute.
   *
   * @param element - The element to remove the attributes from.
   */
  private removeModifiedElementAttributes(element: HTMLElement) {
    const attributes = Array.from(element.attributes);
    for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
      const attribute = attributes[attributeIndex];
      if (attribute.name === "style" || attribute.name === "popover") {
        continue;
      }

      element.removeAttribute(attribute.name);
    }
  }

  /**
   * Handles the mutation observer update for the element that contains the inline menu.
   * This method will ensure that the inline menu elements are always present at the
   * bottom of the container.
   */
  private handleContainerElementMutationObserverUpdate = (mutations: MutationRecord[]) => {
    if (
      (!this.buttonElement && !this.listElement) ||
      this.isTriggeringExcessiveMutationObserverIterations()
    ) {
      return;
    }

    const containerElement = mutations[0].target as HTMLElement;
    requestIdleCallbackPolyfill(() => this.processContainerElementMutation(containerElement), {
      timeout: 500,
    });
  };

  private checkPageRisks = async () => {
    const pageIsOpaque = await this.getPageIsOpaque();

    const risksFound = !pageIsOpaque || !this.inlineMenuEnabled;

    if (risksFound) {
      this.closeInlineMenu();
    }

    return risksFound;
  };

  /*
   * Checks for known risks at the page level
   */
  private handlePageMutations = async (mutations: MutationRecord[]) => {
    if (mutations.some(({ type }) => type === "attributes")) {
      await this.checkPageRisks();
    }
  };

  /**
   * Returns the name of the generated container tags for usage internally to avoid
   * unintentional targeting of the owned experience.
   */
  getOwnedTagNames = (): string[] => {
    return [
      ...(this.buttonElement?.tagName ? [this.buttonElement.tagName] : []),
      ...(this.listElement?.tagName ? [this.listElement.tagName] : []),
    ];
  };

  /**
   * Queries and return elements (excluding those of the inline menu) that exist in the
   * top-layer via popover or dialog
   * @param {boolean} [includeCandidates=false] indicate whether top-layer candidate (which
   * may or may not be active) should be included in the query
   */
  getUnownedTopLayerItems = (includeCandidates = false) => {
    const inlineMenuTagExclusions = [
      ...(this.buttonElement?.tagName ? [`:not(${this.buttonElement.tagName})`] : []),
      ...(this.listElement?.tagName ? [`:not(${this.listElement.tagName})`] : []),
      ":popover-open",
    ].join("");
    const selector = [
      ":modal",
      inlineMenuTagExclusions,
      ...(includeCandidates ? ["[popover], dialog"] : []),
    ].join(",");
    const otherTopLayeritems = globalThis.document.querySelectorAll(selector);

    return otherTopLayeritems;
  };

  /**
   * Internally track owned injected experience refreshes as a side-effect
   * of host page interference.
   */
  private checkAndUpdateRefreshCount = (countType: BackoffCheckType) => {
    if (!this.inlineMenuEnabled) {
      return;
    }

    const { countLimit, timeSpanLimit } = experienceValidationBackoffThresholds[countType];
    const now = Date.now();
    const timeSinceLastTrackedRefresh = now - this.lastTrackedTimestamp[countType];
    const currentlyWithinTimeThreshold = timeSinceLastTrackedRefresh <= timeSpanLimit;
    const withinCountThreshold = this.refreshCountWithinTimeThreshold[countType] <= countLimit;

    if (currentlyWithinTimeThreshold) {
      if (withinCountThreshold) {
        this.refreshCountWithinTimeThreshold[countType]++;
      } else {
        // Set inline menu to be off; page is aggressively trying to take top position of top layer
        this.inlineMenuEnabled = false;
        void this.checkPageRisks();

        const warningMessage = chrome.i18n.getMessage("topLayerHijackWarning");
        globalThis.window.alert(warningMessage);
      }
    } else {
      this.lastTrackedTimestamp[countType] = now;
      this.refreshCountWithinTimeThreshold[countType] = 0;
    }
  };

  private refreshPopoverAttribute = (element: HTMLElement) => {
    this.checkAndUpdateRefreshCount("popoverAttribute");
    element.setAttribute("popover", "manual");
    element.showPopover();
  };

  refreshTopLayerPosition = () => {
    if (!this.inlineMenuEnabled) {
      return;
    }

    const otherTopLayerItems = this.getUnownedTopLayerItems();

    // No need to refresh if there are no other top-layer items
    if (!otherTopLayerItems.length) {
      return;
    }

    const buttonInDocument =
      this.buttonElement &&
      (globalThis.document.getElementsByTagName(this.buttonElement.tagName)[0] as HTMLElement);
    const listInDocument =
      this.listElement &&
      (globalThis.document.getElementsByTagName(this.listElement.tagName)[0] as HTMLElement);

    if (buttonInDocument) {
      buttonInDocument.hidePopover();
      buttonInDocument.showPopover();
    }

    if (listInDocument) {
      listInDocument.hidePopover();
      listInDocument.showPopover();
    }

    if (buttonInDocument || listInDocument) {
      this.checkAndUpdateRefreshCount("topLayer");
    }
  };

  /**
   * Checks the opacity of the page body and body parent, since the inline menu experience
   * will inherit the opacity, despite being otherwise encapsulated from styling changes
   * of parents below the body. Assumes the target element will be a direct child of the page
   * `body` (enforced elsewhere).
   */
  private getPageIsOpaque = () => {
    // @TODO for definitive checks, traverse up the node tree from the inline menu container;
    // nodes can exist between `html` and `body`
    /**
     * `querySelectorAll` for (non-standard) cases where the page has additional copies of
     * page nodes that should be unique
     */
    const pageElements = globalThis.document.querySelectorAll("html, body");

    if (!pageElements.length) {
      return false;
    }

    return [...pageElements].every((element) => {
      // These are computed style values, so we don't need to worry about non-float values
      // for `opacity`, here
      const elementOpacity = globalThis.window.getComputedStyle(element)?.opacity || "0";

      // Any value above this is considered "opaque" for our purposes
      const opacityThreshold = 0.6;

      return parseFloat(elementOpacity) > opacityThreshold;
    });
  };

  /**
   * Processes the mutation of the element that contains the inline menu. Will trigger when an
   * idle moment in the execution of the main thread is detected.
   */
  private processContainerElementMutation = async (containerElement: HTMLElement) => {
    // If the page contains risks, tear down and prevent building the inline menu experience.
    const pageRisksFound = await this.checkPageRisks();
    if (pageRisksFound) {
      return;
    }

    if (!this.buttonElement) {
      return;
    }

    const lastChild = containerElement.lastElementChild;
    const secondToLastChild = lastChild?.previousElementSibling;
    const lastChildIsInlineMenuList = lastChild === this.listElement;
    const lastChildIsInlineMenuButton = lastChild === this.buttonElement;
    const secondToLastChildIsInlineMenuButton = secondToLastChild === this.buttonElement;

    if (!lastChild) {
      return;
    }

    const lastChildEncounterCount = this.lastElementOverrides.get(lastChild) || 0;
    if (!lastChildIsInlineMenuList && !lastChildIsInlineMenuButton && lastChildEncounterCount < 3) {
      this.lastElementOverrides.set(lastChild, lastChildEncounterCount + 1);
    }

    const lastChildEncounterCountAfterUpdate = this.lastElementOverrides.get(lastChild) || 0;
    if (lastChildEncounterCountAfterUpdate >= 3) {
      this.handlePersistentLastChildOverride(lastChild);

      return;
    }

    const isInlineMenuListVisible = await this.isInlineMenuListVisible();
    if (
      !lastChild ||
      (lastChildIsInlineMenuList && secondToLastChildIsInlineMenuButton) ||
      (lastChildIsInlineMenuButton && !isInlineMenuListVisible)
    ) {
      return;
    }

    if (
      (lastChildIsInlineMenuList && !secondToLastChildIsInlineMenuButton) ||
      (lastChildIsInlineMenuButton && isInlineMenuListVisible)
    ) {
      if (!this.listElement) {
        return;
      }
      containerElement.insertBefore(this.buttonElement, this.listElement);
      return;
    }

    containerElement.insertBefore(lastChild, this.buttonElement);
  };

  /**
   * Handles the behavior of a persistent child element that is forcing itself to
   * the bottom of the menu container. This method will ensure that the inline menu
   * elements are not obscured by the persistent child element.
   *
   * @param lastChild - The last child of the menu container.
   */
  private handlePersistentLastChildOverride(lastChild: Element) {
    const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex);
    if (lastChildZIndex >= 2147483647) {
      (lastChild as HTMLElement).style.zIndex = "2147483646";
    }

    this.clearPersistentLastChildOverrideTimeout();
    this.handlePersistentLastChildOverrideTimeout = globalThis.setTimeout(
      () => this.verifyInlineMenuIsNotObscured(lastChild),
      500,
    );
  }

  /**
   * Verifies if the last child of the menu container is overlaying the inline menu elements.
   * This is triggered when the last child of the menu container is being forced by some
   * script to be an element other than the inline menu elements.
   *
   * @param lastChild - The last child of the menu container.
   */
  private verifyInlineMenuIsNotObscured = async (lastChild: Element) => {
    const inlineMenuPosition: InlineMenuPosition = await this.sendExtensionMessage(
      "getAutofillInlineMenuPosition",
    );
    const { button, list } = inlineMenuPosition;

    if (!!button && this.elementAtCenterOfInlineMenuPosition(button) === lastChild) {
      this.closeInlineMenu();
      return;
    }

    if (!!list && this.elementAtCenterOfInlineMenuPosition(list) === lastChild) {
      this.closeInlineMenu();
    }
  };

  /**
   * Returns the element present at the center of the inline menu position.
   *
   * @param position - The position of the inline menu element.
   */
  private elementAtCenterOfInlineMenuPosition(position: InlineMenuElementPosition): Element | null {
    return globalThis.document.elementFromPoint(
      position.left + position.width / 2,
      position.top + position.height / 2,
    );
  }

  /**
   * Clears the timeout that is used to verify that the last child of the menu container
   * is not overlaying the inline menu elements.
   */
  private clearPersistentLastChildOverrideTimeout() {
    if (this.handlePersistentLastChildOverrideTimeout) {
      globalThis.clearTimeout(this.handlePersistentLastChildOverrideTimeout);
    }
  }

  /**
   * Identifies if the mutation observer is triggering excessive iterations.
   * Will trigger a blur of the most recently focused field and remove the
   * autofill inline menu if any set mutation observer is triggering
   * excessive iterations.
   */
  private isTriggeringExcessiveMutationObserverIterations() {
    if (this.mutationObserverIterationsResetTimeout) {
      clearTimeout(this.mutationObserverIterationsResetTimeout);
    }

    this.mutationObserverIterations++;
    this.mutationObserverIterationsResetTimeout = setTimeout(
      () => (this.mutationObserverIterations = 0),
      2000,
    );

    if (this.mutationObserverIterations > 100) {
      clearTimeout(this.mutationObserverIterationsResetTimeout);
      this.mutationObserverIterations = 0;
      this.closeInlineMenu();

      return true;
    }

    return false;
  }

  /**
   * Stops monitoring (which closes the inline menu UI) and then tears
   * down the iframe wrappers for terminal disposal.
   */
  destroy() {
    this.stopMonitoring();
    this.buttonIframe?.destroy();
    this.listIframe?.destroy();
  }
}
