package openai

import (
	"context"
	"errors"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
)

// Usage metering is scoped to the OpenAI-compatible input surface only (SDD
// S04). CLI, A2A, and local-only adapter usage are intentionally excluded from
// these counters; this file is never imported by those surfaces.

// Terminal request status label values.
const (
	usageStatusSuccess = "success"
	usageStatusError   = "error"
	usageStatusCancel  = "cancel"
)

// usage_source label values on iop_openai_requests_total. provider_reported
// means at least one token type was reported for the request; unavailable means
// no usage was observed (status-only request count).
const (
	usageSourceProviderReported = "provider_reported"
	usageSourceUnavailable      = "unavailable"
)

// Endpoint label values distinguish the OpenAI-compatible route.
const (
	usageEndpointChatCompletions = "chat.completions"
	usageEndpointResponses       = "responses"
)

// Token type label values on iop_openai_usage_tokens_total.
const (
	tokenTypeInput       = "input"
	tokenTypeOutput      = "output"
	tokenTypeReasoning   = "reasoning"
	tokenTypeCachedInput = "cached_input"
)

// Label allowlists. These deliberately exclude request_id, session_id, raw
// bearer/provider tokens, and raw prompt/response text so the counters stay
// low-cardinality and never leak secrets (SDD S08). The label sets are exported
// as package vars so tests can assert the allowlist directly.
var (
	usageRequestLabelNames = []string{
		"edge_id", "principal_ref", "principal_alias", "token_ref",
		"model_group", "endpoint", "response_mode", "status", "usage_source",
	}
	usageTokenLabelNames = []string{
		"edge_id", "principal_ref", "principal_alias", "token_ref",
		"model_group", "endpoint", "response_mode", "token_type",
	}
	usageReasoningLabelNames = []string{
		"edge_id", "principal_ref", "principal_alias", "token_ref",
		"model_group", "endpoint", "response_mode",
	}
	usageReasoningEstimateLabelNames = []string{
		"edge_id", "principal_ref", "principal_alias", "token_ref",
		"model_group", "endpoint", "response_mode", "estimation_method",
	}
)

var (
	openAIRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "iop_openai_requests_total",
		Help: "OpenAI-compatible requests processed by the Edge input surface, by terminal status and usage source.",
	}, usageRequestLabelNames)

	openAIUsageTokensTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "iop_openai_usage_tokens_total",
		Help: "Provider-reported OpenAI-compatible token usage by token type. Reasoning tokens are only counted when the provider reports them.",
	}, usageTokenLabelNames)

	openAIReasoningObservedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "iop_openai_reasoning_observed_total",
		Help: "Requests where reasoning text was observed but the provider did not report reasoning token usage.",
	}, usageReasoningLabelNames)

	openAIReasoningCharsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "iop_openai_reasoning_chars_total",
		Help: "Observed reasoning text characters for requests without provider-reported reasoning tokens. The token estimate is emitted separately via iop_openai_reasoning_estimated_tokens_total.",
	}, usageReasoningLabelNames)

	openAIReasoningEstimatedTokensTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "iop_openai_reasoning_estimated_tokens_total",
		Help: "Estimated reasoning tokens for requests with observed reasoning text but without provider-reported reasoning token usage.",
	}, usageReasoningEstimateLabelNames)
)

// usageObservation is the metric-facing view of a request's token usage. Counts
// are provider-reported; a zero count means the provider did not report that
// token type and the emitter never estimates it.
type usageObservation struct {
	inputTokens       int
	outputTokens      int
	reasoningTokens   int
	cachedInputTokens int
	// reasoningChars counts observed reasoning text characters. It feeds the
	// reasoning-observed auxiliary metrics and the separate estimated-token
	// counter when the provider does not report reasoning token usage.
	reasoningChars int
}

func (o usageObservation) hasTokens() bool {
	return o.inputTokens > 0 || o.outputTokens > 0 || o.reasoningTokens > 0 || o.cachedInputTokens > 0
}

func usageObservationFromOpenAIUsage(usage *openAIUsage, reasoningChars int) usageObservation {
	obs := usageObservation{reasoningChars: reasoningChars}
	if usage != nil {
		obs.inputTokens = usage.PromptTokens
		obs.outputTokens = usage.CompletionTokens
		obs.reasoningTokens = usage.ReasoningTokens
		obs.cachedInputTokens = usage.CachedInputTokens
	}
	return obs
}

// usageLabels carries the resolved low-cardinality label values for one request.
type usageLabels struct {
	edgeID         string
	principalRef   string
	principalAlias string
	tokenRef       string
	modelGroup     string
	endpoint       string
	responseMode   string
}

// usageLabelsFor builds the metric label values for a request from its
// authenticated principal context. Identity is taken only from the resolved
// principal (never from caller-supplied metadata.user).
func (s *Server) usageLabelsFor(ctx context.Context, modelGroup, endpoint, responseMode string) usageLabels {
	l := usageLabels{
		edgeID:       s.edgeIDValue(),
		modelGroup:   modelGroup,
		endpoint:     endpoint,
		responseMode: responseMode,
	}
	if p, ok := principalFromContext(ctx); ok {
		l.principalRef = p.PrincipalRef
		l.principalAlias = p.PrincipalAlias
		l.tokenRef = p.TokenRef
	}
	return l
}

func (l usageLabels) reasoningValues() []string {
	return []string{l.edgeID, l.principalRef, l.principalAlias, l.tokenRef, l.modelGroup, l.endpoint, l.responseMode}
}

// estimatedReasoningTokens converts observed reasoning text characters to a
// token estimate using the chars/4 rule of thumb. This is an operator-grade
// observation estimate only, never billing-grade. Caller reports a non-zero
// provider `reasoningTokens` value to skip this path entirely.
func estimatedReasoningTokens(chars int) int {
	return (chars + 3) / 4
}

// emitUsageMetrics increments the request counter for the terminal status and,
// when usage was observed, the per-token-type counters. Reasoning text observed
// without a provider-reported reasoning token count increments the auxiliary
// reasoning counters and emits a separate estimated-token counter; the
// provider-reported `token_type="reasoning"` counter stays exclusive to the
// provider's value.
func emitUsageMetrics(l usageLabels, status string, obs usageObservation) {
	source := usageSourceUnavailable
	if obs.hasTokens() {
		source = usageSourceProviderReported
	}
	openAIRequestsTotal.WithLabelValues(
		l.edgeID, l.principalRef, l.principalAlias, l.tokenRef,
		l.modelGroup, l.endpoint, l.responseMode, status, source,
	).Inc()

	addTokenCount(l, tokenTypeInput, obs.inputTokens)
	addTokenCount(l, tokenTypeOutput, obs.outputTokens)
	addTokenCount(l, tokenTypeReasoning, obs.reasoningTokens)
	addTokenCount(l, tokenTypeCachedInput, obs.cachedInputTokens)

	if obs.reasoningChars > 0 && obs.reasoningTokens == 0 {
		openAIReasoningObservedTotal.WithLabelValues(l.reasoningValues()...).Inc()
		openAIReasoningCharsTotal.WithLabelValues(l.reasoningValues()...).Add(float64(obs.reasoningChars))
		estimate := estimatedReasoningTokens(obs.reasoningChars)
		openAIReasoningEstimatedTokensTotal.WithLabelValues(
			l.edgeID, l.principalRef, l.principalAlias, l.tokenRef,
			l.modelGroup, l.endpoint, l.responseMode, "chars_div_4",
		).Add(float64(estimate))
	}
}

func addTokenCount(l usageLabels, tokenType string, count int) {
	if count <= 0 {
		return
	}
	openAIUsageTokensTotal.WithLabelValues(
		l.edgeID, l.principalRef, l.principalAlias, l.tokenRef,
		l.modelGroup, l.endpoint, l.responseMode, tokenType,
	).Add(float64(count))
}

// usageStatusForError classifies a post-dispatch run error into a terminal
// status label. Caller give-up (cancellation/timeout) is a cancel; everything
// else is an error.
func usageStatusForError(err error) string {
	if err == nil {
		return usageStatusSuccess
	}
	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, errRunTimedOut) {
		return usageStatusCancel
	}
	return usageStatusError
}

const (
	// responseModePassthrough and responseModeNormalized are internal execution
	// labels for the response_mode usage metric. They are derived from the
	// handler execution path, never from caller metadata: provider tunnel routes
	// report passthrough, normalized RunEvent routes report normalized. Callers
	// cannot select a response mode through OpenAI metadata.
	responseModePassthrough = "passthrough"
	responseModeNormalized  = "normalized"
)
