package llm

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"

	"textmachine/backend/internal/obs"
)

// provider_anthropic.go is the native Messages API adapter — the first NEW
// code on top of the vojo port (Р1: OpenAI-compat слоя недостаточно, нужен
// explicit cache_control). It is a thin raw-HTTP adapter like every other
// provider here — NOT the official SDK — so retries, timeouts, proxies and
// raw usage stay under the one transport discipline the ledger depends on
// (обоснование в 03-implementation-notes §3.5).
//
// Wire facts (проверено 2026-07-04, платформенная документация):
//   - POST {base}/v1/messages, headers x-api-key + anthropic-version.
//   - system is a list of text blocks; cache_control {type:"ephemeral",
//     ttl:"5m"|"1h"} on the LAST block of the stable prefix; max 4 breakpoints.
//   - usage.input_tokens is ONLY the uncached remainder;
//     cache_read_input_tokens and cache_creation_input_tokens are separate.
//     Neutral Usage.PromptTokens is the SUM of the three (см. llm.go invariant).
//   - cache write bills ×1.25 (5m) / ×2 (1h); read ×0.1. Min cacheable prefix
//     is model-dependent (короче — кэш молча не создаётся): models.yaml keeps
//     the per-model value for the assembler.
//   - thinking bills as output tokens (inside output_tokens, no separate
//     field) → ReasoningTokens stays 0 here.
//   - stop_reason: end_turn | max_tokens | stop_sequence | refusal (+ tool_use,
//     pause_turn — не наши кейсы в Фазе 0).

const anthropicVersion = "2023-06-01"

// AnthropicConfig configures the adapter.
type AnthropicConfig struct {
	BaseURL string // default https://api.anthropic.com
	APIKey  string
	Profile RetryProfile
	// CacheTTL is the cache_control ttl applied at CacheBoundary blocks:
	// "" (omit — API default 5m), "5m" or "1h". Per-stage tuning comes from
	// the pipeline config (TTL 5m может истекать между вызовами стадии —
	// латентности чанков 63–278 с, см. implementation-notes §3.9).
	CacheTTL string
}

type anthropicClient struct {
	base     string
	key      string
	http     *http.Client
	profile  RetryProfile
	cacheTTL string
	log      *slog.Logger
}

// NewAnthropicClient builds the native Messages API adapter.
//
// Deprecated: Anthropic убран из модельного стека по решению владельца (04.07):
// дорого, в RU-контуре экономически недоступен (Р5), в eval не валидирован. Ни
// один provider с kind:"anthropic" в models.yaml сейчас не объявлен, поэтому
// BuildClient его не строит. Адаптер намеренно ОСТАВЛЕН как рабочий референс
// нативного (не-OpenAI-compat) адаптера — паттерн для нативного Gemini-адаптера
// Фазы 2 (§3.9): явные cache_control-брейкпоинты, инвариант суммы usage,
// маппинг stop_reason, единый транспорт retryLoop. Не удалять без замены этого
// референса; при возврате Anthropic — снять депрекейт и вернуть провайдера в конфиг.
func NewAnthropicClient(cfg AnthropicConfig, logger *slog.Logger) LLMClient {
	base := cfg.BaseURL
	if base == "" {
		base = "https://api.anthropic.com"
	}
	return &anthropicClient{
		base:     base,
		key:      cfg.APIKey,
		http:     &http.Client{},
		profile:  cfg.Profile.withDefaults(),
		cacheTTL: cfg.CacheTTL,
		log:      logger,
	}
}

// --- wire types -------------------------------------------------------------

type anthropicCacheControl struct {
	Type string `json:"type"`
	TTL  string `json:"ttl,omitempty"`
}

type anthropicTextBlock struct {
	Type         string                 `json:"type"`
	Text         string                 `json:"text"`
	CacheControl *anthropicCacheControl `json:"cache_control,omitempty"`
}

type anthropicMessage struct {
	Role    string               `json:"role"`
	Content []anthropicTextBlock `json:"content"`
}

// anthropicRequest deliberately carries NO sampling parameters: актуальные
// Claude-модели (Sonnet 5, Opus 4.7+) отклоняют temperature/top_p с 400 —
// управление стилем идёт промптом. Конфигная temperature стадии остаётся в
// request-hash (детерминизм ключа), но на этот wire не попадает.
type anthropicRequest struct {
	Model     string               `json:"model"`
	MaxTokens int                  `json:"max_tokens"`
	System    []anthropicTextBlock `json:"system,omitempty"`
	Messages  []anthropicMessage   `json:"messages"`
}

type anthropicResponse struct {
	ID      string `json:"id"`
	Model   string `json:"model"`
	Content []struct {
		Type string `json:"type"`
		Text string `json:"text"`
	} `json:"content"`
	StopReason string `json:"stop_reason"`
	Usage      struct {
		InputTokens              int `json:"input_tokens"`
		OutputTokens             int `json:"output_tokens"`
		CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
		CacheReadInputTokens     int `json:"cache_read_input_tokens"`
	} `json:"usage"`
}

func (c *anthropicClient) Complete(ctx context.Context, req LLMRequest) (*LLMResponse, error) {
	wire, err := c.buildRequest(req)
	if err != nil {
		return nil, err
	}
	payload, err := json.Marshal(wire)
	if err != nil {
		return nil, err
	}
	return retryLoop(ctx, c.profile, "anthropic", c.log, func() (*LLMResponse, bool, error) {
		return c.attempt(ctx, payload)
	})
}

// buildRequest maps the neutral request onto the Messages wire. Leading
// system-role messages become system blocks; CacheBoundary flags become
// cache_control on that block. Thinking is not mapped yet (Фаза 0: Anthropic —
// только SFW-роли редактора/эскалации): ""/"off" проходят (без thinking-
// конфигурации), а low/medium/high падают громко — молча оплаченная стадия БЕЗ
// заказанной глубины рассуждений хуже ошибки конфига.
func (c *anthropicClient) buildRequest(req LLMRequest) (*anthropicRequest, error) {
	switch req.ReasoningEffort {
	case "", "off":
	default:
		return nil, fmt.Errorf("anthropic: reasoning %q is not mapped by this adapter yet (thinking — Фаза 2)", req.ReasoningEffort)
	}
	wire := &anthropicRequest{
		Model:     req.Model,
		MaxTokens: req.MaxTokens,
	}
	boundaries := 0
	block := func(m Message) anthropicTextBlock {
		b := anthropicTextBlock{Type: "text", Text: m.Content}
		if m.CacheBoundary {
			boundaries++
			b.CacheControl = &anthropicCacheControl{Type: "ephemeral", TTL: c.cacheTTL}
		}
		return b
	}
	inSystemPrefix := true
	for _, m := range req.Messages {
		if inSystemPrefix && m.Role == "system" {
			wire.System = append(wire.System, block(m))
			continue
		}
		inSystemPrefix = false
		if m.Role == "system" {
			// Mid-conversation system turns are model-gated on this API; our
			// assembler never produces them — fail loud rather than mislabel.
			return nil, fmt.Errorf("anthropic: system message after non-system turn is not supported")
		}
		wire.Messages = append(wire.Messages, anthropicMessage{
			Role:    m.Role,
			Content: []anthropicTextBlock{block(m)},
		})
	}
	if boundaries > 4 {
		return nil, fmt.Errorf("anthropic: %d cache boundaries, API allows at most 4", boundaries)
	}
	if req.JSONOnly {
		// Messages API has no response_format json_object; judge roles on
		// Anthropic would need structured outputs (Фаза 2). Fail loud so a
		// misconfigured pipeline doesn't silently lose the JSON constraint.
		return nil, fmt.Errorf("anthropic: JSONOnly is not supported by this adapter yet")
	}
	return wire, nil
}

func (c *anthropicClient) attempt(ctx context.Context, payload []byte) (*LLMResponse, bool, error) {
	attemptCtx, cancel := context.WithTimeout(ctx, c.profile.AttemptTimeout)
	defer cancel()

	req, err := http.NewRequestWithContext(attemptCtx, http.MethodPost, c.base+"/v1/messages", bytes.NewReader(payload))
	if err != nil {
		return nil, false, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("x-api-key", c.key)
	req.Header.Set("anthropic-version", anthropicVersion)

	resp, err := c.http.Do(req)
	if err != nil {
		return nil, ctx.Err() == nil, err
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes+1))
	truncated := len(data) > maxResponseBytes
	if truncated {
		data = data[:maxResponseBytes]
	}

	obs.LogLLMExchange(ctx, c.log, "anthropic", payload, resp.StatusCode, data)

	// 529 (overloaded) is Anthropic's extra retryable status on top of 429/5xx.
	if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {
		return nil, true, &HTTPStatusError{Provider: "anthropic", Status: resp.StatusCode, Body: snippet(data), RetryAfter: parseRetryAfter(resp.Header.Get("Retry-After"))}
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, false, &HTTPStatusError{Provider: "anthropic", Status: resp.StatusCode, Body: snippet(data)}
	}

	var out anthropicResponse
	if err := json.Unmarshal(data, &out); err != nil {
		// 2xx уже оплачен провайдером — типизируем (см. httpllm.go); усечение
		// терминально, обрыв/мусор — retryable.
		if truncated {
			return nil, false, &BilledDecodeError{Provider: "anthropic", Err: fmt.Errorf("response exceeded %d bytes (truncated, not retried): %w", maxResponseBytes, err)}
		}
		return nil, true, &BilledDecodeError{Provider: "anthropic", Err: err}
	}

	var text string
	for _, b := range out.Content {
		if b.Type == "text" {
			text += b.Text
		}
	}
	// A 2xx with empty content (refusal before output, filtered) is still a
	// billed call — return success and let gates handle the empty text, same
	// discipline as the OpenAI-compat transport.
	return &LLMResponse{
		Text: text,
		Usage: Usage{
			// Neutral invariant: PromptTokens = total input. Anthropic's
			// input_tokens is only the uncached remainder — sum the parts.
			PromptTokens:        out.Usage.InputTokens + out.Usage.CacheReadInputTokens + out.Usage.CacheCreationInputTokens,
			CachedTokens:        out.Usage.CacheReadInputTokens,
			CacheCreationTokens: out.Usage.CacheCreationInputTokens,
			CompletionTokens:    out.Usage.OutputTokens,
			// ReasoningTokens 0: thinking bills inside output_tokens here.
		},
		Model:             out.Model,
		FinishReason:      mapAnthropicStopReason(out.StopReason),
		ProviderRequestID: out.ID,
	}, false, nil
}

func mapAnthropicStopReason(s string) string {
	switch s {
	case "end_turn", "stop_sequence":
		return FinishStop
	case "max_tokens":
		return FinishLength
	case "refusal":
		return FinishRefusal
	default:
		return ""
	}
}
