/**
 * Toaster — renders the NotificationStore as a stack of toasts (spec §10,
 * design language → Toasts). Mounted once at the app root, beside ConfirmDialog.
 *
 * The non-blocking counterpart to ConfirmDialog: bottom-right, stacked
 * newest-nearest-the-corner, each dismissible (placement rationale in
 * Toaster.module.css). Error/warning toasts persist until dismissed (Carbon: a
 * critical message shouldn't vanish on a timer); success/info auto-dismiss. A
 * failure that carries diagnostic `detail` exposes it under a collapsed
 * "Technical details" disclosure — available to report, without shouting.
 *
 * Dismissal is two-phase: the first call to `startExit` applies the exit CSS
 * animation; after TOAST_EXIT_MS the notification is removed from the store.
 * This matches the toast-out @keyframes duration in Toaster.module.css.
 * Under prefers-reduced-motion, base.css drives animation-duration to ~0ms,
 * so the toast vanishes on the next frame — the JS timeout fires immediately
 * afterwards and cleans up the store entry.
 */

import { useCallback, useEffect, useState } from 'react';
import {
  useNotificationStore,
  type Notification,
  type NotificationKind,
} from '../stores/NotificationStore';
import { Icon, type IconName } from './Icon';
import { IconButton } from './IconButton';
import styles from './Toaster.module.css';

/** Auto-dismiss delay for the non-critical kinds (ms). Errors/warnings persist. */
const AUTO_DISMISS_MS = 6000;

/**
 * Duration of the exit animation — must match var(--dur-moderate) (150ms) used
 * by the .exiting class in Toaster.module.css. Under prefers-reduced-motion,
 * base.css zeroes animation durations so the animation completes in ~0ms; the
 * timeout fires on the next tick, keeping cleanup prompt.
 */
export const TOAST_EXIT_MS = 150;

/** Kinds that fade on their own; errors and warnings wait for the user. */
const AUTO_DISMISS: Record<NotificationKind, boolean> = {
  success: true,
  info: true,
  warning: false,
  error: false,
};

const KIND_CLASS: Record<NotificationKind, string> = {
  error: styles.error,
  warning: styles.warning,
  success: styles.success,
  info: styles.info,
};

/** Filled status glyph per kind (arch 09 §5.2) — a non-colour severity channel,
 *  decorative (the title names the toast); coloured by kind in the CSS. */
const KIND_ICON: Record<NotificationKind, IconName> = {
  error: 'status-error',
  warning: 'status-warning',
  success: 'status-success',
  info: 'status-info',
};

function Toast({
  notification,
  exiting,
  onStartExit,
}: {
  notification: Notification;
  exiting: boolean;
  onStartExit: (id: string) => void;
}) {
  const { id, kind, title, message, detail, action, durable } = notification;

  // Trigger exit animation; after the animation duration the store removes it.
  const handleDismiss = useCallback(() => onStartExit(id), [id, onStartExit]);

  // Positive/informational toasts clear themselves after a beat — unless marked
  // durable (e.g. an update prompt the user must act on). Resets only when
  // id/kind changes (stable identity guarantee).
  useEffect(() => {
    if (!AUTO_DISMISS[kind] || durable) return;
    const timer = setTimeout(handleDismiss, AUTO_DISMISS_MS);
    return () => clearTimeout(timer);
  }, [id, kind, durable, handleDismiss]);

  // Run the action, then dismiss the toast (the handler may navigate/reload).
  const handleAction = useCallback(() => {
    action?.onClick();
    handleDismiss();
  }, [action, handleDismiss]);

  return (
    <div
      className={`${styles.toast} ${KIND_CLASS[kind]}${exiting ? ` ${styles.exiting}` : ''}`}
      // Errors/warnings interrupt assistive tech (assertive); the rest are polite.
      role={kind === 'error' || kind === 'warning' ? 'alert' : 'status'}
    >
      <Icon name={KIND_ICON[kind]} size="md" className={styles.statusIcon} />
      <div className={styles.body}>
        <p className={styles.title}>{title}</p>
        <p className={styles.message}>{message}</p>
        {detail !== undefined && (
          <details className={styles.detail}>
            <summary className={styles.detailSummary}>Technical details</summary>
            <pre className={styles.detailText}>{detail}</pre>
            <p className={styles.detailMeta}>Astrolabe v{__APP_VERSION__}</p>
          </details>
        )}
        {action && (
          <div className={styles.actions}>
            <button type="button" className={styles.action} onClick={handleAction}>
              {action.label}
            </button>
          </div>
        )}
      </div>
      <IconButton
        size="sm"
        className={styles.close}
        label="Dismiss notification"
        onClick={handleDismiss}
      >
        <Icon name="close" />
      </IconButton>
    </div>
  );
}

export function Toaster() {
  const notifications = useNotificationStore((s) => s.notifications);
  const dismiss = useNotificationStore((s) => s.dismiss);

  // Local set of ids currently playing their exit animation. Each call to
  // startExit marks the id, then after TOAST_EXIT_MS removes it from the store.
  // Multiple toasts can be leaving simultaneously.
  const [exiting, setExiting] = useState<ReadonlySet<string>>(new Set());

  const startExit = useCallback(
    (id: string) => {
      setExiting((prev) => {
        if (prev.has(id)) return prev; // already leaving — no-op
        const next = new Set(prev);
        next.add(id);
        return next;
      });
      setTimeout(() => {
        dismiss(id);
        // Clean the exiting set so stale ids don't accumulate (the store entry
        // is gone, but the Set would grow unboundedly on repeated notify/dismiss).
        setExiting((prev) => {
          const next = new Set(prev);
          next.delete(id);
          return next;
        });
      }, TOAST_EXIT_MS);
    },
    [dismiss],
  );

  if (notifications.length === 0 && exiting.size === 0) return null;
  return (
    <div className={styles.region} role="region" aria-label="Notifications">
      {notifications.map((n) => (
        <Toast key={n.id} notification={n} exiting={exiting.has(n.id)} onStartExit={startExit} />
      ))}
    </div>
  );
}
