// Package pricing implements the pricing table and cost formula the
// cost_meter middleware uses to convert LLM token usage into a USD cost
// estimate. The table's content arrives from the management server inside
// cost_meter's middleware config (synthesized from the catalog plus the
// operator's stored per-provider prices) — the proxy carries no embedded
// price list. Price updates ride the ordinary mapping push: a chain
// rebuild constructs a fresh table, so there is nothing to reload.
package pricing

import (
	"fmt"
	"math"
)

// Entry is a single model's input and output pricing, expressed in USD per
// 1000 tokens.
//
// CachedInputPer1K applies to OpenAI's cached prompt tokens, which are a
// subset of input_tokens — when set, the cached portion is billed at this
// rate and the non-cached remainder at InputPer1K. Zero means "no discount
// configured", and cached tokens are billed at InputPer1K.
//
// CacheReadPer1K and CacheCreationPer1K apply to Anthropic's two prompt-
// cache fields, which are additive to input_tokens: cache_read is the
// cheaper read-from-cache rate, cache_creation is the more expensive
// write-to-cache rate. Zero means "no rate configured" and the
// corresponding token bucket is billed at InputPer1K.
type Entry struct {
	InputPer1K         float64
	OutputPer1K        float64
	CachedInputPer1K   float64
	CacheReadPer1K     float64
	CacheCreationPer1K float64
}

// EntryJSON is the wire shape of a pricing entry inside cost_meter's
// middleware config. Field names are the management→proxy contract; the
// management synthesizer marshals the same names (its pricing.Entry).
type EntryJSON struct {
	InputPer1K         float64 `json:"input_per_1k"`
	OutputPer1K        float64 `json:"output_per_1k"`
	CachedInputPer1K   float64 `json:"cached_input_per_1k"`
	CacheReadPer1K     float64 `json:"cache_read_per_1k"`
	CacheCreationPer1K float64 `json:"cache_creation_per_1k"`
}

// Table is a provider-surface-to-model pricing lookup. Instances are
// immutable once built; a mapping update builds a whole new middleware
// instance (and with it a new table) rather than mutating this one.
type Table struct {
	entries map[string]map[string]Entry
}

// NewEntries validates and converts a wire-shape map (surface-or-record ->
// model -> rates) into the internal representation. Every rate must be a
// finite, non-negative USD amount; a violation is returned as an error so
// a corrupt config fails the chain build loudly instead of mispricing.
// Management validates the same constraints at its API boundary, so this
// is defense-in-depth. Nil input yields an empty (never-matching) map.
func NewEntries(raw map[string]map[string]EntryJSON) (map[string]map[string]Entry, error) {
	out := make(map[string]map[string]Entry, len(raw))
	for outer, models := range raw {
		inner := make(map[string]Entry, len(models))
		for model, e := range models {
			for field, v := range map[string]float64{
				"input_per_1k":          e.InputPer1K,
				"output_per_1k":         e.OutputPer1K,
				"cached_input_per_1k":   e.CachedInputPer1K,
				"cache_read_per_1k":     e.CacheReadPer1K,
				"cache_creation_per_1k": e.CacheCreationPer1K,
			} {
				if v < 0 || math.IsNaN(v) || math.IsInf(v, 0) {
					return nil, fmt.Errorf("pricing %s/%s: %s must be a finite, non-negative rate, got %v", outer, model, field, v)
				}
			}
			// EntryJSON and Entry are field-identical (tags aside), so a
			// direct conversion carries all five rates.
			inner[model] = Entry(e)
		}
		out[outer] = inner
	}
	return out, nil
}

// NewTable builds an immutable Table from the wire-shape defaults map.
// See NewEntries for validation semantics.
func NewTable(raw map[string]map[string]EntryJSON) (*Table, error) {
	entries, err := NewEntries(raw)
	if err != nil {
		return nil, err
	}
	return &Table{entries: entries}, nil
}

// Lookup returns the entry for the given provider surface and model.
func (t *Table) Lookup(provider, model string) (Entry, bool) {
	if t == nil {
		return Entry{}, false
	}
	byModel, ok := t.entries[provider]
	if !ok {
		return Entry{}, false
	}
	e, ok := byModel[model]
	return e, ok
}

// Has reports whether the provider/model pair is present in the table.
func (t *Table) Has(provider, model string) bool {
	_, ok := t.Lookup(provider, model)
	return ok
}

// Cost returns the estimated USD cost for the given token counts. ok is
// false when the provider or model is not present in the table; the caller
// can still emit token metrics with a model=unknown label.
func (t *Table) Cost(provider, model string, inTokens, outTokens, cachedInput, cacheCreation int64) (float64, bool) {
	c, ok := t.Costs(provider, model, inTokens, outTokens, cachedInput, cacheCreation)
	return c.TotalUSD, ok
}

// Costs returns the estimated USD cost split for the given token counts.
// The provider surface selects the cache formula; see EntryCosts.
func (t *Table) Costs(provider, model string, inTokens, outTokens, cachedInput, cacheCreation int64) (Costs, bool) {
	entry, ok := t.Lookup(provider, model)
	if !ok {
		return Costs{}, false
	}
	return EntryCosts(entry, provider, inTokens, outTokens, cachedInput, cacheCreation), true
}

// Costs is a per-request cost split. The four per-bucket fields are the base
// of the breakdown — one per token bucket the provider bills separately — and
// the two aggregates are derived from them:
//
//	TotalUSD = InputUSD + CachedInputUSD + CacheCreationUSD + OutputUSD
//	CacheUSD = CachedInputUSD + CacheCreationUSD
//
// InputUSD is always the cost of the *non-cached* input bucket, for both
// provider shapes: on OpenAI the cached subset is carved out of inTokens and
// billed as CachedInputUSD, so the two never double-count. Buckets a provider
// doesn't bill are zero, which keeps the identities above true everywhere.
type Costs struct {
	InputUSD         float64
	CachedInputUSD   float64
	CacheCreationUSD float64
	OutputUSD        float64
	TotalUSD         float64
	CacheUSD         float64
}

// newCosts assembles a split from its per-bucket parts, deriving the two
// aggregates so TotalUSD and CacheUSD can never drift from the breakdown.
func newCosts(input, cachedInput, cacheCreation, output float64) Costs {
	return Costs{
		InputUSD:         input,
		CachedInputUSD:   cachedInput,
		CacheCreationUSD: cacheCreation,
		OutputUSD:        output,
		TotalUSD:         input + cachedInput + cacheCreation + output,
		CacheUSD:         cachedInput + cacheCreation,
	}
}

// EntryCosts computes the USD cost split for the given entry and token
// counts. The surface (the llm.provider value the request parser stamps)
// selects the cache formula; the entry may come from the surface-keyed
// defaults table or from a per-provider-record override — the math is
// identical either way.
//
// Provider-shape semantics for cached / cache-creation counts:
//
//   - "openai": cachedInput is a SUBSET of inTokens. The cached portion is
//     billed at CachedInputPer1K (or InputPer1K when no override), and the
//     non-cached remainder of inTokens at InputPer1K. cacheCreation is
//     ignored (OpenAI has no analogue).
//   - "anthropic", "bedrock": cachedInput (cache_read) and cacheCreation are
//     ADDITIVE to inTokens. The three buckets are billed at CacheReadPer1K,
//     CacheCreationPer1K, and InputPer1K respectively, each falling back
//     to InputPer1K when the corresponding rate is zero.
//   - Other surfaces: cached and cacheCreation are ignored; cost is
//     inTokens*InputPer1K + outTokens*OutputPer1K.
func EntryCosts(entry Entry, surface string, inTokens, outTokens, cachedInput, cacheCreation int64) Costs {
	// Clamp negatives to zero before any pricing math so a malformed
	// upstream count can never produce a negative cost.
	if inTokens < 0 {
		inTokens = 0
	}
	if outTokens < 0 {
		outTokens = 0
	}
	if cachedInput < 0 {
		cachedInput = 0
	}
	if cacheCreation < 0 {
		cacheCreation = 0
	}
	output := (float64(outTokens) / 1000.0) * entry.OutputPer1K
	switch surface {
	case "openai":
		// cachedInput is a subset of inTokens; clamp so a malformed
		// upstream (cached > total) can't produce a negative remainder.
		clamped := cachedInput
		if clamped > inTokens {
			clamped = inTokens
		}
		cachedRate := entry.CachedInputPer1K
		if cachedRate <= 0 {
			cachedRate = entry.InputPer1K
		}
		nonCached := float64(inTokens-clamped) / 1000.0 * entry.InputPer1K
		cached := float64(clamped) / 1000.0 * cachedRate
		return newCosts(nonCached, cached, 0, output)
	case "anthropic", "bedrock":
		// Bedrock-Anthropic returns the same additive cache buckets as
		// first-party Anthropic; non-Anthropic Bedrock models simply report
		// zero cache tokens, so this formula degrades to input + output.
		readRate := entry.CacheReadPer1K
		if readRate <= 0 {
			readRate = entry.InputPer1K
		}
		createRate := entry.CacheCreationPer1K
		if createRate <= 0 {
			createRate = entry.InputPer1K
		}
		input := float64(inTokens) / 1000.0 * entry.InputPer1K
		read := float64(cachedInput) / 1000.0 * readRate
		create := float64(cacheCreation) / 1000.0 * createRate
		return newCosts(input, read, create, output)
	default:
		input := float64(inTokens) / 1000.0 * entry.InputPer1K
		return newCosts(input, 0, 0, output)
	}
}
