// Package anthropic implements provider.Adapter for the Anthropic Messages API.
// The Anthropic API differs from OpenAI in:
//   - Endpoint: /v1/messages (not /chat/completions)
//   - Auth headers: x-api-key + anthropic-version (not Authorization: Bearer)
//   - Request: system prompt as top-level field, max_tokens required
//   - Response: content[].text instead of choices[].message.content
//   - Streaming: event-typed SSE (content_block_delta, message_stop) re-emitted as OpenAI-compat
package anthropic

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/veylant/ia-gateway/internal/apierror"
	"github.com/veylant/ia-gateway/internal/provider"
)

const (
	defaultBaseURL   = "https://api.anthropic.com/v1"
	defaultVersion   = "2023-06-01"
	defaultMaxTokens = 4096
)

// Config holds Anthropic adapter configuration.
type Config struct {
	APIKey         string
	BaseURL        string // default: https://api.anthropic.com/v1
	Version        string // Anthropic-Version header, e.g. "2023-06-01"
	TimeoutSeconds int
	MaxConns       int
}

// Adapter implements provider.Adapter for the Anthropic Messages API.
type Adapter struct {
	cfg             Config
	testBaseURL     string // non-empty in tests: overrides BaseURL
	regularClient   *http.Client
	streamingClient *http.Client
}

// anthropicRequest is the wire format for the Anthropic Messages API.
type anthropicRequest struct {
	Model     string             `json:"model"`
	MaxTokens int                `json:"max_tokens"`
	System    string             `json:"system,omitempty"`
	Messages  []provider.Message `json:"messages"`
	Stream    bool               `json:"stream,omitempty"`
}

// anthropicResponse is the non-streaming response from the Anthropic Messages API.
type anthropicResponse struct {
	ID         string             `json:"id"`
	Type       string             `json:"type"`
	Model      string             `json:"model"`
	Content    []anthropicContent `json:"content"`
	StopReason string             `json:"stop_reason"`
	Usage      anthropicUsage     `json:"usage"`
}

type anthropicContent struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type anthropicUsage struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
}

// New creates an Anthropic adapter.
func New(cfg Config) *Adapter {
	return NewWithBaseURL("", cfg)
}

// NewWithBaseURL creates an adapter with a custom base URL — used in tests to
// point the adapter at a mock server instead of the real Anthropic endpoint.
func NewWithBaseURL(baseURL string, cfg Config) *Adapter {
	if cfg.BaseURL == "" {
		cfg.BaseURL = defaultBaseURL
	}
	if cfg.Version == "" {
		cfg.Version = defaultVersion
	}

	dialer := &net.Dialer{
		Timeout:   30 * time.Second,
		KeepAlive: 30 * time.Second,
	}
	transport := &http.Transport{
		DialContext:           dialer.DialContext,
		TLSHandshakeTimeout:   10 * time.Second,
		ResponseHeaderTimeout: 30 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		MaxIdleConns:          cfg.MaxConns,
		MaxIdleConnsPerHost:   cfg.MaxConns,
		IdleConnTimeout:       90 * time.Second,
	}

	timeout := time.Duration(cfg.TimeoutSeconds) * time.Second
	if cfg.TimeoutSeconds == 0 {
		timeout = 30 * time.Second
	}

	return &Adapter{
		cfg:         cfg,
		testBaseURL: baseURL,
		regularClient: &http.Client{
			Transport: transport,
			Timeout:   timeout,
		},
		streamingClient: &http.Client{
			Transport: transport,
		},
	}
}

// Validate checks that req is well-formed for the Anthropic adapter.
func (a *Adapter) Validate(req *provider.ChatRequest) error {
	if req.Model == "" {
		return apierror.NewBadRequestError("field 'model' is required")
	}
	if len(req.Messages) == 0 {
		return apierror.NewBadRequestError("field 'messages' must not be empty")
	}
	return nil
}

// Send performs a non-streaming chat completion request to the Anthropic Messages API.
func (a *Adapter) Send(ctx context.Context, req *provider.ChatRequest) (*provider.ChatResponse, error) {
	anthropicReq := toAnthropicRequest(req)

	body, err := json.Marshal(anthropicReq)
	if err != nil {
		return nil, apierror.NewInternalError(fmt.Sprintf("marshaling request: %v", err))
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, a.messagesURL(), bytes.NewReader(body))
	if err != nil {
		return nil, apierror.NewInternalError(fmt.Sprintf("building request: %v", err))
	}
	a.setHeaders(httpReq)

	resp, err := a.regularClient.Do(httpReq)
	if err != nil {
		if ctx.Err() != nil {
			return nil, apierror.NewTimeoutError("Anthropic request timed out")
		}
		return nil, apierror.NewUpstreamError(fmt.Sprintf("calling Anthropic: %v", err))
	}
	defer resp.Body.Close() //nolint:errcheck

	if resp.StatusCode != http.StatusOK {
		return nil, mapUpstreamStatus(resp.StatusCode)
	}

	var anthropicResp anthropicResponse
	if err := json.NewDecoder(resp.Body).Decode(&anthropicResp); err != nil {
		return nil, apierror.NewUpstreamError(fmt.Sprintf("decoding response: %v", err))
	}
	return fromAnthropicResponse(&anthropicResp), nil
}

// Stream performs a streaming chat completion request and pipes normalized SSE chunks to w.
// Anthropic's event-typed SSE format is converted to OpenAI-compatible chunks on the fly.
func (a *Adapter) Stream(ctx context.Context, req *provider.ChatRequest, w http.ResponseWriter) error {
	anthropicReq := toAnthropicRequest(req)
	anthropicReq.Stream = true

	body, err := json.Marshal(anthropicReq)
	if err != nil {
		return apierror.NewInternalError(fmt.Sprintf("marshaling stream request: %v", err))
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, a.messagesURL(), bytes.NewReader(body))
	if err != nil {
		return apierror.NewInternalError(fmt.Sprintf("building stream request: %v", err))
	}
	a.setHeaders(httpReq)

	resp, err := a.streamingClient.Do(httpReq)
	if err != nil {
		if ctx.Err() != nil {
			return apierror.NewTimeoutError("Anthropic stream timed out")
		}
		return apierror.NewUpstreamError(fmt.Sprintf("opening stream to Anthropic: %v", err))
	}
	defer resp.Body.Close() //nolint:errcheck

	if resp.StatusCode != http.StatusOK {
		return mapUpstreamStatus(resp.StatusCode)
	}

	return PipeAnthropicSSE(bufio.NewScanner(resp.Body), w)
}

// HealthCheck verifies the Anthropic API is reachable by listing models.
func (a *Adapter) HealthCheck(ctx context.Context) error {
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL()+"/models", nil)
	if err != nil {
		return fmt.Errorf("building Anthropic health check request: %w", err)
	}
	a.setHeaders(req)

	resp, err := a.regularClient.Do(req)
	if err != nil {
		return fmt.Errorf("Anthropic health check failed: %w", err)
	}
	defer resp.Body.Close() //nolint:errcheck

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("Anthropic health check returned HTTP %d", resp.StatusCode)
	}
	return nil
}

// messagesURL returns the Anthropic Messages API endpoint.
func (a *Adapter) messagesURL() string {
	return a.baseURL() + "/messages"
}

// baseURL returns testBaseURL when set (tests), otherwise cfg.BaseURL.
func (a *Adapter) baseURL() string {
	if a.testBaseURL != "" {
		return a.testBaseURL
	}
	return a.cfg.BaseURL
}

// setHeaders attaches required Anthropic authentication and version headers.
func (a *Adapter) setHeaders(req *http.Request) {
	req.Header.Set("x-api-key", a.cfg.APIKey)
	req.Header.Set("anthropic-version", a.cfg.Version)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
}

// toAnthropicRequest converts an OpenAI-compat ChatRequest to Anthropic Messages API format.
// System-role messages are extracted to the top-level "system" field.
// max_tokens defaults to 4096 if not set in the original request.
func toAnthropicRequest(req *provider.ChatRequest) anthropicRequest {
	var systemParts []string
	var messages []provider.Message

	for _, m := range req.Messages {
		if m.Role == "system" {
			systemParts = append(systemParts, m.Content)
		} else {
			messages = append(messages, m)
		}
	}

	maxTokens := defaultMaxTokens
	if req.MaxTokens != nil {
		maxTokens = *req.MaxTokens
	}

	return anthropicRequest{
		Model:     req.Model,
		MaxTokens: maxTokens,
		System:    strings.Join(systemParts, "\n"),
		Messages:  messages,
	}
}

// fromAnthropicResponse converts an Anthropic non-streaming response to the
// OpenAI-compatible ChatResponse format used internally.
func fromAnthropicResponse(r *anthropicResponse) *provider.ChatResponse {
	var content string
	if len(r.Content) > 0 {
		content = r.Content[0].Text
	}
	return &provider.ChatResponse{
		ID:     r.ID,
		Object: "chat.completion",
		Model:  r.Model,
		Choices: []provider.Choice{{
			Index:        0,
			Message:      provider.Message{Role: "assistant", Content: content},
			FinishReason: r.StopReason,
		}},
		Usage: provider.Usage{
			PromptTokens:     r.Usage.InputTokens,
			CompletionTokens: r.Usage.OutputTokens,
			TotalTokens:      r.Usage.InputTokens + r.Usage.OutputTokens,
		},
	}
}

func mapUpstreamStatus(status int) *apierror.APIError {
	switch status {
	case http.StatusUnauthorized:
		return apierror.NewAuthError("Anthropic rejected the API key")
	case http.StatusTooManyRequests:
		return apierror.NewRateLimitError("Anthropic rate limit reached")
	case http.StatusBadRequest:
		return apierror.NewBadRequestError("Anthropic rejected the request")
	case http.StatusGatewayTimeout, http.StatusRequestTimeout:
		return apierror.NewTimeoutError("Anthropic request timed out")
	default:
		return apierror.NewUpstreamError(fmt.Sprintf("Anthropic returned HTTP %d", status))
	}
}
