/**
 * Tabela de preços versionada (stack.md §2: usage × pricing.ts → llm_calls.cost_cents).
 * ÚNICO lugar com preço de modelo DECLARADO no repo — preço do CATÁLOGO da
 * instalação (`ai_models`/`ai_pricing`), quando o id não está nesta tabela, é
 * resolvido por `pricing-do-catalogo.ts` (issue #3013).
 *
 * Fontes: https://platform.claude.com/docs/en/about-claude/pricing (Anthropic) e
 * https://developers.openai.com/api/docs/pricing (OpenAI, tabela Standard, conferida em
 * 23/09/2026). Google: entrada/saída do `ai_pricing` do catálogo curado; leitura de cache de
 * https://ai.google.dev/gemini-api/docs/pricing, conferida em 02/10/2026 (ver o bloco Gemini).
 * Cache da Anthropic: leitura = 0.1× a entrada (0.05× no Sonnet 5.5 e no Opus 5.5); gravação =
 * 1.25× no TTL de 5 minutos e 2× no de 1 hora — os dois TTLs que o knob `LLM_CACHE_TTL` aceita
 * (`lib/agent-engine/env.ts`), e é por isso que `costCents` recebe o TTL em vigor em vez de
 * supor a doutrina.
 *
 * Preço por MINUTO de áudio (só `whisper-1`, só na OpenAI): `custoDeAudioCents`, pela
 * duração que a própria OpenAI cobra (`usage.seconds` da transcrição).
 *
 * Modelo fora da tabela → custo NULL (desconhecido): mais honesto que inventar 0 —
 * o budget soma coalesce(cost_cents, 0), então modelo sem preço não consome teto;
 * quem habilitar um modelo novo para uma org adiciona a linha de preço aqui.
 *
 * O `null` daqui NÃO é o fim da linha: o seam (`run-model-call.ts`) tenta o preço
 * do catálogo da instalação antes de aceitá-lo (`pricing-do-catalogo.ts`), porque
 * o provedor PERSONALIZADO serve qualquer modelo — e quando nem o catálogo tem o
 * preço, o nulo é gravado COM aviso no log. Custo nulo por preço desconhecido é
 * legítimo; custo nulo CALADO foi o defeito da issue #3013.
 *
 * TRÊS armadilhas já pagas, e as três são do MESMO defeito — a tabela ficou na
 * geração 4 enquanto o catálogo (`ai_models`, migration 0101) andou:
 *
 *   1. A geração 5 (`claude-sonnet-5`, o padrão de atendimento, e `claude-opus-5`)
 *      não tinha linha. Custo NULL numa instalação real: a tela Uso e orçamento
 *      mostrava gasto zero e o teto mensal nunca disparava — medido numa VPS com
 *      28 chamadas reais, todas com `cost_cents` nulo.
 *   2. O antigo match por `startsWith` fazia `claude-opus-4` casar com
 *      `claude-opus-4-5` em diante e cobrar o preço do Opus 4/4.1 (aposentados,
 *      US$ 15/75) por um modelo que custa US$ 5/25 — 3× a mais, com o sinal
 *      invertido do defeito 1: aqui o teto disparava cedo demais.
 *   3. O mesmo `startsWith` daria preço a um id FUTURO que apenas começasse igual
 *      (`claude-sonnet-50`, `claude-opus-4-9`) — e custo errado não-nulo é pior
 *      que custo desconhecido, porque não acende o sinal de gasto incompleto.
 *
 * Por isso o match é EXATO, com duas tolerâncias: o sufixo de data do vendor
 * (`claude-opus-4-1-20250805`) e o prefixo `provider/` do OpenRouter
 * (`anthropic/claude-sonnet-5`) — este último recortado até a primeira barra,
 * na mesma ordem da tolerância do sufixo, para um id com prefixo+sufixo também
 * casar (issue #1880). Id que a tabela não conhece volta NULL, que é o
 * contrato escrito acima.
 */

import type { CacheTtl } from './stable-prefix';

interface Preco {
  input: number;
  output: number;
  cacheRead: number;
  /** 1.25× a entrada — TTL de 5 minutos. */
  cacheWrite5m?: number;
  /** 2× a entrada — TTL de 1 hora, a doutrina de caching (CLAUDE.md regra 15). */
  cacheWrite1h?: number;
  /** OpenAI: campos próprios, nunca inferidos do TTL Anthropic 5m/1h. */
  cacheWrite30m?: number;
  cacheWrite12h?: number;
}

/** USD por MILHÃO de tokens, por id EXATO de modelo (o sufixo de data é tolerado). */
const USD_PER_MTOK: Record<string, Preco> = {
  // Família 5.5, conferida em platform.claude.com/docs/en/about-claude/pricing em 11/10/2026.
  // Fora da tabela: o Haiku 5.5 cobra 5× (0.5 / 2.5, cache 0.05 / 0.625 / 1) quando o prompt
  // de UMA requisição passa de 100 mil tokens. O `usage` que chega aqui soma os passos do laço
  // de ferramentas, então não diz o tamanho de cada requisição — a faixa não é calculável aqui.
  'claude-haiku-5-5': { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite5m: 0.125, cacheWrite1h: 0.2 },
  'claude-sonnet-5-5': { input: 2, output: 10, cacheRead: 0.1, cacheWrite5m: 2.5, cacheWrite1h: 4 },
  'claude-opus-5-5': { input: 4, output: 20, cacheRead: 0.2, cacheWrite5m: 5, cacheWrite1h: 8 },
  'claude-sonnet-5': { input: 2, output: 10, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4 },
  'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 3.75, cacheWrite1h: 6 },
  'claude-sonnet-4-5': { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 3.75, cacheWrite1h: 6 },
  'claude-sonnet-4': { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 3.75, cacheWrite1h: 6 },
  'claude-haiku-4-5': { input: 1, output: 5, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2 },
  'claude-opus-5': { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
  'claude-opus-4-8': { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
  'claude-opus-4-7': { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
  'claude-opus-4-6': { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
  'claude-opus-4-5': { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
  // Opus 4 e 4.1 são aposentados e custam 3× o Opus 4.5 — por isso id exato, e não
  // um prefixo `claude-opus-4` que engoliria toda a família.
  'claude-opus-4-1': { input: 15, output: 75, cacheRead: 1.5, cacheWrite5m: 18.75, cacheWrite1h: 30 },
  'claude-opus-4': { input: 15, output: 75, cacheRead: 1.5, cacheWrite5m: 18.75, cacheWrite1h: 30 },

  // OpenAI — gpt-4o, gpt-4o-mini e linhas 5.x e 6 do catálogo (issue #1478). Valores da
  // tabela Standard de developers.openai.com/api/docs/pricing, conferida em 23/09/2026
  // (linha 6 em 11/10/2026; gpt-6.1-terra em 27/10/2026).
  // Cache de leitura: 0.5× a entrada no gpt-4o; 0.1× nas linhas 5.x e 6, exceto o
  // gpt-6.1-sol e o gpt-6.1-terra (0.05×); os `-pro` e o snapshot gpt-4o-2024-05-13 não têm desconto
  // (cacheRead = entrada).
  // Gravação de cache: as linhas 5.6 e 6 cobram 1.25× a entrada; nas demais é 1×.
  // Linhas históricas preservadas; Terra6.1 usa tarifas próprias 30m/12h.
  // Fonte: developers.openai.com/api/docs/guides/prompt-caching, 28/10/2026.
  // Fora da tabela: a faixa de contexto longo (> 272K tokens, preço 2×) da 5.4/5.5/5.6,
  // e a da linha 6 (2× a entrada e o cache, 1.5× a saída).
  'gpt-6-astra': { input: 10, output: 50, cacheRead: 1, cacheWrite5m: 12.5, cacheWrite1h: 12.5 },
  'gpt-6.1-sol': { input: 2, output: 10, cacheRead: 0.1, cacheWrite5m: 2.5, cacheWrite1h: 2.5 },
  'gpt-6.1-terra': { input: 1, output: 5, cacheRead: 0.05, cacheWrite30m: 1.25, cacheWrite12h: 2.5 },
  'gpt-6-luna': { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite5m: 0.125, cacheWrite1h: 0.125 },
  'gpt-4o-mini': { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite5m: 0.15, cacheWrite1h: 0.15 },
  'gpt-4o': { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite5m: 2.5, cacheWrite1h: 2.5 },
  // Snapshot com preço PRÓPRIO: sem esta linha a tolerância ao sufixo de data o
  // cobraria como `gpt-4o` (armadilha 2 acima, com o sinal do defeito 1).
  'gpt-4o-2024-05-13': { input: 5, output: 15, cacheRead: 5, cacheWrite5m: 5, cacheWrite1h: 5 },
  'gpt-5.6-terra': { input: 2, output: 12, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 2.5 },
  // Preço promocional, válido até pelo menos 21/11/2026; revisar depois.
  'gpt-5.6-sol': { input: 4, output: 20, cacheRead: 0.4, cacheWrite5m: 5, cacheWrite1h: 5 },
  'gpt-5.6-luna': { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite5m: 0.25, cacheWrite1h: 0.25 },
  'gpt-5.5': { input: 5, output: 30, cacheRead: 0.5, cacheWrite5m: 5, cacheWrite1h: 5 },
  'gpt-5.5-pro': { input: 30, output: 180, cacheRead: 30, cacheWrite5m: 30, cacheWrite1h: 30 },
  'gpt-5.4': { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite5m: 2.5, cacheWrite1h: 2.5 },
  'gpt-5.4-mini': { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite5m: 0.75, cacheWrite1h: 0.75 },
  'gpt-5.4-nano': { input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite5m: 0.2, cacheWrite1h: 0.2 },
  'gpt-5.4-pro': { input: 30, output: 180, cacheRead: 30, cacheWrite5m: 30, cacheWrite1h: 30 },

  // Google — os seis Gemini do catálogo curado (migrations 0023 e 0101), com o
  // preço do `ai_pricing` em vigor (centavos/100). Sem estas linhas o custo de
  // toda org em Google saía NULL e o teto mensal não disparava.
  // Cache: o caching implícito liga sozinho do 2.5 em diante e o motor monta um
  // prefixo estável para reaproveitá-lo, então num turno típico a maior parte da
  // entrada chega como `cacheReadTokens` (o @ai-sdk/google mapeia
  // `cachedContentTokenCount` para lá). Cobrar essa parte pelo preço cheio
  // superfaturava ~10× e, com o teto em bloqueio, cortava o atendimento antes do
  // teto escolhido — a armadilha 2 com o sinal invertido. Leitura = 0.1× a
  // entrada, conforme "Context caching price" de ai.google.dev/gemini-api/docs/pricing
  // (faixa ≤200K, conferida em 02/10/2026). O implícito não cobra gravação, e o
  // motor não usa cache explícito (sem `cachedContent`), então gravação = entrada
  // fica sem efeito. O 2.0 Flash não tem cache implícito nem aparece mais na
  // página: leitura = entrada, que nunca é aplicada porque nunca vem leitura.
  // Fora da tabela: a faixa acima de 200K tokens de entrada do 2.5 Pro e do 3.1 Pro.
  // Gemini 3.x da migration 0600 (#2533). 3.6/3.7/3.8 Flash: tarifa Standard
  // DEPOIS da promo, conferida em 12/10/2026 em
  // ai.google.dev/gemini-api/docs/pricing (entrada 1.50, saída 7.50, leitura de
  // cache 0.15 a partir de January 1, 2027). A promo (0.75 / 3.75 / 0.075) vale
  // até o instante anterior ao `ateMs` de PROMOCOES — precoDoModelo troca.
  // Gravação = entrada, como o resto do Google: o implícito não cobra gravação.
  // O 3.7 saiu da página de preços (depreciado, roteado ao 3.8); a tarifa é a do 3.8.
  'gemini-3.8-flash': { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite5m: 1.5, cacheWrite1h: 1.5 },
  'gemini-3.7-flash': { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite5m: 1.5, cacheWrite1h: 1.5 },
  'gemini-3.6-flash': { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite5m: 1.5, cacheWrite1h: 1.5 },
  // Gemini 4 Argon (migration 0703): tarifa Standard de 01/12/2026, lida em
  // 19/10/2026 em ai.google.dev/gemini-api/docs/pricing (entrada 4, saída 20,
  // cache 0.20). Até lá vale a promo de PROMOCOES. Leitura de cache aqui é
  // 0.05× a entrada, não o 0.1× do resto do Google. Só cache implícito.
  'gemini-4-argon': { input: 4, output: 20, cacheRead: 0.2, cacheWrite5m: 4, cacheWrite1h: 4 },
  'gemini-3.1-flash-lite': { input: 0.25, output: 1.5, cacheRead: 0.025, cacheWrite5m: 0.25, cacheWrite1h: 0.25 },
  'gemini-3.5-flash': { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite5m: 1.5, cacheWrite1h: 1.5 },
  // 3.5 Flash-Lite: tarifa Standard de ai.google.dev/gemini-api/docs/pricing
  // (06/10/2026). O armazenamento de cache ($1/MTok/h) não é por chamada.
  'gemini-3.5-flash-lite': { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite5m: 0.3, cacheWrite1h: 0.3 },
  'gemini-3.1-pro-preview': { input: 2, output: 12, cacheRead: 0.2, cacheWrite5m: 2, cacheWrite1h: 2 },
  'gemini-2.5-pro': { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite5m: 1.25, cacheWrite1h: 1.25 },
  'gemini-2.5-flash': { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite5m: 0.3, cacheWrite1h: 0.3 },
  'gemini-2.5-flash-lite': { input: 0.1, output: 0.4, cacheRead: 0.01, cacheWrite5m: 0.1, cacheWrite1h: 0.1 },
  'gemini-2.0-flash': { input: 0.1, output: 0.4, cacheRead: 0.1, cacheWrite5m: 0.1, cacheWrite1h: 0.1 },

  // Jev (TypeSafe AI), a versão FIXADA em lib/ai/decisao/cliente.ts. Fonte:
  // docs.typesafe.ai/models.md, conferida em 23/09/2026 — "Charged per input
  // token. Output tokens are free." A API devolve output_tokens > 0 mesmo assim:
  // grava-se o real e cobra-se zero. Sem cache no fornecedor: cache = entrada.
  // Id exato de propósito: quando a versão fixada subir, esta linha sobe junto,
  // e até lá a versão nova sai com custo NULL — nunca com o preço de outra.
  'jev-1.13.0': { input: 0.042, output: 0, cacheRead: 0.042, cacheWrite5m: 0.042, cacheWrite1h: 0.042 },
};

export interface TokenUsage {
  inputTokens: number;
  outputTokens: number;
  cacheReadTokens: number;
  cacheWriteTokens: number;
  /**
   * Quantos dos `cacheWriteTokens` foram gravados com TTL de 1 HORA; o resto foi
   * de 5 minutos. Quando o provedor diz (a Anthropic manda
   * `usage.cache_creation.ephemeral_1h_input_tokens` e `…_5m_…` em cada
   * resposta), cada parcela é cobrada pela sua tarifa e o `cacheTtl` não decide
   * nada. Sem ele, vale o `cacheTtl` para a gravação inteira.
   *
   * Por que existe: medido contra a fatura da Anthropic (01–17/10/2026, uma
   * organização), ~80% da gravação de cache do agente foi de 5 MINUTOS — os
   * pontos de cache do histórico não levam o TTL longo do prefixo estável — e o
   * custo cobrava tudo a 1 hora: US$ 30,50 de gravação contra US$ 21,38 na
   * fatura, 16% a mais no total do mês. Entrada, leitura e saída batiam no
   * centavo.
   */
  cacheWrite1hTokens?: number;
  /** OpenAI: parcela explicitamente gravada por 12h; ausente = padrão 30m. */
  cacheWrite12hTokens?: number;
}

/**
 * O preço de um id, tolerando o sufixo de data do vendor (`-20250805` ou `-2024-08-06`).
 *
 * Exportada para o teste poder provar o que a tabela recusa — é o caso que o
 * `startsWith` antigo deixava passar silenciosamente.
 *
 * `em` escolhe a tarifa quando o id tem preço com data (PROMOCOES). A promo
 * vale até o instante ANTERIOR a `ateMs`; nele, a tarifa da tabela passa a
 * valer. UTC: a página do Google diz a data sem fuso; o repo grava instante em
 * ISO-8601 UTC (CLAUDE.md).
 */
const ATE_GEMINI_FLASH_36_38_MS = Date.parse('2027-01-01T00:00:00.000Z');

/** Leitura = 0.1× a entrada; gravação = entrada (sem efeito no cache implícito). */
const PROMO_GEMINI_FLASH_36_38: Preco = {
  input: 0.75,
  output: 3.75,
  cacheRead: 0.075,
  cacheWrite5m: 0.75,
  cacheWrite1h: 0.75,
};

/**
 * Ids cuja tabela (e `ai_pricing`, PK só em `model`) guarda a tarifa POSTERIOR
 * à promo. Gemini 3.6/3.7/3.8 Flash: "January 1, 2027". Gemini 4 Argon: promo
 * "through Nov 30, 2026" e o dobro "from Dec 1, 2026" (página de pricing,
 * lida em 19/10/2026).
 */
const PROMOCOES: Record<string, { ateMs: number; preco: Preco }> = {
  'gemini-3.8-flash': { ateMs: ATE_GEMINI_FLASH_36_38_MS, preco: PROMO_GEMINI_FLASH_36_38 },
  'gemini-3.7-flash': { ateMs: ATE_GEMINI_FLASH_36_38_MS, preco: PROMO_GEMINI_FLASH_36_38 },
  'gemini-3.6-flash': { ateMs: ATE_GEMINI_FLASH_36_38_MS, preco: PROMO_GEMINI_FLASH_36_38 },
  'gemini-4-argon': {
    ateMs: Date.parse('2026-12-01T00:00:00.000Z'),
    preco: { input: 2, output: 10, cacheRead: 0.1, cacheWrite5m: 2, cacheWrite1h: 2 },
  },
};

function chaveDoModelo(model: string): string | undefined {
  // O OpenRouter devolve o id com o prefixo `provider/` (`anthropic/claude-…`);
  // a tabela é indexada sem ele. Recorta até a primeira barra e tenta de novo,
  // na MESMA ordem da tolerância do sufixo de data, para um id com prefixo+sufixo
  // (`anthropic/claude-sonnet-5-20250929`) casar também. O id completo segue
  // tentado PRIMEIRO: um id que a tabela conheça com barra vence o recorte.
  //
  // O catálogo do OpenRouter escreve a versão com PONTO (`anthropic/claude-haiku-5.5`)
  // e a tabela indexa com HÍFEN (`claude-haiku-5-5`). Por isso as variantes com o
  // ponto trocado por hífen entram no FIM: o id exato, o sem data e o sem prefixo
  // continuam vencendo, e um id que a tabela já conhece COM ponto (`gpt-6.1-terra`)
  // não é reescrito (issue #1931).
  const semPrefixo = model.includes("/") ? model.slice(model.indexOf("/") + 1) : model;
  const comHifen = (id: string) => id.replace(/\./g, "-");
  const candidatos = [
    model,
    model.replace(/-\d{8}$/, ""),
    model.replace(/-\d{4}-\d{2}-\d{2}$/, ""),
    semPrefixo,
    semPrefixo.replace(/-\d{8}$/, ""),
    semPrefixo.replace(/-\d{4}-\d{2}-\d{2}$/, ""),
    comHifen(semPrefixo),
    comHifen(semPrefixo.replace(/-\d{8}$/, "")),
    comHifen(semPrefixo.replace(/-\d{4}-\d{2}-\d{2}$/, "")),
    comHifen(model),
  ];
  return candidatos.find((id) => USD_PER_MTOK[id] !== undefined);
}

export function precoDoModelo(model: string, em: Date = new Date()): Preco | undefined {
  const id = chaveDoModelo(model);
  if (id === undefined) return undefined;
  const promo = PROMOCOES[id];
  if (promo !== undefined && em.getTime() < promo.ateMs) return promo.preco;
  return USD_PER_MTOK[id];
}

/** O motor sabe cobrar este modelo — sem isso o custo sai null e o teto não o vê. */
export function temPrecoNoMotor(model: string): boolean {
  return precoDoModelo(model) !== undefined;
}

/**
 * Custo em CENTS (fracionário; coluna numeric) ou null se o modelo não tem preço
 * conhecido. `inputTokens` aqui é o TOTAL do usage do SDK — a parcela cacheada é
 * descontada e cobrada pela tarifa de cache.
 *
 * `cacheTtl` é o TTL com que o prefixo estável foi gravado (knob `LLM_CACHE_TTL`);
 * o default repete a doutrina ('1h') para quem chama sem ele. Só decide quando o
 * provedor NÃO disse a divisão (`usage.cacheWrite1hTokens`).
 *
 * `em` é o instante da chamada. Sem ele, vale agora — é o que grava
 * `llm_calls.cost_cents`. Só os ids de PROMOCOES trocam de tarifa com ele
 * (3.6/3.7/3.8 Flash em 2027-01-01T00:00:00Z, 4 Argon em 2026-12-01T00:00:00Z);
 * os outros ids ignoram o instante.
 */
export function costCents(
  model: string,
  usage: TokenUsage,
  cacheTtl: CacheTtl = '1h',
  em: Date = new Date(),
): number | null {
  const p = precoDoModelo(model, em);
  if (p === undefined) {
    return null;
  }
  const noCacheInput = Math.max(0, usage.inputTokens - usage.cacheReadTokens - usage.cacheWriteTokens);
  const longoTerra = chaveDoModelo(model) === 'gpt-6.1-terra' && usage.inputTokens > 272_000;
  const fatorEntrada = longoTerra ? 2 : 1;
  const fatorSaida = longoTerra ? 1.5 : 1;
  const usd =
    ((noCacheInput * p.input +
      usage.cacheReadTokens * p.cacheRead +
      custoDaGravacao(p, usage, cacheTtl)) * fatorEntrada +
      usage.outputTokens * p.output * fatorSaida) /
    1_000_000;
  return usd * 100;
}

// US$ por minuto, só modelos cobrados por DURAÇÃO e só da OpenAI. Fonte:
// https://developers.openai.com/api/docs/pricing, linha "Whisper | Transcription |
// $0.006 / minute", conferida em 31/10/2026. `Map`, e não objeto, para que uma chave do
// protótipo (`constructor`) nunca case como modelo.
const USD_POR_MINUTO_DE_AUDIO: ReadonlyMap<string, number> = new Map([['whisper-1', 0.006]]);

/**
 * Custo em CENTS de uma transcrição cobrada por duração, ou null. Recebe `unknown` porque
 * o número vem do JSON da resposta: fora do formato (não número, infinito, negativo) ou
 * modelo sem preço por minuto devolve null, e nunca lança — a chamada já foi paga.
 */
export function custoDeAudioCents(model: string, segundos: unknown): number | null {
  const usd = USD_POR_MINUTO_DE_AUDIO.get(model);
  if (usd === undefined || typeof segundos !== 'number' || !Number.isFinite(segundos) || segundos < 0) return null;
  return (segundos / 60) * usd * 100;
}

/** A gravação de cache em USD × milhão de tokens: dividida por TTL quando o provedor disse. */
function custoDaGravacao(p: Preco, usage: TokenUsage, cacheTtl: CacheTtl): number {
  if (p.cacheWrite30m !== undefined && p.cacheWrite12h !== undefined) {
    const longo = Math.min(Math.max(0, usage.cacheWrite12hTokens ?? 0), usage.cacheWriteTokens);
    return (usage.cacheWriteTokens - longo) * p.cacheWrite30m + longo * p.cacheWrite12h;
  }
  if (usage.cacheWrite1hTokens === undefined) {
    return usage.cacheWriteTokens * (cacheTtl === '5m' ? p.cacheWrite5m! : p.cacheWrite1h!);
  }
  const umaHora = Math.min(Math.max(0, usage.cacheWrite1hTokens), usage.cacheWriteTokens);
  return umaHora * p.cacheWrite1h! + (usage.cacheWriteTokens - umaHora) * p.cacheWrite5m!;
}

/**
 * Quantos tokens da gravação de cache foram de 1 hora, lidos do `usage` CRU do
 * provedor em cada passo da chamada (`step.usage.raw` no AI SDK). A Anthropic
 * manda `cache_creation: { ephemeral_5m_input_tokens, ephemeral_1h_input_tokens }`
 * (tipo `CacheCreation` do SDK dela).
 *
 * Devolve a parcela de 1 hora PROPORCIONAL sobre `totalGravado` — o total que o
 * motor grava em `cache_write_tokens` —, para a soma das duas parcelas nunca
 * passar dele mesmo que o usage agregado e os passos discordem. `undefined`
 * quando nenhum passo traz a divisão (outro provedor, ou o campo sumiu): aí o
 * custo volta ao `cacheTtl`, que é o comportamento de antes.
 */
export function gravacaoDeUmaHora(
  usosCrus: ReadonlyArray<unknown>,
  totalGravado: number,
): number | undefined {
  let cincoMin = 0;
  let umaHora = 0;
  let viu = false;
  for (const cru of usosCrus) {
    const divisao = (cru as { cache_creation?: unknown } | null | undefined)?.cache_creation;
    if (!divisao || typeof divisao !== 'object') continue;
    const d = divisao as { ephemeral_5m_input_tokens?: unknown; ephemeral_1h_input_tokens?: unknown };
    const c = Number(d.ephemeral_5m_input_tokens ?? 0);
    const u = Number(d.ephemeral_1h_input_tokens ?? 0);
    if (!Number.isFinite(c) || !Number.isFinite(u)) continue;
    viu = true;
    cincoMin += Math.max(0, c);
    umaHora += Math.max(0, u);
  }
  if (!viu) return undefined;
  if (cincoMin + umaHora === 0) return 0;
  return Math.round((totalGravado * umaHora) / (cincoMin + umaHora));
}

/**
 * Custo dos ids de PROMOCOES pelo mesmo corte de `costCents`, ou null se o id
 * não é um deles. Prefixo `provider/` e sufixo de data do vendor entram na
 * chave de `chaveDoModelo` — não há um segundo instante de virada aqui.
 *
 * `computeCost` (`lib/ai/cost.ts`) desvia por aqui. A linha de `ai_pricing`
 * desses ids já está na tarifa posterior à promo. Sem o desvio, o
 * sentimento e o respondedor legado gravariam essa Standard em
 * `llm_calls.cost_cents` durante a promo, e Uso e orçamento somaria o dobro.
 * Só entrada e saída: este caminho não recebe token de cache. Quem tem cache
 * chama `costCents` direto (o seam do agente).
 */
export function costCentsComVirada(
  model: string,
  usage: { inputTokens: number; outputTokens: number },
  em: Date = new Date(),
): number | null {
  const id = chaveDoModelo(model);
  if (id === undefined || PROMOCOES[id] === undefined) return null;
  return costCents(
    model,
    {
      inputTokens: usage.inputTokens,
      outputTokens: usage.outputTokens,
      cacheReadTokens: 0,
      cacheWriteTokens: 0,
    },
    '1h',
    em,
  );
}

/** SDK usage de UMA requisição. O total de um tool loop não determina a faixa. */
interface UsoDePassoTerra {
  inputTokens?: number;
  outputTokens?: number;
  inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number };
  raw?: unknown;
}

/**
 * OpenAI Responses usa implicit cache com TTL padrão 30m. O motor não solicita
 * 12h nem breakpoints OpenAI: nunca inferir isso do knob Anthropic 5m/1h.
 * O SDK instalado pode omitir cache_write_tokens na normalização; o usage cru
 * documentado conserva a informação. Somamos custos por passo, não os contextos.
 */
export function custoTerraPorPassos(model: string, result: {
  usage: UsoDePassoTerra;
  steps?: ReadonlyArray<{ usage?: UsoDePassoTerra }>;
}): number | undefined {
  if (chaveDoModelo(model) !== 'gpt-6.1-terra') return undefined;
  const passos = result.steps?.length ? result.steps.map(s => s.usage) : [result.usage];
  return passos.reduce((total, u) => {
    const raw = u?.raw as { input_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number } } | undefined;
    return total + (costCents(model, {
      inputTokens: u?.inputTokens ?? 0,
      outputTokens: u?.outputTokens ?? 0,
      cacheReadTokens: raw?.input_tokens_details?.cached_tokens ?? u?.inputTokenDetails?.cacheReadTokens ?? 0,
      cacheWriteTokens: raw?.input_tokens_details?.cache_write_tokens ?? u?.inputTokenDetails?.cacheWriteTokens ?? 0,
    }) ?? 0);
  }, 0);
}

/**
 * O custo que a OpenRouter COBROU, em centavos, somado por passo — lido do corpo
 * cru de cada resposta (`step.response.body`, que o seam só pede na OpenRouter
 * oficial). `undefined` quando algum passo não traz `usage.cost` numérico e não
 * negativo, ou é BYOK sem `upstream_inference_cost` numérico e não negativo: aí
 * vale a tabela, como antes, e nunca um número pela metade. Fora do BYOK,
 * upstream ausente ou negativo conta 0.
 *
 * A regra de BYOK (doc usage-accounting: `cost` é o cobrado na conta da
 * OpenRouter; `cost_details.upstream_inference_cost`, o cobrado pelo provedor de
 * origem):
 *   - `is_byok` true  → cost + upstream (a taxa dela mais a conta da sua chave);
 *   - `is_byok` false → cost (o upstream já está dentro dele);
 *   - sem o campo     → max(cost, upstream). O exemplo de Chat Completions da doc
 *     traz cost 0,95 e upstream 19 sem a flag — 0,95 é 5% de 19, a taxa de BYOK:
 *     "só cost" gravaria 5% do gasto (e 0 dentro da franquia), e o teto nunca
 *     fecharia; "somar sempre" dobraria a resposta sem BYOK que traga os dois.
 *     O max nunca dobra e, no BYOK sem flag, subconta no máximo a taxa.
 */
export function custoInformadoPeloOpenRouter(result: {
  response?: { body?: unknown };
  steps?: ReadonlyArray<{ response?: { body?: unknown } }>;
}): number | undefined {
  const corpos = result.steps?.length ? result.steps.map((s) => s.response?.body) : [result.response?.body];
  let total = 0;
  for (const bruto of corpos) {
    const usage = (corpoComoObjeto(bruto) as { usage?: UsoDaOpenRouter | null } | undefined)?.usage;
    const cost = usage?.cost;
    // Custo negativo não é fatura: um número assim abateria o gasto do mês e
    // furaria o teto. Volta à tabela.
    if (typeof cost !== 'number' || !Number.isFinite(cost) || cost < 0) return undefined;
    const upBruto = usage?.cost_details?.upstream_inference_cost;
    const upValido = typeof upBruto === 'number' && Number.isFinite(upBruto) && upBruto >= 0;
    // BYOK sem upstream legível: o `cost` é só a taxa dela (0 dentro da franquia),
    // e gravá-lo sozinho seria a mesma porta do custo negativo. Volta à tabela.
    if (usage?.is_byok === true && !upValido) return undefined;
    const up = upValido ? upBruto : 0;
    total += usage?.is_byok === true ? cost + up : usage?.is_byok === false ? cost : Math.max(cost, up);
  }
  return total * 100;
}

/** Os campos de `usage` que a OpenRouter devolve e o custo lê — tudo `unknown` até conferir. */
interface UsoDaOpenRouter {
  cost?: unknown;
  is_byok?: unknown;
  cost_details?: { upstream_inference_cost?: unknown } | null;
}

/** O corpo cru do passo: objeto, ou texto JSON. Qualquer outra coisa não tem custo a ler. */
export function corpoComoObjeto(bruto: unknown): object | undefined {
  if (typeof bruto === 'string') {
    try {
      const json: unknown = JSON.parse(bruto);
      return json !== null && typeof json === 'object' ? json : undefined;
    } catch {
      // Texto que não é JSON não traz `usage.cost`: o custo volta à tabela.
      return undefined;
    }
  }
  return bruto !== null && typeof bruto === 'object' ? bruto : undefined;
}

/**
 * Síntese de voz (ElevenLabs) é cobrada por CARACTERE, não por token — unidade
 * diferente de tudo acima, por isso uma função própria em vez de forçar
 * `TokenUsage`.
 *
 * Fonte: https://elevenlabs.io/pricing (conferida 2026-10-29). A página pública
 * não divulga USD/crédito direto — só plano em USD + créditos/mês — e os
 * planos NÃO convergem para uma taxa única: Creator (US$11 / 121k créditos)
 * destoa dos demais, provavelmente por desconto de lançamento/anual, enquanto
 * Starter, Pro, Scale e Business concordam entre si em ≈US$0,000165/crédito.
 * É ESSA taxa — a que bate entre quatro planos independentes, não a que destoa
 * de um — que esta tabela usa. 1 caractere = 1 crédito no modelo padrão
 * (`eleven_multilingual_v2`); modelos "Flash/Turbo" custam metade do crédito
 * por caractere, mas este produto não expõe escolha de modelo de voz ainda —
 * quando expuser, este arquivo precisa de uma linha por modelo, não uma
 * constante só.
 *
 * ⚠️ Preço de API de terceiro muda sem aviso e esta tabela não se atualiza
 * sozinha — quem revisar a cobrança real de uma instalação deve CONFERIR a
 * página antes de confiar neste número, não só confiar no código.
 */
const USD_POR_CREDITO_ELEVENLABS = 0.000165;

/** Custo em CENTS de sintetizar `caracteres` de texto em voz — nunca null: é a única tarifa que este arquivo conhece para voz hoje. */
export function custoDeVozCents(caracteres: number): number {
  return Math.max(0, caracteres) * USD_POR_CREDITO_ELEVENLABS * 100;
}
