import styled from 'styled-components';
import React, { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';

import type { ReactElement } from 'react';
import type {
  ToastContextValue,
  ToastItem,
  ToastOptions,
  ToastProviderProps,
  ToastType,
} from '../../../core/types/toast';

import { Portal } from '@redocly/theme/components/Portal/Portal';
import { Toast } from '@redocly/theme/components/Toast/Toast';

import { ToastContext } from '../../../core/contexts/Toast/ToastContext';
import { TOAST_SLIDE_DURATION_MS } from '../../../core/constants/toast';

type ToastState = ToastItem[];

type ToastReducerAction =
  | {
      type: 'add';
      payload: ToastItem;
    }
  | {
      type: 'startExit';
      payload: {
        id: string;
      };
    }
  | {
      type: 'remove';
      payload: {
        id: string;
      };
    }
  | {
      type: 'update';
      payload: {
        id: string;
        updates: Partial<ToastOptions>;
      };
    };

function toastReducer(state: ToastState, action: ToastReducerAction): ToastState {
  switch (action.type) {
    case 'add':
      return [action.payload, ...state];
    case 'startExit':
      return state.map((toast) =>
        toast.id === action.payload.id ? { ...toast, isExiting: true } : toast,
      );
    case 'remove':
      return state.filter((toast) => toast.id !== action.payload.id);
    case 'update':
      return state.map((toast) =>
        toast.id === action.payload.id
          ? {
              ...toast,
              ...action.payload.updates,
              type: action.payload.updates.type ?? toast.type,
            }
          : toast,
      );
    default:
      return state;
  }
}

function getToastType(type?: ToastType): ToastType {
  return type ?? 'info';
}

function createToastId(): string {
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
    return crypto.randomUUID();
  }

  return `toast-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
}

export function ToastProvider({ children, mountId }: ToastProviderProps): ReactElement {
  const [toasts, dispatch] = useReducer(toastReducer, []);
  const toastsRef = useRef<ToastItem[]>([]);
  const removeTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());

  useEffect(() => {
    toastsRef.current = toasts;
  }, [toasts]);

  useEffect(() => {
    const timeoutsMap = removeTimeoutsRef.current;

    return () => {
      timeoutsMap.forEach((timeoutId) => {
        clearTimeout(timeoutId);
      });

      timeoutsMap.clear();
    };
  }, []);

  const dismissToast = useCallback((id: string): void => {
    const currentToast = toastsRef.current.find((toast) => toast.id === id);

    if (!currentToast || currentToast.isExiting) {
      return;
    }

    currentToast.onClose?.();
    dispatch({ type: 'startExit', payload: { id } });

    const existingTimeout = removeTimeoutsRef.current.get(id);

    if (existingTimeout) {
      clearTimeout(existingTimeout);
    }

    const timeoutId = setTimeout(() => {
      dispatch({ type: 'remove', payload: { id } });
      removeTimeoutsRef.current.delete(id);
    }, TOAST_SLIDE_DURATION_MS);

    removeTimeoutsRef.current.set(id, timeoutId);
  }, []);

  const showToast = useCallback((options: ToastOptions): string => {
    const id = createToastId();

    dispatch({
      type: 'add',
      payload: {
        ...options,
        id,
        type: getToastType(options.type),
        isExiting: false,
      },
    });

    return id;
  }, []);

  const updateToast = useCallback((id: string, updates: Partial<ToastOptions>): void => {
    dispatch({
      type: 'update',
      payload: {
        id,
        updates,
      },
    });
  }, []);

  const contextValue = useMemo<ToastContextValue>(
    () => ({
      showToast,
      dismissToast,
      updateToast,
    }),
    [dismissToast, showToast, updateToast],
  );

  return (
    <ToastContext.Provider value={contextValue}>
      {children}
      {toasts.length > 0 ? (
        <Portal mountId={mountId}>
          <ToastViewport aria-label="Notifications">
            {toasts.map((toast, index) => (
              <Toast
                key={toast.id}
                stackIndex={index}
                stackZIndex={toasts.length - index}
                toast={toast}
                onDismiss={dismissToast}
              />
            ))}
          </ToastViewport>
        </Portal>
      ) : null}
    </ToastContext.Provider>
  );
}

const ToastViewport = styled.div`
  position: fixed;
  right: var(--spacing-md);
  bottom: var(--spacing-md);
  z-index: 1100;
  display: flex;
  flex-direction: column-reverse;
  gap: var(--spacing-sm);
  width: 320px;
  min-width: 240px;
  max-width: 360px;
  pointer-events: none;

  @media (max-width: 480px) {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: calc(100vw - var(--spacing-md) * 2);
    min-width: 0;
    max-width: none;
  }
`;
