package llm

import (
	"context"
	"log/slog"
	"net/http"
)

// provider_openai.go is the generic adapter for every OpenAI-compatible cloud
// provider (DeepSeek, GLM/Z.AI, Kimi, xAI, ru-агрегаторы). Vojo had a
// per-vendor file each (provider_xai.go и т.п.), but their bodies were
// identical shells over the shared transport; TextMachine's differences are
// data (base URL, key, usage semantics, extra body), so one adapter
// parameterized from models.yaml replaces the family.

// ReasoningSemantics tells the adapter how a provider accounts thinking
// tokens (см. Usage.ReasoningTokens в llm.go).
type ReasoningSemantics string

const (
	// ReasoningSubset: thinking is counted INSIDE completion_tokens (OpenAI
	// spec, ollama, DeepSeek). ReasoningTokens stays 0 to avoid double-billing.
	ReasoningSubset ReasoningSemantics = "subset"
	// ReasoningAdditive: thinking is reported separately and billed ON TOP of
	// completion_tokens (xAI — verified in vojo against cost_in_usd_ticks;
	// dropping it undercounted Grok spend by 30–44%). Read from
	// completion_tokens_details.reasoning_tokens.
	ReasoningAdditive ReasoningSemantics = "additive"
	// ReasoningAdditiveTotal: thinking is billed as output but reported ONLY in
	// total_tokens — Gemini via the OpenAI-compat layer omits it from
	// completion_tokens AND has no reasoning_tokens field (live-probe 2026-07-10:
	// completion=2, prompt=22, total=847 → 823 hidden thinking). Derived as
	// total − prompt − completion so the mandatory-thinking spend (D6.3) is billed
	// as output instead of blinding the ceiling.
	ReasoningAdditiveTotal ReasoningSemantics = "additive_total"
)

// OpenAICompatConfig configures one provider instance.
type OpenAICompatConfig struct {
	Name      string // provider label for logs/telemetry
	BaseURL   string // ".../v1"-style base; transport appends /chat/completions
	APIKey    string
	Profile   RetryProfile
	Headers   map[string]string // static extra headers, may be nil
	Reasoning ReasoningSemantics
	// Cap is the resolved per-model wire shape (capability.go). Zero value =
	// OpenAI-compat baseline (max_tokens + send temperature + off-by-omission).
	Cap Capability
	// ExtraBody is merged into every request JSON for this provider's models.
	// Per-model extras from models.yaml are passed the same way when the client
	// is built per-model. Provider-specific reasoning switches now live in Cap
	// (capabilities.reasoning), not here; ExtraBody remains for other tuning.
	ExtraBody map[string]any
	// HTTPClient overrides the default client (nil = default). The local
	// provider uses this for the no-proxy transport.
	HTTPClient *http.Client
}

type openAICompatClient struct {
	http      *openAIClient
	reasoning ReasoningSemantics
	cap       Capability
	extra     map[string]any
}

// NewOpenAICompatClient builds an adapter for one OpenAI-compatible provider.
func NewOpenAICompatClient(cfg OpenAICompatConfig, logger *slog.Logger) LLMClient {
	sem := cfg.Reasoning
	if sem == "" {
		sem = ReasoningSubset // the spec default; additive is the xAI exception
	}
	return &openAICompatClient{
		http:      newOpenAIClient(cfg.Name, cfg.BaseURL, cfg.APIKey, cfg.Profile, cfg.Headers, cfg.HTTPClient, logger),
		reasoning: sem,
		cap:       cfg.Cap,
		extra:     cfg.ExtraBody,
	}
}

func (c *openAICompatClient) Complete(ctx context.Context, req LLMRequest) (*LLMResponse, error) {
	resp, err := c.http.complete(ctx, openAIRequest{
		model:           req.Model,
		messages:        toOpenAIMessages(req.Messages),
		maxTokens:       req.MaxTokens,
		temperature:     req.Temperature,
		stream:          false,
		reasoningEffort: req.ReasoningEffort, // neutral; cap maps it to the wire
		responseFormat:  jsonResponseFormat(req.JSONOnly),
		cap:             c.cap,
		extra:           c.extra,
	})
	if err != nil {
		return nil, err
	}
	usage := Usage{
		PromptTokens:     resp.Usage.PromptTokens,
		CachedTokens:     resp.Usage.cacheRead(),
		CompletionTokens: resp.Usage.CompletionTokens,
	}
	switch c.reasoning {
	case ReasoningAdditive:
		usage.ReasoningTokens = resp.Usage.CompletionTokensDetails.ReasoningTokens
	case ReasoningAdditiveTotal:
		// Gemini: thinking is only in total_tokens (not completion, no reasoning field).
		// Guard >0 so a spec provider whose total == prompt+completion derives 0, not a
		// negative (never inflate the ceiling either).
		if extra := resp.Usage.TotalTokens - resp.Usage.PromptTokens - resp.Usage.CompletionTokens; extra > 0 {
			usage.ReasoningTokens = extra
		}
	}
	model := resp.Model
	if model == "" {
		model = req.Model
	}
	return &LLMResponse{
		Text:              resp.Text(),
		Usage:             usage,
		Model:             model,
		FinishReason:      resp.FinishReason(),
		ProviderRequestID: resp.ID,
	}, nil
}
