'use client';

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { store } from '../core/store';
import type {
  RichColorsMode,
  ToastAction,
  ToastIcons,
  ToastRecord,
  ToastSize,
  ToasterProps,
  SwipeDirection,
} from '../core/types';
import { CloseIcon, ErrorIcon, InfoIcon, LoadingIcon, SuccessIcon, WarningIcon } from './icons';

/** Config the Toaster resolves once and hands to every toast. */
export interface ToastRuntime {
  closeButton: boolean;
  showProgressBar: boolean;
  size: ToastSize;
  richColors: RichColorsMode | false;
  color: boolean;
  invert: boolean;
  expanded: boolean;
  swipeDirections: SwipeDirection[];
  swipeThreshold: number;
  icons: ToastIcons;
  loadingIcon: React.ReactNode;
  defaults: ToasterProps['toastOptions'];
  closeButtonAriaLabel: string;
  closeOnClick: boolean;
}

interface ToastProps {
  toast: ToastRecord;
  /** 0 = front-most. */
  index: number;
  /** Distance from the front, used for the collapsed stack scale. */
  offset: number;
  removed: boolean;
  runtime: ToastRuntime;
  onHeight: (id: string, height: number) => void;
  /** Called when this toast held focus as it went away. */
  onRequestRefocus: () => void;
}

const cx = (...parts: (string | false | undefined)[]) => parts.filter(Boolean).join(' ');

const builtinIcon = (type: string, icons: ToastIcons, loadingIcon: React.ReactNode) => {
  switch (type) {
    case 'success':
      return icons.success ?? <SuccessIcon />;
    case 'error':
      return icons.error ?? <ErrorIcon />;
    case 'info':
      return icons.info ?? <InfoIcon />;
    case 'warning':
      return icons.warning ?? <WarningIcon />;
    case 'loading':
      return icons.loading ?? loadingIcon ?? <LoadingIcon />;
    default:
      return null;
  }
};

export const Toast: React.FC<ToastProps> = ({
  toast,
  index,
  offset,
  removed,
  runtime,
  onHeight,
  onRequestRefocus,
}) => {
  const ref = useRef<HTMLLIElement>(null);
  const [mounted, setMounted] = useState(false);
  const [swipe, setSwipe] = useState<{ x: number; y: number } | null>(null);
  const [swiping, setSwiping] = useState(false);
  const dragStart = useRef<{ x: number; y: number } | null>(null);
  const [inputValue, setInputValue] = useState(toast.input?.defaultValue || '');

  const all = runtime.defaults || {};
  // Same precedence as the store: toast > per-type default > global default.
  const defaults = { ...all, ...(all.types ? all.types[toast.type] : undefined) };
  const classNames = { ...defaults.classNames, ...toast.classNames };

  const dismissible = toast.dismissible ?? defaults.dismissible ?? true;
  const closeButton =
    toast.closeButton ?? toast.showCloseButton ?? defaults.closeButton ?? runtime.closeButton;
  const showProgressBar =
    toast.showProgressBar ?? defaults.showProgressBar ?? runtime.showProgressBar;
  const size: ToastSize = toast.size ?? defaults.size ?? runtime.size;
  const unstyled = toast.unstyled ?? defaults.unstyled ?? false;
  const invert = toast.invert ?? defaults.invert ?? runtime.invert;

  const richRaw = toast.richColors ?? defaults.richColors ?? runtime.richColors;
  const richColors: RichColorsMode | false =
    richRaw === true ? 'minimal' : richRaw === false || richRaw === undefined ? false : richRaw;

  const style = { ...defaults.style, ...defaults.customStyles, ...toast.customStyles, ...toast.style };
  const actions: ToastAction[] = toast.actions
    ? toast.actions
    : toast.action
      ? [toast.action]
      : [];

  const isLoading = toast.type === 'loading' || toast.status === 'loading';

  // `expandable` toasts keep their actions collapsed until the user opens them.
  const [selfExpanded, setSelfExpanded] = useState(false);
  const expandable = toast.expandable === true;
  const isOpen = expandable ? toast.expanded ?? selfExpanded : true;
  const toggle = () => {
    if (expandable && toast.expanded === undefined) setSelfExpanded((open) => !open);
  };
  const hasCountdown =
    showProgressBar && toast.progress === undefined && isFinite(toast.duration) && toast.duration > 0;

  // Report height so the Toaster can compute real stack offsets rather than
  // guessing from content shape.
  useEffect(() => {
    const element = ref.current;
    if (!element) return;
    const report = () => onHeight(toast.id, element.getBoundingClientRect().height);
    report();

    const Observer = typeof ResizeObserver !== 'undefined' ? ResizeObserver : null;
    if (!Observer) return;
    const observer = new Observer(report);
    observer.observe(element);
    return () => observer.disconnect();
  }, [toast.id, toast.content, toast.description, toast.expanded, onHeight]);

  useEffect(() => {
    const frame = setTimeout(() => setMounted(true), 0);
    return () => clearTimeout(frame);
  }, []);

  const dismiss = useCallback(() => {
    // Check before removal: afterwards this element is on its way out and
    // focus would silently fall back to <body>.
    const held =
      typeof document !== 'undefined' &&
      !!ref.current &&
      ref.current.contains(document.activeElement);
    store.dismiss(toast.id);
    if (held) onRequestRefocus();
  }, [toast.id, onRequestRefocus]);

  const handleClose = useCallback(() => {
    if (!dismissible) return;
    dismiss();
  }, [dismiss, dismissible]);

  const runAction = (action: ToastAction, event: React.MouseEvent<HTMLButtonElement>) => {
    event.stopPropagation();
    action.onClick(event);
    if (action.dismiss !== false) dismiss();
  };

  const handleKeyDown = (event: React.KeyboardEvent<HTMLLIElement>) => {
    if (event.key === 'Escape') {
      handleClose();
      return;
    }
    if (event.key !== 'Enter' && event.key !== ' ') return;
    if (!toast.onClick && !expandable) return;
    event.preventDefault();
    toggle();
    toast.onClick?.(event as unknown as React.MouseEvent<HTMLElement>);
  };

  const shouldCloseOnClick =
    (toast.closeOnClick ?? defaults.closeOnClick ?? runtime.closeOnClick) && dismissible;

  const handleClick = (event: React.MouseEvent<HTMLLIElement>) => {
    toggle();
    toast.onClick?.(event);
    // Buttons inside the toast stop propagation, so this only fires for the body.
    if (shouldCloseOnClick) dismiss();
  };

  // Pointer-driven swipe. Cheaper than a gesture library and lets us keep
  // swiping enabled on toasts that also have buttons.
  const onPointerDown = (event: React.PointerEvent<HTMLLIElement>) => {
    if (!dismissible || !runtime.swipeDirections.length) return;
    if ((event.target as HTMLElement).closest('button,input,a,textarea,select')) return;
    dragStart.current = { x: event.clientX, y: event.clientY };
    setSwiping(true);
    // Without capture, releasing outside the toast never fires pointerup here
    // and the toast stays stuck mid-swipe.
    const element = event.currentTarget;
    if (typeof element.setPointerCapture === 'function') {
      try {
        element.setPointerCapture(event.pointerId);
      } catch {
        /* some pointer types refuse capture; the swipe still works */
      }
    }
  };

  const onPointerMove = (event: React.PointerEvent<HTMLLIElement>) => {
    const start = dragStart.current;
    if (!start) return;
    const dx = event.clientX - start.x;
    const dy = event.clientY - start.y;
    const allowed = runtime.swipeDirections;
    setSwipe({
      x: (dx < 0 && allowed.includes('left')) || (dx > 0 && allowed.includes('right')) ? dx : 0,
      y: (dy < 0 && allowed.includes('up')) || (dy > 0 && allowed.includes('down')) ? dy : 0,
    });
  };

  const endSwipe = () => {
    const moved = swipe;
    dragStart.current = null;
    setSwiping(false);
    setSwipe(null);
    if (!moved) return;
    const element = ref.current;
    const width = element ? element.offsetWidth || 320 : 320;
    const height = element ? element.offsetHeight || 64 : 64;
    if (
      Math.abs(moved.x) > width * runtime.swipeThreshold ||
      Math.abs(moved.y) > height * runtime.swipeThreshold
    ) {
      handleClose();
    }
  };

  const submitInput = (event: React.FormEvent) => {
    event.preventDefault();
    const value = inputValue;
    if (!toast.input) return;
    if (!toast.input.allowEmpty && !value.trim()) return;
    toast.input.onSubmit(value);
    setInputValue('');
    dismiss();
  };

  if (toast.customComponent) {
    const Custom = toast.customComponent;
    return (
      <li ref={ref} data-vyrn-toast="" data-custom="true">
        <Custom toast={toast} dismiss={dismiss} />
      </li>
    );
  }

  const icon = toast.icon ?? builtinIcon(toast.type, runtime.icons, runtime.loadingIcon);
  const interactive = Boolean(toast.onClick) || expandable || shouldCloseOnClick;

  return (
    <li
      ref={ref}
      className={cx('vyrn-toast', defaults.className, toast.className, classNames.toast, classNames[toast.type as keyof typeof classNames] as string)}
      data-vyrn-toast=""
      data-type={toast.type}
      data-size={size}
      data-mounted={mounted ? 'true' : 'false'}
      data-removed={removed ? 'true' : 'false'}
      data-front={index === 0 ? 'true' : 'false'}
      data-expanded={runtime.expanded ? 'true' : 'false'}
      data-swiping={swiping ? 'true' : 'false'}
      data-unstyled={unstyled ? 'true' : 'false'}
      data-invert={invert ? 'true' : 'false'}
      data-priority={toast.priority || 'normal'}
      data-colored={runtime.color ? 'true' : 'false'}
      {...(richColors ? { 'data-rich-colors': richColors } : {})}
      {...(interactive ? { role: 'button' } : {})}
      {...(expandable ? { 'aria-expanded': isOpen } : {})}
      tabIndex={0}
      style={{
        ...style,
        ['--vyrn-index' as string]: index,
        ['--vyrn-offset-y' as string]: `${offset}px`,
        ['--vyrn-swipe-x' as string]: `${swipe ? swipe.x : 0}px`,
        ['--vyrn-swipe-y' as string]: `${swipe ? swipe.y : 0}px`,
        ...(hasCountdown ? { ['--vyrn-duration' as string]: `${toast.duration}ms` } : {}),
      }}
      onKeyDown={handleKeyDown}
      onClick={interactive ? handleClick : undefined}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={endSwipe}
      onPointerCancel={endSwipe}
    >
      {toast.jsx !== undefined ? (
        typeof toast.jsx === 'function' ? toast.jsx(toast.id) : toast.jsx
      ) : (
        <>
          {icon ? (
            <div
              className={cx('vyrn-toast-icon', classNames.icon)}
              data-icon=""
              data-loading={isLoading ? 'true' : 'false'}
            >
              {icon}
            </div>
          ) : null}

          <div className={cx('vyrn-toast-body', classNames.content)} data-content="">
            <div className={cx('vyrn-toast-title', classNames.title)} data-title="">
              {toast.content}
            </div>
            {toast.description !== undefined && toast.description !== null ? (
              <div
                className={cx(
                  'vyrn-toast-description',
                  defaults.descriptionClassName,
                  toast.descriptionClassName,
                  classNames.description
                )}
                data-description=""
              >
                {toast.description}
              </div>
            ) : null}

            {toast.input ? (
              <form className="vyrn-toast-input-row" onSubmit={submitInput} data-input="">
                <input
                  className={cx('vyrn-toast-input', toast.input.className, classNames.input)}
                  value={inputValue}
                  placeholder={toast.input.placeholder}
                  aria-label={toast.input.placeholder}
                  onChange={(event) => setInputValue(event.target.value)}
                  onFocus={() => store.pause()}
                  onBlur={() => store.resume()}
                />
                <button
                  type="submit"
                  className={cx('vyrn-toast-action', toast.input.submitClassName)}
                  data-primary="true"
                >
                  {toast.input.submitLabel || 'Submit'}
                </button>
              </form>
            ) : null}

            {(actions.length || toast.cancel) && isOpen ? (
              <div className="vyrn-toast-actions" data-actions="">
                {actions.map((action, position) => (
                  <button
                    key={position}
                    type="button"
                    className={cx('vyrn-toast-action', action.className, classNames.actionButton)}
                    style={{ ...defaults.actionButtonStyle, ...toast.actionButtonStyle }}
                    data-variant={action.style || 'default'}
                    aria-label={action.altText}
                    onClick={(event) => runAction(action, event)}
                  >
                    {action.label}
                  </button>
                ))}
                {toast.cancel ? (
                  <button
                    type="button"
                    className={cx(
                      'vyrn-toast-cancel',
                      toast.cancel.className,
                      classNames.cancelButton
                    )}
                    style={{ ...defaults.cancelButtonStyle, ...toast.cancelButtonStyle }}
                    aria-label={toast.cancel.altText}
                    onClick={(event) => {
                      event.stopPropagation();
                      toast.cancel?.onClick?.(event);
                      dismiss();
                    }}
                  >
                    {toast.cancel.label}
                  </button>
                ) : null}
              </div>
            ) : null}
          </div>

          {closeButton && dismissible ? (
            <button
              type="button"
              className={cx('vyrn-toast-close', classNames.closeButton)}
              data-close=""
              aria-label={runtime.closeButtonAriaLabel}
              onClick={(event) => {
                event.stopPropagation();
                handleClose();
              }}
            >
              {runtime.icons.close ?? <CloseIcon />}
            </button>
          ) : null}

          {showProgressBar && (hasCountdown || toast.progress !== undefined) ? (
            <div
              className={cx('vyrn-toast-progress', classNames.progressBar)}
              data-progress=""
              data-determinate={toast.progress !== undefined ? 'true' : 'false'}
              {...(toast.progress !== undefined
                ? {
                    style: {
                      ['--vyrn-progress' as string]: `${Math.min(
                        100,
                        Math.max(0, toast.progress)
                      )}%`,
                    },
                  }
                : {})}
            />
          ) : null}
        </>
      )}
    </li>
  );
};
