/* Starboard · Feedback v2 Components
   toast() + Toaster (managed, Sonner-style stack), Empty,
   ProgressV2 (linear / circular / steps), Skeleton presets.
   Load AFTER a11y.js + Primitives.jsx:
     <script type="text/babel" src="FeedbackV2.jsx"></script> */

// ─── Snack icons (16px, drawn for the 28px tinted badge) ───
const SNACK_ICONS = {
  success: (
    <svg
      width="16"
      height="16"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="m5 12 5 5 9-11" />
    </svg>
  ),
  error: (
    <svg
      width="16"
      height="16"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
    >
      <path d="m17 7-10 10M7 7l10 10" />
    </svg>
  ),
  warning: (
    <svg
      width="16"
      height="16"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M12 3 2.5 20h19L12 3zM12 10v4m0 3.5h.01" />
    </svg>
  ),
  info: (
    <svg
      width="16"
      height="16"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M12 11v5m0-8.5h.01" />
    </svg>
  ),
};

// ─── Store (module singleton — imperative toast() API) ───
const __snackStore = {
  toasts: [],
  listeners: new Set(),
  seq: 0,
  emit() {
    const snapshot = [...this.toasts];
    this.listeners.forEach((l) => l(snapshot));
  },
  add(t) {
    this.toasts = [t, ...this.toasts];
    this.emit();
  },
  update(id, patch) {
    this.toasts = this.toasts.map((t) => (t.id === id ? { ...t, ...patch } : t));
    this.emit();
  },
  remove(id) {
    this.toasts = this.toasts.filter((t) => t.id !== id);
    this.emit();
  },
};

const SNACK_EXIT_MS = 180; // --motion-overlay-exit + buffer

function toast(opts = {}) {
  const id = opts.id || `snack-${++__snackStore.seq}`;
  const t = {
    variant: 'info',
    duration: 5000,
    ...opts,
    id,
    timerKey: ++__snackStore.seq, // restarts the countdown on update
  };
  if (__snackStore.toasts.some((x) => x.id === id)) __snackStore.update(id, t);
  else __snackStore.add(t);
  if (t.variant === 'error' && typeof adAnnounce === 'function') {
    adAnnounce([t.title, t.description].filter(Boolean).join('. '), 'assertive');
  }
  return id;
}

['success', 'error', 'warning', 'info'].forEach((v) => {
  toast[v] = (title, opts = {}) => toast({ ...opts, variant: v, title });
});

toast.dismiss = (id) => {
  if (!__snackStore.toasts.some((t) => t.id === id && !t.exiting)) return;
  __snackStore.update(id, { exiting: true });
  setTimeout(() => __snackStore.remove(id), SNACK_EXIT_MS + 40);
};

/* toast.promise(fetchManifest(), {
     loading: 'Filing the bridge log…',
     success: (res) => `Bridge log filed for ${res.vessel}.`,
     error: 'The bridge log could not be filed.',
   }) — spinner cross-fades into the outcome in place. */
toast.promise = (promise, msgs = {}) => {
  const id = toast({
    variant: 'loading',
    title: msgs.loading || 'Working on it…',
    description: msgs.loadingDescription,
    duration: Infinity,
  });
  const settle = (variant, msg, value) => {
    const resolved = typeof msg === 'function' ? msg(value) : msg;
    const patch =
      typeof resolved === 'object' && resolved !== null ? resolved : { title: resolved };
    toast({ id, variant, duration: 4500, description: undefined, ...patch });
  };
  Promise.resolve(promise)
    .then((value) => settle('success', msgs.success || 'Done.', value))
    .catch((err) => settle('error', msgs.error || 'Something went wrong.', err));
  return id;
};

// ─── One toast in the stack ───
const SnackItem = ({ t, index, expandedOffset, expanded, top, paused, onDismiss, onHeight }) => {
  const ref = React.useRef(null);
  const drag = React.useRef({ active: false, startX: 0 });
  const [dx, setDx] = React.useState(0);
  const [entering, setEntering] = React.useState(true);
  const [remaining, setRemaining] = React.useState(t.duration);

  const pausedRef = React.useRef(false);
  pausedRef.current = paused || dx !== 0;

  React.useLayoutEffect(() => {
    if (ref.current) onHeight(t.id, ref.current.offsetHeight);
  });

  React.useEffect(() => {
    const id = setTimeout(() => setEntering(false), 240);
    return () => clearTimeout(id);
  }, []);

  // Countdown — 100ms ticks, paused while the region is hovered
  // or the card is being dragged. timerKey restarts it (promise).
  React.useEffect(() => {
    if (!isFinite(t.duration)) {
      setRemaining(Infinity);
      return;
    }
    let rem = t.duration;
    setRemaining(rem);
    const iv = setInterval(() => {
      if (pausedRef.current) return;
      rem -= 100;
      setRemaining(rem);
      if (rem <= 0) {
        clearInterval(iv);
        onDismiss(t.id);
      }
    }, 100);
    return () => clearInterval(iv);
  }, [t.timerKey]);

  // Swipe-to-dismiss — pointer drag right > 80px commits.
  const onPointerDown = (e) => {
    if (e.target.closest('button, a')) return;
    drag.current = { active: true, startX: e.clientX, dx: 0 };
    e.currentTarget.setPointerCapture(e.pointerId);
  };
  const onPointerMove = (e) => {
    if (!drag.current.active) return;
    const next = Math.max(0, e.clientX - drag.current.startX);
    drag.current.dx = next;
    setDx(next);
  };
  const onPointerUp = () => {
    if (!drag.current.active) return;
    const committed = drag.current.dx > 80;
    drag.current.active = false;
    if (committed) onDismiss(t.id);
    else setDx(0);
  };

  const sign = top ? 1 : -1;
  const reduced = typeof adPrefersReducedMotion === 'function' && adPrefersReducedMotion();
  const transform = t.exiting
    ? `translateY(${-8 + sign * (expanded ? expandedOffset : index * 8)}px) translateX(${dx}px)`
    : expanded
      ? `translateY(${sign * expandedOffset}px) translateX(${dx}px)`
      : `translateY(${sign * index * 8}px) translateX(${dx}px) scale(${Math.max(0, 1 - index * 0.06)})`;

  const cls = [
    'ad-snack',
    `ad-snack--${t.variant}`,
    entering && !reduced ? 'is-entering' : '',
    t.exiting ? 'is-exiting' : '',
    dx > 0 ? 'is-dragging' : '',
  ]
    .filter(Boolean)
    .join(' ');

  const pct = isFinite(t.duration) ? Math.max(0, (remaining / t.duration) * 100) : null;

  return (
    <div
      ref={ref}
      className={cls}
      style={{ transform, zIndex: 100 - index, opacity: t.exiting ? 0 : dx > 80 ? 0.5 : undefined }}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      onPointerCancel={onPointerUp}
    >
      <div className="ad-snack__card">
        <div className="ad-snack__badge">
          {/* key swap re-runs the 180ms cross-fade when a promise resolves */}
          <span
            className="ad-snack__swap"
            key={t.variant}
            style={{ display: 'grid', placeItems: 'center' }}
          >
            {t.variant === 'loading' ? (
              <span className="ad-spinner ad-spinner--sm" />
            ) : (
              SNACK_ICONS[t.variant] || SNACK_ICONS.info
            )}
          </span>
        </div>
        <div className="ad-snack__body">
          <div className="ad-snack__swap" key={`txt-${t.variant}-${t.title}`}>
            {t.title && <div className="ad-snack__title">{t.title}</div>}
            {t.description && <div className="ad-snack__text">{t.description}</div>}
          </div>
          {(t.action || t.secondaryAction) && (
            <div className="ad-snack__actions">
              {t.action && (
                <button
                  className="ad-snack__action"
                  onClick={() => {
                    t.action.onClick && t.action.onClick();
                    onDismiss(t.id);
                  }}
                >
                  {t.action.label}
                </button>
              )}
              {t.secondaryAction !== false && t.secondaryAction && (
                <button
                  className="ad-snack__action ad-snack__action--ghost"
                  onClick={() => onDismiss(t.id)}
                >
                  {typeof t.secondaryAction === 'string' ? t.secondaryAction : 'Dismiss'}
                </button>
              )}
            </div>
          )}
        </div>
        {t.dismissible !== false && t.variant !== 'loading' && (
          <button
            className="ad-snack__close"
            aria-label="Dismiss notification"
            onClick={() => onDismiss(t.id)}
          >
            <svg
              width="14"
              height="14"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth="2"
              strokeLinecap="round"
            >
              <path d="m18 6-12 12M6 6l12 12" />
            </svg>
          </button>
        )}
        {pct !== null && (
          <div className="ad-snack__timer" aria-hidden="true">
            <div className="ad-snack__timer-fill" style={{ width: `${pct}%` }} />
          </div>
        )}
      </div>
    </div>
  );
};

// ─── Toaster host ───
const Toaster = ({ position = 'bottom-right', max = 3, gap = 8 }) => {
  const [toasts, setToasts] = React.useState(__snackStore.toasts);
  const [hover, setHover] = React.useState(false);
  const heights = React.useRef(new Map());
  const [, force] = React.useState(0);

  React.useEffect(() => {
    const listener = (snapshot) => setToasts(snapshot);
    __snackStore.listeners.add(listener);
    return () => __snackStore.listeners.delete(listener);
  }, []);

  // Visible window — newest first; older toasts queue beyond max
  // and surface as slots free up.
  const visible = toasts.slice(0, max);

  const onHeight = (id, h) => {
    if (heights.current.get(id) !== h) {
      heights.current.set(id, h);
      force((n) => n + 1);
    }
  };

  // Expanded offsets — cumulative measured heights + gap.
  let acc = 0;
  const offsets = visible.map((t) => {
    const o = acc;
    acc += (heights.current.get(t.id) || 0) + gap;
    return o;
  });

  const frontH = heights.current.get(visible[0] && visible[0].id) || 0;
  const regionHeight = hover
    ? Math.max(0, acc - gap)
    : frontH + Math.max(0, visible.length - 1) * 8;
  const top = position.indexOf('top') === 0;
  const hasAssertive = visible.some((t) => t.variant === 'error');

  return (
    <div
      className={`ad-snack-region ad-snack-region--${position}`}
      role="region"
      aria-live={hasAssertive ? 'assertive' : 'polite'}
      aria-label="Notifications"
      style={{ height: regionHeight }}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
    >
      {visible.map((t, i) => (
        <SnackItem
          key={t.id}
          t={t}
          index={i}
          expandedOffset={offsets[i]}
          expanded={hover}
          top={top}
          paused={hover}
          onDismiss={toast.dismiss}
          onHeight={onHeight}
        />
      ))}
    </div>
  );
};

// ─── Empty ───
const EMPTY_DEFAULT_ICON = (
  <svg
    width="24"
    height="24"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.6"
    strokeLinecap="round"
    strokeLinejoin="round"
  >
    <path d="M3 9h18M3 9v9a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V9M3 9l2.4-4.2A1 1 0 0 1 6.27 4h11.46a1 1 0 0 1 .87.8L21 9" />
    <path d="M9 13h6" />
  </svg>
);

const Empty = ({
  icon,
  title,
  children,
  primary,
  secondary,
  size = 'md',
  tint,
  inline,
  dark,
  className = '',
}) => {
  const cls = [
    'ad-empty',
    size !== 'md' ? `ad-empty--${size}` : '',
    tint ? `ad-empty--${tint}` : '',
    inline ? 'ad-empty--inline' : '',
    dark ? 'ad-empty--dark' : '',
    className,
  ]
    .filter(Boolean)
    .join(' ');
  const iconSize = inline || size === 'sm' ? 18 : 24;
  return (
    <div className={cls}>
      <div className="ad-empty__ring" aria-hidden="true">
        {icon || React.cloneElement(EMPTY_DEFAULT_ICON, { width: iconSize, height: iconSize })}
      </div>
      <div style={inline ? { minWidth: 0 } : undefined}>
        <div className="ad-empty__title">{title}</div>
        {children && <div className="ad-empty__sub">{children}</div>}
      </div>
      {(primary || secondary) && (
        <div className="ad-empty__ctas">
          {primary}
          {secondary}
        </div>
      )}
    </div>
  );
};

// ─── ProgressV2 — linear ───
const ProgressV2 = ({
  value,
  max = 100,
  label,
  description,
  variant = 'primary',
  size = 'md',
  indeterminate,
  showValue = true,
}) => {
  const pct = indeterminate ? null : Math.max(0, Math.min(100, (value / max) * 100));
  const cls = [
    'ad-progress',
    variant !== 'primary' ? `ad-progress--${variant}` : '',
    size !== 'md' ? `ad-progress--${size}` : '',
  ]
    .filter(Boolean)
    .join(' ');
  return (
    <div className={cls}>
      {(label || (showValue && !indeterminate)) && (
        <div className="ad-progress__row">
          {label && <span className="ad-progress__label">{label}</span>}
          {showValue && !indeterminate && (
            <span className="ad-progress__value">{Math.round(pct)}%</span>
          )}
        </div>
      )}
      <div
        className="ad-progress__track"
        role="progressbar"
        aria-label={label}
        aria-valuemin={0}
        aria-valuemax={max}
        aria-valuenow={indeterminate ? undefined : value}
      >
        <div
          className={`ad-progress__fill${indeterminate ? ' ad-progress__fill--indeterminate' : ''}`}
          style={indeterminate ? undefined : { width: `${pct}%` }}
        />
      </div>
      {description && <div className="ad-progress__desc">{description}</div>}
    </div>
  );
};

// ─── ProgressV2 — circular SVG ───
const ProgressCircle = ({
  value,
  max = 100,
  size = 48,
  variant = 'primary',
  indeterminate,
  label,
  showValue = true,
}) => {
  const stroke = size <= 32 ? 3 : 4;
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const pct = indeterminate ? 25 : Math.max(0, Math.min(100, (value / max) * 100));
  const cls = [
    'ad-progress-circle',
    variant !== 'primary' ? `ad-progress-circle--${variant}` : '',
    indeterminate ? 'ad-progress-circle--indeterminate' : '',
  ]
    .filter(Boolean)
    .join(' ');
  return (
    <div
      className={cls}
      role="progressbar"
      aria-label={label}
      aria-valuemin={0}
      aria-valuemax={max}
      aria-valuenow={indeterminate ? undefined : value}
    >
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} aria-hidden="true">
        <circle
          className="ad-progress-circle__track"
          cx={size / 2}
          cy={size / 2}
          r={r}
          strokeWidth={stroke}
        />
        <circle
          className="ad-progress-circle__arc"
          cx={size / 2}
          cy={size / 2}
          r={r}
          strokeWidth={stroke}
          strokeDasharray={c}
          strokeDashoffset={c - (pct / 100) * c}
        />
      </svg>
      {!indeterminate && showValue && size > 32 && (
        <span className="ad-progress-circle__num" style={{ fontSize: size >= 64 ? 14 : 11 }}>
          {Math.round(pct)}%
        </span>
      )}
    </div>
  );
};

// ─── ProgressV2 — steps ───
const ProgressSteps = ({ steps = 4, value = 0, variant = 'primary', label }) => (
  <div
    className={`ad-progress-steps${variant !== 'primary' ? ` ad-progress-steps--${variant}` : ''}`}
    role="progressbar"
    aria-label={label}
    aria-valuemin={0}
    aria-valuemax={steps}
    aria-valuenow={value}
    aria-valuetext={`Step ${value} of ${steps}`}
  >
    {Array.from({ length: steps }, (_, i) => (
      <div key={i} className={`ad-progress-steps__seg${i < value ? ' is-filled' : ''}`} />
    ))}
  </div>
);

// ─── Skeleton presets ───
const Skeleton = ({ width, height = 10, circle, pill, radius, style: s }) => (
  <div
    className={`ad-skel${circle ? ' ad-skel--circle' : ''}${pill ? ' ad-skel--pill' : ''}`}
    aria-hidden="true"
    style={{ width, height: circle ? width : height, borderRadius: radius, ...s }}
  />
);

const SkeletonLoading = () => <span className="ad-sr-only">Loading…</span>;

const SkeletonText = ({ lines = 3, width = '100%' }) => (
  <div className="ad-skel-text" aria-hidden="true" style={{ width }}>
    {Array.from({ length: lines }, (_, i) => (
      <Skeleton key={i} width="100%" />
    ))}
  </div>
);

const SkeletonCircle = ({ size = 32 }) => <Skeleton circle width={size} />;
const SkeletonRect = ({ width = '100%', height = 80, radius = 'var(--radius-md)' }) => (
  <Skeleton width={width} height={height} radius={radius} />
);

const SkeletonTable = ({ rows = 3 }) => (
  <div className="ad-skel-surface">
    <div aria-hidden="true">
      <div className="ad-skel-table__head">
        <Skeleton width={100} height={12} />
        <div style={{ flex: 1 }} />
        <Skeleton width={110} height={24} radius="var(--radius-sm)" />
      </div>
      {Array.from({ length: rows }, (_, i) => (
        <div key={i} className="ad-skel-table__row">
          <Skeleton width={`${75 - i * 8}%`} />
          <Skeleton width="60%" />
          <Skeleton pill width="55%" height={14} />
          <Skeleton width="45%" />
        </div>
      ))}
    </div>
    <SkeletonLoading />
  </div>
);

const SkeletonKpi = () => (
  <div className="ad-skel-surface">
    <div className="ad-skel-kpi" aria-hidden="true">
      <Skeleton width="55%" height={10} />
      <Skeleton width="42%" height={22} />
      <Skeleton pill width="48%" height={12} />
    </div>
    <SkeletonLoading />
  </div>
);

const SkeletonCard = () => (
  <div className="ad-skel-surface">
    <div className="ad-skel-card" aria-hidden="true">
      <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
        <Skeleton circle width={32} />
        <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 6 }}>
          <Skeleton width="55%" height={11} />
          <Skeleton width="35%" height={9} />
        </div>
      </div>
      <SkeletonText lines={2} />
      <Skeleton width={120} height={28} radius="var(--radius-sm)" />
    </div>
    <SkeletonLoading />
  </div>
);

const SkeletonForm = ({ fields = 3 }) => (
  <div className="ad-skel-form">
    <div aria-hidden="true" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      {Array.from({ length: fields }, (_, i) => (
        <div key={i} className="ad-skel-form__field">
          <Skeleton width={`${28 + (i % 3) * 10}%`} height={10} />
          <Skeleton width="100%" height={38} radius="var(--radius-md)" />
        </div>
      ))}
      <Skeleton width={132} height={38} radius="var(--radius-md)" />
    </div>
    <SkeletonLoading />
  </div>
);

// ─── Alert (ported from v1 Feedback) ───
// Variant-based inline alert (info/success/warning/error) with icon,
// title, body, action, and optional dismiss. CSS (.ad-alert) lives in
// components.css; the text/action/close sub-classes are defined there too.
const ALERT_ICONS = {
  info: (
    <svg
      width="18"
      height="18"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M12 8v4m0 4h.01" />
    </svg>
  ),
  success: (
    <svg
      width="18"
      height="18"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="m9 12 2 2 4-4" />
    </svg>
  ),
  warning: (
    <svg
      width="18"
      height="18"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
    >
      <path d="M12 2 2 20h20L12 2zM12 9v5m0 3h.01" />
    </svg>
  ),
  error: (
    <svg
      width="18"
      height="18"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="m15 9-6 6M9 9l6 6" />
    </svg>
  ),
};

const Alert = ({ variant = 'info', title, children, action, onDismiss }) => (
  <div className={`ad-alert ad-alert--${variant}`} role="alert">
    <div className="ad-alert__icon" aria-hidden="true">
      {ALERT_ICONS[variant]}
    </div>
    <div className="ad-alert__body">
      {title && <div className="ad-alert__title">{title}</div>}
      {children && <div className="ad-alert__text">{children}</div>}
      {action && <div className="ad-alert__action">{action}</div>}
    </div>
    {onDismiss && (
      <button
        type="button"
        className="ad-alert__close"
        aria-label="Dismiss alert"
        onClick={onDismiss}
      >
        <svg
          width="14"
          height="14"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
        >
          <path d="m18 6-12 12M6 6l12 12" />
        </svg>
      </button>
    )}
  </div>
);

// ─── Spinner (ported from v1 Feedback) ───
// Size-based loading spinner (sm/md/lg). CSS (.ad-spinner) lives in
// components.css and reuses the shared ad-spin keyframe.
const Spinner = ({ size = 'md', label = 'Loading' }) => (
  <div className={`ad-spinner ad-spinner--${size}`} role="status" aria-label={label} />
);

// ─── ShortcutRow (ported from v1 Feedback; uses KbdCombo from Primitives) ───
// Keyboard-shortcut display row: key combo on the left, label, optional
// context tag. KbdCombo is a window global from Primitives.jsx.
const ShortcutRow = ({ keys = [], label, context }) => (
  <div className="ad-shortcut-row">
    <KbdCombo keys={keys} />
    <div className="ad-shortcut-row__label">{label}</div>
    {context && <span className="ad-shortcut-row__context">{context}</span>}
  </div>
);

Object.assign(window, {
  toast,
  Toaster,
  Empty,
  ProgressV2,
  ProgressCircle,
  ProgressSteps,
  Skeleton,
  SkeletonText,
  SkeletonCircle,
  SkeletonRect,
  SkeletonTable,
  SkeletonKpi,
  SkeletonCard,
  SkeletonForm,
  Alert,
  Spinner,
  ShortcutRow,
});
