/**
 * Global search hook — Cmd-K palette backing logic.
 *
 * Loads a generous initial slice of claims, remittances, and activity
 * from the existing `api.list*` endpoints (or the in-memory zustand
 * store in unconfigured mode) once when the hook mounts, then runs
 * an in-memory filter+rank against whatever the user has typed.
 *
 * Design notes:
 *
 *  - **Why fetch a slice, not everything?** The persistence layer
 *    supports a `limit` param; 200 is small enough to be fast and
 *    large enough to cover the demo dataset. We don't paginate the
 *    search corpus — typing should feel instant.
 *
 *  - **Why in-memory and not a dedicated `/api/search` endpoint?**
 *    Avoids backend churn for a UI feature. The in-memory filter is
 *    cheap (three arrays of ≤200 entries each) and works against the
 *    sample-data fallback identically.
 *
 *  - **Why debounce?** Each keystroke would otherwise trigger three
 *    ranking passes plus a re-render. 150ms is the sweet spot where
 *    typing feels uninterrupted but we don't recompute on every
 *    character.
 */

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useSyncExternalStore } from "react";
import { api, type PaginatedResponse } from "@/lib/api";
import { useAppStore } from "@/store";
import type { Activity, Claim, Remittance } from "@/types";

/** Max items we pull into the in-memory search corpus per entity type. */
const SEARCH_CORPUS_LIMIT = 200;

/** Debounce window for the query input. */
const DEBOUNCE_MS = 150;

/** Hard cap on results returned to the UI. Keeps the palette snappy. */
const MAX_RESULTS = 50;

/** Per-group cap so a single entity type can't dominate the palette. */
const MAX_PER_GROUP = 10;

// ---------------------------------------------------------------------------
// Result types
// ---------------------------------------------------------------------------

export type SearchResultKind = "claim" | "remittance" | "activity";

/** Discriminated union — each variant carries the entity id + a display row. */
export type SearchResult =
  | {
      kind: "claim";
      id: string;
      title: string;
      subtitle: string;
      badge: string;
      score: number;
    }
  | {
      kind: "remittance";
      id: string;
      title: string;
      subtitle: string;
      badge: string;
      score: number;
    }
  | {
      kind: "activity";
      id: string;
      title: string;
      subtitle: string;
      badge: string;
      score: number;
    };

/** Result list grouped by entity type — what the palette renders. */
export interface SearchResultsGrouped {
  claims: SearchResult[];
  remittances: SearchResult[];
  activity: SearchResult[];
  total: number;
  query: string;
}

/** Return shape of {@link useSearch}. */
export interface UseSearchResult {
  /** Debounced query — what the ranking is actually run against. */
  query: string;
  /** Setter for the input field. */
  setQuery: (q: string) => void;
  /** Grouped, scored results (already capped). */
  results: SearchResultsGrouped;
  /** True while the corpus is being fetched. */
  isLoading: boolean;
}

// ---------------------------------------------------------------------------
// Scoring
// ---------------------------------------------------------------------------

/**
 * Score a single candidate string against the (lower-cased) query.
 *
 * Priority (highest → lowest):
 *
 *   1. **exact match** — query equals the candidate (`42` === `42`). The
 *      single most useful hit when typing a numeric ID fragment.
 *   2. **prefix match** — candidate starts with the query (`CLM-10…` for
 *      `CLM-10`). The next best "I'm typing the start of an ID" case.
 *   3. **substring match** — query appears anywhere (`Garcia` matching
 *      `Avery Garcia`).
 *   4. **fuzzy subsequence** — every character of the query appears in
 *      the candidate in order. Catches typos like `nguyne` → `Nguyen`.
 *
 * Returns 0 when there's no match at all.
 */
function scoreField(haystack: string | undefined, needle: string): number {
  if (!haystack) return 0;
  const hay = haystack.toLowerCase();
  if (hay === needle) return 100;
  if (hay.startsWith(needle)) return 60;
  const idx = hay.indexOf(needle);
  if (idx >= 0) {
    // Earlier matches score slightly higher — leading `Bills` matches
    // `Blue Cross Blue Shield` more meaningfully than trailing.
    const positionBonus = Math.max(0, 20 - idx);
    return 30 + positionBonus;
  }
  // Subsequence fuzzy match — every char of needle appears in hay in
  // order, with no minimum-length requirement (1-char queries would
  // otherwise match everything and starve exact matches).
  let hi = 0;
  for (let ni = 0; ni < needle.length; ni++) {
    const ch = needle[ni];
    const found = hay.indexOf(ch, hi);
    if (found === -1) return 0;
    hi = found + 1;
  }
  return 10;
}

/** Sum the best score across a set of candidate fields. */
function scoreAgainstFields(
  query: string,
  fields: (string | undefined)[]
): number {
  let best = 0;
  for (const f of fields) {
    const s = scoreField(f, query);
    if (s > best) best = s;
  }
  return best;
}

// ---------------------------------------------------------------------------
// Per-entity rankers — each returns a SearchResult or null when no match
// ---------------------------------------------------------------------------

function rankClaim(c: Claim, query: string): SearchResult | null {
  // Order matters loosely — ID first so typing "CLM-" jumps to claim
  // rows, then patient, then payer, then code. scoreAgainstFields
  // picks the best across all of them so the order here is purely
  // about which fields carry more weight in practice.
  const score = scoreAgainstFields(query, [
    c.id,
    c.patientName,
    c.payerName,
    c.cptCode,
    c.providerNpi,
    c.denialReason,
  ]);
  if (score === 0) return null;
  return {
    kind: "claim",
    id: c.id,
    title: `${c.id} · ${c.patientName}`,
    subtitle: `${c.payerName} · ${c.cptCode} · $${c.billedAmount.toLocaleString()}`,
    badge: c.status,
    score,
  };
}

function rankRemittance(r: Remittance, query: string): SearchResult | null {
  const score = scoreAgainstFields(query, [
    r.id,
    r.claimId,
    r.payerName,
    r.checkNumber,
    r.denialReason ?? undefined,
  ]);
  if (score === 0) return null;
  return {
    kind: "remittance",
    id: r.id,
    title: `${r.id} · ${r.payerName}`,
    subtitle: `Claim ${r.claimId} · Check ${r.checkNumber} · $${r.paidAmount.toLocaleString()}`,
    badge: r.status,
    score,
  };
}

function rankActivity(a: Activity, query: string): SearchResult | null {
  const score = scoreAgainstFields(query, [
    a.id,
    a.message,
    a.kind,
    a.npi,
  ]);
  if (score === 0) return null;
  return {
    kind: "activity",
    id: a.id,
    title: a.message,
    subtitle: a.timestamp,
    badge: a.kind,
    score,
  };
}

// ---------------------------------------------------------------------------
// Corpus fetch (uses existing hooks so fallback mode "just works")
// ---------------------------------------------------------------------------

interface SearchCorpus {
  claims: Claim[];
  remittances: Remittance[];
  activity: Activity[];
}

/**
 * Fetch the search corpus. Mirrors the fallback-mode strategy used by
 * `useClaims` / `useRemittances` / `useActivity` so the palette works
 * identically with the in-memory zustand store and with a live backend.
 *
 * We use `useQuery` for the configured path so react-query handles
 * caching/refetching, and `useSyncExternalStore` for the fallback path
 * so re-renders fire when the store changes (e.g. after a new claim
 * submission).
 */
function useSearchCorpus(): { data: SearchCorpus; isLoading: boolean } {
  // Fallback snapshot from the zustand store. We pull the three arrays
  // we care about — `useSyncExternalStore` returns the same snapshot
  // reference until the store mutates, so this is cheap.
  const fallbackClaims = useSyncExternalStore(
    (cb) => useAppStore.subscribe(cb),
    () => useAppStore.getState().claims,
    () => useAppStore.getState().claims
  );
  const fallbackRemits = useSyncExternalStore(
    (cb) => useAppStore.subscribe(cb),
    () => useAppStore.getState().remittances,
    () => useAppStore.getState().remittances
  );
  const fallbackActivity = useSyncExternalStore(
    (cb) => useAppStore.subscribe(cb),
    () => useAppStore.getState().activity,
    () => useAppStore.getState().activity
  );

  const claimsQ = useQuery<PaginatedResponse<Claim>>({
    queryKey: ["search-corpus", "claims"],
    queryFn: () => api.listClaims<Claim>({ limit: SEARCH_CORPUS_LIMIT }),
    enabled: api.isConfigured,
    // Keep the corpus cached for a minute — re-fetching on every
    // palette open would defeat the "instant" goal of in-memory search.
    staleTime: 60_000,
  });
  const remitsQ = useQuery<PaginatedResponse<Remittance>>({
    queryKey: ["search-corpus", "remittances"],
    queryFn: () => api.listRemittances<Remittance>({ limit: SEARCH_CORPUS_LIMIT }),
    enabled: api.isConfigured,
    staleTime: 60_000,
  });
  const activityQ = useQuery<PaginatedResponse<Activity>>({
    queryKey: ["search-corpus", "activity"],
    queryFn: () => api.listActivity<Activity>({ limit: SEARCH_CORPUS_LIMIT }),
    enabled: api.isConfigured,
    staleTime: 60_000,
  });

  if (!api.isConfigured) {
    return {
      data: {
        claims: fallbackClaims,
        remittances: fallbackRemits,
        activity: fallbackActivity,
      },
      isLoading: false,
    };
  }
  return {
    data: {
      claims: claimsQ.data?.items ?? [],
      remittances: remitsQ.data?.items ?? [],
      activity: activityQ.data?.items ?? [],
    },
    isLoading:
      claimsQ.isLoading || remitsQ.isLoading || activityQ.isLoading,
  };
}

// ---------------------------------------------------------------------------
// Public hook
// ---------------------------------------------------------------------------

/**
 * The Cmd-K search hook.
 *
 * Returns the debounced query (so consumers can render the input value
 * without lag), the setter to wire to the input, the grouped/capped
 * results, and a loading flag for the corpus fetch.
 *
 * The hook is intentionally pure-ish: it does NOT own the dialog
 * open/close state, navigation, or keyboard wiring. Those belong in
 * `<SearchBar>` so the hook stays easy to unit test (no Radix
 * dependencies, no router, no DOM).
 */
export function useSearch(): UseSearchResult {
  const [rawQuery, setRawQuery] = useState("");
  const [debouncedQuery, setDebouncedQuery] = useState("");
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Debounce raw → debounced. We use a ref-tracked timer rather than
  // a `useDebouncedCallback` library so this file has zero deps
  // beyond what `useClaims` already pulls in.
  useEffect(() => {
    if (timerRef.current !== null) clearTimeout(timerRef.current);
    timerRef.current = setTimeout(() => {
      setDebouncedQuery(rawQuery);
      timerRef.current = null;
    }, DEBOUNCE_MS);
    return () => {
      if (timerRef.current !== null) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
    };
  }, [rawQuery]);

  const corpus = useSearchCorpus();

  // The ranking pass. Memoized on the corpus + the *debounced* query
  // so we don't re-rank on every keystroke — only after the 150ms
  // debounce window settles.
  const results = useMemo<SearchResultsGrouped>(() => {
    const q = debouncedQuery.trim().toLowerCase();
    if (!q) {
      return {
        claims: [],
        remittances: [],
        activity: [],
        total: 0,
        query: debouncedQuery,
      };
    }

    const claims = corpus.data.claims
      .map((c) => rankClaim(c, q))
      .filter((r): r is SearchResult => r !== null)
      .sort((a, b) => b.score - a.score)
      .slice(0, MAX_PER_GROUP);

    const remittances = corpus.data.remittances
      .map((r) => rankRemittance(r, q))
      .filter((r): r is SearchResult => r !== null)
      .sort((a, b) => b.score - a.score)
      .slice(0, MAX_PER_GROUP);

    const activity = corpus.data.activity
      .map((a) => rankActivity(a, q))
      .filter((r): r is SearchResult => r !== null)
      .sort((a, b) => b.score - a.score)
      .slice(0, MAX_PER_GROUP);

    return {
      claims,
      remittances,
      activity,
      total: claims.length + remittances.length + activity.length,
      query: debouncedQuery,
    };
  }, [corpus.data.claims, corpus.data.remittances, corpus.data.activity, debouncedQuery]);

  // Stable setter reference — the consumer (an `<input>`) doesn't
  // need to re-render when our hook internals change.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const setQuery = useCallback((q: string) => setRawQuery(q), []);

  return {
    query: rawQuery,
    setQuery,
    results,
    isLoading: corpus.isLoading,
  };
}

// ---------------------------------------------------------------------------
// Navigation destinations
// ---------------------------------------------------------------------------

/**
 * Map a search hit to a URL the router can push. Keeps all
 * "where does an entity go?" knowledge in one place so the SearchBar
 * component doesn't have to know per-entity routing rules.
 *
 *  - claim       → /claims?claim={id}   (opens the drawer deep-link)
 *  - remittance  → /remittances         (Remittances page; the page
 *                                       already lets you expand a row
 *                                       for the adjustment detail)
 *  - activity    → /activity?kind={kind} (Activity Log, pre-filtered
 *                                       to the matching event kind)
 */
export function destinationFor(result: SearchResult): string {
  switch (result.kind) {
    case "claim":
      return `/claims?claim=${encodeURIComponent(result.id)}`;
    case "remittance":
      return "/remittances";
    case "activity": {
      // The activity id is "ACT-<claim-id>" by convention; the
      // ActivityLog page filters by `kind`, so we surface that.
      const u = new URLSearchParams();
      u.set("kind", result.badge);
      return `/activity?${u.toString()}`;
    }
  }
}

/** Flat list of results in display order — what the keyboard nav steps through. */
export function flattenResults(g: SearchResultsGrouped): SearchResult[] {
  return [...g.claims, ...g.remittances, ...g.activity];
}

/** Total cap sentinel — exported for tests so they don't have to hard-code 50. */
export const SEARCH_MAX_RESULTS = MAX_RESULTS;