/**
 * External Precedent Library hooks.
 *
 * The library is the authoritative case-law corpus — chair-uploaded
 * court rulings + other appeals committee decisions, with halachot
 * extracted automatically and queued for chair approval. Distinct from:
 *   - /api/training (Daphna's style corpus — sample decisions for tone)
 *   - /api/precedents (chair-attached quotes per case section)
 *
 * Endpoints touched (all under /api/precedent-library and /api/halachot):
 *   - POST /upload (multipart) → task_id (consumed by useProgress)
 *   - GET / (filters) → list
 *   - GET /{id} → detail with halachot
 *   - PATCH /{id} → metadata edit
 *   - DELETE /{id} → remove
 *   - POST /{id}/extract-halachot → re-run halacha extractor
 *   - GET /search → semantic search (halachot + chunks)
 *   - GET /stats
 *   - GET /api/halachot?status=... → review queue
 *   - PATCH /api/halachot/{id} → approve/reject/edit
 */

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ApiError, apiRequest } from "./client";

export type PracticeArea =
  | ""
  | "rishuy_uvniya"
  | "betterment_levy"
  | "compensation_197";

export type SourceType = "" | "court_ruling" | "appeals_committee";

export type Precedent = {
  id: string;
  case_number: string;
  case_name: string;
  court: string;
  date: string | null;
  practice_area: PracticeArea | "";
  appeal_subtype: string;
  source_type: SourceType | "";
  precedent_level: string;
  is_binding: boolean;
  summary: string;
  headnote: string;
  subject_tags: string[];
  source_kind: string;
  chair_name: string | null;
  district: string | null;
  citation_formatted: string;
  extraction_status: string;
  halacha_extraction_status: string;
  metadata_extraction_status: string;
  metadata_extraction_requested_at: string | null;
  halacha_extraction_requested_at: string | null;
  created_at: string;
  halachot_count: number;
  approved_count: number;
  pending_count: number;
  rejected_count: number;
  deferred_count: number;
};

export type Halacha = {
  id: string;
  case_law_id: string;
  halacha_index: number;
  rule_statement: string;
  rule_type: string;
  // authority over the committee — DERIVED from the source (INV-DM7), read-only.
  authority?: "binding" | "persuasive" | null;
  reasoning_summary: string;
  supporting_quote: string;
  page_reference: string;
  practice_areas: string[];
  subject_tags: string[];
  cites: string[];
  confidence: number;
  quote_verified: boolean;
  /* #81 strict-rubric quality flags — non_decision | truncated_quote |
   * thin_restatement | quote_unverified. Any flag blocked auto-approval. */
  quality_flags?: string[];
  review_status: "pending_review" | "approved" | "rejected" | "published" | "deferred";
  reviewer: string;
  reviewed_at: string | null;
  created_at: string;
  updated_at: string;
  /* Joined from case_law for review/list views */
  case_number?: string;
  case_name?: string;
  court?: string;
  decision_date?: string | null;
  precedent_level?: string;
  /* X11 citation corroboration — how many distinct later courts/committees
   * adopted this halacha (positive treatment), and whether any cited it
   * negatively (distinguished/criticized/overruled). */
  corroboration_count?: number;
  corroboration_negative?: boolean;
  /* #84.2 near-duplicate clustering (present only when fetched with cluster=true):
   * same-precedent halachot within the cluster cosine share a cluster_id, so the
   * UI collapses them into one review card. cluster_size === 1 → singleton. */
  cluster_id?: string;
  cluster_size?: number;
  /* #84.2 parallel authority (present only when fetched with include_equivalents):
   * the SAME principle stated independently in OTHER precedents — recurrence, not
   * citation (distinct from corroboration_count). */
  equivalents?: {
    halacha_id: string;
    case_number: string;
    rule_statement: string;
    cosine: number | null;
  }[];
  /* #133/FU-2 — the latest 3-judge panel deliberation (present only when fetched
   * with include_panel_round). Surfaced in the review card so the chair sees WHY
   * the panel split before she decides; her decision is the gold label the
   * active-learning loop learns from. Capture-only — does not affect review_status. */
  panel_round?: {
    question: "keep" | "entailed";
    verdict: "unanimous_yes" | "unanimous_no" | "split" | "incomplete";
    applied_action: string;
    round_ts: string | null;
    judges: {
      model: "claude" | "deepseek" | "gemini";
      vote: boolean | null;
      reason: string;
    }[];
  };
  /* V41 — canonical halachot model: one principle, many instances. */
  canonical_id?: string | null;
  instance_type?: "original" | "citation" | "application" | null;
  canonical_statement?: string | null;
  instance_count?: number | null;
};

export type RelatedCase = {
  id: string;
  case_number: string;
  case_name: string;
  court: string;
  precedent_level: string;
  date: string | null;
  relation_type: string;
};

/** A decision that cites THIS ruling — auto-detected from the citation graph
 *  (precedent_internal_citations incoming edges). Read-only (no unlink). */
export type IncomingCitation = {
  id: string;
  case_number: string;
  case_name: string;
  court: string;
  precedent_level: string;
  chair_name: string;
  date: string | null;
  confidence: number | null;
};

export type PrecedentDetail = Precedent & {
  full_text: string;
  halachot: Halacha[];
  related_cases: RelatedCase[];
  incoming_citations: IncomingCitation[];
};

export type SearchHit =
  | {
      type: "halacha";
      score: number;
      halacha_id: string;
      case_law_id: string;
      rule_statement: string;
      reasoning_summary: string;
      supporting_quote: string;
      page_reference: string;
      practice_areas: string[];
      subject_tags: string[];
      confidence: number;
      rule_type: string;
      authority?: "binding" | "persuasive" | null;
      case_number: string;
      case_name: string;
      court: string;
      decision_date: string | null;
      precedent_level: string;
    }
  | {
      type: "passage";
      score: number;
      chunk_id: string;
      case_law_id: string;
      content: string;
      section_type: string;
      page_number: number | null;
      case_number: string;
      case_name: string;
      court: string;
      decision_date: string | null;
      precedent_level: string;
      practice_area: string;
    };

export type LibraryStats = {
  precedents_total: number;
  by_practice_area: { practice_area: string; count: number }[];
  by_precedent_level: { precedent_level: string; count: number }[];
  halachot_total: number;
  halachot_pending: number;
  halachot_approved: number;
  halachot_rejected: number;
  halachot_deferred: number;
};

export type ListFilters = {
  practiceArea?: PracticeArea;
  court?: string;
  precedentLevel?: string;
  sourceType?: SourceType;
  sourceKind?: string;
  search?: string;
  limit?: number;
  offset?: number;
};

export const libraryKeys = {
  all: ["precedent-library"] as const,
  list: (filters: ListFilters) =>
    [...libraryKeys.all, "list", filters] as const,
  detail: (id: string) => [...libraryKeys.all, "detail", id] as const,
  search: (q: string, filters: Record<string, string | boolean>) =>
    [...libraryKeys.all, "search", q, filters] as const,
  stats: () => [...libraryKeys.all, "stats"] as const,
  halachotPending: (search = "") => [...libraryKeys.all, "halachot", "pending", search] as const,
  halachot: (filters: Record<string, string>) =>
    [...libraryKeys.all, "halachot", filters] as const,
};

export function usePrecedents(filters: ListFilters = {}) {
  return useQuery({
    queryKey: libraryKeys.list(filters),
    queryFn: ({ signal }) => {
      const p = new URLSearchParams();
      if (filters.practiceArea) p.set("practice_area", filters.practiceArea);
      if (filters.court) p.set("court", filters.court);
      if (filters.precedentLevel) p.set("precedent_level", filters.precedentLevel);
      if (filters.sourceType) p.set("source_type", filters.sourceType);
      if (filters.sourceKind) p.set("source_kind", filters.sourceKind);
      if (filters.search) p.set("search", filters.search);
      if (filters.limit) p.set("limit", String(filters.limit));
      if (filters.offset) p.set("offset", String(filters.offset));
      const qs = p.toString();
      return apiRequest<{ items: Precedent[]; count: number }>(
        `/api/precedent-library${qs ? `?${qs}` : ""}`,
        { signal },
      );
    },
    staleTime: 30_000,
    /* Poll while any row is mid-processing or queued for the local MCP
     * worker. Once everything settles to completed/failed the polling
     * stops on its own — no fixed background timer. */
    refetchInterval: (query) => {
      const data = query.state.data;
      if (!data) return false;
      const active = data.items.some((p) => isPrecedentActive(p));
      return active ? 5000 : false;
    },
  });
}

/** A precedent is "active" while text/halacha extraction is in flight or
 * legitimately queued for the local MCP worker. Used by the auto-refresh
 * poller and by the row UI to disable destructive actions.
 *
 * Once a status is "completed" or "failed", the row is NEVER active —
 * even if the corresponding `*_requested_at` timestamp still has a value.
 * The worker is supposed to NULL it on success but in practice doesn't
 * always, and treating those rows as active leaves them permanently
 * undeletable. */
export function isPrecedentActive(p: Precedent): boolean {
  // Text extraction
  if (p.extraction_status === "processing") return true;

  // Halacha extraction
  if (p.halacha_extraction_status === "processing") return true;
  if (
    p.halacha_extraction_status === "pending" &&
    p.halacha_extraction_requested_at !== null
  ) {
    return true;
  }

  // Metadata extraction now has its own status column. Active while the
  // worker is processing the row, or while it's queued (timestamp set) and
  // hasn't reached a terminal state yet.
  if (p.metadata_extraction_status === "processing") return true;
  if (
    p.metadata_extraction_requested_at !== null &&
    p.metadata_extraction_status !== "completed" &&
    p.metadata_extraction_status !== "failed"
  ) {
    return true;
  }

  return false;
}

export function usePrecedent(id: string | null) {
  return useQuery({
    queryKey: libraryKeys.detail(id ?? ""),
    queryFn: ({ signal }) =>
      apiRequest<PrecedentDetail>(
        `/api/precedent-library/${encodeURIComponent(id!)}`,
        { signal },
      ),
    enabled: Boolean(id),
    staleTime: 30_000,
    /* Poll while THIS precedent is mid-extraction (text/halacha/metadata) so the
     * detail catches up after the local MCP drainer fills fields like
     * practice_area / chair_name — otherwise the open edit sheet shows stale
     * empties until a hard refresh. Stops once the row settles (mirrors the
     * list poller in usePrecedents). */
    refetchInterval: (query) => {
      const data = query.state.data;
      return data && isPrecedentActive(data) ? 5000 : false;
    },
  });
}

export function useLibraryStats() {
  return useQuery({
    queryKey: libraryKeys.stats(),
    queryFn: ({ signal }) =>
      apiRequest<LibraryStats>("/api/precedent-library/stats", { signal }),
    staleTime: 30_000,
    // The halacha-review tab counts ride on this (real COUNT(*) — not the
    // limit-capped /api/halachot len), so poll so the chips track approvals
    // live instead of only refreshing on navigation (#6/#7/#8).
    refetchInterval: 30_000,
  });
}

export type SearchFilters = {
  practiceArea?: PracticeArea;
  court?: string;
  precedentLevel?: string;
  appealSubtype?: string;
  subjectTag?: string;
  includeHalachot?: boolean;
  limit?: number;
};

export function useLibrarySearch(query: string, filters: SearchFilters = {}) {
  const params: Record<string, string | boolean> = {};
  if (filters.practiceArea) params.practice_area = filters.practiceArea;
  if (filters.court) params.court = filters.court;
  if (filters.precedentLevel) params.precedent_level = filters.precedentLevel;
  if (filters.appealSubtype) params.appeal_subtype = filters.appealSubtype;
  if (filters.subjectTag) params.subject_tag = filters.subjectTag;
  if (filters.includeHalachot !== undefined)
    params.include_halachot = filters.includeHalachot;

  return useQuery({
    queryKey: libraryKeys.search(query, params),
    queryFn: ({ signal }) => {
      const p = new URLSearchParams({ q: query });
      for (const [k, v] of Object.entries(params)) p.set(k, String(v));
      if (filters.limit) p.set("limit", String(filters.limit));
      return apiRequest<{ items: SearchHit[]; count: number }>(
        `/api/precedent-library/search?${p.toString()}`,
        { signal },
      );
    },
    enabled: query.trim().length >= 2,
    staleTime: 10_000,
    placeholderData: (prev) => prev,
  });
}

export type PrecedentUploadInput = {
  file: File;
  citation: string;
  case_name?: string;
  court?: string;
  decision_date?: string;
  source_type?: SourceType;
  precedent_level?: string;
  practice_area?: PracticeArea;
  appeal_subtype?: string;
  subject_tags?: string[];
  is_binding?: boolean;
  headnote?: string;
  summary?: string;
};

export function useUploadPrecedent() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (input: PrecedentUploadInput) => {
      const fd = new FormData();
      fd.append("file", input.file);
      fd.append("citation", input.citation);
      if (input.case_name) fd.append("case_name", input.case_name);
      if (input.court) fd.append("court", input.court);
      if (input.decision_date) fd.append("decision_date", input.decision_date);
      if (input.source_type) fd.append("source_type", input.source_type);
      if (input.precedent_level)
        fd.append("precedent_level", input.precedent_level);
      if (input.practice_area)
        fd.append("practice_area", input.practice_area);
      if (input.appeal_subtype)
        fd.append("appeal_subtype", input.appeal_subtype);
      if (input.subject_tags && input.subject_tags.length)
        fd.append("subject_tags", JSON.stringify(input.subject_tags));
      fd.append("is_binding", String(input.is_binding ?? true));
      if (input.headnote) fd.append("headnote", input.headnote);
      if (input.summary) fd.append("summary", input.summary);

      const res = await fetch("/api/precedent-library/upload", {
        method: "POST",
        body: fd,
      });
      const parsed = await res.json().catch(() => null);
      if (!res.ok) {
        throw new ApiError(
          `Upload failed with ${res.status}`,
          res.status,
          parsed,
        );
      }
      return parsed as { task_id: string };
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: libraryKeys.all });
    },
  });
}

// Valid Hebrew districts for appeals-committee decisions. Mirrors
// VALID_DISTRICTS in mcp-server/src/legal_mcp/tools/internal_decisions.py —
// keep in sync with the service-side guard.
export const COMMITTEE_DISTRICTS = [
  "ירושלים",
  "תל אביב",
  "מרכז",
  "חיפה",
  "צפון",
  "דרום",
  "ארצי",
] as const;

export type CommitteeDistrict = (typeof COMMITTEE_DISTRICTS)[number];

// A citation that targets internal_decision_upload, not the external library.
// Matches the prefix list in precedent_library service (ערר/בל"מ/ARAR).
const COMMITTEE_PREFIXES = ["ערר ", "ערר(", "בל\"מ ", "בל\"מ(", "ARAR "];

export function isCommitteeCitation(citation: string): boolean {
  const c = citation.trimStart();
  return COMMITTEE_PREFIXES.some((p) => c.startsWith(p));
}

export type InternalDecisionUploadInput = {
  file: File;
  case_number: string;
  /** Full citation (מראה-מקום), stored as citation_formatted. Distinct from
   *  the canonical case_number identifier. */
  citation?: string;
  chair_name: string;
  district: CommitteeDistrict | string;
  case_name?: string;
  court?: string;
  decision_date?: string;
  practice_area?: PracticeArea;
  appeal_subtype?: string;
  subject_tags?: string[];
  is_binding?: boolean;
  summary?: string;
};

export function useUploadInternalDecision() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (input: InternalDecisionUploadInput) => {
      const fd = new FormData();
      fd.append("file", input.file);
      fd.append("case_number", input.case_number);
      if (input.citation) fd.append("citation", input.citation);
      fd.append("chair_name", input.chair_name);
      fd.append("district", input.district);
      if (input.case_name) fd.append("case_name", input.case_name);
      if (input.court) fd.append("court", input.court);
      if (input.decision_date) fd.append("decision_date", input.decision_date);
      if (input.practice_area) fd.append("practice_area", input.practice_area);
      if (input.appeal_subtype)
        fd.append("appeal_subtype", input.appeal_subtype);
      if (input.subject_tags && input.subject_tags.length)
        fd.append("subject_tags", JSON.stringify(input.subject_tags));
      fd.append("is_binding", String(input.is_binding ?? false));
      if (input.summary) fd.append("summary", input.summary);

      const res = await fetch("/api/internal-decisions/upload", {
        method: "POST",
        body: fd,
      });
      const parsed = await res.json().catch(() => null);
      if (!res.ok) {
        throw new ApiError(
          `Upload failed with ${res.status}`,
          res.status,
          parsed,
        );
      }
      return parsed as { task_id: string };
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: libraryKeys.all });
    },
  });
}

export function useDeletePrecedent() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      apiRequest<{ deleted: boolean }>(
        `/api/precedent-library/${encodeURIComponent(id)}`,
        { method: "DELETE" },
      ),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: libraryKeys.all });
    },
  });
}

export function useLinkRelatedCase(caseId: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (vars: { relatedId: string; relationType?: string }) =>
      apiRequest<{ linked: boolean }>(
        `/api/precedent-library/${encodeURIComponent(caseId)}/relations`,
        {
          method: "POST",
          body: {
            related_id: vars.relatedId,
            relation_type: vars.relationType ?? "same_case_chain",
          },
        },
      ),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: libraryKeys.detail(caseId) });
    },
  });
}

export function useUnlinkRelatedCase(caseId: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (relatedId: string) =>
      apiRequest<{ unlinked: boolean }>(
        `/api/precedent-library/${encodeURIComponent(caseId)}/relations/${encodeURIComponent(relatedId)}`,
        { method: "DELETE" },
      ),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: libraryKeys.detail(caseId) });
    },
  });
}

export type PrecedentPatch = Partial<{
  case_number: string;
  case_name: string;
  court: string;
  decision_date: string;
  practice_area: PracticeArea;
  appeal_subtype: string;
  subject_tags: string[];
  summary: string;
  headnote: string;
  source_type: SourceType;
  precedent_level: string;
  is_binding: boolean;
  district: string;
  chair_name: string;
  citation_formatted: string;
}>;

export function useUpdatePrecedent() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: ({ id, patch }: { id: string; patch: PrecedentPatch }) =>
      apiRequest<Precedent>(
        `/api/precedent-library/${encodeURIComponent(id)}`,
        { method: "PATCH", body: patch },
      ),
    onSuccess: (_, { id }) => {
      qc.invalidateQueries({ queryKey: libraryKeys.detail(id) });
      qc.invalidateQueries({ queryKey: libraryKeys.all });
    },
  });
}

/* Extraction can't run inside the container (no `claude` CLI). The
 * "request" endpoints below stamp a queue marker in case_law; the chair
 * (or me) drains the queue from Claude Code by invoking the MCP tool
 * `precedent_process_pending`, which runs the actual extractor locally.
 * See the rule in mcp-server/src/legal_mcp/services/claude_session.py. */

export function useRequestMetadataExtraction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      apiRequest<{ queued: boolean }>(
        `/api/precedent-library/${encodeURIComponent(id)}/request-metadata`,
        { method: "POST" },
      ),
    onSuccess: (_, id) => {
      qc.invalidateQueries({ queryKey: libraryKeys.detail(id) });
      qc.invalidateQueries({ queryKey: libraryKeys.all });
    },
  });
}

export function useRequestHalachotExtraction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      apiRequest<{ queued: boolean }>(
        `/api/precedent-library/${encodeURIComponent(id)}/request-halachot`,
        { method: "POST" },
      ),
    onSuccess: (_, id) => {
      qc.invalidateQueries({ queryKey: libraryKeys.detail(id) });
      qc.invalidateQueries({ queryKey: libraryKeys.all });
    },
  });
}

/** #84.1/#84.2/#84.3 — the chair review queue.
 *
 * ONE fetch of the whole pending set (#133 unified queue): all pending halachot,
 * priority-ordered (panel-split first → most-uncertain → oldest, #84.3/FU-3),
 * near-duplicate-clustered, with the latest panel deliberation attached. The
 * review panel splits this client-side by ACTION — "להכרעה" (has a panel round)
 * vs "תיקון-חילוץ" (flagged, never adjudicated) — instead of the old empty
 * clean/needsfix toggle. */
export function useHalachotPending(opts: { limit?: number; search?: string } = {}) {
  const { limit = 200, search = "" } = opts;
  const term = search.trim();
  const qs = `review_status=pending_review&exclude_low_quality=false`
    + `&order_by_priority=true&cluster=true&include_equivalents=true`
    + `&include_panel_round=true&with_total=true&limit=${limit}`
    + (term ? `&search=${encodeURIComponent(term)}` : "");
  return useQuery({
    queryKey: libraryKeys.halachotPending(term),
    queryFn: ({ signal }) =>
      apiRequest<{ items: Halacha[]; count: number; total?: number }>(
        `/api/halachot?${qs}`, { signal }),
    staleTime: 5_000,
    refetchOnMount: "always",
  });
}

/** Genuine extraction defects — the rule TEXT is malformed, so it needs re-extraction
 * (not a chair keep/drop judgment). Mirrors the backend's DEFECT set in
 * scripts/halacha_panel_approve.py; other flags (e.g. `application` = fact-specific
 * application) are judgment signals, NOT defects, and the panel deliberately skips them. */
const EXTRACTION_DEFECT_FLAGS = new Set([
  "quote_unverified",
  "truncated_quote",
  "thin_restatement",
  "near_duplicate",
]);

/** A pending item belongs in the "needs extraction fix" segment when it carries a
 * genuine extraction-DEFECT flag AND the panel never deliberated it (no round).
 * Everything else — deliberated items, clean items, and items flagged only for
 * chair judgment (e.g. `application`) — is a chair-judgment item. (#133 unified queue) */
export function isExtractionFixItem(h: Halacha): boolean {
  return (
    !h.panel_round &&
    (h.quality_flags ?? []).some((f) => EXTRACTION_DEFECT_FLAGS.has(f))
  );
}

export function useHalachotByStatus(
  status: string,
  opts: { limit?: number; search?: string } = {},
) {
  const { limit = 300, search = "" } = opts;
  const term = search.trim();
  // include_panel_round surfaces the 3-judge deliberation on these tabs too
  // (same card as the pending queue), so the chair sees WHY an item was
  // auto-rejected/approved; search locates an item server-side beyond the window.
  const qs = `review_status=${encodeURIComponent(status)}&include_panel_round=true&limit=${limit}`
    + (term ? `&search=${encodeURIComponent(term)}` : "");
  return useQuery({
    queryKey: libraryKeys.halachot({ review_status: status, limit: String(limit), search: term }),
    queryFn: ({ signal }) =>
      apiRequest<{ items: Halacha[]; count: number }>(`/api/halachot?${qs}`, { signal }),
    staleTime: 10_000,
    refetchOnMount: "always",
  });
}

export type HalachaPatch = Partial<{
  review_status: "pending_review" | "approved" | "rejected" | "published" | "deferred";
  reviewer: string;
  rule_statement: string;
  reasoning_summary: string;
  subject_tags: string[];
  practice_areas: string[];
  // #133 — editing the quote re-verifies it against the source server-side and
  // clears/sets the quote_unverified flag (extraction repair).
  supporting_quote: string;
  // V41 — propagated to canonical_halachot.canonical_statement
  canonical_statement: string;
}>;

export type CanonicalInstance = {
  id: string;
  instance_type: "original" | "citation" | "application";
  confidence: number;
  rule_statement: string;
  case_number: string;
  case_name: string;
};

export function useCanonicalInstances(canonical_id: string | null | undefined) {
  return useQuery({
    queryKey: ["canonical-instances", canonical_id],
    queryFn: () =>
      apiRequest<{ instances: CanonicalInstance[] }>(
        `/api/canonical-halachot/${encodeURIComponent(canonical_id!)}/instances`,
      ).then((r) => r.instances),
    enabled: !!canonical_id,
    staleTime: 60_000,
  });
}

export function useUpdateHalacha() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: ({ id, patch }: { id: string; patch: HalachaPatch }) =>
      apiRequest<Halacha>(
        `/api/halachot/${encodeURIComponent(id)}`,
        { method: "PATCH", body: patch },
      ),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: libraryKeys.all });
      // APR-5 / ADM-2 (INV-IA2): approving/rejecting a halacha changes the
      // chair-pending aggregate and the /operations halacha_review counter.
      qc.invalidateQueries({ queryKey: ["chair", "pending"] });
      qc.invalidateQueries({ queryKey: ["operations"] });
    },
  });
}

export type BatchReviewStatus =
  | "approved" | "rejected" | "deferred" | "pending_review" | "published";

/** #84 — apply one review status to many halachot in a single request. */
export function useBatchReviewHalachot() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: ({ ids, status, reviewer }: {
      ids: string[]; status: BatchReviewStatus; reviewer?: string;
    }) =>
      apiRequest<{ updated: number }>(
        `/api/halachot/batch`,
        { method: "POST", body: { halacha_ids: ids, review_status: status, reviewer } },
      ),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: libraryKeys.all });
      // APR-5 / ADM-2 (INV-IA2): a batch review shifts the chair-pending
      // aggregate and the /operations halacha_review counter.
      qc.invalidateQueries({ queryKey: ["chair", "pending"] });
      qc.invalidateQueries({ queryKey: ["operations"] });
    },
  });
}
