// Package llm is the provider-neutral seam between the pipeline and concrete
// model backends (порт vojo/apps/ai-bot llm.go с расширениями Фазы 0 — см.
// docs/architecture/03-implementation-notes.md §3.5). Nothing here names a
// vendor: the runner composes context, the ledger prices usage, and thin
// adapters (provider_openai.go, provider_anthropic.go, provider_local.go) map
// these types to/from each backend's wire format.
package llm

import "context"

// Message is one provider-neutral chat turn.
type Message struct {
	Role    string // "system" | "user" | "assistant"
	Content string
	// CacheBoundary marks THIS message as the end of a stable prompt prefix
	// (раскладка Р5: стабильный префикс = system+brief+style sheet+резюме,
	// волатильный хвост = глоссарий+STM+чанк). The Anthropic adapter attaches
	// cache_control to the corresponding block (max 4 boundaries per request —
	// API limit); OpenAI-compatible providers cache by prefix automatically and
	// ignore the flag. The context assembler decides the layout; adapters never
	// guess where the boundary is.
	CacheBoundary bool
}

// Usage is the provider-neutral token accounting returned with a completion.
// It drives billing (ledger.CostUSD) — the counts are the API's own,
// authoritative even if our price constants drift.
//
// Invariant: PromptTokens is the TOTAL input (cached + cache-written +
// uncached). Anthropic reports the three parts separately (input_tokens is
// only the uncached remainder), so its adapter SUMS them into PromptTokens;
// OpenAI-compatible providers already report the total in prompt_tokens.
type Usage struct {
	PromptTokens int
	// CachedTokens is the subset of PromptTokens served from the provider's
	// prompt cache (cache READ — billed at the discounted cache-hit rate).
	CachedTokens int
	// CacheCreationTokens is the subset of PromptTokens WRITTEN to the cache
	// this call. Anthropic bills writes at a premium (×1.25 for 5m TTL, ×2 for
	// 1h — проверено 2026-07-04); dropping this field would undercount spend,
	// the same failure class as the reasoning-token undercount already paid
	// for in vojo (30–44%). Providers without explicit cache writes leave 0.
	CacheCreationTokens int
	CompletionTokens    int
	// ReasoningTokens are thinking tokens billed at the output rate but NOT
	// included in CompletionTokens. Semantics are per-adapter: xAI reports
	// them additively (verified in vojo against cost_in_usd_ticks); the OpenAI
	// spec and ollama count them INSIDE completion_tokens, so those adapters
	// must leave 0 to avoid double-billing; Anthropic bills thinking inside
	// output_tokens, so its adapter also leaves 0.
	ReasoningTokens int
}

// Neutral finish reasons. The coverage gate branches on these: a length cut
// needs a retry with a bigger max_tokens, NOT an escalation (реальный кейс
// Gemini из эксперимента 02 — thinking съел бюджет, sent_cov упал до 0.11).
const (
	FinishStop          = "stop"           // natural end of turn
	FinishLength        = "length"         // max_tokens exhausted
	FinishContentFilter = "content_filter" // provider-side filter cut the output
	FinishRefusal       = "refusal"        // model/classifier refused (Anthropic stop_reason=refusal)
)

// LLMRequest is a provider-neutral completion request.
//
// Deliberately absent (vs vojo): Tools (tool-calling вне скоупа MVP по Р2) and
// ConvID (xAI-специфичный кэш-хинт; вернём с xAI-адаптером, если понадобится).
type LLMRequest struct {
	Model       string
	Messages    []Message
	MaxTokens   int
	// Temperature: how (and whether) it reaches the wire is decided per-model
	// by the Capability resolver (capability.go: send incl. explicit 0 | omit |
	// force) — the adapter no longer gates on > 0.
	Temperature float64
	// ReasoningEffort: "" = provider default, "off" = explicitly disable
	// thinking, "low"|"medium"|"high" = enable at that depth. Each adapter maps
	// this to its wire form; providers with non-standard switches (GLM
	// thinking.type, Qwen enable_thinking) are handled via per-model ExtraBody
	// in models.yaml instead (эмпирика полигона: thinking у GLM — таймауты ×3).
	ReasoningEffort string
	// JSONOnly asks the backend to constrain output to a single valid JSON
	// object (OpenAI-compat response_format json_object). Used by judge/gate
	// roles; false serializes away.
	JSONOnly bool
}

// LLMResponse is a provider-neutral completion result.
type LLMResponse struct {
	Text  string
	Usage Usage
	// Model is the model that ACTUALLY served this completion. It can differ
	// from LLMRequest.Model when a routing decorator (failover) answered with a
	// different backend — billing and telemetry must follow the responder, or a
	// free local answer books at cloud prices. "" = unknown; consumers fall
	// back to the requested model.
	Model string
	// FinishReason is one of the Finish* constants ("" = unknown/legacy).
	FinishReason      string
	ProviderRequestID string // the backend's response id, logged for support/debug
}

// LLMClient is any chat-completion backend. Implementations are thin adapters
// over a wire protocol; the pipeline depends only on this interface, so
// clients can be swapped or decorated (failover) without touching business
// logic. Streaming is deliberately NOT in the interface yet ([НУЖНО РЕШЕНИЕ]
// п.4 в PROGRESS): when it lands it will be a second, optional interface
// (StreamingLLMClient) so existing adapters keep compiling.
type LLMClient interface {
	Complete(ctx context.Context, req LLMRequest) (*LLMResponse, error)
}
