// Reusable searchable account picker — Task 11. Every account picker on the
// Record screen ("Where did it land?", "What was it for?", "Paid from?",
// "From"/"To") is one of these, differing only in which groups of accounts
// they're handed (see Record.tsx's moneyGroups()/categoryGroups()).
// CLAUDE.md §9: "Categories, not account numbers" — options show the plain
// account name only, grouped under a standard section header ("Bank
// accounts" / "Credit cards" / "Income" / ... — accounts/pickerGroups.ts,
// Chart Management Task 3), never a code.
//
// Combobox behavior: click/focus opens the list and clears the visible text
// so typing searches from scratch; arrow keys move a highlighted option,
// Enter selects it, Escape closes without changing the selection. The
// dropdown's onMouseDown calls preventDefault() so clicking an option fires
// its onClick before the input's onBlur would otherwise close the list out
// from under the click — the standard combobox pattern for this.
//
// That same preventDefault() is also why the input never actually blurs when
// a selection is made by clicking an option: focus stays right where it was.
// That's normally fine (onFocus's "clear query, reopen" only needs to run
// when focus is GAINED) — except a real focus event never fires again if the
// user immediately types more without first clicking/tabbing away, since the
// input was already focused. Without a fix, that keystroke lands in a
// textbox whose visible text is still the previous selection's label, so it
// visibly APPENDS onto that label instead of starting a fresh search (e.g.
// selecting "Cash on Hand" then immediately typing "Savings" would search for
// the literal string "Cash on HandSavings" and find nothing) — caught during
// this task's live browser verification, not just a theoretical case. The
// fix mirrors what native/library comboboxes do: after a selection lands
// while the input still has focus, select() the input's full text so the
// very next keystroke replaces it (standard browser behavior for typing over
// a selection) rather than inserting at the end.

import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";

export interface PickerOption {
  id: string;
  label: string;
}

export interface PickerGroup {
  label: string;
  options: PickerOption[];
}

export function SearchablePicker({
  id,
  label,
  placeholder,
  groups,
  value,
  onChange,
  disabled,
  emptyMessage = "No matches.",
  action,
}: {
  id: string;
  label: string;
  placeholder?: string;
  groups: PickerGroup[];
  value: string | null;
  onChange: (id: string) => void;
  disabled?: boolean;
  emptyMessage?: string;
  /** Optional footer affordance pinned to the bottom of the open menu —
   * Chart Management Task 2's "+ New category" (opens the shared add-account
   * modal from right inside the picker). Always visible while the menu is
   * open, even with zero matches (that's exactly when someone needs it). */
  action?: { label: string; onSelect: () => void };
}) {
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");
  const [highlightIndex, setHighlightIndex] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);

  // See the file header: after `value` changes (a selection just landed) and
  // the input is still the focused element, select all of its text so the
  // next keystroke — typed without an intervening focus event, since focus
  // never left — replaces it instead of appending.
  useEffect(() => {
    if (document.activeElement === inputRef.current) {
      inputRef.current?.select();
    }
  }, [value]);

  const selectedLabel = useMemo(() => {
    for (const group of groups) {
      const found = group.options.find((o) => o.id === value);
      if (found) return found.label;
    }
    return "";
  }, [groups, value]);

  const filteredGroups = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return groups.filter((g) => g.options.length > 0);
    return groups
      .map((g) => ({ label: g.label, options: g.options.filter((o) => o.label.toLowerCase().includes(q)) }))
      .filter((g) => g.options.length > 0);
  }, [groups, query]);

  const flatOptions = useMemo(() => filteredGroups.flatMap((g) => g.options), [filteredGroups]);
  // The action row (when present) is one more arrow-key stop AFTER the last
  // option, so "+ New category" is fully keyboard-reachable (review finding
  // 6) — its index is exactly flatOptions.length.
  const navigableCount = flatOptions.length + (action ? 1 : 0);
  const effectiveHighlight = Math.min(highlightIndex, Math.max(navigableCount - 1, 0));
  const actionHighlighted = action !== undefined && effectiveHighlight === flatOptions.length;

  function selectOption(opt: PickerOption) {
    onChange(opt.id);
    setOpen(false);
    setQuery("");
  }

  function runAction() {
    setOpen(false);
    setQuery("");
    action?.onSelect();
  }

  function handleKeyDown(ev: KeyboardEvent<HTMLInputElement>) {
    if (ev.key === "ArrowDown") {
      ev.preventDefault();
      setOpen(true);
      setHighlightIndex((i) => Math.min(i + 1, navigableCount - 1));
    } else if (ev.key === "ArrowUp") {
      ev.preventDefault();
      setHighlightIndex((i) => Math.max(i - 1, 0));
    } else if (ev.key === "Enter") {
      ev.preventDefault();
      const opt = flatOptions[effectiveHighlight];
      if (opt) selectOption(opt);
      else if (actionHighlighted) runAction();
    } else if (ev.key === "Escape") {
      setOpen(false);
      setQuery("");
    }
  }

  let runningIndex = -1;

  return (
    <div className="field">
      <label htmlFor={id}>{label}</label>
      <div className="picker-wrap">
        <input
          ref={inputRef}
          id={id}
          className="text-input"
          type="text"
          role="combobox"
          aria-expanded={open}
          aria-autocomplete="list"
          autoComplete="off"
          placeholder={placeholder}
          disabled={disabled}
          value={open ? query : selectedLabel}
          onFocus={() => {
            setOpen(true);
            setQuery("");
            setHighlightIndex(0);
          }}
          onChange={(ev) => {
            setQuery(ev.target.value);
            setOpen(true);
            setHighlightIndex(0);
          }}
          onKeyDown={handleKeyDown}
          onBlur={() => {
            setOpen(false);
            setQuery("");
          }}
        />
        {open && (
          <ul className="picker-menu" onMouseDown={(ev) => ev.preventDefault()}>
            {flatOptions.length === 0 && <li className="picker-empty">{emptyMessage}</li>}
            {filteredGroups.map((group) => (
              <li key={group.label} className="picker-group">
                <div className="picker-group-label">{group.label}</div>
                <ul>
                  {group.options.map((opt) => {
                    runningIndex += 1;
                    const idx = runningIndex;
                    return (
                      <li
                        key={opt.id}
                        className={`picker-option${idx === effectiveHighlight ? " picker-option-active" : ""}`}
                        onClick={() => selectOption(opt)}
                      >
                        {opt.label}
                      </li>
                    );
                  })}
                </ul>
              </li>
            ))}
            {action && (
              <li
                className={`picker-action${actionHighlighted ? " picker-option-active" : ""}`}
                onClick={runAction}
              >
                {action.label}
              </li>
            )}
          </ul>
        )}
      </div>
    </div>
  );
}
