import { useEffect, useRef, useState } from "react";
import { Check, ChevronDown, type LucideIcon } from "lucide-react";
import clsx from "clsx";
import { useDismiss } from "../lib/useDismiss";

export interface ViewOption<T extends string> {
  id: T;
  label: string;
  icon: LucideIcon;
}

// "How should this list look" — the current mode's icon in the toolbar, the named modes in a
// menu behind it. Generic over the mode id so any module's list can adopt it (the feed's
// density modes; the managers' layouts) without re-deriving the menu behaviour.
//
// This is a real menu, so it carries the whole keyboard model the role promises: arrows/Home/End
// move a roving focus, Escape closes, and closing always hands focus back to the trigger (the
// menu unmounts under the focused item, which would otherwise strand focus on <body>).
export default function ViewSwitcher<T extends string>({
  value,
  options,
  onChange,
  label,
  labelClassName = "hidden",
}: {
  value: T;
  options: readonly ViewOption<T>[];
  onChange: (v: T) => void;
  label: string;
  /** Where the current mode's NAME is visible beside the icon — a Tailwind display class, e.g.
   * `"hidden xl:inline"`. The caller sets it because only it knows how crowded its own toolbar is:
   * the feed's is packed (show chips, content chips, source, count, sort) and can't spare the room
   * until xl, while an emptier one could show it always. Defaults to icon-only. */
  labelClassName?: string;
}) {
  const [open, setOpen] = useState(false);
  // Which item holds the roving tabindex; seeded to the active one when the menu opens.
  const [focusIdx, setFocusIdx] = useState(0);
  const btnRef = useRef<HTMLButtonElement | null>(null);
  const menuRef = useRef<HTMLDivElement | null>(null);
  const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);

  function close({ refocus = true }: { refocus?: boolean } = {}) {
    setOpen(false);
    if (refocus) btnRef.current?.focus();
  }
  // Outside-click must NOT refocus the trigger — that would steal focus from whatever the user
  // just clicked. Escape is handled on the menu itself (below), where returning focus is right.
  useDismiss(open, () => close({ refocus: false }), [btnRef, menuRef]);

  // A `value` outside `options` is a caller bug; degrade to the first option rather than render
  // nothing. The menu then shows no checked item, which is honest — nothing IS selected.
  const currentIdx = Math.max(
    0,
    options.findIndex((o) => o.id === value),
  );
  // Seeding the roving index HERE — rather than in an effect keyed on `open` — matters: an effect
  // would only schedule the update, so the focus effect below would still see the previous index
  // and focus the wrong item for one render before correcting.
  function openMenu() {
    setFocusIdx(currentIdx);
    setOpen(true);
  }
  // Clamped at render, so a caller whose option list shrinks while the menu is open can't leave
  // the roving tabindex pointing past the end (which would make every item un-tabbable).
  const rovingIdx = Math.min(focusIdx, options.length - 1);
  useEffect(() => {
    if (open) itemRefs.current[rovingIdx]?.focus();
  }, [open, rovingIdx]);

  const current = options[currentIdx];
  if (!current) return null;
  const CurrentIcon = current.icon;

  function onMenuKeyDown(e: React.KeyboardEvent) {
    const last = options.length - 1;
    if (e.key === "Escape") {
      // Stop it reaching useDismiss's document listener, which would close without refocusing.
      e.stopPropagation();
      close();
    } else if (e.key === "ArrowDown") {
      e.preventDefault();
      setFocusIdx((i) => (i >= last ? 0 : i + 1));
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      setFocusIdx((i) => (i <= 0 ? last : i - 1));
    } else if (e.key === "Home") {
      e.preventDefault();
      setFocusIdx(0);
    } else if (e.key === "End") {
      e.preventDefault();
      setFocusIdx(last);
    } else if (e.key === "Tab") {
      // Tabbing out of a menu closes it, but the focus is moving on by itself.
      close({ refocus: false });
    }
  }

  return (
    <div className="relative">
      <button
        ref={btnRef}
        onClick={() => (open ? close() : openMenu())}
        aria-haspopup="menu"
        aria-expanded={open}
        // The aria-label carries the mode's name at every size — the visible text below is a
        // nicety for wide windows, not the accessible name.
        aria-label={`${label}: ${current.label}`}
        title={`${label}: ${current.label}`}
        className="shrink-0 inline-flex items-center gap-1 pl-2 pr-1 py-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
      >
        <CurrentIcon className="w-4 h-4 shrink-0" />
        <span className={clsx("text-sm whitespace-nowrap", labelClassName)}>{current.label}</span>
        <ChevronDown
          className={`w-3 h-3 shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
        />
      </button>
      {open && (
        <div
          ref={menuRef}
          role="menu"
          aria-label={label}
          onKeyDown={onMenuKeyDown}
          className="glass-menu absolute right-0 top-full mt-1 z-chrome min-w-44 p-1.5 rounded-xl animate-[popIn_0.16s_ease]"
        >
          {/* The modes are one exclusive choice, so the radio items need a group to scope their
              checked-ness to — otherwise they read as independent checkable items. */}
          <div role="group">
            {options.map((o, i) => {
              const Icon = o.icon;
              const on = o.id === value;
              return (
                <button
                  key={o.id}
                  ref={(el) => {
                    itemRefs.current[i] = el;
                  }}
                  role="menuitemradio"
                  aria-checked={on}
                  tabIndex={i === rovingIdx ? 0 : -1}
                  onClick={() => {
                    onChange(o.id);
                    close();
                  }}
                  className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-lg text-sm text-left transition ${
                    on ? "text-accent" : "text-fg hover:bg-card"
                  }`}
                >
                  <Icon className="w-4 h-4 shrink-0" />
                  <span className="flex-1 truncate">{o.label}</span>
                  {on && <Check className="w-3.5 h-3.5 shrink-0" />}
                </button>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}
