package runner

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"strings"
)

type cacheAffinityKeys struct {
	Primary string
	Lookup  []string
	Record  []string
}

const cacheAffinityMinimumStableMessages = 2

func buildCacheAffinityKey(kind string, modelType string, body map[string]any) string {
	return buildCacheAffinityKeys(kind, modelType, body).Primary
}

func buildCacheAffinityKeys(kind string, modelType string, body map[string]any) cacheAffinityKeys {
	if len(body) == 0 {
		return cacheAffinityKeys{}
	}
	for _, key := range []string{"cacheAffinityKey", "cache_affinity_key"} {
		if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
			key := "explicit:" + sha256Text(value)
			return cacheAffinityKeys{Primary: key, Lookup: []string{key}, Record: []string{key}}
		}
	}
	for _, key := range []string{"sessionId", "session_id", "conversationId", "conversation_id"} {
		if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
			key := key + ":" + sha256Text(value)
			return cacheAffinityKeys{Primary: key, Lookup: []string{key}, Record: []string{key}}
		}
	}
	if !cacheAffinityPromptHashSupported(kind, modelType) {
		return cacheAffinityKeys{}
	}
	basePayload := map[string]any{
		"version": "v2",
	}
	if tools, ok := body["tools"]; ok {
		basePayload["tools"] = tools
	}
	messageHashes := cacheAffinityMessageHashes(body)
	if len(messageHashes) >= cacheAffinityMinimumStableMessages {
		baseHash := stableJSONHash(basePayload)
		keys := cacheAffinityPrefixKeys("prompt_lcp_v2:", baseHash, messageHashes)
		if len(keys) == 0 {
			return cacheAffinityKeys{}
		}
		lookup := reverseCacheAffinityKeys(keys)
		legacyKeys := legacyCacheAffinityPrefixKeys(kind, modelType, body)
		lookup = normalizedCacheAffinityKeyList(append(lookup, reverseCacheAffinityKeys(legacyKeys)...))
		return cacheAffinityKeys{
			Primary: keys[len(keys)-1],
			Lookup:  lookup,
			Record:  keys,
		}
	}
	return cacheAffinityKeys{}
}

func cacheAffinityPromptHashSupported(kind string, modelType string) bool {
	switch strings.TrimSpace(kind) {
	case "chat.completions", "responses":
		return true
	}
	switch strings.TrimSpace(modelType) {
	case "chat", "text_generate", "responses":
		return true
	default:
		return false
	}
}

func cacheAffinityMessageHashes(body map[string]any) []string {
	if hashes := stringListFromAny(body["messageHashes"]); len(hashes) > 0 {
		return hashes
	}
	if hashes := stringListFromAny(body["message_hashes"]); len(hashes) > 0 {
		return hashes
	}
	messages := cacheAffinityStableMessages(body)
	if len(messages) == 0 {
		return nil
	}
	hashes := make([]string, 0, len(messages))
	for _, message := range messages {
		hash := stableJSONHash(message)
		if hash == "" {
			continue
		}
		hashes = append(hashes, hash)
	}
	return hashes
}

func cacheAffinityStableMessages(body map[string]any) []any {
	messages := make([]any, 0)
	if instructions := strings.TrimSpace(stringFromAny(body["instructions"])); instructions != "" {
		messages = append(messages, map[string]any{"role": "system", "content": instructions})
	}
	if rawMessages, ok := body["messages"].([]any); ok {
		for _, message := range rawMessages {
			if normalized := normalizeCacheAffinityMessage(message); normalized != nil {
				messages = append(messages, normalized)
			}
		}
		return messages
	}
	switch input := body["input"].(type) {
	case string:
		if strings.TrimSpace(input) != "" {
			messages = append(messages, map[string]any{"role": "user", "content": input})
		}
	case []any:
		for _, message := range input {
			if normalized := normalizeCacheAffinityMessage(message); normalized != nil {
				messages = append(messages, normalized)
			}
		}
	}
	return messages
}

func normalizeCacheAffinityMessage(value any) any {
	message, ok := value.(map[string]any)
	if !ok {
		if text := strings.TrimSpace(stringFromAny(value)); text != "" {
			return map[string]any{"role": "user", "content": text}
		}
		return nil
	}
	messageType := strings.TrimSpace(stringFromAny(message["type"]))
	switch messageType {
	case "function_call":
		return map[string]any{
			"role": "assistant",
			"tool_calls": []any{map[string]any{
				"id":   firstNonEmptyCacheAffinityString(message["call_id"], message["id"]),
				"type": "function",
				"function": map[string]any{
					"name":      message["name"],
					"arguments": message["arguments"],
				},
			}},
		}
	case "function_call_output":
		return map[string]any{
			"role":         "tool",
			"tool_call_id": firstNonEmptyCacheAffinityString(message["call_id"], message["id"]),
			"content":      message["output"],
		}
	}
	normalized := make(map[string]any, len(message))
	for key, item := range message {
		switch key {
		case "id", "status":
			continue
		case "type":
			if messageType == "message" {
				continue
			}
		}
		normalized[key] = item
	}
	if role := strings.TrimSpace(stringFromAny(normalized["role"])); role != "" {
		normalized["role"] = role
	}
	return normalized
}

func firstNonEmptyCacheAffinityString(values ...any) string {
	for _, value := range values {
		if text := strings.TrimSpace(stringFromAny(value)); text != "" {
			return text
		}
	}
	return ""
}

func cacheAffinityPrefixKeys(prefix string, baseHash string, messageHashes []string) []string {
	if len(messageHashes) < cacheAffinityMinimumStableMessages {
		return nil
	}
	keys := make([]string, 0, len(messageHashes)-cacheAffinityMinimumStableMessages+1)
	for prefixLength := cacheAffinityMinimumStableMessages; prefixLength <= len(messageHashes); prefixLength++ {
		keyPayload := map[string]any{
			"base":          baseHash,
			"messageHashes": messageHashes[:prefixLength],
			"prefixLength":  prefixLength,
		}
		keys = append(keys, prefix+stableJSONHash(keyPayload))
	}
	return keys
}

func legacyCacheAffinityPrefixKeys(kind string, modelType string, body map[string]any) []string {
	messageHashes := legacyCacheAffinityMessageHashes(body)
	if len(messageHashes) < cacheAffinityMinimumStableMessages {
		return nil
	}
	basePayload := map[string]any{
		"kind":      kind,
		"modelType": modelType,
	}
	if tools, ok := body["tools"]; ok {
		basePayload["tools"] = tools
	}
	if instructions, ok := body["instructions"]; ok {
		basePayload["instructions"] = instructions
	}
	return cacheAffinityPrefixKeys("prompt_lcp:", stableJSONHash(basePayload), messageHashes)
}

func legacyCacheAffinityMessageHashes(body map[string]any) []string {
	if hashes := stringListFromAny(body["messageHashes"]); len(hashes) > 0 {
		return hashes
	}
	if hashes := stringListFromAny(body["message_hashes"]); len(hashes) > 0 {
		return hashes
	}
	messages, ok := body["messages"].([]any)
	if !ok {
		return nil
	}
	hashes := make([]string, 0, len(messages))
	for _, message := range messages {
		raw, err := json.Marshal(message)
		if err == nil && len(raw) > 0 {
			hashes = append(hashes, sha256Text(string(raw)))
		}
	}
	return hashes
}

func reverseCacheAffinityKeys(keys []string) []string {
	reversed := make([]string, 0, len(keys))
	for index := len(keys) - 1; index >= 0; index-- {
		reversed = append(reversed, keys[index])
	}
	return reversed
}

func normalizedCacheAffinityKeyList(keys []string) []string {
	seen := map[string]bool{}
	out := make([]string, 0, len(keys))
	for _, key := range keys {
		key = strings.TrimSpace(key)
		if key == "" || seen[key] {
			continue
		}
		seen[key] = true
		out = append(out, key)
	}
	return out
}

func stableJSONHash(value any) string {
	raw, err := json.Marshal(value)
	if err != nil || len(raw) == 0 {
		return ""
	}
	return sha256Text(string(raw))
}

func sha256Text(value string) string {
	sum := sha256.Sum256([]byte(value))
	return hex.EncodeToString(sum[:])
}
