package clients

import (
	"container/list"
	"errors"
	"fmt"
	"math"
	"net/url"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

const parameterCorrectionCacheCapacity = 512

var safeUpstreamCorrectionParameters = stringSet(
	"reasoning_effort", "reasoning.effort", "enable_thinking", "thinking_budget",
	"temperature", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "n",
	"max_tokens", "max_completion_tokens", "max_output_tokens",
	"logprobs", "top_logprobs", "service_tier", "verbosity",
	"response_format",
)

var (
	anchoredParameterPattern   = regexp.MustCompile(`(?i)^(?:unknown|unsupported|invalid) parameter\s*:?\s*[` + "`" + `'\"]?([a-z][a-z0-9_.]*)`)
	restrictedParameterPattern = regexp.MustCompile(`(?i)^the value of the ([a-z][a-z0-9_.]*) parameter is restricted to\s+([^\s,.;]+)`)
	fixedValuePattern          = regexp.MustCompile(`(?i)(?:restricted to|must be(?: set)?(?: to)?|only supports?(?: the)? value(?: of)?)\s*[` + "`" + `'\"]?([^\s,.;` + "`" + `'\"]+)`)
	betweenPattern             = regexp.MustCompile(`(?i)\bbetween\s+(-?[0-9]+(?:\.[0-9]+)?)\s+and\s+(-?[0-9]+(?:\.[0-9]+)?)\b`)
	maximumPattern             = regexp.MustCompile(`(?i)(?:less than or equal to|at most|maximum(?: value)?(?: is| of)?|<=)\s+(-?[0-9]+(?:\.[0-9]+)?)`)
	minimumPattern             = regexp.MustCompile(`(?i)(?:greater than or equal to|at least|minimum(?: value)?(?: is| of)?|>=)\s+(-?[0-9]+(?:\.[0-9]+)?)`)
)

type parameterCorrectionAction string

const (
	parameterCorrectionRemove parameterCorrectionAction = "remove"
	parameterCorrectionSet    parameterCorrectionAction = "set"
	parameterCorrectionClamp  parameterCorrectionAction = "clamp"
)

type parameterCorrectionRule struct {
	Param  string
	Action parameterCorrectionAction
	Value  any
	Min    *float64
	Max    *float64
}

type parameterCorrectionScope struct {
	Provider string
	BaseURL  string
	Protocol string
	Kind     string
	Model    string
}

func newParameterCorrectionScope(request Request, endpointKind string) parameterCorrectionScope {
	return parameterCorrectionScope{
		Provider: normalizedReasoningString(request.Candidate.Provider),
		BaseURL:  normalizedCorrectionBaseURL(request.Candidate.BaseURL),
		Protocol: strings.TrimSpace(request.UpstreamProtocol),
		Kind:     strings.TrimSpace(endpointKind),
		Model:    normalizedReasoningString(upstreamModelName(request.Candidate)),
	}
}

func normalizedCorrectionBaseURL(raw string) string {
	trimmed := strings.TrimRight(strings.TrimSpace(raw), "/")
	parsed, err := url.Parse(trimmed)
	if err != nil || parsed.Host == "" {
		return strings.ToLower(trimmed)
	}
	parsed.Scheme = strings.ToLower(parsed.Scheme)
	parsed.Host = strings.ToLower(parsed.Host)
	parsed.RawQuery = ""
	parsed.Fragment = ""
	return strings.TrimRight(parsed.String(), "/")
}

func (scope parameterCorrectionScope) prefix() string {
	return strings.Join([]string{scope.Provider, scope.BaseURL, scope.Protocol, scope.Kind, scope.Model}, "\x1f") + "\x1f"
}

func (scope parameterCorrectionScope) key(param string) string {
	return scope.prefix() + param
}

type parameterCorrectionCacheEntry struct {
	key  string
	rule parameterCorrectionRule
}

type ParameterCorrectionCache struct {
	mu       sync.Mutex
	capacity int
	entries  map[string]*list.Element
	lru      *list.List
}

func NewParameterCorrectionCache() *ParameterCorrectionCache {
	return &ParameterCorrectionCache{
		capacity: parameterCorrectionCacheCapacity,
		entries:  make(map[string]*list.Element),
		lru:      list.New(),
	}
}

func (cache *ParameterCorrectionCache) apply(scope parameterCorrectionScope, body map[string]any, protected ...map[string]struct{}) []string {
	if cache == nil {
		return nil
	}
	cache.mu.Lock()
	defer cache.mu.Unlock()
	prefix := scope.prefix()
	elements := make([]*list.Element, 0)
	for key, element := range cache.entries {
		if strings.HasPrefix(key, prefix) {
			elements = append(elements, element)
		}
	}
	sort.Slice(elements, func(i, j int) bool {
		return elements[i].Value.(parameterCorrectionCacheEntry).rule.Param < elements[j].Value.(parameterCorrectionCacheEntry).rule.Param
	})
	applied := make([]string, 0, len(elements))
	for _, element := range elements {
		rule := element.Value.(parameterCorrectionCacheEntry).rule
		if correctionParamProtected(rule.Param, protected...) {
			continue
		}
		if applyParameterCorrectionRule(body, rule) {
			applied = append(applied, rule.Param)
			cache.lru.MoveToFront(element)
		}
	}
	return applied
}

func (cache *ParameterCorrectionCache) commit(scope parameterCorrectionScope, rules []parameterCorrectionRule) {
	if cache == nil || len(rules) == 0 {
		return
	}
	cache.mu.Lock()
	defer cache.mu.Unlock()
	for _, rule := range rules {
		key := scope.key(rule.Param)
		if existing := cache.entries[key]; existing != nil {
			existing.Value = parameterCorrectionCacheEntry{key: key, rule: rule}
			cache.lru.MoveToFront(existing)
			continue
		}
		element := cache.lru.PushFront(parameterCorrectionCacheEntry{key: key, rule: rule})
		cache.entries[key] = element
		for cache.lru.Len() > cache.capacity {
			oldest := cache.lru.Back()
			if oldest == nil {
				break
			}
			delete(cache.entries, oldest.Value.(parameterCorrectionCacheEntry).key)
			cache.lru.Remove(oldest)
		}
	}
}

func (cache *ParameterCorrectionCache) invalidate(scope parameterCorrectionScope, param string) {
	if cache == nil {
		return
	}
	cache.mu.Lock()
	defer cache.mu.Unlock()
	key := scope.key(param)
	if element := cache.entries[key]; element != nil {
		delete(cache.entries, key)
		cache.lru.Remove(element)
	}
}

func (cache *ParameterCorrectionCache) size() int {
	if cache == nil {
		return 0
	}
	cache.mu.Lock()
	defer cache.mu.Unlock()
	return cache.lru.Len()
}

func deriveParameterCorrection(err error, body map[string]any, candidate store.RuntimeModelCandidate, protected map[string]struct{}) (parameterCorrectionRule, bool) {
	var clientErr *ClientError
	if !errors.As(err, &clientErr) || (clientErr.StatusCode != 400 && clientErr.StatusCode != 422) {
		return parameterCorrectionRule{}, false
	}
	param, code, message := structuredParameterError(clientErr)
	if param == "" {
		param = anchoredParameterFromMessage(message)
	}
	param = normalizeCorrectionParam(param)
	if !isSafeCorrectionParam(param) {
		return parameterCorrectionRule{}, false
	}
	if correctionParamProtected(param, protected) {
		return parameterCorrectionRule{}, false
	}
	lowerMessage := strings.ToLower(message)
	lowerCode := strings.ToLower(code)
	if conflictRule, ok := deriveConflictCorrection(param, lowerMessage); ok {
		if correctionParamProtected(conflictRule.Param, protected) {
			return parameterCorrectionRule{}, false
		}
		return conflictRule, true
	}
	if strings.Contains(lowerMessage, "unknown parameter") ||
		strings.Contains(lowerMessage, "unsupported parameter") ||
		strings.Contains(lowerMessage, "not supported") ||
		strings.Contains(lowerMessage, "does not support") ||
		strings.Contains(lowerCode, "unknown_parameter") ||
		strings.Contains(lowerCode, "unsupported_parameter") {
		return parameterCorrectionRule{Param: param, Action: parameterCorrectionRemove}, true
	}
	if param == "reasoning_effort" || param == "reasoning.effort" {
		if effort := nearestErrorSupportedReasoningEffort(message, normalizedReasoningString(valueAtCorrectionPath(body, param)), candidate); effort != "" {
			return parameterCorrectionRule{Param: param, Action: parameterCorrectionSet, Value: effort}, true
		}
	}
	if match := fixedValuePattern.FindStringSubmatch(message); len(match) == 2 {
		if value, ok := parseCorrectionScalar(match[1]); ok {
			return parameterCorrectionRule{Param: param, Action: parameterCorrectionSet, Value: value}, true
		}
	}
	if min, max, ok := explicitNumericBounds(message); ok {
		if isOutputLimitCorrectionParam(param) {
			min = nil
		}
		return parameterCorrectionRule{Param: param, Action: parameterCorrectionClamp, Min: min, Max: max}, true
	}
	return parameterCorrectionRule{}, false
}

func correctionParamProtected(param string, protected ...map[string]struct{}) bool {
	for _, values := range protected {
		if _, ok := values[normalizeCorrectionParam(param)]; ok {
			return true
		}
	}
	return false
}

func callerProtectedCorrectionParameters(request Request, endpointKind string) map[string]struct{} {
	if request.OriginalBody == nil {
		return nil
	}
	protected := make(map[string]struct{})
	protect := func(param string) {
		param = normalizeCorrectionParam(param)
		if isSafeCorrectionParam(param) {
			protected[param] = struct{}{}
		}
	}
	for key := range request.OriginalBody {
		protect(key)
	}
	if reasoning, ok := request.OriginalBody["reasoning"].(map[string]any); ok {
		if _, explicit := reasoning["effort"]; explicit {
			if endpointKind == "chat.completions" {
				protect("reasoning_effort")
			} else {
				protect("reasoning.effort")
			}
		}
	}
	if request.Kind == "responses" && endpointKind == "chat.completions" {
		if _, explicit := request.OriginalBody["max_output_tokens"]; explicit {
			protect("max_completion_tokens")
		}
		if _, explicit := request.OriginalBody["top_logprobs"]; explicit {
			protect("top_logprobs")
			protect("logprobs")
		}
		if responseIncludeContains(request.OriginalBody["include"], "message.output_text.logprobs") {
			protect("logprobs")
		}
		if text, ok := request.OriginalBody["text"].(map[string]any); ok {
			if _, explicit := text["verbosity"]; explicit {
				protect("verbosity")
			}
		}
	}
	if endpointKind == "images.generations" || endpointKind == "images.edits" {
		// The Gateway canonicalizes image output independently of the provider's
		// URL/Base64 preference. Allow an explicitly supplied response_format to
		// be removed only after the selected upstream rejects it as unsupported.
		delete(protected, "response_format")
	}
	return protected
}

func responseIncludeContains(value any, target string) bool {
	switch values := value.(type) {
	case []any:
		for _, value := range values {
			if stringFromAny(value) == target {
				return true
			}
		}
	case []string:
		for _, value := range values {
			if value == target {
				return true
			}
		}
	}
	return false
}

func requestIDFromParameterError(err error) string {
	var clientErr *ClientError
	if errors.As(err, &clientErr) {
		return clientErr.RequestID
	}
	return ""
}

func structuredParameterError(clientErr *ClientError) (string, string, string) {
	param := strings.TrimSpace(clientErr.Param)
	code := strings.TrimSpace(clientErr.Code)
	message := strings.TrimSpace(clientErr.Message)
	if clientErr.Wire == nil {
		return param, code, message
	}
	errorObject, _ := clientErr.Wire.Body["error"].(map[string]any)
	if errorObject == nil {
		return param, code, message
	}
	if value := strings.TrimSpace(stringFromAny(errorObject["param"])); value != "" {
		param = value
	}
	if value := strings.TrimSpace(stringFromAny(errorObject["code"])); value != "" {
		code = value
	}
	if value := strings.TrimSpace(stringFromAny(errorObject["message"])); value != "" {
		message = value
	}
	return param, code, message
}

func anchoredParameterFromMessage(message string) string {
	trimmed := strings.TrimSpace(message)
	for _, pattern := range []*regexp.Regexp{anchoredParameterPattern, restrictedParameterPattern} {
		if match := pattern.FindStringSubmatch(trimmed); len(match) >= 2 {
			return match[1]
		}
	}
	return ""
}

func normalizeCorrectionParam(param string) string {
	return strings.ToLower(strings.Trim(strings.TrimSpace(param), "`'\"[]"))
}

func isSafeCorrectionParam(param string) bool {
	_, ok := safeUpstreamCorrectionParameters[param]
	return ok
}

func deriveConflictCorrection(param string, message string) (parameterCorrectionRule, bool) {
	if !strings.Contains(message, "cannot be used together") &&
		!strings.Contains(message, "mutually exclusive") &&
		!strings.Contains(message, "not allowed when") {
		return parameterCorrectionRule{}, false
	}
	if strings.Contains(message, "thinking_budget") && strings.Contains(message, "reasoning_effort") {
		return parameterCorrectionRule{Param: "reasoning_effort", Action: parameterCorrectionRemove}, true
	}
	return parameterCorrectionRule{Param: param, Action: parameterCorrectionRemove}, true
}

func nearestErrorSupportedReasoningEffort(message string, requested string, candidate store.RuntimeModelCandidate) string {
	allowed := make(map[string]struct{})
	lowerMessage := strings.ToLower(message)
	for _, effort := range chatReasoningEffortOrder {
		if regexp.MustCompile(`\b` + regexp.QuoteMeta(effort) + `\b`).MatchString(lowerMessage) {
			allowed[effort] = struct{}{}
		}
	}
	if len(allowed) == 0 {
		if resolved, state := resolveCandidateReasoningEffort(requested, candidate); state == reasoningCapabilitySupported {
			return resolved
		}
		return ""
	}
	requestedIndex := reasoningEffortIndex(requested)
	best := ""
	bestDistance := len(chatReasoningEffortOrder) + 1
	for index, effort := range chatReasoningEffortOrder {
		if _, ok := allowed[effort]; !ok {
			continue
		}
		distance := int(math.Abs(float64(index - requestedIndex)))
		if distance < bestDistance {
			best = effort
			bestDistance = distance
		}
	}
	return best
}

func parseCorrectionScalar(raw string) (any, bool) {
	trimmed := strings.Trim(strings.TrimSpace(raw), "`'\"")
	switch strings.ToLower(trimmed) {
	case "true":
		return true, true
	case "false":
		return false, true
	}
	if number, err := strconv.ParseFloat(trimmed, 64); err == nil {
		if math.Trunc(number) == number {
			return int(number), true
		}
		return number, true
	}
	if isOpenAIReasoningEffort(strings.ToLower(trimmed)) {
		return strings.ToLower(trimmed), true
	}
	return nil, false
}

func explicitNumericBounds(message string) (*float64, *float64, bool) {
	if match := betweenPattern.FindStringSubmatch(message); len(match) == 3 {
		minimum, minErr := strconv.ParseFloat(match[1], 64)
		maximum, maxErr := strconv.ParseFloat(match[2], 64)
		if minErr == nil && maxErr == nil {
			return &minimum, &maximum, true
		}
	}
	var minimum *float64
	var maximum *float64
	if match := minimumPattern.FindStringSubmatch(message); len(match) == 2 {
		if value, err := strconv.ParseFloat(match[1], 64); err == nil {
			minimum = &value
		}
	}
	if match := maximumPattern.FindStringSubmatch(message); len(match) == 2 {
		if value, err := strconv.ParseFloat(match[1], 64); err == nil {
			maximum = &value
		}
	}
	return minimum, maximum, minimum != nil || maximum != nil
}

func isOutputLimitCorrectionParam(param string) bool {
	return param == "max_tokens" || param == "max_completion_tokens" || param == "max_output_tokens"
}

func applyParameterCorrectionRule(body map[string]any, rule parameterCorrectionRule) bool {
	current := valueAtCorrectionPath(body, rule.Param)
	switch rule.Action {
	case parameterCorrectionRemove:
		if current == nil {
			return false
		}
		deleteAtCorrectionPath(body, rule.Param)
		return true
	case parameterCorrectionSet:
		if current == nil {
			return false
		}
		if fmt.Sprint(current) == fmt.Sprint(rule.Value) {
			return false
		}
		setAtCorrectionPath(body, rule.Param, rule.Value)
		return true
	case parameterCorrectionClamp:
		number, ok := finiteFloatFromAny(current)
		if !ok {
			return false
		}
		corrected := number
		if rule.Min != nil && corrected < *rule.Min {
			corrected = *rule.Min
		}
		if rule.Max != nil && corrected > *rule.Max {
			corrected = *rule.Max
		}
		if corrected == number {
			return false
		}
		if math.Trunc(corrected) == corrected {
			setAtCorrectionPath(body, rule.Param, int(corrected))
		} else {
			setAtCorrectionPath(body, rule.Param, corrected)
		}
		return true
	default:
		return false
	}
}

func valueAtCorrectionPath(body map[string]any, param string) any {
	if param != "reasoning.effort" {
		return body[param]
	}
	reasoning, _ := body["reasoning"].(map[string]any)
	return reasoning["effort"]
}

func setAtCorrectionPath(body map[string]any, param string, value any) {
	if param != "reasoning.effort" {
		body[param] = value
		return
	}
	reasoning, _ := body["reasoning"].(map[string]any)
	if reasoning == nil {
		reasoning = map[string]any{}
		body["reasoning"] = reasoning
	}
	reasoning["effort"] = value
}

func deleteAtCorrectionPath(body map[string]any, param string) {
	if param != "reasoning.effort" {
		delete(body, param)
		return
	}
	reasoning, _ := body["reasoning"].(map[string]any)
	delete(reasoning, "effort")
	if len(reasoning) == 0 {
		delete(body, "reasoning")
	}
}
