/**
 * Typed fetch wrappers for the Task-3 harness HTTP contract the chat selection
 * UI depends on. Every response body is untrusted and is normalized through the
 * runtime guards before it reaches state — a 404 (catalog) and a 422 (selection)
 * are mapped to typed, body-free error results so a raw gateway body is never
 * rendered, and the caller's requested tuple is preserved verbatim on failure.
 *
 * This module talks ONLY to the harness/chat-preferences endpoints. It never
 * calls `/api/providers*` — provider identity lives inside the harness catalog.
 */
import { asHarnessCatalog, asHarnessSelection, asHarnessSummaries } from './runtime-guards';
import type { HarnessCatalog, HarnessSelection, HarnessSummary } from '@/lib/types';

/** A catalog fetch either yields the typed catalog or a typed unavailability —
 * never a thrown raw body. */
export type CatalogResult =
  | { ok: true; catalog: HarnessCatalog }
  | { ok: false; code: 'catalog_unavailable'; harnessId: string; message: string };

export type SelectionErrorCode = 'selection_invalid' | 'model_unavailable';

/** A persist either confirms the stored tuple or reports a typed domain failure
 * that echoes back the exact tuple the caller requested. */
export type SelectionPersistResult =
  | { ok: true; selection: HarnessSelection }
  | { ok: false; code: SelectionErrorCode; message: string; requested: HarnessSelection };

/** A safe, generic message for an unavailable catalog — the raw 404 body is
 * never surfaced. */
const CATALOG_UNAVAILABLE_MESSAGE = 'This harness catalog is currently unavailable.';

/** A safe, generic message for a rejected selection. The untrusted 422 body's
 * own `message` is deliberately NEVER surfaced — only this fixed copy — so a
 * raw gateway detail can never leak into the UI. Only the closed `code` enum is
 * read from the body. */
const SELECTION_REJECTED_MESSAGE = 'This selection was rejected.';

async function readJson(response: Response): Promise<unknown> {
  return response.json().catch(() => null);
}

function safeSelectionCode(body: unknown): SelectionErrorCode {
  if (typeof body === 'object' && body !== null && 'code' in body) {
    const code = (body as { code: unknown }).code;
    if (code === 'selection_invalid' || code === 'model_unavailable') return code;
  }
  // Default to the more conservative "invalid" classification for anything
  // unrecognized rather than guessing "model_unavailable".
  return 'selection_invalid';
}

/** `GET /api/harnesses` → the list of harness summaries. A non-OK response
 * normalizes to an empty list (the UI then has no harness to select). */
export async function fetchHarnesses(): Promise<HarnessSummary[]> {
  const response = await fetch('/api/harnesses', {
    credentials: 'include',
    headers: { Accept: 'application/json' },
  });
  if (!response.ok) return [];
  return asHarnessSummaries(await readJson(response));
}

/** `GET /api/harnesses/:harnessId/catalog` → the harness-scoped catalog. A 404
 * (or any non-OK) becomes a typed `catalog_unavailable` result rather than a
 * fallback catalog or a rendered raw body. */
export async function fetchCatalog(harnessId: string): Promise<CatalogResult> {
  const response = await fetch(`/api/harnesses/${encodeURIComponent(harnessId)}/catalog`, {
    credentials: 'include',
    headers: { Accept: 'application/json' },
  });
  if (!response.ok) {
    return {
      ok: false,
      code: 'catalog_unavailable',
      // Scoped to the requested harness id, never the untrusted body's echo.
      harnessId,
      message: CATALOG_UNAVAILABLE_MESSAGE,
    };
  }
  return { ok: true, catalog: asHarnessCatalog(await readJson(response), harnessId) };
}

/** `GET /api/chat/preferences/selection` → the persisted tuple, or null when
 * unset or malformed. */
export async function fetchPersistedSelection(): Promise<HarnessSelection | null> {
  const response = await fetch('/api/chat/preferences/selection', {
    credentials: 'include',
    headers: { Accept: 'application/json' },
  });
  if (!response.ok) return null;
  const body = await readJson(response);
  if (typeof body !== 'object' || body === null) return null;
  return asHarnessSelection((body as { selection?: unknown }).selection);
}

/** `PUT /api/chat/preferences/selection` with the structured tuple as the body.
 * On success returns the confirmed selection; on a typed domain failure (422)
 * or validation error, returns a typed result carrying the EXACT requested
 * tuple — never the body's echo — and never the raw body text. */
export async function persistSelection(
  selection: HarnessSelection,
): Promise<SelectionPersistResult> {
  const requested: HarnessSelection = {
    harnessId: selection.harnessId,
    providerId: selection.providerId,
    modelId: selection.modelId,
  };
  const response = await fetch('/api/chat/preferences/selection', {
    method: 'PUT',
    credentials: 'include',
    headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
    body: JSON.stringify(requested),
  });
  if (!response.ok) {
    const body = await readJson(response);
    return {
      ok: false,
      code: safeSelectionCode(body),
      // Fixed copy only — the untrusted body's message is never surfaced.
      message: SELECTION_REJECTED_MESSAGE,
      requested,
    };
  }
  const body = await readJson(response);
  const confirmed =
    typeof body === 'object' && body !== null
      ? asHarnessSelection((body as { selection?: unknown }).selection)
      : null;
  // A malformed 2xx body is treated as a confirmation of exactly what we sent —
  // the server accepted the tuple, so the requested tuple is the source of truth.
  return { ok: true, selection: confirmed ?? requested };
}
