'use client';

import type * as React from 'react';
import { useRef } from 'react';
import { arraysEquals } from '../../helpers/array';
import { FOCUSABLE_ELEMENTS_LIST } from '../../lib/accessibility';
import { getWindow } from '../../lib/dom';
import { useMutationObserver } from '../useMutationObserver';
import { useStableCallback } from '../useStableCallback';
import { FocusGuard } from './FocusGuard';
import { useAutoFocus } from './useAutoFocus';
import { useRestoreFocus } from './useRestoreFocus';

const FOCUSABLE_ELEMENTS = FOCUSABLE_ELEMENTS_LIST.join();

const collectFocusable = (root: HTMLElement): HTMLElement[] => {
  const newFocusableElements =
    // eslint-disable-next-line no-restricted-properties
    root.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENTS);
  const nodes = Array.from(newFocusableElements).filter((node) => {
    // Исключаем гарды из списка фокусируемых элементов
    if (node.hasAttribute('data-focus-guard')) {
      return false;
    }

    const { display, visibility } = getComputedStyle(node);
    const isHidden = display === 'none' || visibility === 'hidden';
    const isDisabled = (node as HTMLButtonElement).disabled ?? false;

    return !isHidden && !isDisabled;
  });

  if (nodes.length === 0) {
    nodes.push(root);
  }

  return nodes;
};

const getFirstFocusable = (root: HTMLElement | null): HTMLElement | null => {
  if (!root) {
    return null;
  }

  const nodes = collectFocusable(root);
  return nodes[0] || null;
};

const getLastFocusable = (root: HTMLElement | null): HTMLElement | null => {
  if (!root) {
    return null;
  }

  const nodes = collectFocusable(root);
  return nodes[nodes.length - 1] || null;
};

export type UseFocusTrapProps = {
  /**
   * Форсированное отключение захвата фокуса
   *
   * @default false
   */
  disabled?: boolean | undefined;
  /**
   * Управление поведением автофокуса при появлении всплывающего окна.
   * При прокидывании `true` фокус будет установлен на первый элемент.
   * При прокидывании `root` фокус будет установлен в корень.
   * @default true
   */
  autoFocus?: boolean | 'root' | undefined;
  /**
   * Управление поведением возврата фокуса при закрытии всплывающего окна.
   * @default true
   */
  restoreFocus?: boolean | (() => boolean | HTMLElement) | undefined;
  /**
   * Время в миллисекундах после которого срабатывает автофокус при появлении всплывающего окна.
   * @default 0
   */
  autoFocusDelay?: number | undefined;
  /**
   * @default true
   */
  mount?: boolean | undefined;
};

export const useFocusTrap = (
  ref: React.RefObject<HTMLElement | null>,
  {
    mount = true,
    disabled = false,
    autoFocus = true,
    restoreFocus = true,
    autoFocusDelay = 0,
  }: UseFocusTrapProps,
) => {
  const prevFocusableRef = useRef<HTMLElement[]>([]);

  const createFocusFn = (getFocusElement: (root: HTMLElement | null) => HTMLElement | null) => {
    return () => {
      const node = getFocusElement(ref.current);
      if (node) {
        node.focus({ preventScroll: true });
      } else if (ref.current) {
        ref.current.focus();
      }
    };
  };

  const focusFirst = useStableCallback(createFocusFn(getFirstFocusable));

  const focusLast = useStableCallback(createFocusFn(getLastFocusable));

  useRestoreFocus({
    restoreFocus,
    autoFocusDelay,
    mount,
    ref,
  });

  useAutoFocus(ref, {
    autoFocus,
    disabled,
    mount,
    autoFocusDelay,
    focusFirst,
  });

  const onMutateParentHandler = useStableCallback((parentNode: HTMLElement) => {
    if (disabled || !autoFocus || !mount) {
      return;
    }

    const doc = getWindow(parentNode)?.document;
    if (!doc) {
      return;
    }

    const activeElement = doc.activeElement as HTMLElement;
    const focusableNodes = collectFocusable(parentNode);

    if (arraysEquals(focusableNodes, prevFocusableRef.current)) {
      return;
    }

    if (focusableNodes.length === 0) {
      prevFocusableRef.current = [];
      return;
    }

    const currentElementIndex = Math.max(
      activeElement ? focusableNodes.indexOf(activeElement) : -1,
      0,
    );

    const nodeToFocus = focusableNodes[currentElementIndex];
    if (nodeToFocus) {
      nodeToFocus.focus({ preventScroll: true });
    }

    prevFocusableRef.current = focusableNodes;
  });

  useMutationObserver(ref, () => ref.current && onMutateParentHandler(ref.current));

  const createGuardFocusHandler = (focusFn: () => void, focusFromOutside: () => void) => {
    return (event: React.FocusEvent<HTMLSpanElement>) => {
      if (!mount || disabled || !ref.current) {
        return;
      }

      // Проверяем, был ли предыдущий активный элемент внутри root
      // Если нет, значит фокус пришёл извне, и нужно использовать focusFromOutside
      const relatedTarget = event.relatedTarget as HTMLElement | null;

      if (relatedTarget === null || (relatedTarget && !ref.current.contains(relatedTarget))) {
        focusFromOutside();
        return;
      }

      focusFn();
    };
  };

  const onBeforeGuardFocus = useStableCallback(createGuardFocusHandler(focusLast, focusFirst));
  const onAfterGuardFocus = useStableCallback(createGuardFocusHandler(focusFirst, focusLast));

  const shouldRenderGuards = mount && !disabled;

  return {
    beforeGuard: shouldRenderGuards && <FocusGuard onFocus={onBeforeGuardFocus} />,
    afterGuard: shouldRenderGuards && <FocusGuard onFocus={onAfterGuardFocus} />,
  };
};
