// ---------------------------------------------------------------------------
// Inbox lanes hook (sub-project 6, Phase 7 Task 19).
//
// Owns the four-lane payload (`rejected` / `candidates` / `unmatched` /
// `done_today`) for the Inbox page. T19 replaced the original 5s polling
// with the SP5 tail: the hook opens the claim and remittance streams via
// `useTailStream`, and whenever the tail store grows (a new `item` event
// arrived) it triggers a debounced refetch of the lanes payload. The
// debounce coalesces a burst of events into one network round-trip.
//
// The streams themselves stay open as long as a consumer of this hook is
// mounted (e.g. the `/inbox` page). When the user navigates away, the
// hook unmounts and `useTailStream`'s cleanup aborts the SSE connection.
// ---------------------------------------------------------------------------

import { useCallback, useEffect, useRef, useState } from "react";
import { fetchInboxLanes, type InboxLanes } from "@/lib/inbox-api";
import { useTailStream } from "./useTailStream";
import { useTailStore } from "@/store/tail-store";

/**
 * Coalesce bursty tail events into a single network round-trip. With 5s
 * polling this was implicitly handled by the interval; with the tail we
 * have to do it explicitly. 250ms is short enough that the user sees the
 * new row within their natural attention span (well under the spec's
 * "live updates" SLO), and long enough that a 10-event burst doesn't
 * fire 10 fetches.
 */
const REFETCH_DEBOUNCE_MS = 250;

export function useInboxLanes() {
  const [lanes, setLanes] = useState<InboxLanes>({
    rejected: [],
    // SP14: the 5th working-surface lane (277CA STC A4/A6/A7).
    payer_rejected: [],
    candidates: [],
    unmatched: [],
    done_today: [],
  });
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  // Keep the SP5 tail connections alive while this hook is mounted. The
  // hook itself doesn't read their return value — events are dispatched
  // into the tail store, which we subscribe to below. The `useTailStream`
  // call is what opens the SSE stream and keeps it alive; without these
  // two lines the inbox would silently stop updating once the user
  // navigated away from a page that already mounted a tail.
  useTailStream("claims");
  useTailStream("remittances");

  // Subscribe to the tail-store order arrays. We watch the *length* (not
  // the contents) because the store's `addClaim`/`addRemittance` returns
  // a new array reference on every insert, which is exactly what zustand
  // uses to flag a re-render.
  const claimOrderLen = useTailStore((s) => s.claimOrder.length);
  const remitOrderLen = useTailStore((s) => s.remitOrder.length);

  const refetch = useCallback(async () => {
    try {
      const next = await fetchInboxLanes();
      setLanes(next);
      setError(null);
    } catch (e) {
      setError(e as Error);
    } finally {
      setLoading(false);
    }
  }, []);

  // Hold the latest `refetch` in a ref so the debounce timer always
  // closes over the current closure without forcing the effect below to
  // re-subscribe on every render.
  const refetchRef = useRef(refetch);
  refetchRef.current = refetch;

  // Track the order lengths we last observed so the very first render
  // (with the store already at its initial size, possibly non-zero if a
  // sibling page has been populating it) doesn't trigger a spurious
  // refetch. -1 sentinel means "haven't observed yet".
  const prevLenRef = useRef<{ claims: number; remits: number }>({
    claims: -1,
    remits: -1,
  });
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    const prev = prevLenRef.current;
    const claimsChanged = prev.claims !== claimOrderLen;
    const remitsChanged = prev.remits !== remitOrderLen;
    prevLenRef.current = { claims: claimOrderLen, remits: remitOrderLen };

    // Skip the initial observation — the lanes have already been
    // fetched via the mount effect below. Only react to *changes*.
    if (prev.claims === -1) return;
    if (!claimsChanged && !remitsChanged) return;

    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => {
      debounceRef.current = null;
      void refetchRef.current();
    }, REFETCH_DEBOUNCE_MS);

    return () => {
      if (debounceRef.current) {
        clearTimeout(debounceRef.current);
        debounceRef.current = null;
      }
    };
  }, [claimOrderLen, remitOrderLen]);

  // Initial fetch on mount. Runs once; the tail-driven effect above
  // handles subsequent updates.
  useEffect(() => {
    void refetch();
  }, [refetch]);

  return { lanes, loading, error, refetch };
}
