// ---------------------------------------------------------------------------
// Inbox API client (sub-project 6).
//
// Mirrors the five-lane working surface: rejected / payer_rejected /
// candidates / unmatched / done_today. Plus the actions: manual match,
// dismiss, resubmit, acknowledge, export. All non-2xx responses throw
// (so callers can branch on status).
//
// SP14: added the `payer_rejected` lane (277CA STC A4/A6/A7) and the
// `acknowledge` action — distinct from the 999 envelope `rejected` lane.
// ---------------------------------------------------------------------------

export type ScoreBreakdown = {
  patient: number;
  date: number;
  amount: number;
  provider: number;
};

export type ScoreTier = "strong" | "weak" | "hidden";

export type InboxClaimRow = {
  id: string;
  kind: "claim";
  patient_control_number: string;
  charge_amount: number | null;
  payer_id: string | null;
  provider_npi: string | null;
  state: string;
  rejection_reason: string | null;
  rejected_at: string | null;
  service_date_from: string | null;
  score?: number | null;
  score_tier?: ScoreTier | null;
  score_breakdown?: ScoreBreakdown | null;
  /**
   * SP7: populated on `rejected` / `done_today` claim rows when the claim
   * has a matched remittance. Carries the line-count badge the
   * `MatchedRemitCard` displays (``3/4 matched lines``).
   */
  matched_remittance?: {
    id: string;
    matched_lines: number;
    total_lines: number;
  } | null;
  // SP10/SP14: payer-side rejection fields. Populated on the
  // `payer_rejected` lane with the 277CA STC category, reason,
  // and source 277CA id. The ack fields are always null on the
  // lane (we filter acknowledged claims out) but exposed for
  // forward-compat.
  payer_rejected_at?: string | null;
  payer_rejected_reason?: string | null;
  payer_rejected_status_code?: string | null;
  payer_rejected_by_277ca_id?: string | null;
  payer_rejected_acknowledged_at?: string | null;
  payer_rejected_acknowledged_actor?: string | null;
};

export type InboxCandidateRow = {
  id: string;
  kind: "remit";
  payer_claim_control_number: string;
  charge_amount: number | null;
  payer_id: string | null;
  rendering_provider_npi: string | null;
  service_date: string | null;
  candidates: Array<{
    claim_id: string;
    score: number;
    tier: ScoreTier;
    breakdown: ScoreBreakdown;
  }>;
};

export type InboxLanes = {
  rejected: InboxClaimRow[];
  /** SP14: 277CA STC A4/A6/A7 — payer-side rejection, distinct from 999. */
  payer_rejected: InboxClaimRow[];
  candidates: InboxCandidateRow[];
  unmatched: InboxClaimRow[];
  done_today: InboxClaimRow[];
};

const BASE_URL =
  (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? "";

function joinUrl(path: string): string {
  return `${BASE_URL.replace(/\/$/, "")}${path}`;
}

async function getJson<T>(path: string): Promise<T> {
  const r = await fetch(joinUrl(path));
  if (!r.ok) {
    throw new Error(`GET ${path} failed: ${r.status}`);
  }
  return (await r.json()) as T;
}

async function postJson<T>(path: string, body: unknown): Promise<T> {
  const r = await fetch(joinUrl(path), {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!r.ok) {
    throw new Error(`POST ${path} failed: ${r.status}`);
  }
  return (await r.json()) as T;
}

export async function fetchInboxLanes(): Promise<InboxLanes> {
  return getJson<InboxLanes>("/api/inbox/lanes");
}

export async function matchCandidate(
  remitId: string,
  claimId: string,
): Promise<void> {
  await postJson(`/api/inbox/candidates/${encodeURIComponent(remitId)}/match`, {
    claim_id: claimId,
  });
}

export async function dismissCandidates(
  pairs: Array<{ claim_id: string; remit_id: string }>,
): Promise<void> {
  await postJson("/api/inbox/candidates/dismiss", { pairs });
}

export type ResubmitResult = {
  ok: boolean;
  resubmitted: string[];
  conflicts: Array<{ claim_id: string; current_state: string }>;
};

export async function resubmitRejected(
  claimIds: string[],
): Promise<ResubmitResult> {
  return postJson<ResubmitResult>("/api/inbox/rejected/resubmit", {
    claim_ids: claimIds,
  });
}

/**
 * Resubmit rejected claims AND ask the backend for a downloadable bundle
 * of regenerated 837 files (one .x12 per successfully resubmitted claim).
 *
 * Returns the raw `Blob` plus the filename the backend suggested in its
 * `Content-Disposition` header (e.g. `resubmit-3-claims.zip`). Callers
 * usually pipe both into `downloadTextFile`'s sibling helper for binary
 * downloads — see `downloadBlob` in `@/lib/download`.
 *
 * The endpoint never fails because of a partial bundle — per-claim
 * serialization errors are surfaced via the `X-Cyclone-Serialize-Errors`
 * response header (JSON-encoded array) so the UI can show "10
 * resubmitted, 2 couldn't be regenerated" without parsing the zip. The
 * returned blob still contains the claims that did serialize.
 */
export async function resubmitRejectedWithDownload(
  claimIds: string[],
): Promise<{ blob: Blob; filename: string; serializeErrors: Array<{ claim_id: string; reason: string }> }> {
  const res = await fetch(
    joinUrl("/api/inbox/rejected/resubmit?download=true"),
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ claim_ids: claimIds }),
    },
  );
  if (!res.ok) {
    const detail = await res.text().catch(() => "");
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`,
    );
  }
  const blob = await res.blob();
  const cd = res.headers.get("content-disposition") ?? "";
  const match = /filename="?([^";]+)"?/i.exec(cd);
  const filename = match?.[1] ?? "resubmit-bundle.zip";
  const errHeader = res.headers.get("x-cyclone-serialize-errors");
  let serializeErrors: Array<{ claim_id: string; reason: string }> = [];
  if (errHeader) {
    try {
      serializeErrors = JSON.parse(errHeader);
    } catch {
      serializeErrors = [];
    }
  }
  return { blob, filename, serializeErrors };
}

export function exportInboxCsvUrl(
  lane:
    | "rejected"
    | "payer_rejected"
    | "candidates"
    | "unmatched"
    | "done_today",
): string {
  return joinUrl(`/api/inbox/export.csv?lane=${lane}`);
}

// ---------------------------------------------------------------------------
// SP14: Payer-Rejected acknowledge
//
// Marks the listed claims as acknowledged by the operator. Acknowledged
// claims drop out of the Payer-Rejected Inbox lane. The original 277CA
// rejection event stays in the audit log (SP11) — we only flip a
// working-surface flag.
// ---------------------------------------------------------------------------
export type AcknowledgeResult = {
  ok: boolean;
  transitioned: number;
  already_acked: number;
  not_found: number;
  not_rejected: number;
};

export async function acknowledgePayerRejected(
  claimIds: string[],
  actor: string = "operator",
): Promise<AcknowledgeResult> {
  return postJson<AcknowledgeResult>(
    "/api/inbox/payer-rejected/acknowledge",
    { claim_ids: claimIds, actor },
  );
}
