"use client";

import * as React from "react";

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  useComboboxAnchor,
} from "@/components/ui/combobox";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { requestDashboardApi } from "@/lib/dashboard-api-client";

import type { UnderlyingOption } from "../_lib/schema";

const SEARCH_HREF = "/dashboard/master-table/structure-master/bulk-edit/search-underlying";

type Props = {
  id?: string;
  value: string;
  name: string | null;
  onSelect: (option: UnderlyingOption | null) => void;
  placeholder?: string;
};

/**
 * Searches the five denormalised underlying slots on Structure_Master.
 *
 * The selected value is a plain ISIN string so the form stays serialisable, but
 * the option's name is passed back too — it labels the action in history.
 */
export function UnderlyingCombobox({
  id,
  value,
  name,
  onSelect,
  placeholder = "Search by ISIN or name…",
}: Props) {
  const anchor = useComboboxAnchor();
  const [term, setTerm] = React.useState("");
  const debouncedTerm = useDebouncedValue(term, 300);

  /**
   * Results are stored together with the term they were fetched for. That makes
   * both "which options to show" and "are we still loading" derivable, so the
   * effect never has to call setState synchronously to clear stale state.
   */
  const [result, setResult] = React.useState<{
    term: string;
    items: UnderlyingOption[];
    error: string | null;
  }>({ term: "", items: [], error: null });

  const trimmedTerm = debouncedTerm.trim();
  const hasEnoughInput = trimmedTerm.length >= 2;
  const isSettled = result.term === trimmedTerm;
  const isLoading = hasEnoughInput && !isSettled;
  // Memoised so the `[]` branch does not produce a new array identity on every
  // render, which would defeat the `items` memo below.
  const options = React.useMemo(
    () => (hasEnoughInput && isSettled ? result.items : []),
    [hasEnoughInput, isSettled, result.items],
  );

  React.useEffect(() => {
    if (!hasEnoughInput || isSettled) {
      return;
    }

    const controller = new AbortController();

    requestDashboardApi<{ items?: UnderlyingOption[] }>({
      url: `${SEARCH_HREF}?term=${encodeURIComponent(trimmedTerm)}`,
      method: "GET",
      cache: "no-store",
      signal: controller.signal,
      fallbackError: "Could not search underlyings.",
    })
      .then((data) =>
        setResult({ term: trimmedTerm, items: data.items ?? [], error: null }),
      )
      .catch((error: unknown) => {
        // An aborted request is the expected outcome of typing another
        // character, not a failure worth surfacing.
        if (!controller.signal.aborted) {
          setResult({
            term: trimmedTerm,
            items: [],
            error:
              error instanceof Error && error.message
                ? error.message
                : "Could not search underlyings.",
          });
        }
      });

    return () => controller.abort();
  }, [hasEnoughInput, isSettled, trimmedTerm]);

  const selected = React.useMemo<UnderlyingOption | null>(
    () => (value ? { isin: value, name, currency: null } : null),
    [name, value],
  );

  // A previously chosen ISIN will not be in a fresh result set, so it is kept in
  // the list to stay renderable as the selected value.
  const items = React.useMemo(() => {
    if (!selected || options.some((option) => option.isin === selected.isin)) {
      return options;
    }
    return [selected, ...options];
  }, [options, selected]);

  const emptyMessage =
    term.trim().length < 2
      ? "Type at least 2 characters"
      : isLoading
        ? "Searching…"
        : // A failed search must not read as "this ISIN is not in the master".
          (isSettled ? result.error : null) ?? "No underlying found";

  return (
    <Combobox
      items={items}
      value={selected}
      onValueChange={(next) => onSelect((next as UnderlyingOption | null) ?? null)}
      onInputValueChange={setTerm}
      itemToStringLabel={(item: UnderlyingOption) =>
        item.name ? `${item.isin} — ${item.name}` : item.isin
      }
      isItemEqualToValue={(item: UnderlyingOption, current: UnderlyingOption) =>
        item.isin === current.isin
      }
    >
      {/* The anchor ref is a div ref, while ComboboxInput forwards to the input
          element itself — so the popup is anchored to a wrapper instead. */}
      <div ref={anchor}>
        <ComboboxInput id={id} placeholder={placeholder} />
      </div>
      <ComboboxContent anchor={anchor} className="w-(--anchor-width)">
        <ComboboxEmpty>{emptyMessage}</ComboboxEmpty>
        <ComboboxList>
          {(item: UnderlyingOption) => (
            <ComboboxItem key={item.isin} value={item}>
              <span className="flex flex-col">
                <span className="font-medium">{item.isin}</span>
                {item.name ? (
                  <span className="text-muted-foreground text-xs">
                    {item.name}
                    {item.currency ? ` · ${item.currency}` : ""}
                  </span>
                ) : null}
              </span>
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}
