// SecurityPicker — autocomplete over the existing `balance_securities` catalogue
// with inline creation (Issue #214 / Bilan détail par titre).
//
// Behavior (decisions logged in the autopilot run of 2026-06-04 / 2026-06-06):
//   - The input accepts ANY normalized string (UPPER + TRIM). There is NO live
//     symbol validation — the price fetch (PriceFetchControl) is a separate,
//     best-effort step. A symbol the user types but that does not exist in the
//     catalogue is offered as an inline "create" option.
//   - Picking an existing security emits its stored `symbol` + `asset_type`
//     (+ optional `name`). Creating a new symbol emits the typed (normalized)
//     symbol + the asset_type chosen via the stock/crypto toggle (default
//     'stock', matching `makeEmptyHolding`).
//   - The symbol format mirrors the price-fetching one: `normalizeSecuritySymbol`
//     (UPPER(TRIM(...))), the exact function the service + migrations use, so a
//     picker-created security collapses onto the same `balance_securities` row.
//
// The UI idiom follows `CategoryCombobox` (controlled input + listbox, keyboard
// nav, click-outside close) so the Bilan editor stays visually consistent.
//
// This component is presentation + selection only. It receives the catalogue
// (the parent loads it once via `listSecurities()`), the current row symbol,
// and emits a `SecurityPick` on choose/create. Persisting a brand-new security
// happens server-side at save time (`findOrCreateSecurity` inside the atomic
// save), so no DB write happens here.

import {
  useState,
  useRef,
  useEffect,
  useCallback,
  useId,
  useMemo,
} from "react";
import { useTranslation } from "react-i18next";
import type { BalanceAssetType, BalanceSecurity } from "../../shared/types";
import { normalizeSecuritySymbol } from "../../services/balance.service";

/** What the picker emits when the user selects or creates a security. */
export interface SecurityPick {
  /** Normalized (UPPER/TRIM) symbol. */
  symbol: string;
  asset_type: BalanceAssetType;
  /** Existing security's name, or null for a freshly-created symbol. */
  name: string | null;
  /** True when the symbol is not (yet) in the catalogue — created inline. */
  isNew: boolean;
}

interface SecurityPickerProps {
  /** The full securities catalogue (loaded once by the parent). */
  securities: BalanceSecurity[];
  /** Currently selected symbol on this row (controlled). */
  value: string;
  /** Asset type currently on the row — seeds the create toggle default. */
  assetType: BalanceAssetType;
  onSelect: (pick: SecurityPick) => void;
  disabled?: boolean;
  ariaLabel?: string;
  placeholder?: string;
}

// ---------------------------------------------------------------------------
// Pure helpers (exported for unit tests — the project has no jsdom harness, so
// component logic is tested through these rather than via DOM rendering).
// ---------------------------------------------------------------------------

/**
 * Filter the catalogue by a raw query. Matching is case-insensitive over both
 * the symbol and the (optional) name. An empty query returns the whole list.
 * The catalogue is assumed already symbol-sorted (listSecurities orders by
 * symbol); we preserve that order.
 */
export function filterSecurities(
  securities: BalanceSecurity[],
  query: string
): BalanceSecurity[] {
  const q = query.trim().toLowerCase();
  if (q.length === 0) return securities;
  return securities.filter((s) => {
    if (s.symbol.toLowerCase().includes(q)) return true;
    if (s.name && s.name.toLowerCase().includes(q)) return true;
    return false;
  });
}

/**
 * Decide, for a typed query, whether an inline "create" option should be
 * offered and what it would create. Returns null when the query is empty or
 * when an EXACT (normalized) symbol match already exists in the catalogue —
 * in that case there is nothing new to create. The create symbol is the
 * normalized form so it round-trips to the same `balance_securities` row.
 */
export function decideCreateOption(
  securities: BalanceSecurity[],
  query: string
): { symbol: string } | null {
  const normalized = normalizeSecuritySymbol(query);
  if (normalized.length === 0) return null;
  const exact = securities.some(
    (s) => normalizeSecuritySymbol(s.symbol) === normalized
  );
  if (exact) return null;
  return { symbol: normalized };
}

export default function SecurityPicker({
  securities,
  value,
  assetType,
  onSelect,
  disabled,
  ariaLabel,
  placeholder,
}: SecurityPickerProps) {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");
  const [highlightIndex, setHighlightIndex] = useState(0);
  // Asset type to use when CREATING a new symbol. Seeded from the row's current
  // asset type (default 'stock' via makeEmptyHolding); the user can flip it.
  const [createAssetType, setCreateAssetType] =
    useState<BalanceAssetType>(assetType);

  const inputRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLUListElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const baseId = useId();
  const listboxId = `${baseId}-listbox`;
  const optionId = (i: number) => `${baseId}-option-${i}`;

  // Keep the create-toggle default in sync if the row's asset type changes
  // externally (e.g. a different security was picked then cleared).
  useEffect(() => {
    setCreateAssetType(assetType);
  }, [assetType]);

  const filtered = useMemo(
    () => filterSecurities(securities, query),
    [securities, query]
  );
  const createOption = useMemo(
    () => decideCreateOption(securities, query),
    [securities, query]
  );

  // Layout: [existing matches...] then (optionally) the create row last.
  const totalItems = filtered.length + (createOption ? 1 : 0);
  const createIndex = createOption ? filtered.length : -1;

  // The text shown in the input when closed: the selected symbol verbatim.
  const displayLabel = value;

  useEffect(() => {
    if (open && listRef.current) {
      const el = listRef.current.children[highlightIndex] as
        | HTMLElement
        | undefined;
      el?.scrollIntoView({ block: "nearest" });
    }
  }, [highlightIndex, open]);

  useEffect(() => {
    if (!open) return;
    const handler = (e: MouseEvent) => {
      if (
        containerRef.current &&
        !containerRef.current.contains(e.target as Node)
      ) {
        setOpen(false);
        setQuery("");
      }
    };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);

  const choosePick = useCallback(
    (pick: SecurityPick) => {
      onSelect(pick);
      setOpen(false);
      setQuery("");
      inputRef.current?.blur();
    },
    [onSelect]
  );

  const selectItem = useCallback(
    (index: number) => {
      if (index === createIndex && createOption) {
        choosePick({
          symbol: createOption.symbol,
          asset_type: createAssetType,
          name: null,
          isNew: true,
        });
        return;
      }
      const sec = filtered[index];
      if (sec) {
        choosePick({
          symbol: sec.symbol,
          asset_type: sec.asset_type,
          name: sec.name,
          isNew: false,
        });
      }
    },
    [filtered, createIndex, createOption, createAssetType, choosePick]
  );

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (!open) {
      if (e.key === "ArrowDown" || e.key === "Enter") {
        e.preventDefault();
        setOpen(true);
        setHighlightIndex(0);
      }
      return;
    }
    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        if (totalItems > 0)
          setHighlightIndex((i) => (i + 1) % totalItems);
        break;
      case "ArrowUp":
        e.preventDefault();
        if (totalItems > 0)
          setHighlightIndex((i) => (i - 1 + totalItems) % totalItems);
        break;
      case "Enter":
        e.preventDefault();
        if (totalItems > 0) selectItem(highlightIndex);
        break;
      case "Escape":
        e.preventDefault();
        setOpen(false);
        setQuery("");
        inputRef.current?.blur();
        break;
    }
  };

  const activeId =
    open && totalItems > 0 ? optionId(highlightIndex) : undefined;

  return (
    <div ref={containerRef} className="relative w-40">
      <input
        ref={inputRef}
        type="text"
        role="combobox"
        aria-label={ariaLabel ?? t("balance.snapshot.detailed.symbolLabel")}
        aria-expanded={open}
        aria-controls={listboxId}
        aria-autocomplete="list"
        aria-activedescendant={activeId}
        autoComplete="off"
        spellCheck={false}
        disabled={disabled}
        value={open ? query : displayLabel}
        placeholder={
          placeholder ?? t("balance.snapshot.detailed.picker.placeholder")
        }
        onChange={(e) => {
          setQuery(e.target.value);
          setHighlightIndex(0);
          if (!open) setOpen(true);
        }}
        onFocus={() => {
          setOpen(true);
          setQuery("");
          setHighlightIndex(0);
        }}
        onKeyDown={handleKeyDown}
        className="w-full px-2 py-1.5 text-sm rounded-lg border border-[var(--border)] bg-[var(--card)] text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:outline-none focus:ring-1 focus:ring-[var(--primary)] disabled:opacity-50"
      />
      {open && (
        <div className="absolute z-50 mt-1 w-72 rounded-lg border border-[var(--border)] bg-[var(--card)] shadow-lg overflow-hidden">
          {/* Asset-type toggle for the create option (stock / crypto). Only
              meaningful when a new symbol would be created; shown alongside it
              so the user sets the class before committing the create. */}
          {createOption && (
            <div className="flex items-center gap-2 px-2 py-1.5 border-b border-[var(--border)] bg-[var(--muted)]/40">
              <span className="text-[11px] text-[var(--muted-foreground)]">
                {t("balance.snapshot.detailed.picker.assetTypeLabel")}
              </span>
              <div className="flex rounded-md overflow-hidden border border-[var(--border)]">
                {(["stock", "crypto"] as const).map((at) => (
                  <button
                    key={at}
                    type="button"
                    // Prevent the input blur (mousedown) from closing the list.
                    onMouseDown={(e) => e.preventDefault()}
                    onClick={() => setCreateAssetType(at)}
                    className={`px-2 py-0.5 text-[11px] ${
                      createAssetType === at
                        ? "bg-[var(--primary)] text-white"
                        : "bg-[var(--card)] text-[var(--foreground)] hover:bg-[var(--muted)]"
                    }`}
                    aria-pressed={createAssetType === at}
                  >
                    {t(`balance.snapshot.detailed.picker.assetType.${at}`)}
                  </button>
                ))}
              </div>
            </div>
          )}

          {totalItems === 0 ? (
            <p className="px-3 py-2 text-xs text-[var(--muted-foreground)]">
              {t("balance.snapshot.detailed.picker.empty")}
            </p>
          ) : (
            <ul
              ref={listRef}
              id={listboxId}
              role="listbox"
              className="max-h-56 overflow-auto"
            >
              {filtered.map((sec, i) => (
                <li
                  key={sec.id}
                  id={optionId(i)}
                  role="option"
                  aria-selected={i === highlightIndex}
                  onMouseDown={(e) => e.preventDefault()}
                  onClick={() => selectItem(i)}
                  onMouseEnter={() => setHighlightIndex(i)}
                  className={`flex items-center justify-between gap-2 px-3 py-1.5 text-sm cursor-pointer ${
                    i === highlightIndex
                      ? "bg-[var(--primary)] text-white"
                      : "text-[var(--foreground)] hover:bg-[var(--muted)]"
                  }`}
                >
                  <span className="flex items-center gap-1.5 min-w-0">
                    <span className="font-medium">{sec.symbol}</span>
                    {sec.name && (
                      <span
                        className={`truncate text-xs ${
                          i === highlightIndex
                            ? "text-white/80"
                            : "text-[var(--muted-foreground)]"
                        }`}
                      >
                        {sec.name}
                      </span>
                    )}
                  </span>
                  <span
                    className={`shrink-0 text-[10px] uppercase tracking-wide ${
                      i === highlightIndex
                        ? "text-white/80"
                        : "text-[var(--muted-foreground)]"
                    }`}
                  >
                    {t(
                      `balance.snapshot.detailed.picker.assetType.${sec.asset_type}`
                    )}
                  </span>
                </li>
              ))}
              {createOption && (
                <li
                  id={optionId(createIndex)}
                  role="option"
                  aria-selected={createIndex === highlightIndex}
                  onMouseDown={(e) => e.preventDefault()}
                  onClick={() => selectItem(createIndex)}
                  onMouseEnter={() => setHighlightIndex(createIndex)}
                  className={`px-3 py-1.5 text-sm cursor-pointer border-t border-[var(--border)] ${
                    createIndex === highlightIndex
                      ? "bg-[var(--primary)] text-white"
                      : "text-[var(--foreground)] hover:bg-[var(--muted)]"
                  }`}
                >
                  {t("balance.snapshot.detailed.picker.create", {
                    symbol: createOption.symbol,
                  })}
                </li>
              )}
            </ul>
          )}
        </div>
      )}
    </div>
  );
}
