/**
 * Document upload + progress hooks.
 *
 * Upload hits `POST /api/cases/{n}/documents/upload-tagged` as multipart
 * form-data (FastAPI UploadFile), and receives a `task_id` that streams
 * progress events via `GET /api/progress/{task_id}` (SSE). We expose
 * both as a single `useUploadDocument` mutation returning the task id
 * plus a `useProgress(taskId)` hook that subscribes to the stream.
 */

import { useEffect, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ApiError } from "./client";
import { casesKeys } from "./cases";
import { openSSE } from "@/lib/sse";

export type UploadTaggedResponse = {
  task_id: string;
  filename: string;
  original_name: string;
  doc_type: string;
};

export type ProgressEvent = {
  /* "unknown" is sent by the backend when the task TTL expired or the
   * caller subscribed before any state was published. Treat it as a
   * terminal hint to refetch case state from the source of truth. */
  status: "queued" | "processing" | "completed" | "failed" | "unknown" | string;
  filename?: string;
  step?: string;
  error?: string;
  result?: unknown;
  case_number?: string;
  doc_type?: string;
};

export type UploadVars = {
  caseNumber: string;
  file: File;
  docType?: string;
  partyName?: string;
  title?: string;
};

async function uploadTagged({
  caseNumber,
  file,
  docType = "auto",
  partyName = "",
  title = "",
}: UploadVars): Promise<UploadTaggedResponse> {
  const fd = new FormData();
  fd.append("file", file);
  fd.append("doc_type", docType);
  fd.append("party_name", partyName);
  fd.append("title", title);

  const res = await fetch(
    `/api/cases/${encodeURIComponent(caseNumber)}/documents/upload-tagged`,
    { method: "POST", body: fd },
  );
  const contentType = res.headers.get("content-type") ?? "";
  const parsed = contentType.includes("application/json")
    ? await res.json().catch(() => null)
    : await res.text().catch(() => null);
  if (!res.ok) {
    throw new ApiError(
      `Upload failed with ${res.status}`,
      res.status,
      parsed,
    );
  }
  return parsed as UploadTaggedResponse;
}

export function useUploadDocument(caseNumber: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (vars: Omit<UploadVars, "caseNumber">) =>
      uploadTagged({ caseNumber, ...vars }),
    onSuccess: () => {
      /* Nudge the case detail to refetch so the new document row appears
       * immediately — the actual "processing" badge will update once the
       * SSE stream reports status=completed. */
      qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
    },
  });
}

// ── PATCH document tags ───────────────────────────────────────────

export type DocumentPatch = {
  doc_type?: string;
  appraiser_side?: string; // "" clears; "committee" | "appellant" | "deciding" sets
  is_post_hearing?: boolean; // true = received after the hearing (→ group 4); false clears
  protocol_scope?: string; // "" clears; "appeal" | "lower" (protocol docs only)
};

export type PatchDocumentResponse = {
  status: "completed" | "noop";
  document: {
    id: string;
    doc_type: string;
    title: string;
    metadata?: Record<string, unknown>;
  };
};

async function patchDocument(
  caseNumber: string,
  docId: string,
  patch: DocumentPatch,
): Promise<PatchDocumentResponse> {
  const res = await fetch(
    `/api/cases/${encodeURIComponent(caseNumber)}/documents/${encodeURIComponent(docId)}`,
    {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(patch),
    },
  );
  const contentType = res.headers.get("content-type") ?? "";
  const parsed = contentType.includes("application/json")
    ? await res.json().catch(() => null)
    : await res.text().catch(() => null);
  if (!res.ok) {
    throw new ApiError(`Patch failed with ${res.status}`, res.status, parsed);
  }
  return parsed as PatchDocumentResponse;
}

export function usePatchDocument(caseNumber: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: ({ docId, patch }: { docId: string; patch: DocumentPatch }) =>
      patchDocument(caseNumber, docId, patch),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
    },
  });
}


// ── Extract appraiser facts (on-demand, per case) ──────────────────

export type ExtractAppraiserFactsResponse =
  | {
      status: "completed";
      appraisal_count: number;
      total_facts: number;
      conflicts: unknown[];
      by_document?: unknown[];
    }
  | {
      status: "no_appraisals";
      appraisal_count: 0;
      total_facts: 0;
      conflicts: unknown[];
    }
  | {
      status: "sides_missing";
      appraisal_count: number;
      missing: { document_id: string; title: string; current_side: string }[];
      message: string;
    }
  | {
      // The chair clicked the button; backend created a child Paperclip
      // issue assigned to the legal-analyst, which will run the MCP tool
      // on the host (where the Claude CLI lives) and post results back.
      status: "queued";
      sub_issue_id: string;
      analyst_id: string;
      main_issue_id: string;
    }
  | {
      // No analyst route was available (no API key / no analyst configured /
      // no Paperclip issue linked to the case). Non-fatal — the chair can
      // still trigger extraction manually from Claude Code.
      status: "skipped";
      reason: "no_api_key" | "no_analyst" | "no_issue" | string;
      company_id?: string;
    };

async function extractAppraiserFacts(
  caseNumber: string,
): Promise<ExtractAppraiserFactsResponse> {
  const res = await fetch(
    `/api/cases/${encodeURIComponent(caseNumber)}/extract-appraiser-facts`,
    { method: "POST" },
  );
  const contentType = res.headers.get("content-type") ?? "";
  const parsed = contentType.includes("application/json")
    ? await res.json().catch(() => null)
    : await res.text().catch(() => null);
  if (!res.ok) {
    throw new ApiError(
      `Extraction failed with ${res.status}`,
      res.status,
      parsed,
    );
  }
  return parsed as ExtractAppraiserFactsResponse;
}

export function useExtractAppraiserFacts(caseNumber: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: () => extractAppraiserFacts(caseNumber),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
    },
  });
}


export function useProgress(taskId: string | null, caseNumber?: string) {
  const [event, setEvent] = useState<ProgressEvent | null>(null);
  const qc = useQueryClient();

  useEffect(() => {
    if (!taskId) return;
    // eslint-disable-next-line react-hooks/set-state-in-effect -- reset on taskId change
    setEvent(null);

    /* Self-heal fallback: if no SSE message arrives within 10s — usually
     * because the proxy chain held the chunks or the EventSource is
     * silently retrying — synthesize a refresh by invalidating the case
     * detail. The actual document state is in the case detail anyway, so
     * the UI heals from the source of truth without depending on SSE. */
    let firstMessageReceived = false;
    const fallback = window.setTimeout(() => {
      if (firstMessageReceived) return;
      if (caseNumber) qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
      setEvent({ status: "completed" });
    }, 10_000);

    const close = openSSE<ProgressEvent>(
      `/api/progress/${encodeURIComponent(taskId)}`,
      {
        onMessage: (data) => {
          firstMessageReceived = true;
          setEvent(data);
          if (
            data.status === "completed" ||
            data.status === "failed" ||
            data.status === "unknown"
          ) {
            /* Close from within the callback so EventSource does not
             * auto-reconnect after the server's EOF. For "unknown" we
             * also nudge a case-detail refetch — the task state is gone
             * but the document row will tell us the truth. */
            if (data.status === "unknown" && caseNumber) {
              qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
            }
            close();
          }
        },
      },
    );
    return () => {
      window.clearTimeout(fallback);
      close();
    };
  }, [taskId, caseNumber, qc]);

  return event;
}
