/**
 * Socket.IO payloads are only statically typed at the call site — a
 * misbehaving or compromised gateway can send anything at runtime. These
 * guards protect the dereference sites that would otherwise throw (`.map` on
 * a non-array, `.toFixed` on a non-number) or render an object as a React
 * child.
 */
import type {
  HarnessAuthState,
  HarnessCatalog,
  HarnessCatalogEntry,
  HarnessModelAvailability,
  HarnessSelection,
  HarnessSummary,
} from '@/lib/types';

export function asString(value: unknown, fallback = ''): string {
  return typeof value === 'string' ? value : fallback;
}

/** Like `asString`, but an empty string also falls back — used for guarded
 * contract-provided reason strings (e.g. a denial or failure message) where
 * an empty string is not a meaningful value to display in place of the
 * stable fallback copy. */
export function asNonEmptyString(value: unknown, fallback: string): string {
  return typeof value === 'string' && value.length > 0 ? value : fallback;
}

export function asFiniteNumber(value: unknown, fallback = 0): number {
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
}

/** Like `asFiniteNumber`, but returns `null` on failure instead of a numeric
 * fallback — callers that must not fabricate a plausible-looking value (e.g.
 * `0 tokens` / `$0.0000` for genuinely unknown usage) use this to render an
 * honest "unavailable" label instead. */
export function asFiniteNumberOrNull(value: unknown): number | null {
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
}

export function asStringArray(value: unknown): string[] {
  return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : [];
}

export function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null;
}

/**
 * The HTTP harness/catalog/selection JSON bodies are as untrusted as the socket
 * payloads above — a misbehaving or compromised gateway can send anything. The
 * guards below normalize those bodies into the typed client shapes without ever
 * rendering a raw body, so a 404/422/malformed response can never inject an
 * object into React or a non-tuple into the selection state.
 */

/** Normalizes an untrusted `authState` to the closed set, defaulting to the
 * safest value (`unavailable`) for anything unrecognized. */
export function asHarnessAuthState(value: unknown): HarnessAuthState {
  return value === 'ready' || value === 'auth_required' || value === 'unavailable'
    ? value
    : 'unavailable';
}

/** Normalizes an untrusted `availability` to the closed set, defaulting to
 * `unavailable` so a malformed row can never present as sendable. */
export function asHarnessAvailability(value: unknown): HarnessModelAvailability {
  return value === 'available' ? 'available' : 'unavailable';
}

/** A tuple is valid only when all three ids are non-empty strings — a partial
 * or malformed selection is rejected (null) rather than half-adopted. */
export function asHarnessSelection(value: unknown): HarnessSelection | null {
  if (!isRecord(value)) return null;
  const harnessId = value.harnessId;
  const providerId = value.providerId;
  const modelId = value.modelId;
  if (
    typeof harnessId !== 'string' ||
    typeof providerId !== 'string' ||
    typeof modelId !== 'string' ||
    harnessId.length === 0 ||
    providerId.length === 0 ||
    modelId.length === 0
  ) {
    return null;
  }
  return { harnessId, providerId, modelId };
}

/** Normalizes an untrusted array into typed harness summaries, dropping any row
 * without a usable id. */
export function asHarnessSummaries(value: unknown): HarnessSummary[] {
  if (!Array.isArray(value)) return [];
  const out: HarnessSummary[] = [];
  for (const item of value) {
    if (!isRecord(item)) continue;
    const id = asString(item.id);
    if (id.length === 0) continue;
    out.push({
      id,
      displayName: asNonEmptyString(item.displayName, id),
      capabilities: asStringArray(item.capabilities),
    });
  }
  return out;
}

function asHarnessCatalogEntry(value: unknown): HarnessCatalogEntry | null {
  const selection = asHarnessSelection(value);
  if (selection === null || !isRecord(value)) return null;
  return {
    ...selection,
    displayName: asNonEmptyString(value.displayName, selection.modelId),
    reasoningCapability: value.reasoningCapability === true,
    inputTypes: asStringArray(value.inputTypes),
    authState: asHarnessAuthState(value.authState),
    availability: asHarnessAvailability(value.availability),
  };
}

/** Normalizes an untrusted catalog body into the typed client catalog. The
 * caller supplies `harnessId` (from the request path) so the returned catalog
 * is scoped to the harness that was actually requested, never a body-echoed id.
 * Malformed model rows are dropped rather than invalidating the whole catalog. */
export function asHarnessCatalog(value: unknown, harnessId: string): HarnessCatalog {
  const record = isRecord(value) ? value : {};
  const rawModels = Array.isArray(record.models) ? record.models : [];
  const models: HarnessCatalogEntry[] = [];
  for (const row of rawModels) {
    const entry = asHarnessCatalogEntry(row);
    if (entry !== null) models.push(entry);
  }
  return {
    harnessId,
    version: asString(record.version),
    fingerprint: asString(record.fingerprint),
    models,
  };
}

/** The single point of truth for what counts as a valid conversation ID
 * anywhere a scoped server event may adopt one into state — a non-empty
 * string, nothing else. Every site that establishes or compares
 * `state.conversationId` against a raw socket payload must route through
 * this guard so a malformed first frame (null/object/number/empty string)
 * can never be adopted verbatim. */
export function asConversationId(value: unknown): string | null {
  return typeof value === 'string' && value.length > 0 ? value : null;
}
