package embedding

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math"
	"net/http"
	"strings"
	"time"

	"github.com/mem9-ai/drive9/pkg/metrics"
)

// Dimensions is the fixed app-managed vector width used by Drive9 schemas.
const Dimensions = 1024

// OpenAIClientConfig configures an OpenAI-compatible embeddings endpoint.
type OpenAIClientConfig struct {
	BaseURL    string
	APIKey     string
	Model      string
	Dimensions int
	Timeout    time.Duration
	Client     *http.Client
}

// OpenAIClient calls an OpenAI-compatible /v1/embeddings API.
type OpenAIClient struct {
	endpoint   string
	apiKey     string
	model      string
	dimensions int
	client     *http.Client
}

// ProviderFailureCategory is a bounded classification for provider failures.
// Error() may retain provider details for immediate diagnosis at the call
// boundary; callers must use ProviderFailure before logging, persisting, or
// returning an error outside the process.
type ProviderFailureCategory string

const (
	ProviderFailureAuthRejected    ProviderFailureCategory = "auth_rejected"
	ProviderFailureNotFound        ProviderFailureCategory = "not_found"
	ProviderFailureRateLimited     ProviderFailureCategory = "rate_limited"
	ProviderFailureRequestRejected ProviderFailureCategory = "request_rejected"
	ProviderFailureUnavailable     ProviderFailureCategory = "unavailable"
	ProviderFailureInvalidResponse ProviderFailureCategory = "invalid_response"
)

type providerError struct {
	category   ProviderFailureCategory
	statusCode int
	safeReason string
	message    string
	cause      error
}

func (e *providerError) Error() string { return e.message }

func (e *providerError) Unwrap() error { return e.cause }

// ProviderFailure returns bounded provider metadata suitable for metrics and
// client responses. It never returns the provider's response body or message.
func ProviderFailure(err error) (ProviderFailureCategory, int, string, bool) {
	var providerErr *providerError
	if !errors.As(err, &providerErr) {
		return "", 0, "", false
	}
	return providerErr.category, providerErr.statusCode, providerErr.safeReason, true
}

func newProviderResponseError(category ProviderFailureCategory, status int, safeReason, message string, cause error) error {
	return &providerError{category: category, statusCode: status, safeReason: safeReason, message: message, cause: cause}
}

func classifyProviderStatus(status int) (ProviderFailureCategory, string) {
	switch status {
	case http.StatusUnauthorized, http.StatusForbidden:
		return ProviderFailureAuthRejected, "provider rejected the credentials"
	case http.StatusNotFound:
		return ProviderFailureNotFound, "provider endpoint or model was not found"
	case http.StatusTooManyRequests:
		return ProviderFailureRateLimited, "provider rate limit was exceeded"
	default:
		if status >= http.StatusInternalServerError {
			return ProviderFailureUnavailable, "provider is unavailable"
		}
		return ProviderFailureRequestRejected, "provider rejected the request"
	}
}

// NewOpenAIClient builds a Client backed by an OpenAI-compatible embeddings API.
func NewOpenAIClient(cfg OpenAIClientConfig) (*OpenAIClient, error) {
	base := strings.TrimRight(cfg.BaseURL, "/")
	if base == "" {
		return nil, fmt.Errorf("embedding base url is required")
	}
	if cfg.APIKey == "" {
		return nil, fmt.Errorf("embedding api key is required")
	}
	if cfg.Model == "" {
		return nil, fmt.Errorf("embedding model is required")
	}
	if cfg.Dimensions == 0 {
		cfg.Dimensions = Dimensions
	}
	if cfg.Dimensions != Dimensions {
		return nil, fmt.Errorf("embedding dimensions must be %d", Dimensions)
	}
	endpoint := base + "/v1/embeddings"
	if strings.HasSuffix(base, "/v1") {
		endpoint = base + "/embeddings"
	}
	client := cfg.Client
	if client == nil {
		timeout := cfg.Timeout
		if timeout <= 0 {
			timeout = 20 * time.Second
		}
		client = &http.Client{Timeout: timeout}
	}
	return &OpenAIClient{
		endpoint:   endpoint,
		apiKey:     cfg.APIKey,
		model:      cfg.Model,
		dimensions: cfg.Dimensions,
		client:     client,
	}, nil
}

// EmbedText implements Client. It records drive9_service_operations_total and
// drive9_service_operation_duration_seconds under component="embedding" so the
// outbound LLM embedding provider's latency and error rate are observable (these
// calls were previously silent — failures only surfaced indirectly as semantic
// task retries / extract errors).
func (c *OpenAIClient) EmbedText(ctx context.Context, text string) ([]float32, error) {
	start := time.Now()
	emb, err := c.embedText(ctx, text)
	metrics.RecordOperation("embedding", "embed_text", embeddingResult(err), time.Since(start))
	return emb, err
}

func embeddingResult(err error) string {
	switch {
	case err == nil:
		return "ok"
	case errors.Is(err, context.Canceled):
		return "canceled"
	case errors.Is(err, context.DeadlineExceeded):
		return "timeout"
	default:
		return "error"
	}
}

func (c *OpenAIClient) embedText(ctx context.Context, text string) ([]float32, error) {
	payload := map[string]any{
		"model": c.model,
		"input": text,
	}
	if c.dimensions > 0 {
		payload["dimensions"] = c.dimensions
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+c.apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()

	raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return nil, err
	}
	var parsed struct {
		Error *struct {
			Message string `json:"message"`
		} `json:"error"`
		Data []struct {
			Embedding []float32 `json:"embedding"`
		} `json:"data"`
	}
	if err := json.Unmarshal(raw, &parsed); err != nil {
		if resp.StatusCode >= 300 {
			category, safeReason := classifyProviderStatus(resp.StatusCode)
			message := fmt.Sprintf("embedding api status %d: %s", resp.StatusCode, truncateString(string(raw), 256))
			return nil, newProviderResponseError(category, resp.StatusCode, safeReason, message, err)
		}
		return nil, newProviderResponseError(ProviderFailureInvalidResponse, resp.StatusCode,
			"provider returned an invalid response", fmt.Sprintf("decode embedding response: %v", err), err)
	}
	if resp.StatusCode >= 300 {
		category, safeReason := classifyProviderStatus(resp.StatusCode)
		if parsed.Error != nil && parsed.Error.Message != "" {
			return nil, newProviderResponseError(category, resp.StatusCode, safeReason,
				fmt.Sprintf("embedding api status %d: %s", resp.StatusCode, parsed.Error.Message), nil)
		}
		return nil, newProviderResponseError(category, resp.StatusCode, safeReason,
			fmt.Sprintf("embedding api status %d", resp.StatusCode), nil)
	}
	if len(parsed.Data) == 0 || len(parsed.Data[0].Embedding) == 0 {
		return nil, newProviderResponseError(ProviderFailureInvalidResponse, resp.StatusCode,
			"provider returned no embedding", "embedding api returned no embedding", nil)
	}
	vec := parsed.Data[0].Embedding
	if len(vec) != Dimensions {
		message := fmt.Sprintf("embedding api returned %d dimensions, want %d", len(vec), Dimensions)
		return nil, newProviderResponseError(ProviderFailureInvalidResponse, resp.StatusCode, message, message, nil)
	}
	for _, value := range vec {
		if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
			return nil, newProviderResponseError(ProviderFailureInvalidResponse, resp.StatusCode,
				"provider returned a non-finite embedding value", "embedding api returned a non-finite value", nil)
		}
	}
	return append([]float32(nil), vec...), nil
}

func truncateString(s string, max int) string {
	if max <= 0 || len(s) <= max {
		return s
	}
	return s[:max]
}
