// useBatchExport — owns the per-claim selection state and the
// "click Export → POST → download ZIP" flow for the Upload page's
// streaming view.
//
// Extracted from Upload.tsx in SP9 (June 2026) so the logic is unit-
// testable in isolation from the page (and so the page itself stays
// focused on layout). All state is local to the hook — selection is
// not persisted across navigations, and the captured `batchId` is the
// most-recently-completed parse's server-side id.

import { useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { api } from "@/lib/api";
import { downloadBlob } from "@/lib/download";

/** Minimal shape of a streamed claim this hook needs to read the id. */
export interface BatchExportItem {
  kind: "837p" | "835";
  data: { claim_id?: string; payer_claim_control_number?: string };
}

export interface UseBatchExportResult {
  /** Currently-selected 837P claim ids. */
  selectedClaimIds: Set<string>;
  /** True while an export POST is in flight. */
  exporting: boolean;
  /** Total 837P claims currently in the stream. */
  total837InStream: number;
  /** Toggle a single claim's selection. */
  onToggleSelect: (claimId: string) => void;
  /** Toggle between "all selected" and "none selected" across the stream. */
  onToggleAll: () => void;
  /** Fire the export. Returns the promise so tests can await it. */
  onExport: () => Promise<void>;
  /**
   * Called by the parent after a successful parse to record the
   * server-side batch id. Captured into a ref so a re-parse that
   * completes mid-export can't swap in a different id under the
   * export promise.
   */
  recordBatchId: (batchId: string | null) => void;
}

export function useBatchExport(items: BatchExportItem[]): UseBatchExportResult {
  const [selectedClaimIds, setSelectedClaimIds] = useState<Set<string>>(
    () => new Set(),
  );
  const [exporting, setExporting] = useState(false);
  const lastBatchIdRef = useRef<string | null>(null);
  // Tracks every claim id that has ever been in the stream. Used by the
  // auto-select effect to distinguish "new claim, default-select it"
  // from "user previously deselected this claim — leave it alone".
  // Without this, a re-render with the same items list would re-add
  // every claim that the user explicitly deselected.
  const seenClaimIdsRef = useRef<Set<string>>(new Set());

  // Auto-select newly streamed 837P claims.
  //
  // Pre-compute the new claim ids OUTSIDE the setState callback so
  // the ref is mutated exactly once per items.length change and the
  // setState callback only runs when there's actually something new
  // to add. The previous inline pattern (mutating the ref inside the
  // setState callback) failed under React StrictMode's effect
  // double-invoke: the second invoke saw an empty prev (the first
  // invoke's queued update was dropped) AND a ref that already
  // contained the ids, so the second invoke added nothing — and the
  // selection ended up empty even though the stream populated.
  useEffect(() => {
    const newIds: string[] = [];
    for (const item of items) {
      if (item.kind === "837p" && item.data.claim_id) {
        const id = item.data.claim_id;
        // Only add if we've never seen this id before (so a
        // re-render with the same items doesn't re-add anything the
        // user just deselected) AND it's not already selected (no
        // need to add it again).
        if (!seenClaimIdsRef.current.has(id)) {
          seenClaimIdsRef.current.add(id);
          newIds.push(id);
        }
      }
    }
    if (newIds.length === 0) return;
    setSelectedClaimIds((prev) => {
      const next = new Set(prev);
      for (const id of newIds) next.add(id);
      return next;
    });
    // Key on items.length so a re-render with the same set of items
    // (just new array identity) doesn't re-run the effect.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [items.length]);

  const total837InStream = useMemo(
    () => items.filter((i) => i.kind === "837p").length,
    [items],
  );

  function onToggleSelect(claimId: string) {
    setSelectedClaimIds((prev) => {
      const next = new Set(prev);
      if (next.has(claimId)) {
        next.delete(claimId);
      } else {
        next.add(claimId);
      }
      return next;
    });
  }

  function onToggleAll() {
    const streamed837Ids = items
      .filter((i): i is BatchExportItem & { data: { claim_id: string } } =>
        i.kind === "837p" && !!i.data.claim_id,
      )
      .map((i) => i.data.claim_id);
    if (
      streamed837Ids.length > 0 &&
      streamed837Ids.every((id) => selectedClaimIds.has(id))
    ) {
      setSelectedClaimIds(new Set());
    } else {
      setSelectedClaimIds(new Set(streamed837Ids));
    }
  }

  function recordBatchId(batchId: string | null) {
    lastBatchIdRef.current = batchId;
  }

  async function onExport() {
    const batchId = lastBatchIdRef.current;
    if (!batchId) {
      toast.error("No batch to export — parse a file first.");
      return;
    }
    const ids = Array.from(selectedClaimIds);
    if (ids.length === 0) return;
    setExporting(true);
    try {
      const { blob, filename, serializeErrors } =
        await api.exportBatch837(batchId, ids);
      downloadBlob(filename, blob);
      if (serializeErrors.length === 0) {
        toast.success(`Exported ${ids.length} claims as X12`, {
          description: filename,
        });
      } else {
        toast.warning(
          `Exported ${ids.length - serializeErrors.length} · ${serializeErrors.length} couldn't be regenerated`,
          {
            description: serializeErrors
              .map((e) => `${e.claim_id}: ${e.reason}`)
              .join("\n"),
          },
        );
      }
    } catch (err) {
      toast.error(err instanceof Error ? err.message : "Export failed");
    } finally {
      setExporting(false);
    }
  }

  return {
    selectedClaimIds,
    exporting,
    total837InStream,
    onToggleSelect,
    onToggleAll,
    onExport,
    recordBatchId,
  };
}
