import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { parse } from "yaml";
import { envString } from "../env.js";

export type ModelDefinition = {
  id: string;
  label: string;
  provider: "openai-compatible";
  base_url?: string;
  api_key_env?: string;
  model: string;
  max_tokens?: number;
  // Ordered list of model ids to try if this one fails (5xx, 429, network
  // error, timeout). Each id must resolve to a known model; unknown ids are
  // dropped at load time. Use ["mr-auto"] for a single fallback. The
  // fallback chain for the chain itself is resolved at request time by
  // `resolveFallbackChain`.
  fallback?: string[];
};

export type PublicModelDefinition = Pick<ModelDefinition, "id" | "label" | "provider" | "max_tokens">;

// Resolve the API key for a model. Order of precedence:
//   1. Per-model env var (model.api_key_env) — useful when different providers
//      use different keys (e.g. local proxy, dedicated self-hosted).
//   2. Global LLM_API_KEY / MINIMAX_API_KEY fallbacks shared by all models.
//   3. Literal "dummy" so OpenAI-compatible servers that don't require auth
//      (e.g. local OLLAMA, self-hosted reverse proxy) still work out of the box.
export const resolveModelApiKey = (model: ModelDefinition): string => {
  const fromModel = model.api_key_env ? process.env[model.api_key_env] : undefined;
  if (fromModel && fromModel.trim().length > 0) return fromModel.trim();
  return (
    envString(process.env.LLM_API_KEY, envString(process.env.MINIMAX_API_KEY, "dummy"))
  );
};

const expandEnv = (value: string | undefined) =>
  value?.replace(/\$\{([A-Z0-9_]+)\}/g, (_match, key: string) => process.env[key] ?? "");

export const loadModelDefinitions = (): ModelDefinition[] => {
  const configPath = envString(process.env.MODELS_CONFIG_PATH, resolve(process.cwd(), "../../config/models.yml"));
  const parsed = parse(readFileSync(configPath, "utf8")) as { models?: ModelDefinition[] } | null;
  const known = new Set((parsed?.models ?? []).map((model) => String(model.id ?? "").trim()));

  return (parsed?.models ?? []).map((model) => {
    const rawFallback = Array.isArray(model.fallback) ? model.fallback : [];
    const fallback = rawFallback
      .map((id) => String(id).trim())
      .filter((id) => id.length > 0 && known.has(id) && id !== model.id);
    return {
      ...model,
      base_url: expandEnv(model.base_url),
      fallback,
    };
  });
};

export const getDefaultModelId = () => envString(process.env.DEFAULT_MODEL, "fast");

export const findModelDefinition = (modelId: string) =>
  loadModelDefinitions().find((model) => model.id === modelId);

/**
 * Resolve the ordered fallback chain starting at `modelId`. Walks each model's
 * `fallback` array until exhausted, dedupes by id, and stops if a cycle is
 * detected. The starting model is always first. If the model is unknown the
 * chain is just `[modelId]` (caller will surface model_not_found).
 */
export const resolveFallbackChain = (modelId: string): string[] => {
  const all = loadModelDefinitions();
  const byId = new Map(all.map((model) => [model.id, model]));
  const chain: string[] = [];
  const seen = new Set<string>();

  // LLM_FALLBACK_CHAIN (comma-separated) overrides the YAML chain for the
  // selected model. Empty / unset means "use the YAML chain".
  const override = envString(process.env.LLM_FALLBACK_CHAIN, "")
    .split(",")
    .map((id) => id.trim())
    .filter((id) => id.length > 0);

  let cursor: string | undefined = modelId;
  let nextCursor: string | undefined = override[0];
  while (cursor && !seen.has(cursor)) {
    seen.add(cursor);
    chain.push(cursor);
    const model = byId.get(cursor);
    if (nextCursor !== undefined) {
      cursor = nextCursor;
      nextCursor = undefined;
      continue;
    }
    if (!model || !model.fallback || model.fallback.length === 0) break;
    cursor = model.fallback[0];
  }
  return chain;
};

export const toPublicModel = (model: ModelDefinition): PublicModelDefinition => ({
  id: model.id,
  label: model.label,
  provider: model.provider,
  max_tokens: model.max_tokens,
});
