// Package ledger is the money seam: per-model prices, usage→USD computation,
// and reserve/settle bookkeeping against the store. Дисциплина vojo целиком
// (Р7): биллинг по usage из ответа API, reserve-before-call / settle-after,
// потолки $; расширение Фазы 0 — стоимость записи в кэш (Anthropic ×1.25/×2).
package ledger

import (
	"fmt"

	"textmachine/backend/internal/llm"
)

// ModelPrice is the per-1M-token USD price for one model, applied to the
// API's returned usage so the ceilings track real cost even as prices drift.
// Prices live ONLY in configs/models.yaml with a checked-date (Р4/Р5).
type ModelPrice struct {
	InputPerM float64 // non-cached prompt tokens
	// CachedPerM prices prompt tokens served FROM the cache (cache read).
	CachedPerM float64
	// CacheWritePerM prices prompt tokens WRITTEN to the cache. Anthropic bills
	// writes at ×1.25 (5m TTL) / ×2 (1h) of input; providers without explicit
	// writes keep 0 and the term vanishes.
	CacheWritePerM float64
	OutputPerM     float64 // completion + reasoning tokens
}

// Pricer resolves model→price with an explicit default anchor. An unknown
// model falls back to the DEFAULT price rather than $0 — a $0 price would
// silently blind the ceilings to that call, the one failure mode we never
// want (у vojo якорем была XAIModel; здесь мультипровайдерный конфиг требует
// явного default_price).
type Pricer struct {
	prices       map[string]ModelPrice
	defaultPrice ModelPrice
}

// NewPricer builds a pricer. defaultPrice must be a real (non-zero) price —
// enforced at config load (fail-fast), re-checked here defensively.
func NewPricer(prices map[string]ModelPrice, defaultPrice ModelPrice) (*Pricer, error) {
	if defaultPrice.InputPerM <= 0 || defaultPrice.OutputPerM <= 0 {
		return nil, fmt.Errorf("ledger: default price must be non-zero (a $0 fallback would blind the spend ceilings)")
	}
	return &Pricer{prices: prices, defaultPrice: defaultPrice}, nil
}

// PriceFor returns the configured price for a model, falling back to the
// default anchor for unknown models (never $0).
func (p *Pricer) PriceFor(model string) ModelPrice {
	if mp, ok := p.prices[model]; ok {
		return mp
	}
	return p.defaultPrice
}

// PriceForResponse prices a completion by the model that ACTUALLY answered,
// but if that id is unknown (провайдер вернул канонизированный/датированный
// слаг вроде claude-sonnet-5-2026xxxx) falls back to the REQUESTED model's
// price BEFORE the global anchor — иначе премиум-ответ книжился бы по дешёвому
// дефолту (систематический недоучёт, находка ревью). Только если неизвестны
// обе — дефолтный якорь (never $0).
func (p *Pricer) PriceForResponse(requested, actual string) ModelPrice {
	if mp, ok := p.prices[actual]; ok {
		return mp
	}
	if mp, ok := p.prices[requested]; ok {
		return mp
	}
	return p.defaultPrice
}

// CostUSD prices one completion by its usage. Formula (invariant from
// llm.Usage: PromptTokens = total input = uncached + cached + cache-written):
//
//	(prompt − cached − cacheCreation)·in + cached·cacheRead
//	  + cacheCreation·cacheWrite + (completion + reasoning)·out
//
// For providers without cache-write accounting the third term is 0 and the
// formula degrades to vojo's computeUSD.
func CostUSD(price ModelPrice, u llm.Usage) float64 {
	// Каждое слагаемое зажимается в ≥0: провайдер с багом учёта, приславший
	// отрицательные токены, иначе УМЕНЬШИЛ бы committed и ослабил потолок
	// (находка ревью). Стоимость может только расти или быть нулевой.
	nn := func(x int) int {
		if x < 0 {
			return 0
		}
		return x
	}
	cached := nn(u.CachedTokens)
	cacheWrite := nn(u.CacheCreationTokens)
	completion := nn(u.CompletionTokens) + nn(u.ReasoningTokens)
	uncached := nn(u.PromptTokens) - cached - cacheWrite
	if uncached < 0 {
		// cached+write > prompt (провайдер отрапортовал подмножество больше
		// целого) — не уводим некэш-слагаемое в минус.
		uncached = 0
	}
	perTok := func(perM float64) float64 { return perM / 1_000_000 }
	return float64(uncached)*perTok(price.InputPerM) +
		float64(cached)*perTok(price.CachedPerM) +
		float64(cacheWrite)*perTok(price.CacheWritePerM) +
		float64(completion)*perTok(price.OutputPerM)
}

// EstimateUSD is the pre-call reservation estimate: assume the whole prompt
// misses the cache — or, worse, is entirely WRITTEN to the cache (Anthropic
// write ×1.25/×2 дороже input) — and the completion runs to maxTokens.
// Deliberately pessimistic: реальная цена не должна превышать резерв, иначе
// settle пробивает потолок, пропущенный на допуске.
//
// reasoningBudgetTokens (D13.6) reserves an EXPLICIT allowance for providers that
// bill reasoning ADDITIVELY (xAI: reasoning tokens land ON TOP of completion, not
// inside maxTokens — the overshoot pricing.go used to only acknowledge). It is
// priced at OutputPerM (reasoning bills as output) and MUST be > 0 for a think-ON
// stage on such a provider (config fail-fasts otherwise — pipeline.go); it is 0 for
// subset-billing providers (deepseek/zai/kimi: reasoning ⊆ completion ≤ maxTokens,
// already covered) and for reasoning-off stages. This closes the D6.2 block on
// think-ON grok: the ceiling is no longer blind to the additive reasoning spend.
func EstimateUSD(price ModelPrice, promptTokens, maxTokens, reasoningBudgetTokens int) float64 {
	inPerM := price.InputPerM
	if price.CacheWritePerM > inPerM {
		inPerM = price.CacheWritePerM
	}
	if reasoningBudgetTokens < 0 {
		reasoningBudgetTokens = 0
	}
	return float64(promptTokens)*inPerM/1_000_000 +
		float64(maxTokens+reasoningBudgetTokens)*price.OutputPerM/1_000_000
}
