import {
  CLASS_ACCORDION_BUTTON,
  CLASS_ACCORDION_ICON,
  CLASS_HIDE_LG_UP,
  CLASS_MOBILE,
  CLASS_MOBILE_OVERLAY,
  CLASS_NAV_BUTTON,
  CLASS_NAV_DROPDOWN,
  CLASS_OVERLAY,
} from "./constants";

interface MegamenuConfig {
  dataToggle?: string;
  activeClass?: string;
  removeOnDestroy?: boolean;
}

const MODAL_OPEN_CLASS = "has-modal";
const LOCKED_SCROLL_TOP_ATTR = "data-lock-scrolltop";
const BRAND_MARK_SELECTOR = "[data-megamenu-brand-mark]";

const defaultConfig: Required<MegamenuConfig> = {
  dataToggle: "data-toggle",
  activeClass: "is-active",
  removeOnDestroy: false,
};

export default class Megamenu {
  element!: HTMLElement;
  config!: Required<MegamenuConfig>;
  toggleButtons!: NodeListOf<HTMLButtonElement>;
  dropdowns!: NodeListOf<HTMLElement>;
  mobileMenuButtons!: HTMLButtonElement[];
  mobilePanels!: HTMLElement[];
  mobileCloseButtons!: HTMLButtonElement[];
  activeMobilePanel!: HTMLElement | null;
  activeMobileTarget!: string | null;
  desktopOverlay!: HTMLButtonElement | null;
  accordionButtons!: NodeListOf<HTMLButtonElement>;
  desktopOverlayClickHandler!: (event: Event) => void;
  mobileCloseHandler!: (event: Event) => void;
  focusTrapHandler!: (event: KeyboardEvent) => void;
  focusTrapElement!: HTMLElement | null;
  lastFocusedElement!: HTMLElement | null;
  isKeyboardUser!: boolean;
  keydownHandler!: (e: KeyboardEvent) => void;
  mousedownHandler!: () => void;
  touchstartHandler!: () => void;
  instanceName!: string;

  private isModalOpen(): boolean {
    return document.body.classList.contains(MODAL_OPEN_CLASS);
  }

  private getLockedScrollTop(): number | null {
    const lockedScrollTopRaw = document.body.getAttribute(
      LOCKED_SCROLL_TOP_ATTR,
    );
    const lockedScrollTop = Number.parseInt(lockedScrollTopRaw || "", 10);

    return Number.isFinite(lockedScrollTop) ? lockedScrollTop : null;
  }

  private getEffectiveScrollY(): number {
    if (!this.isModalOpen()) {
      return window.scrollY;
    }

    return this.getLockedScrollTop() ?? window.scrollY;
  }

  constructor(element: HTMLElement, config: MegamenuConfig = {}) {
    if ((element as any).ODS_Megamenu) {
      return (element as any).ODS_Megamenu;
    }

    this.element = element;
    this.config = { ...defaultConfig, ...config };

    this.handleToggleClick = this.handleToggleClick.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
    this.handleScroll = this.handleScroll.bind(this);
    this.handleAccordionClick = this.handleAccordionClick.bind(this);
    this.handleArrowNavigation = this.handleArrowNavigation.bind(this);
    this.trapFocus = this.trapFocus.bind(this);
    this.handleMobileMenuToggle = this.handleMobileMenuToggle.bind(this);
    this.desktopOverlayClickHandler = () => this.closeAllDropdowns();
    this.mobileCloseHandler = (event: Event) => {
      event.preventDefault();
      this.closeMobileMenu();
    };

    (this.element as any).ODS_Megamenu = this;

    this.init();
    return this;
  }

  static getInstance(el: HTMLElement): Megamenu | null {
    return el && (el as any).ODS_Megamenu ? (el as any).ODS_Megamenu : null;
  }

  init() {
    // Find all nav buttons with aria-controls attribute (dropdown buttons)
    this.toggleButtons = this.element.querySelectorAll(
      `.${CLASS_NAV_BUTTON}[aria-controls]`,
    );
    this.dropdowns = this.element.querySelectorAll(`.${CLASS_NAV_DROPDOWN}`);

    // Resolve an optional instance name so external triggers can target this menu.
    this.instanceName =
      this.element.getAttribute("data-megamenu-name") || this.element.id || "";

    // Find mobile panel elements.
    this.mobilePanels = Array.from(
      this.element.querySelectorAll(`.${CLASS_MOBILE}`),
    );
    this.activeMobilePanel = null;
    this.activeMobileTarget = null;
    this.focusTrapElement = null;

    // Find all mobile toggles both in menu and in document scope.
    const localToggleButtons = Array.from(
      this.element.querySelectorAll(
        `[data-megamenu-mobile-toggle], .${CLASS_HIDE_LG_UP} button`,
      ),
    ) as HTMLButtonElement[];
    const globalToggleButtons = Array.from(
      document.querySelectorAll(`[data-megamenu-mobile-toggle]`),
    ) as HTMLButtonElement[];

    this.mobileMenuButtons = Array.from(
      new Set([...localToggleButtons, ...globalToggleButtons]),
    ).filter((button) => this.shouldUseMobileTrigger(button));

    this.mobileCloseButtons = Array.from(
      this.element.querySelectorAll(
        `.${CLASS_MOBILE_OVERLAY}, [data-megamenu-close-button]`,
      ),
    ) as HTMLButtonElement[];

    this.mobilePanels.forEach((panel) => {
      panel.setAttribute("aria-hidden", "true");
    });
    this.syncMobileButtonState();

    // Find desktop overlay element
    this.desktopOverlay = this.element.querySelector(
      `.${CLASS_OVERLAY}`,
    ) as HTMLButtonElement;

    // Find accordion buttons
    this.accordionButtons = this.element.querySelectorAll(
      `.${CLASS_ACCORDION_BUTTON}`,
    );

    // Add click event listeners to toggle buttons
    this.toggleButtons.forEach((button) => {
      button.addEventListener("click", this.handleToggleClick);
    });

    // Add mobile menu event listeners
    this.mobileMenuButtons.forEach((button) => {
      button.addEventListener("click", this.handleMobileMenuToggle);
    });
    this.mobileCloseButtons.forEach((button) => {
      button.addEventListener("click", this.mobileCloseHandler);
    });

    // Add desktop overlay event listener
    if (this.desktopOverlay) {
      this.desktopOverlay.addEventListener(
        "click",
        this.desktopOverlayClickHandler,
      );
    }

    // Add accordion event listeners
    this.accordionButtons.forEach((button) => {
      button.addEventListener("click", this.handleAccordionClick);
    });

    // Add scoped event listeners to this element
    this.element.addEventListener("keydown", this.handleKeyPress);
    window.addEventListener("scroll", this.handleScroll);

    // Track keyboard usage for proper focus management
    this.isKeyboardUser = false;
    this.keydownHandler = (e) => {
      if (e.key === "Tab" || e.key === " " || e.key === "Enter") {
        this.isKeyboardUser = true;
      }
    };
    this.mousedownHandler = () => {
      this.isKeyboardUser = false;
    };
    this.touchstartHandler = () => {
      this.isKeyboardUser = false;
    };

    this.element.addEventListener("keydown", this.keydownHandler);
    this.element.addEventListener("mousedown", this.mousedownHandler);
    this.element.addEventListener("touchstart", this.touchstartHandler);

    // Set initial tab indices for accessibility
    this.updateTabIndices();
    this.handleScroll();
  }

  shouldUseMobileTrigger(button: HTMLButtonElement) {
    if (this.element.contains(button)) {
      return true;
    }

    const targetName = button.getAttribute("data-megamenu-mobile-for");
    if (!targetName) {
      const allMegamenus = document.querySelectorAll("[data-megamenu]");
      return allMegamenus.length === 1 && allMegamenus[0] === this.element;
    }

    if (!this.instanceName) {
      return false;
    }

    return targetName === this.instanceName;
  }

  getDefaultMobileTarget() {
    const firstPanel = this.mobilePanels[0];
    return firstPanel?.getAttribute("data-megamenu-mobile-panel") || "main";
  }

  getTargetFromElement(control: HTMLElement | null) {
    return (
      control?.getAttribute("data-megamenu-mobile-target") ||
      this.getDefaultMobileTarget()
    );
  }

  getPanelByTarget(target: string) {
    const matchingPanel = this.mobilePanels.find(
      (panel) => panel.getAttribute("data-megamenu-mobile-panel") === target,
    );

    if (matchingPanel) {
      return matchingPanel;
    }

    // Backward compatibility for existing markup with a single mobile panel.
    if (
      target === this.getDefaultMobileTarget() &&
      this.mobilePanels.length === 1
    ) {
      return this.mobilePanels[0];
    }

    return null;
  }

  syncMobileButtonState() {
    this.mobileMenuButtons.forEach((button) => {
      const target = this.getTargetFromElement(button);
      const isActive = this.activeMobileTarget === target;
      button.setAttribute("aria-expanded", isActive ? "true" : "false");
      button.classList.toggle(this.config.activeClass, isActive);
    });
  }

  isInMobilePanel(element: HTMLElement | null) {
    return Boolean(
      element && this.mobilePanels.some((panel) => panel.contains(element)),
    );
  }

  getFocusRestoreTarget() {
    const lastFocused = this.lastFocusedElement;
    if (
      lastFocused &&
      lastFocused.isConnected &&
      !this.isInMobilePanel(lastFocused)
    ) {
      return lastFocused;
    }

    const target = this.activeMobileTarget;
    if (target) {
      const matchingTrigger = this.mobileMenuButtons.find(
        (button) =>
          this.getTargetFromElement(button) === target &&
          !this.isInMobilePanel(button),
      );
      if (matchingTrigger) {
        return matchingTrigger;
      }
    }

    return (
      this.mobileMenuButtons.find((button) => !this.isInMobilePanel(button)) ||
      null
    );
  }

  setMobilePanelState(panel: HTMLElement, isOpen: boolean) {
    panel.classList.toggle(this.config.activeClass, isOpen);
    panel.setAttribute("aria-hidden", isOpen ? "false" : "true");
  }

  handleToggleClick(event: Event) {
    const button = event.currentTarget as HTMLButtonElement;
    const isActive = button.classList.contains(this.config.activeClass);

    // Close all other dropdowns
    this.closeAllDropdowns();

    // Toggle current dropdown
    if (!isActive) {
      this.openDropdown(button);
      // Keep focus on the button that opened the dropdown
      // Users can navigate into the dropdown using arrow keys
    } else {
      // If closing, ensure focus remains on the button
      button.focus();
    }
  }

  handleKeyPress(event: KeyboardEvent) {
    if (event.key === "Escape") {
      // Find the active dropdown button (the one with aria-expanded="true")
      const activeButton = Array.from(this.toggleButtons).find(
        (button) => button.getAttribute("aria-expanded") === "true",
      );

      this.closeAllDropdowns();
      this.closeMobileMenu();
      this.closeAllAccordions();

      // Return focus to button that opened the menu
      if (activeButton) {
        activeButton.focus();
      }
    }

    // Handle Enter and Space keys for dropdown buttons and accordion buttons
    if (event.key === "Enter" || event.key === " ") {
      const target = event.target as HTMLElement;
      if (
        (target.tagName === "BUTTON" && target.hasAttribute("aria-controls")) ||
        target.classList.contains(CLASS_ACCORDION_BUTTON)
      ) {
        event.preventDefault();
        target.click();
      }
    }

    // Handle Home and End keys for navigation within dropdowns
    if (event.key === "Home" || event.key === "End") {
      const dropdown = (event.target as HTMLElement)?.closest(
        `.${CLASS_NAV_DROPDOWN}`,
      ) as HTMLElement;
      if (dropdown) {
        event.preventDefault();
        const focusableElements = Array.from(
          dropdown.querySelectorAll(
            'a, button, [tabindex]:not([tabindex="-1"])',
          ),
        ) as HTMLElement[];

        if (event.key === "Home" && focusableElements.length > 0) {
          focusableElements[0].focus();
        } else if (event.key === "End" && focusableElements.length > 0) {
          focusableElements[focusableElements.length - 1].focus();
        }
      }
    }

    // Arrow key navigation for dropdowns and from buttons to dropdowns
    if (event.key === "ArrowDown") {
      const target = event.target as HTMLElement;

      // If focus is on a dropdown button, move to first item in dropdown
      if (target.tagName === "BUTTON" && target.hasAttribute("aria-controls")) {
        const isExpanded = target.getAttribute("aria-expanded") === "true";
        if (isExpanded) {
          event.preventDefault();
          const dropdown = target.parentElement?.querySelector(
            `.${CLASS_NAV_DROPDOWN}`,
          ) as HTMLElement;
          const firstFocusable = dropdown?.querySelector(
            "a, button",
          ) as HTMLElement;
          if (firstFocusable) {
            firstFocusable.focus();
          }
        }
      }
      // If focus is within a dropdown, navigate within it
      else {
        const dropdown = target.closest(
          `.${CLASS_NAV_DROPDOWN}`,
        ) as HTMLElement;
        if (dropdown) {
          event.preventDefault();
          this.handleArrowNavigation(event, dropdown);
        }
      }
    }

    if (event.key === "ArrowUp") {
      const dropdown = (event.target as HTMLElement)?.closest(
        `.${CLASS_NAV_DROPDOWN}`,
      ) as HTMLElement;
      if (dropdown) {
        event.preventDefault();
        this.handleArrowNavigation(event, dropdown);
      }
    }
  }

  handleAccordionClick(event: Event) {
    const button = event.currentTarget as HTMLButtonElement;
    const isExpanded = button.getAttribute("aria-expanded") === "true";

    // Close all other accordions first
    this.closeAllAccordions();

    // Toggle current accordion
    if (!isExpanded) {
      this.openAccordion(button);
    }
  }

  handleScroll() {
    const topElement = this.element.querySelector(
      '[data-hide-when-sticky="true"]',
    ) as HTMLElement;
    const hasBrandMark = Boolean(
      this.element.querySelector(BRAND_MARK_SELECTOR),
    );
    const effectiveScrollY = this.getEffectiveScrollY();
    const hideThreshold = topElement?.offsetHeight || 15;
    const shouldHideTop = effectiveScrollY > hideThreshold;
    const isTopHidden = this.element.classList.contains("top-hidden");

    if (topElement) {
      if (shouldHideTop && !isTopHidden) {
        topElement.classList.add("is-hidden");
        this.element.classList.add("top-hidden");
      } else if (!shouldHideTop && isTopHidden) {
        topElement.classList.remove("is-hidden");
        this.element.classList.remove("top-hidden");
      }
    } else if (this.element.classList.contains("is-sticky") && hasBrandMark) {
      this.element.classList.toggle("top-hidden", shouldHideTop);
    }
  }

  openDropdown(button: HTMLButtonElement) {
    const dropdown = button.parentElement?.querySelector(
      `.${CLASS_NAV_DROPDOWN}`,
    ) as HTMLElement;

    if (dropdown) {
      button.classList.add(this.config.activeClass);
      button.setAttribute("aria-expanded", "true");
      dropdown.style.display = "block";
      dropdown.classList.add("is-visible");
      this.showDesktopOverlay();
      this.updateTabIndices();
    }
  }

  closeDropdown(button: HTMLButtonElement) {
    const dropdown = button.parentElement?.querySelector(
      `.${CLASS_NAV_DROPDOWN}`,
    ) as HTMLElement;

    if (dropdown) {
      button.classList.remove(this.config.activeClass);
      button.setAttribute("aria-expanded", "false");
      dropdown.classList.remove("is-visible");
      dropdown.style.display = "none";
      this.updateTabIndices();
    }
  }

  closeAllDropdowns() {
    this.toggleButtons.forEach((button) => {
      this.closeDropdown(button);
    });
    this.hideDesktopOverlay();
  }

  handleMobileMenuToggle(event: Event) {
    event.preventDefault();
    const trigger = event.currentTarget as HTMLButtonElement;
    const target = this.getTargetFromElement(trigger);
    const panel = this.getPanelByTarget(target);

    if (!panel) {
      return;
    }

    if (
      this.activeMobileTarget === target &&
      panel.classList.contains(this.config.activeClass)
    ) {
      this.closeMobileMenu();
    } else {
      this.openMobileMenu(target, trigger);
    }
  }

  openMobileMenu(target = this.getDefaultMobileTarget(), source?: HTMLElement) {
    const panel = this.getPanelByTarget(target);

    if (!panel) {
      return;
    }

    // Store the element that opened the menu for focus restoration
    // BEFORE changing panel states, so activeElement is captured correctly.
    this.lastFocusedElement =
      source || (document.activeElement as HTMLElement | null);

    // Move focus out of any panel about to be hidden to prevent
    // the browser from blocking aria-hidden on a focused descendant.
    const focused = document.activeElement as HTMLElement | null;
    if (
      focused &&
      focused !== panel &&
      this.mobilePanels.some((p) => p !== panel && p.contains(focused))
    ) {
      focused.blur();
    }

    // Keep one side panel active at a time.
    this.mobilePanels.forEach((mobilePanel) => {
      this.setMobilePanelState(mobilePanel, mobilePanel === panel);
    });
    this.activeMobilePanel = panel;
    this.activeMobileTarget = target;
    this.syncMobileButtonState();

    document.body.style.overflow = "hidden";
    this.removeFocusTrap();
    this.trapFocus(panel);

    if (this.isKeyboardUser) {
      const firstFocusable = panel.querySelector("button, a") as HTMLElement;
      if (firstFocusable) {
        setTimeout(() => firstFocusable.focus(), 50);
      }
    }

    this.updateTabIndices();
  }

  closeMobileMenu() {
    if (!this.activeMobilePanel) {
      return;
    }

    // Move focus out of the panel BEFORE setting aria-hidden to prevent
    // the browser from blocking aria-hidden on a focused descendant.
    const restoreTarget = this.getFocusRestoreTarget();
    if (restoreTarget) {
      restoreTarget.focus();
    } else {
      (document.activeElement as HTMLElement | null)?.blur?.();
    }

    this.mobilePanels.forEach((panel) => {
      this.setMobilePanelState(panel, false);
    });

    document.body.style.overflow = "";
    this.closeAllAccordions();
    this.removeFocusTrap();

    this.activeMobilePanel = null;
    this.activeMobileTarget = null;
    this.lastFocusedElement = null;
    this.syncMobileButtonState();
    this.updateTabIndices();
  }

  showDesktopOverlay() {
    if (this.desktopOverlay) {
      this.desktopOverlay.classList.add(this.config.activeClass);
    }
  }

  hideDesktopOverlay() {
    if (this.desktopOverlay) {
      this.desktopOverlay.classList.remove(this.config.activeClass);
    }
  }

  openAccordion(button: HTMLButtonElement) {
    const accordionId = button.getAttribute("aria-controls");
    const accordionBody = accordionId
      ? (this.element.querySelector(`#${accordionId}`) as HTMLElement)
      : null;
    const icon = button.querySelector(
      `.${CLASS_ACCORDION_ICON}`,
    ) as HTMLElement;

    if (accordionBody) {
      button.setAttribute("aria-expanded", "true");
      button.classList.add(this.config.activeClass);
      accordionBody.classList.add("is-visible");
      accordionBody.style.display = "block";

      if (icon) {
        icon.classList.add(this.config.activeClass);
      }
    }
  }

  closeAccordion(button: HTMLButtonElement) {
    const accordionId = button.getAttribute("aria-controls");
    const accordionBody = accordionId
      ? (this.element.querySelector(`#${accordionId}`) as HTMLElement)
      : null;
    const icon = button.querySelector(
      `.${CLASS_ACCORDION_ICON}`,
    ) as HTMLElement;

    if (accordionBody) {
      button.setAttribute("aria-expanded", "false");
      button.classList.remove(this.config.activeClass);
      accordionBody.classList.remove("is-visible");
      accordionBody.style.display = "none";

      if (icon) {
        icon.classList.remove(this.config.activeClass);
      }
    }
  }

  closeAllAccordions() {
    this.accordionButtons.forEach((button) => {
      this.closeAccordion(button);
    });
  }

  handleArrowNavigation(event: KeyboardEvent, dropdown: HTMLElement) {
    const focusableElements = Array.from(
      dropdown.querySelectorAll('a, button, [tabindex]:not([tabindex="-1"])'),
    ) as HTMLElement[];

    if (focusableElements.length === 0) return;

    const currentIndex = focusableElements.findIndex(
      (item) => item === event.target,
    );

    let nextIndex;
    if (event.key === "ArrowDown") {
      nextIndex =
        currentIndex < focusableElements.length - 1 ? currentIndex + 1 : 0;
    } else if (event.key === "ArrowUp") {
      nextIndex =
        currentIndex > 0 ? currentIndex - 1 : focusableElements.length - 1;
    } else {
      return; // Don't handle other keys
    }

    // Ensure nextIndex is valid
    if (
      nextIndex >= 0 &&
      nextIndex < focusableElements.length &&
      focusableElements[nextIndex]
    ) {
      focusableElements[nextIndex].focus();
    }
  }

  trapFocus(element: HTMLElement) {
    const focusableElements = element.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
    ) as NodeListOf<HTMLElement>;
    const firstFocusable = focusableElements[0];
    const lastFocusable = focusableElements[focusableElements.length - 1];

    this.focusTrapHandler = (e: KeyboardEvent) => {
      if (e.key === "Tab") {
        if (e.shiftKey) {
          if (document.activeElement === firstFocusable) {
            e.preventDefault();
            lastFocusable.focus();
          }
        } else {
          if (document.activeElement === lastFocusable) {
            e.preventDefault();
            firstFocusable.focus();
          }
        }
      }
    };

    this.focusTrapElement = element;
    element.addEventListener("keydown", this.focusTrapHandler);
  }

  removeFocusTrap() {
    if (this.focusTrapElement && this.focusTrapHandler) {
      this.focusTrapElement.removeEventListener(
        "keydown",
        this.focusTrapHandler,
      );
      this.focusTrapElement = null;
    }
  }

  updateTabIndices() {
    // Set tabindex=-1 for inactive dropdown items
    this.dropdowns.forEach((dropdown) => {
      const isVisible = dropdown.classList.contains("is-visible");
      const focusableElements = dropdown.querySelectorAll(
        "a, button, [tabindex]:not([tabindex='-1'])",
      ) as NodeListOf<HTMLElement>;

      focusableElements.forEach((element) => {
        // Only update tabIndex if element is not already properly set
        const shouldBeFocusable = isVisible;
        if (shouldBeFocusable && element.tabIndex === -1) {
          element.tabIndex = 0;
        } else if (!shouldBeFocusable && element.tabIndex !== -1) {
          element.tabIndex = -1;
        }
      });
    });

    // Also update mobile menu elements when mobile menu is active
    this.mobilePanels.forEach((panel) => {
      const isMobilePanelActive = panel.classList.contains(
        this.config.activeClass,
      );
      const mobilePanelFocusableElements = panel.querySelectorAll(
        "a, button, [tabindex]:not([tabindex='-1'])",
      ) as NodeListOf<HTMLElement>;

      mobilePanelFocusableElements.forEach((element) => {
        element.tabIndex = isMobilePanelActive ? 0 : -1;
      });
    });
  }

  destroy() {
    // Remove event listeners
    this.toggleButtons.forEach((button) => {
      button.removeEventListener("click", this.handleToggleClick);
    });

    // Remove mobile menu event listeners
    this.mobileMenuButtons.forEach((button) => {
      button.removeEventListener("click", this.handleMobileMenuToggle);
    });
    this.mobileCloseButtons.forEach((button) => {
      button.removeEventListener("click", this.mobileCloseHandler);
    });

    // Remove desktop overlay event listener
    if (this.desktopOverlay) {
      this.desktopOverlay.removeEventListener(
        "click",
        this.desktopOverlayClickHandler,
      );
    }

    // Remove accordion event listeners
    this.accordionButtons.forEach((button) => {
      button.removeEventListener("click", this.handleAccordionClick);
    });

    this.element.removeEventListener("keydown", this.handleKeyPress);
    window.removeEventListener("scroll", this.handleScroll);

    // Remove keyboard tracking event listeners
    this.element.removeEventListener("keydown", this.keydownHandler);
    this.element.removeEventListener("mousedown", this.mousedownHandler);
    this.element.removeEventListener("touchstart", this.touchstartHandler);

    // Restore body overflow if menu was open
    document.body.style.overflow = "";
    this.removeFocusTrap();

    // Clean up instance reference
    (this.element as any).ODS_Megamenu = null;

    if (this.config.removeOnDestroy) {
      this.element.remove();
    }
  }
}
