import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import type { PiggyModelOption } from '@pig/core';
import { z } from 'zod';

/**
 * The provider id under which Prime Inference is registered with the harness.
 *
 * 0.84.1 of the agent SDK ships no `prime-inference` provider of its own — the
 * published docs describe a build that is not on npm — so the runtime registers
 * one from `models.json`. The id is a constant because three places have to
 * agree on it: the models.json key, `modelRuntime.setRuntimeApiKey`, and
 * `modelRuntime.getModel`. A typo in any one of them fails as a 401 or an
 * undefined model rather than as a missing-provider error.
 */
export const PIGGY_PROVIDER_ID = 'prime-inference';

const costSchema = z.object({
  /** US dollars per million tokens, which is the unit every provider publishes. */
  input: z.number().nonnegative(),
  output: z.number().nonnegative(),
  cacheRead: z.number().nonnegative(),
  cacheWrite: z.number().nonnegative(),
});

/**
 * The reasoning-effort map, declared here so a typo cannot be silent.
 *
 * This field is the fix for the most expensive defect in the harness swap: with
 * no map, `thinkingLevel: 'off'` makes the harness omit `reasoning_effort`
 * altogether and the endpoint's own default wins — 6,195 output tokens of
 * reasoning and an empty answer on nemotron. It is optional because the
 * frontier models in the catalogue are fine on their defaults.
 *
 * It is declared even though nothing here reads it, because the parsed
 * catalogue is not what the harness sees: the harness reads the verbatim
 * `MODELS_JSON_TEXT`. A field this schema had never heard of would therefore be
 * dropped from the parsed catalogue in silence while still reaching the
 * harness — and a MISSPELLED one (`thinkinglevelmap`) would reach neither, with
 * nothing in any log to say so. `.strict()` is what turns that into a startup
 * failure naming the offending key.
 */
const thinkingLevelMapSchema = z
  .record(
    z.enum(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']),
    z.string().min(1),
  )
  .refine((map) => Object.keys(map).length > 0, {
    message: 'must map at least one thinking level, or be omitted entirely',
  });

const modelSchema = z
  .object({
    id: z.string().min(1),
    name: z.string().min(1),
    reasoning: z.boolean(),
    input: z.array(z.enum(['text', 'image'])).min(1),
    contextWindow: z.number().int().positive(),
    maxTokens: z.number().int().positive(),
    cost: costSchema,
    thinkingLevelMap: thinkingLevelMapSchema.optional(),
  })
  .strict();

const documentSchema = z.object({
  providers: z.object({
    'prime-inference': z.object({
      baseUrl: z.string().url(),
      api: z.string().min(1),
      models: z.array(modelSchema).min(1),
    }),
  }),
});

type PiggyProviderModel = z.infer<typeof modelSchema>;

/**
 * `models.json` is read rather than imported so it can be validated once, at
 * startup, with a message that names the offending field. The same text is
 * copied verbatim into the agent data directory for the harness to read, so an
 * unparseable file has to fail here — loudly — rather than inside the SDK,
 * where it surfaces as a model that simply does not exist.
 */
const MODELS_JSON_PATH = fileURLToPath(new URL('./models.json', import.meta.url));
const MODELS_JSON_TEXT = readFileSync(MODELS_JSON_PATH, 'utf8');

function parseModelsDocument(): z.infer<typeof documentSchema> {
  const parsed = documentSchema.safeParse(JSON.parse(MODELS_JSON_TEXT) as unknown);
  if (!parsed.success) {
    const issues = parsed.error.issues.map((issue) => `  ${issue.path.join('.')}: ${issue.message}`);
    throw new Error(`Invalid Piggy models.json:\n${issues.join('\n')}`);
  }
  return parsed.data;
}

const PROVIDER = parseModelsDocument().providers[PIGGY_PROVIDER_ID];

/**
 * What the picker says about a model, over and above what the harness needs.
 *
 * Price, context window and reasoning support live in `models.json` because the
 * harness reads them there; duplicating them here is how a picker ends up
 * quoting a price the runtime is not billing. Only the sales pitch lives here.
 * Every id in `models.json` must appear below, and the reverse — a model with
 * no hint would render as a blank row, and a hint with no model would offer a
 * choice that 404s at the endpoint.
 */
interface PiggyModelPresentation {
  hint: string;
  isDefault?: true;
}

const PRESENTATION: Record<string, PiggyModelPresentation> = {
  'nvidia/nemotron-3-nano-30b-a3b': {
    hint: 'Cheapest by far, but currently unreliable upstream — see the note on the default below.',
  },
  /*
   * The default is the SUPER, not the nano, and the reason is not quality.
   *
   * On 2026-08-14 `nvidia/nemotron-3-nano-30b-a3b` stopped answering on Prime
   * Inference: the endpoint accepted the connection and never sent response
   * headers (UND_ERR_HEADERS_TIMEOUT, three attempts, 45s each), having 429'd
   * shortly before. Every other model in this catalogue answered in under two
   * seconds on the same key in the same minute, so it was that model's capacity
   * rather than our account. The nano had also just fabricated a figure rather
   * than admit it had no tool for the question.
   *
   * Six times the price of the nano is still about $0.0017 a turn, which is
   * roughly 117,000 turns on a $200 credit. Availability is worth more than
   * that margin for the model everyone lands on. The nano stays in the picker
   * for anyone who wants it back.
   */
  'nvidia/nemotron-3-super-120b-a12b': {
    hint: 'The default. Same family as the nano, six times the price, and materially steadier.',
    isDefault: true,
  },
  'deepseek/deepseek-v4-pro': {
    hint: 'Strong arithmetic at open-weight prices. Good for margin and break-even questions.',
  },
  'anthropic/claude-opus-5': {
    hint: 'Frontier reasoning. Worth it for multi-step commercial analysis you will act on.',
  },
  'openai/gpt-5.6': {
    hint: 'Frontier alternative with the largest context. Use for long conversations.',
  },
};

function toModelOption(model: PiggyProviderModel): PiggyModelOption {
  const presentation = PRESENTATION[model.id];
  if (!presentation) {
    throw new Error(
      `Piggy model ${model.id} is registered in models.json but has no picker entry, so it would render as a blank row.`,
    );
  }
  return {
    id: model.id,
    label: model.name,
    hint: presentation.hint,
    costPerMTokIn: model.cost.input,
    costPerMTokOut: model.cost.output,
    contextWindow: model.contextWindow,
    reasoning: model.reasoning,
    ...(presentation.isDefault ? { isDefault: true as const } : {}),
  };
}

function buildCatalogue(): PiggyModelOption[] {
  const options = PROVIDER.models.map(toModelOption);
  const orphans = Object.keys(PRESENTATION).filter(
    (id) => !options.some((option) => option.id === id),
  );
  if (orphans.length > 0) {
    throw new Error(
      `Piggy picker entries have no model in models.json and would offer a choice the endpoint rejects: ${orphans.join(', ')}.`,
    );
  }
  const defaults = options.filter((option) => option.isDefault);
  if (defaults.length !== 1) {
    throw new Error(
      `Exactly one Piggy model must be marked as the default; found ${defaults.length}.`,
    );
  }
  return options;
}

const CATALOGUE = buildCatalogue();

/**
 * The models the picker may offer, in the order it should show them.
 *
 * A copy, because the returned array is handed to a JSON serialiser on its way
 * to the browser and one careless `sort()` there would reorder the picker for
 * every session in the process.
 */
export function piggyModelCatalogue(): PiggyModelOption[] {
  return CATALOGUE.map((option) => ({ ...option }));
}

export function piggyDefaultModelId(): string {
  const fallback = CATALOGUE.find((option) => option.isDefault) ?? CATALOGUE[0];
  if (!fallback) throw new Error('The Piggy model catalogue is empty.');
  return fallback.id;
}

/** Whether an id is one the runtime can actually resolve against the provider. */
export function isPiggyModelId(id: string): boolean {
  return CATALOGUE.some((option) => option.id === id);
}

/** The provider document, verbatim, for the copy the harness reads from disk. */
export function piggyModelsJsonText(): string {
  return MODELS_JSON_TEXT;
}

export function piggyInferenceBaseUrl(): string {
  return PROVIDER.baseUrl;
}
