import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useReducer,
  useRef,
} from 'react';
import { Platform, View } from 'react-native';
import { useSharedValue } from 'react-native-reanimated';
import { DefaultToast } from '../../components/Toast';
import { InsetsContainer } from './insets-container';
import { ToastLifecycleContext, type ToastLifecycleContextValue } from './lifecycle.context';
import { toastReducer } from './reducer';
import { ToastConfigContext } from './toast-config.context';
import { ToastItemRenderer } from './toast-item-renderer';
import type {
  ToastComponentProps,
  ToasterContextValue,
  ToastGlobalConfig,
  ToastProviderProps,
  ToastShowConfig,
  ToastShowOptions,
  ToastShowOptionsWithComponent,
} from './types';

const DEFAULT_DURATION = 4000;

/**
 * Context for toast manager
 */
const ToasterContext = createContext<ToasterContextValue | null>(null);

/**
 * Merges global config with local config, ensuring local config takes precedence
 * Only includes defined values from localConfig to avoid overriding global config with undefined
 */
function mergeToastConfig(
  globalConfig: ToastGlobalConfig | undefined,
  localConfig: Partial<ToastGlobalConfig>,
): Partial<ToastGlobalConfig> {
  const result: Partial<ToastGlobalConfig> = { ...globalConfig };

  // Only override with defined values from localConfig
  if (localConfig.color !== undefined) {
    result.color = localConfig.color;
  }
  if (localConfig.placement !== undefined) {
    result.placement = localConfig.placement;
  }
  if (localConfig.isSwipeable !== undefined) {
    result.isSwipeable = localConfig.isSwipeable;
  }
  if (localConfig.animation !== undefined) {
    result.animation = localConfig.animation;
  }

  return result;
}

/**
 * Creates a component function for simple string toast
 */
function createStringToastComponent(
  label: string,
  globalConfig: ToastGlobalConfig | undefined,
): (props: ToastComponentProps) => React.ReactElement {
  return (props: ToastComponentProps) => {
    const mergedConfig = mergeToastConfig(globalConfig, {
      color: 'neutral',
    });
    return (
      <DefaultToast
        {...props}
        label={label}
        color={mergedConfig.color}
        placement={mergedConfig.placement}
        isSwipeable={mergedConfig.isSwipeable}
        animation={mergedConfig.animation}
      />
    );
  };
}

/**
 * Creates a component function for config-based toast
 */
function createConfigToastComponent(
  config: ToastShowConfig,
  globalConfig: ToastGlobalConfig | undefined,
): (props: ToastComponentProps) => React.ReactElement {
  return (props: ToastComponentProps) => {
    const mergedConfig = mergeToastConfig(globalConfig, {
      color: config.color,
      placement: config.placement,
      isSwipeable: config.isSwipeable,
      animation: config.animation,
    });
    return (
      <DefaultToast
        {...props}
        color={mergedConfig.color}
        placement={mergedConfig.placement}
        isSwipeable={mergedConfig.isSwipeable}
        animation={mergedConfig.animation}
        label={config.label}
        description={config.description}
        actionLabel={config.actionLabel}
        onActionPress={config.onActionPress}
        icon={config.icon}
      />
    );
  };
}

/**
 * Toast provider component
 * Wraps your app to enable toast functionality
 */
export function ToastProvider({
  defaultProps,
  insets,
  maxVisibleToasts = 3,
  contentWrapper,
  children,
  disableFullWindowOverlay = false,
  unstable_accessibilityContainerViewIsModal,
  announceLabel,
}: Readonly<ToastProviderProps>) {
  const [toasts, dispatch] = useReducer(toastReducer, []);

  /**
   * Memoize global config to prevent unnecessary re-renders
   */
  const globalConfig = useMemo<ToastGlobalConfig | undefined>(() => defaultProps, [defaultProps]);

  const isToastVisible = toasts.length > 0;

  const heights = useSharedValue<Record<string, number>>({});

  const total = useSharedValue<number>(0);

  /**
   * Derive total from toasts.length so the animated opacity/scale/translateY
   * interpolations always use the real count.  Manual increment/decrement
   * was prone to drift when hide + show ran in the same tick or when
   * auto-dismiss raced with a manual hide (stale-closure mismatch).
   */
  useEffect(() => {
    total.set(toasts.length);
  }, [toasts.length, total]);

  const idCounter = useRef(0);

  /**
   * Timer state per toast — tracks remaining time so pause/resume can
   * restart with only the leftover duration (WCAG 2.2.1).
   */
  const timersRef = useRef<
    Map<
      string,
      {
        startedAt: number;
        remaining: number;
        timer: ReturnType<typeof setTimeout> | null;
        duration: number;
      }
    >
  >(new Map());
  const pausedRef = useRef(false);
  const hideRef = useRef<((ids?: (string & {}) | string[] | 'all') => void) | null>(null);

  /**
   * Safety reset: when all toasts are gone the InsetsContainer unmounts and
   * no resume event can fire. Clear the paused flag and any orphaned timer
   * entries so the next toast's timer starts normally.
   */
  useEffect(() => {
    if (toasts.length === 0) {
      pausedRef.current = false;
      timersRef.current.forEach((entry) => {
        if (entry.timer) clearTimeout(entry.timer);
      });
      timersRef.current.clear();
    }
  }, [toasts.length]);

  /**
   * Remove one or more toasts from the array. On web this is the second half
   * of the two-phase hide flow — the toast component calls it from
   * `onTransitionEnd` (or a safety timeout) after the exit transition plays.
   * Also cleans up per-id height state.
   */
  const remove = useCallback(
    (ids: string | string[]) => {
      const idsArray = Array.isArray(ids) ? ids : [ids];
      if (idsArray.length === 0) return;

      dispatch({ type: 'REMOVE', payload: { ids: idsArray } });

      heights.modify(<T extends Record<string, number>>(value: T): T => {
        'worklet';
        const result = { ...value };
        for (const id of idsArray) {
          // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
          delete result[id];
        }
        return result;
      });
    },
    [heights],
  );

  /**
   * Hide one or more toasts
   * - No argument: hides the last toast in the array
   * - "all": hides all toasts
   * - Single ID: hides that toast
   * - Array of IDs: hides those toasts
   *
   * On web this only marks toasts as `pendingRemoval` so their CSS exit
   * transition can play; the actual removal happens when the toast
   * component calls `remove(id)` on transition end.
   * On native this dispatches removal directly — Reanimated's `exiting`
   * layout animation plays before unmount.
   */
  const hide = useCallback(
    (ids?: (string & {}) | string[] | 'all') => {
      const isWeb = Platform.OS === 'web';

      if (ids === undefined) {
        if (toasts.length === 0) return;
        // Hide the last non-pending toast in the array. Using the last
        // pending toast would be a no-op on web.
        let lastToast = null;
        for (let i = toasts.length - 1; i >= 0; i--) {
          const candidate = toasts[i];
          if (!candidate.pendingRemoval) {
            lastToast = candidate;
            break;
          }
        }
        if (!lastToast) return;

        const timerEntry = timersRef.current.get(lastToast.id);
        if (timerEntry?.timer) {
          clearTimeout(timerEntry.timer);
        }
        timersRef.current.delete(lastToast.id);

        if (lastToast.onHide) {
          lastToast.onHide();
        }

        if (isWeb) {
          dispatch({ type: 'MARK_CLOSED', payload: { ids: [lastToast.id] } });
        } else {
          remove([lastToast.id]);
        }
      } else if (ids === 'all') {
        timersRef.current.forEach((entry) => {
          if (entry.timer) clearTimeout(entry.timer);
        });
        timersRef.current.clear();

        const targetIds: string[] = [];
        toasts.forEach((toast) => {
          if (toast.pendingRemoval) return;
          targetIds.push(toast.id);
          if (toast.onHide) {
            toast.onHide();
          }
        });

        if (targetIds.length === 0) return;

        if (isWeb) {
          dispatch({ type: 'MARK_CLOSED', payload: { ids: targetIds } });
        } else {
          remove(targetIds);
        }
      } else {
        const idsArray = Array.isArray(ids) ? ids : [ids];
        const targetIds: string[] = [];

        idsArray.forEach((id) => {
          const toast = toasts.find((t) => t.id === id);
          if (!toast || toast.pendingRemoval) return;

          const timerEntry = timersRef.current.get(id);
          if (timerEntry?.timer) {
            clearTimeout(timerEntry.timer);
          }
          timersRef.current.delete(id);

          targetIds.push(id);
          if (toast.onHide) {
            toast.onHide();
          }
        });

        if (targetIds.length === 0) return;

        if (isWeb) {
          dispatch({ type: 'MARK_CLOSED', payload: { ids: targetIds } });
        } else {
          remove(targetIds);
        }
      }
    },
    [remove, toasts],
  );

  // Keep hide ref up to date
  hideRef.current = hide;

  /**
   * Pause all running auto-dismiss timers (WCAG 2.2.1).
   * Stores elapsed time so resumeAll can restart with the remainder.
   */
  const pauseAll = useCallback(() => {
    if (pausedRef.current) return;
    pausedRef.current = true;

    const now = Date.now();
    timersRef.current.forEach((entry) => {
      if (entry.timer) {
        clearTimeout(entry.timer);
        entry.timer = null;
      }
      const elapsed = now - entry.startedAt;
      entry.remaining = Math.max(0, entry.remaining - elapsed);
    });
  }, []);

  /**
   * Resume all paused auto-dismiss timers with their remaining duration.
   */
  const resumeAll = useCallback(() => {
    pausedRef.current = false;

    const now = Date.now();
    timersRef.current.forEach((entry, id) => {
      if (entry.timer) return;
      if (entry.remaining <= 0) {
        if (hideRef.current) hideRef.current(id);
        timersRef.current.delete(id);
        return;
      }
      entry.startedAt = now;
      entry.timer = setTimeout(() => {
        if (hideRef.current) hideRef.current(id);
        timersRef.current.delete(id);
      }, entry.remaining);
    });
  }, []);

  const lifecycleContextValue = useMemo<ToastLifecycleContextValue>(() => {
    const pending = new Set<string>();
    let frontmostId: string | null = null;
    for (const toast of toasts) {
      if (toast.pendingRemoval) {
        pending.add(toast.id);
      } else {
        frontmostId = toast.id;
      }
    }
    return {
      remove: (id: string) => remove([id]),
      pendingRemovalIds: pending,
      frontmostId,
    };
  }, [remove, toasts]);

  /**
   * Show a toast
   * Supports three usage patterns:
   * 1. Simple string: toast.show('This is toast')
   * 2. Config object: toast.show({ label, color, ... })
   * 3. Custom component: toast.show({ component: (props) => <Toast>...</Toast> })
   */
  const show = useCallback(
    (options: string | ToastShowOptions): string => {
      let normalizedOptions: ToastShowOptionsWithComponent;
      let duration: number | 'persistent' | undefined = DEFAULT_DURATION; // Default duration
      let explicitId: string | undefined;

      // Case 1: Simple string
      if (typeof options === 'string') {
        normalizedOptions = {
          id: undefined,
          component: createStringToastComponent(options, globalConfig),
          duration: DEFAULT_DURATION,
        };
        duration = DEFAULT_DURATION;
        explicitId = undefined;
      }
      // Case 2: Config object without component
      else if (!('component' in options) || options.component === undefined) {
        const config = options as ToastShowConfig;
        duration = config.duration ?? DEFAULT_DURATION;
        explicitId = config.id;
        normalizedOptions = {
          id: config.id,
          component: createConfigToastComponent(config, globalConfig),
          duration,
          onShow: config.onShow,
          onHide: config.onHide,
        };
      }
      // Case 3: Config object with component (existing behavior)
      else {
        normalizedOptions = options;
        duration = normalizedOptions.duration ?? DEFAULT_DURATION;
        explicitId = normalizedOptions.id;
      }

      const id =
        normalizedOptions.id ?? `toast-${String(Date.now())}-${String(idCounter.current++)}`;

      // If an explicit ID was provided, check if a toast with that ID already exists
      // If it exists, skip adding a new toast and return the existing ID
      if (explicitId !== undefined) {
        const existingToast = toasts.find((toast) => toast.id === explicitId);
        if (existingToast) {
          return existingToast.id;
        }
      }

      dispatch({
        type: 'SHOW',
        payload: {
          id,
          component: normalizedOptions.component,
          duration,
          onShow: normalizedOptions.onShow,
          onHide: normalizedOptions.onHide,
        },
      });

      if (normalizedOptions.onShow) {
        normalizedOptions.onShow();
      }

      // Set up auto-dismiss timeout synchronously
      if (
        duration !== 'persistent' &&
        typeof duration === 'number' &&
        !Number.isNaN(duration) &&
        duration >= 0 &&
        duration !== Infinity
      ) {
        // Handle immediate dismissal
        if (duration === 0) {
          if (hideRef.current) {
            hideRef.current(id);
          }
        } else {
          const now = Date.now();
          const timer = setTimeout(() => {
            if (hideRef.current) {
              hideRef.current(id);
            }
            timersRef.current.delete(id);
          }, duration);
          const entry = {
            startedAt: now,
            remaining: duration,
            timer: timer as ReturnType<typeof setTimeout> | null,
            duration,
          };
          timersRef.current.set(id, entry);

          if (pausedRef.current) {
            clearTimeout(timer);
            entry.timer = null;
          }
        }
      }

      return id;
    },
    [toasts, globalConfig],
  );

  const contextValue = useMemo<ToasterContextValue>(
    () => ({
      toast: {
        show,
        hide,
        pauseAll,
        resumeAll,
      },
      isToastVisible,
    }),
    [show, hide, pauseAll, resumeAll, isToastVisible],
  );

  return (
    <ToastConfigContext.Provider value={globalConfig}>
      <ToasterContext.Provider value={contextValue}>
        <ToastLifecycleContext.Provider value={lifecycleContextValue}>
          {children}
          {toasts.length > 0 && (
            <InsetsContainer
              insets={insets}
              announceLabel={announceLabel}
              contentWrapper={contentWrapper}
              disableFullWindowOverlay={disableFullWindowOverlay}
              unstable_accessibilityContainerViewIsModal={
                unstable_accessibilityContainerViewIsModal
              }
              pauseAll={pauseAll}
              resumeAll={resumeAll}
              hide={hide}
            >
              <View className="flex-1">
                {toasts.map((toastItem, index) => (
                  <ToastItemRenderer
                    key={toastItem.id}
                    toastItem={toastItem}
                    show={show}
                    hide={hide}
                    index={index}
                    total={total}
                    heights={heights}
                    maxVisibleToasts={maxVisibleToasts}
                  />
                ))}
              </View>
            </InsetsContainer>
          )}
        </ToastLifecycleContext.Provider>
      </ToasterContext.Provider>
    </ToastConfigContext.Provider>
  );
}

/**
 * Hook to access toast functionality
 *
 * @returns Object containing toast manager and visibility state
 *
 * @example
 * ```tsx
 * const { toast, isToastVisible } = useToast();
 *
 * // Show a toast
 * toast.show({ component: <Toast>Hello</Toast> });
 *
 * // Hide a toast
 * toast.hide('my-toast');
 *
 * // Check if any toast is visible
 * if (isToastVisible) {
 *   console.log('A toast is currently displayed');
 * }
 * ```
 */
export function useToast() {
  const context = useContext(ToasterContext);

  if (!context) {
    throw new Error('useToast must be used within a ToastProvider provider');
  }

  return {
    toast: context.toast,
    isToastVisible: context.isToastVisible,
  };
}
