import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';

import type { RefObject } from 'react';
import type { ToastItem } from '../types/toast';

import { getAutoDismissDuration } from '../utils';

const STACK_SHIFT_ENTER_DURATION_MS = 280;
const STACK_SHIFT_EXIT_DURATION_MS = 200;

interface UseToastLogicOptions {
  toast: ToastItem;
  onDismiss: (id: string) => void;
  stackIndex: number;
}

interface UseToastLogicReturn {
  wrapperRef: RefObject<HTMLDivElement | null>;
  hasDetails: boolean;
  dismissToast: () => void;
  handleMouseEnter: () => void;
  handleMouseLeave: () => void;
  ariaRole: 'alert' | 'status';
  ariaLive: 'assertive' | 'polite';
}

export function useToastLogic({
  toast,
  onDismiss,
  stackIndex,
}: UseToastLogicOptions): UseToastLogicReturn {
  const wrapperRef = useRef<HTMLDivElement | null>(null);
  const previousTopRef = useRef<number | null>(null);
  const previousStackIndexRef = useRef<number | null>(null);
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const startedAtRef = useRef<number | null>(null);
  const remainingTimeRef = useRef<number>(0);
  const animationFrameRef = useRef<number | null>(null);
  const isHoveredRef = useRef<boolean>(false);

  const hasDetails = Boolean(toast.description);
  const resolvedDuration = useMemo(
    () => getAutoDismissDuration(hasDetails, toast.type, toast.duration),
    [hasDetails, toast.duration, toast.type],
  );
  const ariaRole = toast.type === 'error' ? 'alert' : 'status';
  const ariaLive = toast.type === 'error' ? 'assertive' : 'polite';

  const clearTimer = useCallback((): void => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
      timerRef.current = null;
    }

    startedAtRef.current = null;
  }, []);

  const dismissToast = useCallback((): void => {
    onDismiss(toast.id);
  }, [onDismiss, toast.id]);

  const startTimer = useCallback(
    (delay: number): void => {
      clearTimer();

      if (delay <= 0) {
        dismissToast();
        return;
      }

      remainingTimeRef.current = delay;
      startedAtRef.current = Date.now();
      timerRef.current = setTimeout(() => {
        dismissToast();
      }, delay);
    },
    [clearTimer, dismissToast],
  );

  useEffect(() => {
    if (toast.isExiting || resolvedDuration === null) {
      clearTimer();
      return undefined;
    }

    if (!isHoveredRef.current) {
      startTimer(resolvedDuration);
    }

    return () => {
      clearTimer();
    };
  }, [
    clearTimer,
    resolvedDuration,
    startTimer,
    toast.description,
    toast.isExiting,
    toast.title,
    toast.type,
  ]);

  useLayoutEffect(() => {
    const node = wrapperRef.current;

    if (!node) {
      return undefined;
    }

    node.style.transition = 'none';
    node.style.transform = '';

    const currentTop = node.getBoundingClientRect().top;
    const previousTop = previousTopRef.current;
    const previousStackIndex = previousStackIndexRef.current;

    if (toast.isExiting) {
      previousTopRef.current = currentTop;
      previousStackIndexRef.current = stackIndex;

      return () => {
        if (animationFrameRef.current !== null) {
          window.cancelAnimationFrame(animationFrameRef.current);
          animationFrameRef.current = null;
        }
      };
    }

    const didStackIndexChange = previousStackIndex !== null && previousStackIndex !== stackIndex;

    if (didStackIndexChange && previousTop !== null && previousTop !== currentTop) {
      const delta = previousTop - currentTop;
      const shiftDuration =
        delta > 0 ? STACK_SHIFT_ENTER_DURATION_MS : STACK_SHIFT_EXIT_DURATION_MS;

      node.style.transform = `translateY(${delta}px)`;
      node.getBoundingClientRect();

      animationFrameRef.current = window.requestAnimationFrame(() => {
        animationFrameRef.current = null;

        if (!wrapperRef.current) {
          return;
        }

        wrapperRef.current.style.transition = `transform ${shiftDuration}ms ease-out`;
        wrapperRef.current.style.transform = 'translateY(0)';
      });
    }

    previousTopRef.current = currentTop;
    previousStackIndexRef.current = stackIndex;

    return () => {
      if (animationFrameRef.current !== null) {
        window.cancelAnimationFrame(animationFrameRef.current);
        animationFrameRef.current = null;
      }
    };
  });

  useEffect(() => {
    return () => {
      clearTimer();

      if (animationFrameRef.current !== null) {
        window.cancelAnimationFrame(animationFrameRef.current);
      }
    };
  }, [clearTimer]);

  const handleMouseEnter = useCallback((): void => {
    isHoveredRef.current = true;

    if (resolvedDuration === null || startedAtRef.current === null) {
      return;
    }

    const elapsed = Date.now() - startedAtRef.current;
    remainingTimeRef.current = Math.max(remainingTimeRef.current - elapsed, 0);
    clearTimer();
  }, [clearTimer, resolvedDuration]);

  const handleMouseLeave = useCallback((): void => {
    isHoveredRef.current = false;

    if (toast.isExiting || resolvedDuration === null) {
      return;
    }

    startTimer(remainingTimeRef.current || resolvedDuration);
  }, [resolvedDuration, startTimer, toast.isExiting]);

  return {
    wrapperRef,
    hasDetails,
    dismissToast,
    handleMouseEnter,
    handleMouseLeave,
    ariaRole,
    ariaLive,
  };
}
