/**
 * The spend decision: a pure function, no crypto and no network, mirroring
 * entitlement.ts. The gates decide who may call; this decides what a call may
 * cost: which model, how many tokens per request, and (via the worker and the
 * budget store) how many tokens per caller per day.
 */
import { parseList } from "./entitlement.ts";
import type { Env } from "./env.ts";

export interface SpendConfig {
  allowedModels: Set<string> | null; // null = any model
  maxTokensLimit: number | null; // per-request cap
  dailyTokenBudget: number | null; // per-caller daily budget
}

/** Spend controls are active when any of the three knobs is set. */
export function spendConfig(env: Env): SpendConfig | null {
  const models = parseList(env.ALLOWED_MODELS);
  const allowedModels = models.size > 0 ? models : null;
  const maxTokensLimit = parsePositiveInt(env.MAX_TOKENS_LIMIT);
  const dailyTokenBudget = parsePositiveInt(env.DAILY_TOKEN_BUDGET);
  if (!allowedModels && maxTokensLimit == null && dailyTokenBudget == null) {
    return null;
  }
  return { allowedModels, maxTokensLimit, dailyTokenBudget };
}

export type SpendResult =
  | { ok: true; maxTokens: number }
  | { ok: false; status: 400 | 403; reason: string };

/** Reject oversized bodies before parsing JSON. */
export const MAX_BODY_BYTES = 1_048_576;

/**
 * Decide whether the parsed request body may be forwarded. A malformed shape
 * is 400; a policy denial is 403. Budget exhaustion is 429, decided in the
 * worker, because it needs the store.
 */
export function evaluateSpend(body: unknown, cfg: SpendConfig): SpendResult {
  if (typeof body !== "object" || body === null || Array.isArray(body)) {
    return { ok: false, status: 400, reason: "body_not_json_object" };
  }
  const { model, max_tokens: maxTokens } = body as {
    model?: unknown;
    max_tokens?: unknown;
  };
  if (cfg.allowedModels) {
    if (typeof model !== "string" || !cfg.allowedModels.has(model)) {
      return { ok: false, status: 403, reason: "model_not_allowed" };
    }
  }
  // A cap can only be enforced, and a budget can only debit, what the request
  // declares, so max_tokens is mandatory whenever either is configured.
  if (cfg.maxTokensLimit != null || cfg.dailyTokenBudget != null) {
    if (
      typeof maxTokens !== "number" ||
      !Number.isInteger(maxTokens) ||
      maxTokens <= 0
    ) {
      return { ok: false, status: 400, reason: "max_tokens_required" };
    }
    if (cfg.maxTokensLimit != null && maxTokens > cfg.maxTokensLimit) {
      return { ok: false, status: 400, reason: "max_tokens_exceeds_limit" };
    }
    return { ok: true, maxTokens };
  }
  return { ok: true, maxTokens: 0 };
}

function parsePositiveInt(value: string | undefined): number | null {
  if (!value) return null;
  const n = Number(value);
  return Number.isInteger(n) && n > 0 ? n : null;
}
