/**
 * `decisions` transport: backends that take `{ state, model, questions }` and answer with typed
 * decisions (TypeSafe System One, OpenRouter's Decisions API). Also hosts the catalogue lookups
 * used by `/decide status` and `/decide models`.
 */

import { joinUrl, type BackendConfig } from "./config.ts";
import { JevError } from "./errors.ts";
import { parseJsonBody, requestWithRetry } from "./http.ts";
import type { JevAnswer, JevQuestion, JsonValue } from "./questions.ts";
import type { TokenCounts } from "./usage.ts";

export interface ModelCard {
  name: string;
  description?: string;
  releaseDate?: string;
}

/** One model route as reported by a router's catalogue detail endpoint (OpenRouter `/v1/models/<id>/endpoints`). */
export interface ModelRoute {
  id: string;
  name?: string;
  modality?: string;
  outputModalities: string[];
  endpointCount: number;
}

export interface DecisionRequest {
  state: JsonValue;
  model: string;
  questions: Record<string, JevQuestion>;
}

export interface DecisionResult {
  /** Model id reported by the API (the versioned id behind an alias). */
  model: string;
  /** Upstream provider reported by routers such as OpenRouter. */
  provider?: string;
  answers: Record<string, JevAnswer>;
  tokens: TokenCounts;
  endpoint: string;
  latencyMs: number;
}

export async function askDecisions(
  backend: BackendConfig,
  request: DecisionRequest,
  signal?: AbortSignal,
): Promise<DecisionResult> {
  if (!backend.apiKey) {
    throw new JevError(`Backend "${backend.id}" has no API key.`, {
      hint: `Point ${backend.id}.apiKey at an environment variable in ${backend.id} config, or export the provider key.`,
    });
  }
  const url = joinUrl(backend.baseUrl, backend.path);
  const started = Date.now();
  const response = await requestWithRetry({
    url,
    method: "POST",
    headers: { authorization: `Bearer ${backend.apiKey}` },
    body: request,
    timeoutMs: backend.timeoutMs,
    maxRetries: backend.maxRetries,
    signal,
  });
  const latencyMs = Date.now() - started;
  const parsed = parseJsonBody(response.text, url);
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new JevError(`${url} returned an unexpected payload (expected a JSON object).`);
  }
  const body = parsed as Record<string, unknown>;
  if (body.answers === null || typeof body.answers !== "object" || Array.isArray(body.answers)) {
    throw new JevError(`${url} returned no "answers" object.`, { body: response.text });
  }
  const usage = (body.usage ?? {}) as Record<string, unknown>;
  return {
    model: typeof body.model === "string" && body.model.trim() !== "" ? body.model : request.model,
    provider: typeof body.provider === "string" ? body.provider : undefined,
    answers: body.answers as Record<string, JevAnswer>,
    // TypeSafe returns snake_case, OpenRouter's router returns camelCase.
    tokens: {
      input: numberOrUndefined(usage.input_tokens ?? usage.inputTokens),
      output: numberOrUndefined(usage.output_tokens ?? usage.outputTokens),
    },
    endpoint: url,
    latencyMs,
  };
}

export async function listCatalog(
  backend: BackendConfig,
  modelsPath: string,
  signal?: AbortSignal,
): Promise<ModelCard[]> {
  const url = joinUrl(backend.baseUrl, modelsPath);
  const response = await requestWithRetry({
    url,
    method: "GET",
    headers: backend.apiKey ? { authorization: `Bearer ${backend.apiKey}` } : {},
    timeoutMs: Math.min(backend.timeoutMs, 20_000),
    maxRetries: 0,
    signal,
  });
  return readModelList(parseJsonBody(response.text, url));
}

/**
 * Look up a single model route. OpenRouter's catalogue detail route reports the modality, which is how a
 * `text->decisions` model such as ~typesafe/jev-latest is confirmed even though family aliases are absent
 * from the public /v1/models listing.
 */
export async function describeModelRoute(
  backend: BackendConfig,
  modelsPath: string,
  model: string,
  signal?: AbortSignal,
): Promise<ModelRoute> {
  const url = joinUrl(backend.baseUrl, `${modelsPath}/${model}/endpoints`);
  const response = await requestWithRetry({
    url,
    method: "GET",
    headers: backend.apiKey ? { authorization: `Bearer ${backend.apiKey}` } : {},
    timeoutMs: Math.min(backend.timeoutMs, 20_000),
    maxRetries: 0,
    signal,
  });
  const parsed = parseJsonBody(response.text, url);
  const body =
    parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && (parsed as Record<string, unknown>).data !== undefined
      ? ((parsed as Record<string, unknown>).data as Record<string, unknown>)
      : ((parsed ?? {}) as Record<string, unknown>);
  const architecture = (body.architecture ?? {}) as Record<string, unknown>;
  return {
    id: typeof body.id === "string" ? body.id : model,
    name: typeof body.name === "string" ? body.name : undefined,
    modality: typeof architecture.modality === "string" ? architecture.modality : undefined,
    outputModalities: Array.isArray(architecture.output_modalities)
      ? architecture.output_modalities.filter((entry): entry is string => typeof entry === "string")
      : [],
    endpointCount: Array.isArray(body.endpoints) ? body.endpoints.length : 0,
  };
}

/** Accepts both TypeSafe's `{ models: [...] }` and OpenAI's `{ data: [...] }`. */
export function readModelList(parsed: unknown): ModelCard[] {
  const entries =
    parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
      ? ((parsed as Record<string, unknown>).models ?? (parsed as Record<string, unknown>).data)
      : undefined;
  if (!Array.isArray(entries)) return [];
  const cards: ModelCard[] = [];
  for (const entry of entries) {
    if (typeof entry === "string") {
      cards.push({ name: entry });
      continue;
    }
    if (entry === null || typeof entry !== "object") continue;
    const record = entry as Record<string, unknown>;
    const name = typeof record.name === "string" ? record.name : typeof record.id === "string" ? record.id : undefined;
    if (name === undefined) continue;
    cards.push({
      name,
      description: typeof record.description === "string" ? record.description : undefined,
      releaseDate: typeof record.release_date === "string" ? record.release_date : undefined,
    });
  }
  return cards;
}

function numberOrUndefined(value: unknown): number | undefined {
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
