import { Swiper } from "swiper";
import {
  A11y,
  Keyboard,
  Mousewheel,
  Navigation,
  Pagination,
  Scrollbar,
} from "swiper/modules";
import type { SwiperOptions } from "swiper/types";

import Tooltip from "../Tooltip/Tooltip.static";

import {
  CLASS_ACTIVE,
  CLASS_BLEED_RIGHT,
  CLASS_DOT,
  CLASS_SCROLLBAR_DRAG,
  CLASS_SCROLLBAR_HORIZONTAL,
  CLASS_SLIDE,
  CLASS_SLIDE_NEXT,
  CLASS_SLIDE_PREV,
  CLASS_TRACK,
  CLASS_VIEWPORT_WRAPPER,
  SELECTOR_ACTIVE,
  SELECTOR_DOTS,
  SELECTOR_NEXT,
  SELECTOR_PREV,
  SELECTOR_SCROLLBAR,
  SELECTOR_TRACK,
  SELECTOR_VIEWPORT,
} from "./constants";

interface ExternalControlsState {
  isAtStart: boolean;
  isAtEnd: boolean;
}

export const defaultConfig: SwiperOptions = {
  navigation: {
    nextEl: SELECTOR_NEXT,
    prevEl: SELECTOR_PREV,
  },
  pagination: {
    el: SELECTOR_DOTS,
    clickable: true,
    bulletClass: CLASS_DOT,
    bulletActiveClass: CLASS_ACTIVE,
    renderBullet: (index, className) => {
      return `<button aria-label="Page ${index + 1}" role="tab" class="${className}" type="button"></button>`;
    },
  },
  scrollbar: {
    el: SELECTOR_SCROLLBAR,
    draggable: true,
    enabled: true,
    horizontalClass: CLASS_SCROLLBAR_HORIZONTAL,
    dragClass: CLASS_SCROLLBAR_DRAG,
    hide: false,
  },
  slidesPerView: 1.2,
  spaceBetween: 20,
  mousewheel: {
    forceToAxis: true,
    sensitivity: 1,
  },
  a11y: {
    enabled: true,
    scrollOnFocus: false,
    prevSlideMessage: "Predchádzajúci snímok",
    nextSlideMessage: "Nasledujúci snímok",
    paginationBulletMessage: "Prejsť na snímok {index}",
    containerMessage: "Carousel so snímkami",
    containerRoleDescriptionMessage: "carousel",
    itemRoleDescriptionMessage: "snímok",
    firstSlideMessage: "Prvý snímok",
    lastSlideMessage: "Posledný snímok",
    slideLabelMessage: "Snímok",
  },
  loop: false,
  cssMode: false,
  direction: "horizontal",
  breakpoints: {
    1240: {
      slidesPerView: 1.6,
    },
  },
  wrapperClass: CLASS_TRACK,
  slideClass: CLASS_SLIDE,
  slideActiveClass: CLASS_ACTIVE,
  slideNextClass: CLASS_SLIDE_NEXT,
  slidePrevClass: CLASS_SLIDE_PREV,
};

export default class Carousel {
  element: HTMLElement;
  config: SwiperOptions;
  viewport!: HTMLElement;
  track!: HTMLElement;
  instance!: Swiper;
  carouselId?: string;
  private isScrollbarDragging = false;
  private externalControlsState: ExternalControlsState | null = null;
  private resizeObserver?: ResizeObserver;
  private resizeRafId?: number;
  private boundWindowResizeHandler: () => void;
  private bleedResizeHandler?: () => void;
  private static readonly OVERFLOW_EPSILON_PX = 1;

  private getViewportWrapper(): HTMLElement | null {
    return this.element.querySelector(
      `.${CLASS_VIEWPORT_WRAPPER}`,
    ) as HTMLElement | null;
  }

  private getSlidesPerView(): number {
    return Number(this.instance?.params?.slidesPerView) || 1;
  }

  private setScrollbarVisible(visible: boolean): void {
    const scrollbarEl = this.element.querySelector(
      SELECTOR_SCROLLBAR,
    ) as HTMLElement | null;
    if (scrollbarEl) {
      scrollbarEl.style.display = visible ? "" : "none";
    }
  }

  private getSlidesContentWidth(): number {
    if (!this.instance) {
      return this.track?.scrollWidth || 0;
    }

    const slides = Array.from(this.instance.slides) as HTMLElement[];
    const widthsSum = slides.reduce((sum, slide) => sum + slide.offsetWidth, 0);
    const spaceBetween = Number(this.instance.params?.spaceBetween) || 0;
    const gaps = Math.max(slides.length - 1, 0) * spaceBetween;
    const measuredWidth = widthsSum + gaps;

    // Fallback for early lifecycle/test DOM where slides can report 0 width.
    if (widthsSum <= 0 || measuredWidth <= 0) {
      return this.track?.scrollWidth || 0;
    }

    return measuredWidth;
  }

  private getOverflowWidth(viewportWrapper?: HTMLElement): number {
    const isBleedRight = this.element.classList.contains(CLASS_BLEED_RIGHT);
    const containerWidth = isBleedRight
      ? this.element.clientWidth || viewportWrapper?.clientWidth || 0
      : this.viewport?.clientWidth || 0;

    if (containerWidth <= 0) {
      return 0;
    }

    const contentWidth = this.getSlidesContentWidth();

    return contentWidth - containerWidth;
  }

  private hasScrollableContent(viewportWrapper?: HTMLElement): boolean {
    if (!this.instance || !this.instance.params) {
      return false;
    }

    const overflowWidth = this.getOverflowWidth(viewportWrapper);
    if (overflowWidth !== 0) {
      return overflowWidth > Carousel.OVERFLOW_EPSILON_PX;
    }

    // Fallback for hidden/zero-width initialization.
    const slidesCount = this.instance.slides.length;
    const slidesPerView = this.getSlidesPerView();
    return slidesCount > slidesPerView;
  }

  constructor(element: HTMLElement, config?: Partial<SwiperOptions>) {
    this.element = element;
    this.config = {
      ...defaultConfig,
      ...config,
      scrollbar: {
        ...((defaultConfig.scrollbar ?? {}) as object),
        ...((config?.scrollbar ?? {}) as object),
        el: this.element.querySelector(
          SELECTOR_SCROLLBAR,
        ) as HTMLElement | null,
      },
    };

    this.handleSlideChange = this.handleSlideChange.bind(this);
    this.updateTooltipPosition = this.updateTooltipPosition.bind(this);
    this.boundWindowResizeHandler = this.handleWindowResize.bind(this);

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

    requestAnimationFrame(() => {
      this.init();
    });
    return this;
  }

  init() {
    this.getElements();

    if (this.element.hasAttribute("data-swiper-options")) {
      this.getCustomOptions();
    }

    const isBleedRight = this.element.classList.contains(CLASS_BLEED_RIGHT);

    this.instance = new Swiper(this.viewport, {
      ...this.config,
      // Swiper watchOverflow can mis-detect with slides offsets; manage bleed-right overflow ourselves.
      watchOverflow: isBleedRight ? false : this.config.watchOverflow,
      enabled: false,
      modules: [Navigation, Pagination, Scrollbar, A11y, Keyboard, Mousewheel],
      on: {
        slideChange: this.handleSlideChange,
        slideChangeTransitionEnd: () => {
          this.updateExternalControlsState();
        },
        scrollbarDragStart: () => {
          this.isScrollbarDragging = true;
        },
        scrollbarDragEnd: () => {
          this.isScrollbarDragging = false;
          this.updateExternalControlsState(true);
        },
      },
    });

    this.updateCarouselEnabledState();

    if (this.element.classList.contains(CLASS_BLEED_RIGHT)) {
      this.adjustConfigForBleedRight();
      this.fixBleedRightScrollbar();
    }

    // Sync scrollbar position after initialization (important for initialSlide config)
    this.syncScrollbar();

    if (this.instance && typeof this.instance.on === "function") {
      this.instance.on("resize", () => {
        this.updateCarouselEnabledState();
      });
    }

    this.observeVisibilityChanges();
    window.addEventListener("resize", this.boundWindowResizeHandler);

    this.initExternalControls();

    // Trigger a final update to notify listeners (like SameHeight) that carousel is ready
    requestAnimationFrame(() => {
      if (this.instance) {
        this.instance.update();
      }
    });
  }

  private handleWindowResize(): void {
    if (this.resizeRafId) {
      cancelAnimationFrame(this.resizeRafId);
    }

    this.resizeRafId = requestAnimationFrame(() => {
      this.resizeRafId = undefined;

      if (!this.instance || !this.viewport) {
        return;
      }

      if (this.viewport.clientWidth <= 0) {
        return;
      }

      this.instance.update();
      this.updateCarouselEnabledState();
      this.bleedResizeHandler?.();
      this.updateExternalControlsState();
      this.instance.scrollbar?.updateSize();
    });
  }

  /**
   * Sync scrollbar position with current slide.
   * This ensures the scrollbar reflects the correct position when carousel is initialized with initialSlide.
   */
  syncScrollbar() {
    requestAnimationFrame(() => {
      if (this.instance?.scrollbar) {
        this.instance.scrollbar.updateSize();
      }
    });
  }

  private observeVisibilityChanges(): void {
    if (typeof ResizeObserver === "undefined") {
      return;
    }

    this.resizeObserver?.disconnect();
    this.resizeObserver = new ResizeObserver(() => {
      if (!this.instance || !this.viewport) {
        return;
      }

      if (this.viewport.clientWidth <= 0) {
        return;
      }

      this.instance.update();
      this.updateCarouselEnabledState();
      this.instance.scrollbar?.updateSize();
    });

    this.resizeObserver.observe(this.viewport);
  }

  /**
   * Fix scrollbar drag size and position for bleed-right carousels.
   * Overrides Swiper's default scrollbar calculations to work correctly with bleed-right layouts.
   */
  fixBleedRightScrollbar() {
    const updateScrollbar = () => {
      const viewportWrapper = this.getViewportWrapper();
      const scrollbar = this.instance?.scrollbar;
      const swiper = this.instance;

      if (!viewportWrapper || !scrollbar || !scrollbar.dragEl || !swiper) {
        return;
      }

      const hasScrollableContent = this.hasScrollableContent(viewportWrapper);

      if (!hasScrollableContent) {
        this.setScrollbarVisible(false);
        return;
      }

      this.setScrollbarVisible(true);

      const scrollbarWidth = scrollbar.el.offsetWidth;
      const viewportWidth =
        this.instance?.width ||
        this.element.clientWidth ||
        viewportWrapper.clientWidth;
      const contentWidth = this.getSlidesContentWidth();
      const minTranslate =
        typeof swiper.minTranslate === "function" ? swiper.minTranslate() : 0;
      const maxTranslate =
        typeof swiper.maxTranslate === "function" ? swiper.maxTranslate() : 0;
      const translateRange = Math.max(Math.abs(maxTranslate - minTranslate), 0);
      const normalizedContentWidth = Math.max(
        contentWidth,
        viewportWidth + translateRange,
      );

      const visibleRatio =
        normalizedContentWidth > 0
          ? Math.min(viewportWidth / normalizedContentWidth, 1)
          : 1;
      const dragSize = visibleRatio * scrollbarWidth;
      const finalDragSize = Math.max(dragSize, 30);

      scrollbar.dragEl.style.width = `${finalDragSize}px`;
      scrollbar.dragEl.style.display = "";

      const scrollRatio = Math.min(Math.max(swiper.progress || 0, 0), 1);
      const maxDragTranslate = Math.max(scrollbarWidth - finalDragSize, 0);
      const dragTranslateX = scrollRatio * maxDragTranslate;

      scrollbar.dragEl.style.transform = `translate3d(${dragTranslateX}px, 0, 0)`;
    };

    // Initial update (double requestAnimationFrame ensures DOM is fully ready)
    requestAnimationFrame(() => {
      requestAnimationFrame(updateScrollbar);
    });

    // Keep scrollbar in sync with carousel state changes
    this.instance.on("progress", updateScrollbar);
    this.instance.on("slideChange", updateScrollbar);
    this.instance.on("resize", updateScrollbar);
    this.instance.on("update", updateScrollbar);
    this.instance.on("setTranslate", updateScrollbar);
  }

  getElements() {
    this.viewport = this.element.querySelector(SELECTOR_VIEWPORT)!;
    this.track = this.viewport.querySelector(SELECTOR_TRACK)!;

    const paginationEl = this.element.querySelector(
      SELECTOR_DOTS,
    ) as HTMLElement | null;
    if (paginationEl && !paginationEl.hasAttribute("role")) {
      paginationEl.setAttribute("role", "tablist");
    }

    const elements = {
      navigation: {
        ...((this.config.navigation ?? {}) as object),
        nextEl: this.element.querySelector(SELECTOR_NEXT) as HTMLElement | null,
        prevEl: this.element.querySelector(SELECTOR_PREV) as HTMLElement | null,
      },
      pagination: {
        ...((this.config.pagination ?? {}) as object),
        el: paginationEl,
        clickable: true,
        bulletClass: CLASS_DOT,
        bulletActiveClass: CLASS_ACTIVE,
      },
    };

    this.config = {
      ...this.config,
      ...elements,
    };
  }

  getCustomOptions() {
    const passedSwiperOptions = this.element.getAttribute(
      "data-swiper-options",
    );
    if (!passedSwiperOptions) return;
    const parsedSwiperOptions = JSON.parse(passedSwiperOptions);

    this.config = {
      ...this.config,
      ...parsedSwiperOptions,
    };
  }

  /**
   * Enable or disable the carousel based on whether content requires scrolling.
   * Carousel is enabled only when there are more slides than can fit in the current viewport.
   */
  private updateCarouselEnabledState(): void {
    if (!this.instance || !this.instance.params) return;

    const viewportWrapper = this.getViewportWrapper();
    const hasScrollableContent = this.hasScrollableContent(
      viewportWrapper || undefined,
    );

    if (hasScrollableContent) {
      this.instance.enable();
    } else {
      this.instance.disable();
    }

    this.setScrollbarVisible(this.instance.enabled);
  }

  private applyBleedInsets(viewportWrapper: HTMLElement): void {
    if (!this.instance || !this.instance.params) {
      return;
    }

    const containerWidth =
      this.element.clientWidth || viewportWrapper.clientWidth || 0;
    const viewportWidth =
      document.documentElement?.clientWidth || window.innerWidth || 0;
    const wrapperRect = viewportWrapper.getBoundingClientRect();
    const rootRect = this.element.getBoundingClientRect();
    const stableLeftInsetSource =
      rootRect.left > 0 ? rootRect.left : wrapperRect.left;
    const leftInset = Math.max(Math.floor(stableLeftInsetSource), 0);
    const shellWidth = Math.max(Math.floor(viewportWidth), containerWidth, 0);
    const shellMarginLeft = -leftInset;
    const bleedViewportWidth = Math.max(
      shellWidth - leftInset,
      containerWidth,
      0,
    );

    const projectedContentWidth = this.getSlidesContentWidth();
    const projectedOverflowWidth = projectedContentWidth - containerWidth;
    const slidesPerView = this.getSlidesPerView();
    const slidesCount = this.instance.slides.length;
    const hasIntrinsicOverflow =
      projectedContentWidth > 0
        ? projectedOverflowWidth > Carousel.OVERFLOW_EPSILON_PX
        : slidesCount > slidesPerView;
    const shouldDisableBleed = !hasIntrinsicOverflow;
    const shouldEnableBleedClass = !shouldDisableBleed;
    const hadBleedClass = this.element.classList.contains(CLASS_BLEED_RIGHT);
    const nextBleedViewportWidth = shouldDisableBleed
      ? "100%"
      : `${bleedViewportWidth}px`;
    const nextBleedMarginLeft = shouldDisableBleed ? "0px" : `${leftInset}px`;
    const nextBleedShellWidth = shouldDisableBleed ? "100%" : `${shellWidth}px`;
    const nextBleedShellMarginLeft = shouldDisableBleed
      ? "0px"
      : `${shellMarginLeft}px`;
    const geometryChanged =
      hadBleedClass !== shouldEnableBleedClass ||
      this.instance.params.width !==
        (shouldDisableBleed ? undefined : containerWidth) ||
      this.element.style.getPropertyValue("--carousel-bleed-viewport-width") !==
        nextBleedViewportWidth ||
      this.element.style.getPropertyValue("--carousel-bleed-margin-left") !==
        nextBleedMarginLeft ||
      this.element.style.getPropertyValue("--carousel-bleed-shell-width") !==
        nextBleedShellWidth ||
      this.element.style.getPropertyValue(
        "--carousel-bleed-shell-margin-left",
      ) !== nextBleedShellMarginLeft;

    this.element.classList.toggle(CLASS_BLEED_RIGHT, shouldEnableBleedClass);

    const baseWidth = shouldDisableBleed ? undefined : containerWidth;
    this.instance.params.width = baseWidth;
    if (this.instance.originalParams) {
      this.instance.originalParams.width = baseWidth;
    }

    this.instance.params.slidesOffsetBefore = 0;
    this.instance.params.slidesOffsetAfter = 0;
    this.element.style.setProperty(
      "--carousel-bleed-viewport-width",
      nextBleedViewportWidth,
    );
    this.element.style.setProperty(
      "--carousel-bleed-margin-left",
      nextBleedMarginLeft,
    );
    this.element.style.setProperty(
      "--carousel-bleed-shell-width",
      nextBleedShellWidth,
    );
    this.element.style.setProperty(
      "--carousel-bleed-shell-margin-left",
      nextBleedShellMarginLeft,
    );

    if (geometryChanged) {
      this.instance.update();
    }
    this.updateCarouselEnabledState();
  }

  /**
   * Configure bleed-right carousel to extend beyond the container edge.
   * Calculates the exact offset needed for the carousel to reach the viewport edge
   * while keeping the last slide aligned with the container edge when scrolled to the end.
   * When slidesPerView is a whole number (integer), the bleed effect is disabled.
   */
  adjustConfigForBleedRight() {
    requestAnimationFrame(() => {
      if (!this.instance) return;

      const viewportWrapper = this.getViewportWrapper();
      if (!viewportWrapper) return;

      const updateBleedState = () => {
        this.applyBleedInsets(viewportWrapper);
      };

      this.bleedResizeHandler = updateBleedState;

      updateBleedState();

      this.instance.on("resize", updateBleedState);
    });
  }

  /**
   * Handle carousel slide change events.
   * Updates tooltip positions and accessibility states for the active slide.
   */
  handleSlideChange() {
    const activeSlide = this.track.querySelector(SELECTOR_ACTIVE);
    const nonActiveSlides = this.track.querySelectorAll(
      `.${CLASS_SLIDE}:not(${SELECTOR_ACTIVE})`,
    );

    if (activeSlide) {
      this.updateTooltipPosition(activeSlide as HTMLElement);
    }

    if (nonActiveSlides.length > 0) {
      this.hideAllTooltips(nonActiveSlides);
    }

    this.updateExternalControlsState();
  }

  /**
   * Update tooltip positions for elements within a slide.
   */
  updateTooltipPosition(element: HTMLElement) {
    const tooltipTriggers = element.querySelectorAll(
      '[data-tooltip-trigger="true"]',
    );

    if (tooltipTriggers.length > 0) {
      tooltipTriggers.forEach((tooltipElement) => {
        const tooltipId = tooltipElement.getAttribute("aria-describedby");
        if (!tooltipId) return;
        const tooltipDiv = document.getElementById(tooltipId);
        if (!tooltipDiv) return;
        const tooltipInstance = Tooltip.getInstance(tooltipDiv);

        if (tooltipInstance) {
          tooltipInstance.update();
        }
      });
    }
  }

  /**
   * Hide all tooltips for non-active slides.
   */
  hideAllTooltips(elements: NodeListOf<Element>) {
    elements.forEach((element) => {
      const allTooltips = element.querySelectorAll(
        '[data-tooltip-trigger="true"]',
      );

      if (allTooltips.length > 0) {
        allTooltips.forEach((tooltipElement) => {
          const tooltipId = tooltipElement.getAttribute("aria-describedby");
          if (!tooltipId) return;
          const tooltipDiv = document.getElementById(tooltipId);
          if (!tooltipDiv) return;
          const tooltipInstance = Tooltip.getInstance(tooltipDiv);

          if (tooltipInstance) {
            tooltipInstance.hide();
            (tooltipElement as HTMLElement).blur();
          }
        });
      }
    });
  }

  /**
   * Initialize external navigation controls that reference this carousel via data attributes.
   * Supports prev/next buttons placed anywhere in the DOM.
   */
  initExternalControls() {
    const carouselId = this.element.dataset.carouselId || this.element.id;

    if (!carouselId) {
      return;
    }

    this.carouselId = carouselId;

    const controlElements = document.querySelectorAll(
      `[data-carousel-controls="${carouselId}"]`,
    );

    controlElements.forEach((control) => {
      const htmlControl = control as HTMLElement;

      if (htmlControl.hasAttribute("data-carousel-initialized")) {
        return;
      }

      if ((htmlControl as any)._carouselClickHandler) {
        htmlControl.removeEventListener(
          "click",
          (htmlControl as any)._carouselClickHandler,
        );
      }

      const action = htmlControl.dataset.carouselAction;

      const clickHandler = (e: Event) => {
        e.preventDefault();
        if (htmlControl.hasAttribute("disabled")) {
          return;
        }
        if (action === "next") {
          this.slideNext();
        } else if (action === "prev") {
          this.slidePrev();
        }
      };

      if (action === "next" || action === "prev") {
        htmlControl.addEventListener("click", clickHandler);

        htmlControl.setAttribute(
          "aria-label",
          action === "next"
            ? (typeof this.config.a11y === "object"
                ? this.config.a11y?.nextSlideMessage
                : undefined) || "Nasledujúci snímok"
            : (typeof this.config.a11y === "object"
                ? this.config.a11y?.prevSlideMessage
                : undefined) || "Predchádzajúci snímok",
        );
        htmlControl.setAttribute("type", "button");

        (htmlControl as any)._carouselClickHandler = clickHandler;
      }

      htmlControl.setAttribute("data-carousel-initialized", "true");
    });

    this.updateExternalControlsState();
  }

  /**
   * Update the disabled state of external navigation controls.
   * Controls are disabled at the start/end of the carousel based on slide position.
   */
  updateExternalControlsState(force = false) {
    if (!this.carouselId || !this.instance) {
      return;
    }

    if (!this.instance.params) {
      return;
    }

    if (this.isScrollbarDragging && !force) {
      return;
    }

    const isDisabled = !this.instance.enabled;
    const isAtStart = isDisabled || this.instance.isBeginning;
    const isAtEnd = isDisabled || this.instance.isEnd;

    this.externalControlsState = { isAtStart, isAtEnd };

    this.applyExternalControlsState(this.externalControlsState);
  }

  private applyExternalControlsState(state: ExternalControlsState) {
    const { isAtStart, isAtEnd } = state;

    const prevControls = document.querySelectorAll(
      `[data-carousel-controls="${this.carouselId}"][data-carousel-action="prev"]`,
    );

    const nextControls = document.querySelectorAll(
      `[data-carousel-controls="${this.carouselId}"][data-carousel-action="next"]`,
    );

    prevControls.forEach((control) => {
      const htmlControl = control as HTMLElement;
      if (isAtStart) {
        htmlControl.setAttribute("disabled", "true");
        htmlControl.setAttribute("aria-disabled", "true");
        htmlControl.style.cursor = "not-allowed";
      } else {
        htmlControl.removeAttribute("disabled");
        htmlControl.setAttribute("aria-disabled", "false");
        htmlControl.style.cursor = "pointer";
      }
    });

    nextControls.forEach((control) => {
      const htmlControl = control as HTMLElement;
      if (isAtEnd) {
        htmlControl.setAttribute("disabled", "true");
        htmlControl.setAttribute("aria-disabled", "true");
        htmlControl.style.cursor = "not-allowed";
      } else {
        htmlControl.removeAttribute("disabled");
        htmlControl.setAttribute("aria-disabled", "false");
        htmlControl.style.cursor = "pointer";
      }
    });
  }

  /**
   * Navigate to the next slide.
   */
  slideNext() {
    if (this.instance) {
      this.instance.slideNext();
    }
  }

  /**
   * Navigate to the previous slide.
   */
  slidePrev() {
    if (this.instance) {
      this.instance.slidePrev();
    }
  }

  /**
   * Get the current active slide index.
   */
  getActiveIndex(): number {
    return this.instance ? this.instance.activeIndex : 0;
  }

  /**
   * Register a callback for slide change events.
   */
  onSlideChange(callback: (activeIndex: number) => void) {
    if (this.instance) {
      this.instance.on("slideChange", () => {
        callback(this.instance.activeIndex);
      });
    }
  }

  destroy() {
    // Clean up external controls
    if (this.carouselId) {
      const controlElements = document.querySelectorAll(
        `[data-carousel-controls="${this.carouselId}"]`,
      );
      controlElements.forEach((control) => {
        const htmlControl = control as HTMLElement;
        if ((htmlControl as any)._carouselClickHandler) {
          htmlControl.removeEventListener(
            "click",
            (htmlControl as any)._carouselClickHandler,
          );
          delete (htmlControl as any)._carouselClickHandler;
        }
        htmlControl.removeAttribute("data-carousel-initialized");
      });
    }

    window.removeEventListener("resize", this.boundWindowResizeHandler);

    if (this.resizeRafId) {
      cancelAnimationFrame(this.resizeRafId);
      this.resizeRafId = undefined;
    }

    if (this.instance && this.bleedResizeHandler) {
      this.instance.off("resize", this.bleedResizeHandler);
      this.bleedResizeHandler = undefined;
    }

    if (this.instance) {
      this.instance.destroy();
    }

    this.isScrollbarDragging = false;
    this.externalControlsState = null;
    this.resizeObserver?.disconnect();
    this.resizeObserver = undefined;

    if ((this.element as any).ODS_Carousel === this) {
      delete (this.element as any).ODS_Carousel;
    }
  }

  update() {
    if (this.instance) {
      this.instance.update();
    }
  }

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

  /**
   * Find a carousel instance by its ID or data-carousel-id attribute.
   */
  static getInstanceById(carouselId: string): Carousel | null {
    const element =
      document.querySelector(`[data-carousel-id="${carouselId}"]`) ||
      document.getElementById(carouselId);
    return element ? this.getInstance(element as HTMLElement) : null;
  }
}
