/**
 * Cyclone API client.
 *
 * Talks to the FastAPI backend that lives at `VITE_API_BASE_URL`
 * (defaulting to an empty string → "no backend configured" mode). When the
 * base URL is empty, every method throws `notConfiguredError()`; the
 * higher-level hooks in `src/hooks/` catch that by reading from the
 * in-memory zustand store instead. This keeps the pages agnostic of the
 * switch.
 *
 * Endpoints:
 *   - `parse837(file, { onProgress })` POSTs to `/api/parse-837`. With
 *     `onProgress` set, streams NDJSON (one `{type,data}` per line) so the
 *     UI can render claims incrementally. Without it, asks the backend for
 *     a single JSON object via `Accept: application/json`.
 *   - `parse835(...)` mirrors the 837 shape for `/api/parse-835`.
 *   - `health()` GETs `/api/health` and returns `{ status, version }`.
 *   - `listBatches / getBatch / listClaims / listRemittances / listProviders
 *     / getProvider / listActivity` are plain JSON GETs against the
 *     persistence surface.
 *   - `listUnmatched / matchRemit / unmatchClaim` hit the reconciliation
 *     surface. POSTs throw `ApiError` so callers can branch on `.status`.
 */

import type {
  Ack,
  BatchDiff,
  ClaimDetail,
  ClaimOutput,
  ClaimPayment,
  Envelope,
  FinancialInfo,
  MatchResponse,
  NdjsonEvent,
  ParseResult837,
  ParseResult835,
  Payer,
  Payer835,
  Payee835,
  Provider,
  ReassociationTrace,
  UnmatchedClaim,
  UnmatchedResponse,
  BatchSummary as ParserBatchSummary,
} from "@/types";

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

export const isConfigured = BASE_URL.length > 0;

// ---------------------------------------------------------------------------
// Shared types
// ---------------------------------------------------------------------------

export type NdjsonProgressEvent =
  | { type: "envelope"; data: Envelope | null }
  | { type: "claim"; data: ClaimOutput }
  | { type: "claim_payment"; data: ClaimPayment }
  | { type: "financial_info"; data: FinancialInfo }
  | { type: "trace"; data: ReassociationTrace }
  | { type: "payer"; data: Payer | Payer835 }
  | { type: "payee"; data: Payee835 }
  | { type: "summary"; data: ParserBatchSummary };

export type ParseProgress = (event: NdjsonProgressEvent) => void;

export interface ParseOptions {
  /** Defaults to "co_medicaid" for 837 / "co_medicaid_835" for 835. */
  payer?: string;
  /** Promote validation warnings to errors on the backend. */
  strict?: boolean;
  /** Include raw EDI segments in the response (default: true). */
  includeRawSegments?: boolean;
  /**
   * Optional progress callback. When provided, the client streams NDJSON from
   * the backend and invokes the callback for every line. When omitted, the
   * client requests a single JSON object via `Accept: application/json`.
   */
  onProgress?: ParseProgress;
}

export interface HealthResponse {
  status: string;
  version: string;
}

// ---------------------------------------------------------------------------
// List-endpoint types (GET surfaces, sub-project 1)
// ---------------------------------------------------------------------------

export interface ListClaimsParams {
  batch_id?: string;
  status?: string;
  payer?: string;
  provider_npi?: string;
  date_from?: string;
  date_to?: string;
  sort?: string;
  order?: "asc" | "desc";
  limit?: number;
  offset?: number;
}

export interface ListRemittancesParams {
  batch_id?: string;
  payer?: string;
  claim_id?: string;
  date_from?: string;
  date_to?: string;
  sort?: string;
  order?: "asc" | "desc";
  limit?: number;
  offset?: number;
}

export interface ListProvidersParams {
  npi?: string;
  state?: string;
  limit?: number;
  offset?: number;
}

export interface ListActivityParams {
  kind?: string;
  since?: string;
  limit?: number;
}

export interface PaginatedResponse<T> {
  items: T[];
  total: number;
  returned: number;
  has_more: boolean;
}

/**
 * Lightweight summary used by the GET /api/batches list endpoint. Distinct
 * from the parser's `BatchSummary` (which lives in `@/types` and carries the
 * full validation + per-rule counts) — this one is what the UI needs to
 * render a list row.
 */
export interface BatchSummary {
  id: string;
  kind: "837p" | "835";
  inputFilename: string;
  parsedAt: string;
  claimCount: number;
}

// ---------------------------------------------------------------------------
// Low-level helpers
// ---------------------------------------------------------------------------

function notConfiguredError(): Error {
  return new Error(
    "API not configured. Set VITE_API_BASE_URL in .env.local to use the backend."
  );
}

/**
 * Error thrown for non-2xx responses. Carries the HTTP `status` so callers can
 * branch on it (e.g. 404 vs 500) without parsing the message string.
 */
export class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
  }
}

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

async function readErrorBody(res: Response): Promise<string> {
  try {
    const t = await res.text();
    if (!t) return "";
    // FastAPI errors are `{ "error": "...", "detail": "..." }`. Surface the
    // detail if present, else the raw text.
    try {
      const obj = JSON.parse(t) as { detail?: unknown; error?: unknown };
      if (typeof obj.detail === "string") return obj.detail;
      if (typeof obj.error === "string") return obj.error;
    } catch {
      // not JSON; fall through
    }
    return t;
  } catch {
    return "";
  }
}

/** Build a query string, skipping undefined/empty params. */
function qs(params: Record<string, unknown> | undefined): string {
  if (!params) return "";
  const u = new URLSearchParams();
  for (const [k, v] of Object.entries(params)) {
    if (v === undefined || v === null || v === "") continue;
    u.set(k, String(v));
  }
  const s = u.toString();
  return s ? `?${s}` : "";
}

async function parseNdjsonStream(
  res: Response,
  onProgress: ParseProgress
): Promise<ParserBatchSummary> {
  if (!res.body) {
    throw new Error("Response had no body to stream.");
  }
  const reader = res.body.getReader();
  const decoder = new TextDecoder("utf-8");
  let buffer = "";
  let lastSummary: ParserBatchSummary | null = null;

  // Read the body chunk-by-chunk. A `while (true)` loop is the standard
  // pattern for ReadableStreamDefaultReader; `done` breaks us out.
  // eslint-disable-next-line no-constant-condition
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    // Split on newlines. The backend terminates every line with "\n".
    let nl = buffer.indexOf("\n");
    while (nl !== -1) {
      const line = buffer.slice(0, nl).replace(/\r$/, "");
      buffer = buffer.slice(nl + 1);
      if (line.length > 0) {
        let event: NdjsonEvent;
        try {
          event = JSON.parse(line) as NdjsonEvent;
        } catch (err) {
          throw new Error(
            `Failed to parse NDJSON line: ${
              err instanceof Error ? err.message : String(err)
            }\n${line}`
          );
        }
        onProgress(event as NdjsonProgressEvent);
        if (event.type === "summary") {
          lastSummary = event.data as ParserBatchSummary;
        }
      }
      nl = buffer.indexOf("\n");
    }
  }

  // Flush any trailing partial line (no final newline).
  const tail = buffer.replace(/\r$/, "");
  if (tail.length > 0) {
    const event = JSON.parse(tail) as NdjsonEvent;
    onProgress(event as NdjsonProgressEvent);
    if (event.type === "summary") {
      lastSummary = event.data as ParserBatchSummary;
    }
  }

  if (!lastSummary) {
    throw new Error("Stream ended without a summary line.");
  }
  return lastSummary;
}

// ---------------------------------------------------------------------------
// Public surface — parser endpoints
// ---------------------------------------------------------------------------

async function parse837(
  file: File,
  options: ParseOptions = {}
): Promise<ParseResult837 | ParserBatchSummary> {
  if (!isConfigured) throw notConfiguredError();
  const { payer = "co_medicaid", strict = false, includeRawSegments = true, onProgress } = options;

  const params = new URLSearchParams({
    payer,
    strict: String(strict),
    include_raw_segments: String(includeRawSegments),
  });

  const form = new FormData();
  form.append("file", file, file.name);

  const accept = onProgress ? "application/x-ndjson" : "application/json";
  const res = await fetch(joinUrl(`/api/parse-837?${params.toString()}`), {
    method: "POST",
    body: form,
    headers: { Accept: accept },
  });

  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }

  if (onProgress) {
    return parseNdjsonStream(res, onProgress);
  }
  return (await res.json()) as ParseResult837;
}

async function parse835(
  file: File,
  options: ParseOptions = {}
): Promise<ParseResult835 | ParserBatchSummary> {
  if (!isConfigured) throw notConfiguredError();
  const { payer = "co_medicaid_835", strict = false, includeRawSegments = true, onProgress } = options;

  const params = new URLSearchParams({
    payer,
    strict: String(strict),
    include_raw_segments: String(includeRawSegments),
  });

  const form = new FormData();
  form.append("file", file, file.name);

  const accept = onProgress ? "application/x-ndjson" : "application/json";
  const res = await fetch(joinUrl(`/api/parse-835?${params.toString()}`), {
    method: "POST",
    body: form,
    headers: { Accept: accept },
  });

  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }

  if (onProgress) {
    return parseNdjsonStream(res, onProgress);
  }
  return (await res.json()) as ParseResult835;
}

/**
 * Upload a 999 ACK file for parsing. The backend persists the row
 * and returns both the parsed result and the persisted ack metadata
 * (including the regenerated raw 999 text for download).
 */
async function parse999(
  file: File
): Promise<{ ack: Ack & { raw_999_text: string }; parsed: unknown }> {
  if (!isConfigured) throw notConfiguredError();
  const form = new FormData();
  form.append("file", file, file.name);
  const res = await fetch(joinUrl("/api/parse-999"), {
    method: "POST",
    body: form,
    headers: { Accept: "application/json" },
  });
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  const body = (await res.json()) as {
    ack: {
      id: number;
      accepted_count: number;
      rejected_count: number;
      received_count: number;
      ack_code: "A" | "E" | "R" | "P";
      source_batch_id: string;
      raw_999_text: string;
    };
    parsed: unknown;
  };
  return {
    ack: {
      id: body.ack.id,
      sourceBatchId: body.ack.source_batch_id,
      acceptedCount: body.ack.accepted_count,
      rejectedCount: body.ack.rejected_count,
      receivedCount: body.ack.received_count,
      ackCode: body.ack.ack_code,
      parsedAt: "",
      // raw_999_text is appended below (not part of the canonical Ack
      // type) so the UI can trigger a download without a second
      // round-trip to /api/acks/{id}.
      ...({ raw_999_text: body.ack.raw_999_text } as { raw_999_text: string }),
    } as Ack & { raw_999_text: string },
    parsed: body.parsed,
  };
}

async function health(): Promise<HealthResponse | null> {
  if (!isConfigured) return null;
  const res = await fetch(joinUrl("/api/health"));
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }
  return (await res.json()) as HealthResponse;
}

// ---------------------------------------------------------------------------
// Public surface — GET endpoints (sub-project 1)
// All throw `notConfiguredError` when no backend is wired. The hooks in
// `src/hooks/` are responsible for the in-memory zustand fallback.
// ---------------------------------------------------------------------------

async function listBatches(limit?: number): Promise<BatchSummary[]> {
  if (!isConfigured) throw notConfiguredError();
  const params: Record<string, unknown> = {};
  if (limit !== undefined) params.limit = limit;
  const res = await fetch(joinUrl(`/api/batches${qs(params)}`), {
    headers: { Accept: "application/json" },
  });
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }
  const body = (await res.json()) as { items: BatchSummary[] };
  return body.items;
}

async function getBatch(id: string): Promise<ParseResult837 | ParseResult835> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(joinUrl(`/api/batches/${encodeURIComponent(id)}`), {
    headers: { Accept: "application/json" },
  });
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }
  return (await res.json()) as ParseResult837 | ParseResult835;
}

async function listClaims<T = unknown>(
  params: ListClaimsParams
): Promise<PaginatedResponse<T>> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(
    joinUrl(`/api/claims${qs(params as Record<string, unknown>)}`),
    { headers: { Accept: "application/json" } }
  );
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }
  return (await res.json()) as PaginatedResponse<T>;
}

/**
 * Fetch one claim with the full SP4 detail-drawer context (header, state,
 * parties, validation, service lines, diagnoses, raw segments, recent
 * ``stateHistory``, and a populated ``matchedRemittance`` when paired).
 *
 * Drives ``GET /api/claims/{claim_id}``. Throws ``ApiError`` on non-2xx —
 * including 404, which the spec §3.4 calls out as a distinct "claim
 * doesn't exist" state in the drawer (separate from a transient fetch
 * failure). Callers should branch on ``error.status === 404`` to render
 * the not-found state instead of the generic error toast.
 */
async function getClaimDetail(id: string): Promise<ClaimDetail> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(joinUrl(`/api/claims/${encodeURIComponent(id)}`), {
    headers: { Accept: "application/json" },
  });
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  return (await res.json()) as ClaimDetail;
}

/**
 * Serialize a claim back to an X12 837P file via the backend's outbound
 * serializer (SP8).
 *
 * Returns both the regenerated X12 text and the filename the backend
 * suggested (from the `Content-Disposition: attachment; filename=...`
 * header). Callers usually pipe both into `downloadTextFile` so the user
 * gets a byte-faithful file named after the claim id.
 *
 * Throws `ApiError` on non-2xx — 404 (claim missing) and 422 (stored
 * `raw_json` unparseable / serializer rejected the payload) are both
 * reachable, and the caller may want to branch on `.status` to surface a
 * useful message ("claim data is corrupted — open the batch file
 * manually" vs. "this claim no longer exists").
 */
async function serializeClaim837(
  id: string,
): Promise<{ text: string; filename: string }> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(
    joinUrl(`/api/claims/${encodeURIComponent(id)}/serialize-837`),
  );
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  const text = await res.text();
  // Filename comes from Content-Disposition (set by the backend). Fall back
  // to a sensible default if the header is missing or malformed so the UI
  // always has something to hand to `downloadTextFile`.
  const cd = res.headers.get("content-disposition") ?? "";
  const match = /filename="?([^";]+)"?/i.exec(cd);
  const filename = match?.[1] ?? `claim-${id}.x12`;
  return { text, filename };
}

async function listRemittances<T = unknown>(
  params: ListRemittancesParams
): Promise<PaginatedResponse<T>> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(
    joinUrl(`/api/remittances${qs(params as Record<string, unknown>)}`),
    { headers: { Accept: "application/json" } }
  );
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }
  return (await res.json()) as PaginatedResponse<T>;
}

/**
 * Fetch one remittance with its labeled CAS `adjustments` array.
 * Throws `ApiError` on 404 so callers can branch on `.status`.
 */
async function getRemittance<T = unknown>(id: string): Promise<T> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(joinUrl(`/api/remittances/${encodeURIComponent(id)}`), {
    headers: { Accept: "application/json" },
  });
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  return (await res.json()) as T;
}

/**
 * Fetch the side-by-side diff between two batches.
 *
 * Drives ``GET /api/batch-diff?a=<batch_id>&b=<batch_id>``. Both ids
 * are required; the endpoint returns 400 when either is missing and
 * 404 when the id is unknown — both surface here as ``ApiError`` so
 * the page can branch on ``.status`` like every other diff / fetch
 * surface in the app.
 */
async function getBatchDiff(a: string, b: string): Promise<BatchDiff> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(
    joinUrl(
      `/api/batch-diff?${qs({ a, b })}`,
    ),
    { headers: { Accept: "application/json" } },
  );
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  return (await res.json()) as BatchDiff;
}

async function listProviders<T = unknown>(
  params: ListProvidersParams = {}
): Promise<PaginatedResponse<T>> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(
    joinUrl(`/api/providers${qs(params as Record<string, unknown>)}`),
    { headers: { Accept: "application/json" } }
  );
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }
  return (await res.json()) as PaginatedResponse<T>;
}

/**
 * Fetch one configured provider by NPI, used by the provider drill-down
 * drawer (SP21 Task 2.2 / 1.6).
 *
 * Drives ``GET /api/config/providers/{npi}``. Note the ``/api/config/``
 * prefix — this is the config-side route namespace, distinct from the
 * persistence-side ``/api/providers`` used by ``listProviders``. The
 * response is the full ``Provider`` shape (including the optional
 * ``recent_claims`` and ``recent_activity`` arrays populated by Task
 * 1.6 when the backend can serve them).
 *
 * Throws ``ApiError`` on non-2xx — including 404, which the drawer may
 * want to branch on for a "provider no longer configured" state.
 */
async function getProvider(npi: string): Promise<Provider> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(
    joinUrl(`/api/config/providers/${encodeURIComponent(npi)}`),
    { headers: { Accept: "application/json" } }
  );
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  return (await res.json()) as Provider;
}

/**
 * Aggregate stats for one payer, used by the peek modal on Dashboard /
 * Claims tables (SP21 universal drill-down). Drives
 * `GET /api/payers/{payer_id}/summary`.
 *
 * `denial_rate` is a fraction in `[0, 1]` (the API does NOT pre-multiply).
 * `top_providers` is the top 3 (or fewer) by claim count, ordered
 * server-side.
 */
export interface PayerSummary {
  payer_id: string;
  name: string;
  claim_count: number;
  billed_total: number;
  received_total: number;
  denial_rate: number;
  top_providers: Array<{ npi: string; count: number }>;
}

async function getPayerSummary(payerId: string): Promise<PayerSummary> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(
    joinUrl(`/api/payers/${encodeURIComponent(payerId)}/summary`)
  );
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }
  return (await res.json()) as PayerSummary;
}

async function listActivity<T = unknown>(
  params: ListActivityParams = {}
): Promise<PaginatedResponse<T>> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(
    joinUrl(`/api/activity${qs(params as Record<string, unknown>)}`),
    { headers: { Accept: "application/json" } }
  );
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new Error(
      `${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
    );
  }
  return (await res.json()) as PaginatedResponse<T>;
}

// ---------------------------------------------------------------------------
// Public surface — reconciliation endpoints (sub-project 2)
// POSTs throw `ApiError` so callers can inspect `.status`; the GET is shaped
// like the other list endpoints above.
// ---------------------------------------------------------------------------

async function listUnmatched(): Promise<UnmatchedResponse> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(joinUrl(`/api/reconciliation/unmatched`), {
    headers: { Accept: "application/json" },
  });
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  return (await res.json()) as UnmatchedResponse;
}

async function matchRemit(
  claimId: string,
  remitId: string
): Promise<MatchResponse> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(joinUrl(`/api/reconciliation/match`), {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: JSON.stringify({ claim_id: claimId, remit_id: remitId }),
  });
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  return (await res.json()) as MatchResponse;
}

async function unmatchClaim(claimId: string): Promise<{ claim: UnmatchedClaim }> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(joinUrl(`/api/reconciliation/unmatch`), {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: JSON.stringify({ claim_id: claimId }),
  });
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  return (await res.json()) as { claim: UnmatchedClaim };
}

// ---------------------------------------------------------------------------
// Public surface — 999 ACKs (SP3 P3 T16)
// Re-shapes the snake_case backend response into the camelCase `Ack` shape
// used throughout the UI.
// ---------------------------------------------------------------------------

/**
 * Raw snake_case row from `GET /api/acks`. Re-shaped by `mapAck`
 * before the UI sees it.
 */
interface RawAckRow {
  id: number;
  source_batch_id: string;
  accepted_count: number;
  rejected_count: number;
  received_count: number;
  ack_code: "A" | "E" | "R" | "P";
  parsed_at: string;
}

function mapAck(row: RawAckRow): Ack {
  return {
    id: row.id,
    sourceBatchId: row.source_batch_id,
    acceptedCount: row.accepted_count,
    rejectedCount: row.rejected_count,
    receivedCount: row.received_count,
    ackCode: row.ack_code,
    parsedAt: row.parsed_at,
  };
}

async function listAcks(params: { limit?: number } = {}): Promise<PaginatedResponse<Ack>> {
  if (!isConfigured) throw notConfiguredError();
  const query: Record<string, unknown> = {};
  if (params.limit !== undefined) query.limit = params.limit;
  const res = await fetch(
    joinUrl(`/api/acks${qs(query)}`),
    { headers: { Accept: "application/json" } }
  );
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  const body = (await res.json()) as { items: RawAckRow[]; total: number; returned: number; has_more: boolean };
  return {
    items: body.items.map(mapAck),
    total: body.total,
    returned: body.returned,
    has_more: body.has_more,
  };
}

async function getAck(id: number): Promise<Ack & { rawJson: unknown }> {
  if (!isConfigured) throw notConfiguredError();
  const res = await fetch(joinUrl(`/api/acks/${encodeURIComponent(String(id))}`), {
    headers: { Accept: "application/json" },
  });
  if (!res.ok) {
    const detail = await readErrorBody(res);
    throw new ApiError(res.status, detail || res.statusText);
  }
  const row = (await res.json()) as RawAckRow & { raw_json: unknown };
  return { ...mapAck(row), rawJson: row.raw_json };
}

export const api = {
  isConfigured,
  baseUrl: BASE_URL,
  health,
  parse837,
  parse835,
  parse999,
  listBatches,
  getBatch,
  getBatchDiff,
  listClaims,
  getClaimDetail,
  serializeClaim837,
  listRemittances,
  getRemittance,
  listProviders,
  getProvider,
  getPayerSummary,
  listActivity,
  listUnmatched,
  matchRemit,
  unmatchClaim,
  listAcks,
  getAck,
};
