import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import type { CodeWalkthroughStepAttr } from '@redocly/config';

import { getAdjacentValues } from '@redocly/theme/core/utils';
import { ACTIVE_STEP_QUERY_PARAM } from '@redocly/theme/core/constants';

type ActiveStep = string | null;
type CodeWalkthroughStep = CodeWalkthroughStepAttr & {
  compRef?: HTMLElement;
};

export type WalkthroughStepsState = {
  activeStep: ActiveStep;
  setActiveStep: (stepId: ActiveStep) => void;
  register: (element: HTMLElement) => void;
  unregister: (element: HTMLElement) => void;
  lockObserver?: React.RefObject<boolean>;
  filtersElementRef?: React.RefObject<HTMLDivElement | null>;
};

export function useCodeWalkthroughSteps(
  steps: CodeWalkthroughStep[],
  enableDeepLink: boolean,
): WalkthroughStepsState {
  const location = useLocation();
  const navigate = useNavigate();
  const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);

  const observerRef = useRef<IntersectionObserver | null>(null);
  const filtersElementRef = useRef<HTMLDivElement>(null);
  const lockObserver = useRef<boolean>(false);
  // Track observed elements in case new observer needs to be created
  const observedElementsRef = useRef(new Set<HTMLElement>());

  const [activeStep, setActiveStep] = useState<ActiveStep>(
    enableDeepLink ? searchParams.get(ACTIVE_STEP_QUERY_PARAM) : null,
  );

  // eslint-disable-next-line react-hooks/exhaustive-deps
  const _steps = useMemo(() => steps, [JSON.stringify(steps)]);

  const register = useCallback(
    (element: HTMLElement) => {
      // for some reason, the observer is not ready immediately
      setTimeout(() => {
        if (observerRef.current) {
          const stepKey = Number(element.dataset.stepKey);
          if (Number.isInteger(stepKey) && stepKey >= 0 && _steps[stepKey]) {
            _steps[stepKey].compRef = element;
          }

          observerRef.current.observe(element);
          observedElementsRef.current.add(element);
        }
      }, 10);
    },
    [_steps],
  );

  const unregister = useCallback(
    (element: HTMLElement) => {
      if (observerRef.current) {
        const stepKey = Number(element.dataset.stepKey);
        if (Number.isInteger(stepKey) && stepKey >= 0 && _steps[stepKey]) {
          _steps[stepKey].compRef = undefined;
        }

        observerRef.current.unobserve(element);
        observedElementsRef.current.delete(element);
      }
    },
    [_steps],
  );

  const observerCallback = useCallback(
    (entries: IntersectionObserverEntry[]) => {
      if (lockObserver.current) {
        return;
      }

      const renderedSteps = _steps.filter((step) => Boolean(step.compRef));

      if (renderedSteps.length < 2) {
        setActiveStep(renderedSteps[0]?.id || null);
        return;
      }

      for (const entry of entries) {
        const stepKey = Number((entry.target as HTMLElement)?.dataset?.stepKey);

        if (!Number.isInteger(stepKey) || stepKey < 0) {
          continue;
        }

        const { intersectionRatio, boundingClientRect, rootBounds, isIntersecting } = entry;
        const step = _steps[stepKey];

        const stepIndex = renderedSteps.findIndex(
          (renderedStep) => renderedStep.stepKey === step.stepKey,
        );
        const { next } = getAdjacentValues(renderedSteps, stepIndex);

        const intersectionAtTop =
          rootBounds?.bottom !== undefined && boundingClientRect.top < rootBounds.top;
        const stepGoesIn = isIntersecting;

        if (
          intersectionRatio > 0.8 &&
          intersectionRatio < 1 &&
          intersectionAtTop &&
          activeStep === null
        ) {
          setActiveStep(step.id);
          break;
        }

        if (intersectionRatio < 1 && intersectionRatio !== 0 && intersectionAtTop) {
          let newStep: string | null = null;

          if (stepGoesIn) {
            newStep = step.id;
          } else if (next) {
            newStep = next.id;
          }

          if (newStep !== activeStep) {
            setActiveStep(newStep);
          }

          break;
        }
      }
    },
    [_steps, activeStep],
  );
  useEffect(() => {
    const filtersElementHeight = filtersElementRef.current?.clientHeight || 0;
    const navbarHeight = document.querySelector('nav')?.clientHeight || 0;
    const newObserver = new IntersectionObserver(observerCallback, {
      threshold: [0.8, 0.85, 0.9, 0.95],
      rootMargin: `-${filtersElementHeight + navbarHeight}px 0px 0px 0px`,
    });

    for (const observedElement of observedElementsRef.current) {
      newObserver.observe(observedElement);
    }

    // Unobserve all from the old observer
    observerRef.current?.disconnect();
    observerRef.current = newObserver;
  }, [observerCallback]);

  /**
   * Update the URL search params with the current state of the filters and inputs
   */
  useEffect(() => {
    if (!enableDeepLink) {
      return;
    }

    const newSearchParams = new URLSearchParams(Array.from(searchParams.entries()));

    if (activeStep) {
      newSearchParams.set(ACTIVE_STEP_QUERY_PARAM, activeStep);
    } else {
      newSearchParams.delete(ACTIVE_STEP_QUERY_PARAM);
    }

    const newSearch = newSearchParams.toString();
    if (newSearch === location.search.substring(1)) return;

    navigate({ search: newSearch }, { replace: true });

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [activeStep]);

  return { register, unregister, lockObserver, filtersElementRef, activeStep, setActiveStep };
}
