package runner

import (
	"math"
	"sort"
	"strconv"
	"strings"
)

func validateAndAdjustAspectRatio(aspectRatio string, capability map[string]any, allowed []string) (string, bool) {
	if !isMediaModelTypeWithAspectRatio(capability) {
		return "", false
	}
	if ratioRange, ok := numberPair(capability["aspect_ratio_range"]); ok {
		ratio, valid := aspectRatioNumber(aspectRatio)
		if !valid || ratio < ratioRange[0] || ratio > ratioRange[1] {
			return adjustAspectRatioToRange(aspectRatio, ratioRange[0], ratioRange[1], allowed), true
		}
	}
	if allowed == nil {
		return aspectRatio, true
	}
	if len(allowed) == 0 {
		return "", false
	}
	if (aspectRatio == "adaptive" || aspectRatio == "keep_ratio") && !containsString(allowed, aspectRatio) {
		return "", false
	}
	if containsString(allowed, aspectRatio) {
		return aspectRatio, true
	}
	return closestAspectRatio(aspectRatio, allowed), true
}

func isMediaModelTypeWithAspectRatio(capability map[string]any) bool {
	return capability != nil
}

func aspectRatioAllowed(value any, resolution string) []string {
	switch typed := value.(type) {
	case []any:
		return stringListFromAny(typed)
	case []string:
		return typed
	case map[string]any:
		if resolution != "" {
			if values := stringListFromAny(typed[resolution]); len(values) > 0 {
				return values
			}
		}
		return nil
	default:
		return nil
	}
}

func scopedNumberList(value any, scopes ...string) []float64 {
	switch typed := value.(type) {
	case []any:
		out := make([]float64, 0, len(typed))
		for _, item := range typed {
			if number := floatFromAny(item); number > 0 {
				out = append(out, number)
			}
		}
		return out
	case []float64:
		return typed
	case []int:
		out := make([]float64, 0, len(typed))
		for _, item := range typed {
			out = append(out, float64(item))
		}
		return out
	case map[string]any:
		for _, scope := range scopes {
			if scope == "" {
				continue
			}
			if values := scopedNumberList(typed[scope]); len(values) > 0 {
				return values
			}
		}
		for _, item := range typed {
			if values := scopedNumberList(item); len(values) > 0 {
				return values
			}
		}
	}
	return nil
}

func scopedRange(value any, scopes ...string) (float64, float64, bool) {
	if pair, ok := numberPair(value); ok {
		return pair[0], pair[1], true
	}
	if typed, ok := value.(map[string]any); ok {
		for _, scope := range scopes {
			if scope == "" {
				continue
			}
			if minValue, maxValue, ok := scopedRange(typed[scope]); ok {
				return minValue, maxValue, true
			}
		}
		for _, item := range typed {
			if minValue, maxValue, ok := scopedRange(item); ok {
				return minValue, maxValue, true
			}
		}
	}
	return 0, 0, false
}

func durationStep(value any, scopes ...string) float64 {
	if step := floatFromAny(value); step > 0 {
		return step
	}
	if typed, ok := value.(map[string]any); ok {
		for _, scope := range scopes {
			if scope == "" {
				continue
			}
			if step := durationStep(typed[scope]); step > 0 {
				return step
			}
		}
		for _, item := range typed {
			if step := durationStep(item); step > 0 {
				return step
			}
		}
	}
	return 0
}

func normalizeDurationByRange(target float64, minValue float64, maxValue float64, step float64) float64 {
	if minValue > maxValue {
		minValue, maxValue = maxValue, minValue
	}
	if step <= 0 {
		step = 1
	}
	clamped := math.Min(math.Max(target, minValue), maxValue)
	snapped := math.Ceil(((clamped-minValue)/step)-1e-9)*step + minValue
	snapped = math.Min(math.Max(snapped, minValue), maxValue)
	return math.Round(snapped*1_000_000) / 1_000_000
}

func normalizeDurationByStep(target float64, step float64) float64 {
	if step <= 0 {
		step = 1
	}
	snapped := math.Ceil((target/step)-1e-9) * step
	return math.Round(snapped*1_000_000) / 1_000_000
}

func nextAllowedNumber(target float64, values []float64) float64 {
	if len(values) == 0 {
		return target
	}
	sorted := append([]float64(nil), values...)
	sort.Float64s(sorted)
	for _, value := range sorted {
		if value >= target || math.Abs(value-target) < 1e-9 {
			return value
		}
	}
	return sorted[len(sorted)-1]
}

func contentItems(value any) []map[string]any {
	switch typed := value.(type) {
	case []any:
		out := make([]map[string]any, 0, len(typed))
		for _, item := range typed {
			if object, ok := item.(map[string]any); ok {
				out = append(out, cloneMap(object))
			}
		}
		return out
	case []map[string]any:
		out := make([]map[string]any, 0, len(typed))
		for _, item := range typed {
			out = append(out, cloneMap(item))
		}
		return out
	default:
		return nil
	}
}

func mapsToAnySlice(values []map[string]any) []any {
	out := make([]any, 0, len(values))
	for _, value := range values {
		out = append(out, value)
	}
	return out
}

func isImageContent(item map[string]any) bool {
	return stringFromAny(item["type"]) == "image_url" || item["image_url"] != nil
}

func isVideoContent(item map[string]any) bool {
	return stringFromAny(item["type"]) == "video_url" || item["video_url"] != nil
}

func isAudioContent(item map[string]any) bool {
	return stringFromAny(item["type"]) == "audio_url" || item["audio_url"] != nil
}

func capabilityForType(capabilities map[string]any, modelType string) map[string]any {
	if capabilities == nil {
		return nil
	}
	if typed, ok := capabilities[modelType].(map[string]any); ok {
		return typed
	}
	return nil
}

func capabilityPath(modelType string, key string) string {
	modelType = strings.TrimSpace(modelType)
	if modelType == "" {
		return ""
	}
	if strings.TrimSpace(key) == "" {
		return "capabilities." + modelType
	}
	return "capabilities." + modelType + "." + key
}

func capabilityValue(capabilities map[string]any, modelType string, key string) any {
	capability := capabilityForType(capabilities, modelType)
	if capability == nil {
		return nil
	}
	if strings.TrimSpace(key) == "" {
		return cloneMap(capability)
	}
	return cloneAny(capability[key])
}

func capabilityEvidence(capabilities map[string]any, modelType string, key string) (string, any) {
	return capabilityPath(modelType, key), capabilityValue(capabilities, modelType, key)
}

func audioInputCapabilityEvidence(context *paramProcessContext, modelType string) (string, any) {
	if isOmniVideoLike(context) {
		path, value := omniCapabilityEvidence(context, "input_audio")
		return path, mergeMetrics(map[string]any{"input_audio": value}, omniCapabilityBundle(context, "max_audios"))
	}
	return capabilityEvidence(context.modelCapability, modelType, "input_audio")
}

func omniCapabilityType(context *paramProcessContext) string {
	if context != nil && capabilityForType(context.modelCapability, "omni_video") != nil {
		return "omni_video"
	}
	if context != nil && capabilityForType(context.modelCapability, "omni") != nil {
		return "omni"
	}
	return "omni_video"
}

func omniCapabilityEvidence(context *paramProcessContext, key string) (string, any) {
	modelType := omniCapabilityType(context)
	var capabilities map[string]any
	if context != nil {
		capabilities = context.modelCapability
	}
	return capabilityPath(modelType, key), capabilityValue(capabilities, modelType, key)
}

func omniCapabilityBundle(context *paramProcessContext, keys ...string) map[string]any {
	modelType := omniCapabilityType(context)
	var capabilities map[string]any
	if context != nil {
		capabilities = context.modelCapability
	}
	out := map[string]any{}
	for _, key := range keys {
		out[key] = capabilityValue(capabilities, modelType, key)
	}
	return out
}

func numericField(values map[string]any, key string) (float64, bool) {
	if values == nil {
		return 0, false
	}
	if _, ok := values[key]; !ok {
		return 0, false
	}
	return floatFromAny(values[key]), true
}

func boolFromAny(value any) bool {
	typed, _ := value.(bool)
	return typed
}

func firstNonEmptyStringValue(values map[string]any, keys ...string) string {
	for _, key := range keys {
		if value := stringFromAny(values[key]); value != "" {
			return value
		}
	}
	return ""
}

func firstNonEmptyStringListFromAny(values ...any) []string {
	for _, value := range values {
		items := stringListFromAny(value)
		if len(items) > 0 {
			return items
		}
	}
	return nil
}

func stringListFromAny(value any) []string {
	switch typed := value.(type) {
	case []string:
		out := make([]string, 0, len(typed))
		for _, item := range typed {
			if text := strings.TrimSpace(item); text != "" {
				out = append(out, text)
			}
		}
		return out
	case []any:
		out := make([]string, 0, len(typed))
		for _, item := range typed {
			if text := stringFromAny(item); text != "" {
				out = append(out, text)
			}
		}
		return out
	case string:
		if strings.TrimSpace(typed) == "" {
			return nil
		}
		return []string{strings.TrimSpace(typed)}
	default:
		return nil
	}
}

func containsString(values []string, target string) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}

func appendUniqueString(values *[]string, value string) {
	value = strings.TrimSpace(value)
	if value == "" {
		return
	}
	for _, existing := range *values {
		if existing == value {
			return
		}
	}
	*values = append(*values, value)
}

func numberPair(value any) ([2]float64, bool) {
	switch typed := value.(type) {
	case []any:
		if len(typed) < 2 {
			return [2]float64{}, false
		}
		return [2]float64{floatFromAny(typed[0]), floatFromAny(typed[1])}, true
	case []float64:
		if len(typed) < 2 {
			return [2]float64{}, false
		}
		return [2]float64{typed[0], typed[1]}, true
	case []int:
		if len(typed) < 2 {
			return [2]float64{}, false
		}
		return [2]float64{float64(typed[0]), float64(typed[1])}, true
	default:
		return [2]float64{}, false
	}
}

func validAspectRatio(value string) bool {
	_, ok := normalizedAspectRatio(value)
	return ok
}

func normalizedAspectRatio(value string) (string, bool) {
	compact := strings.Map(func(r rune) rune {
		switch r {
		case ' ', '\t', '\r', '\n':
			return -1
		default:
			return r
		}
	}, strings.TrimSpace(value))
	parts := strings.Split(compact, ":")
	if len(parts) != 2 {
		return "", false
	}
	width := parsePositiveFloat(parts[0])
	height := parsePositiveFloat(parts[1])
	if width <= 0 || height <= 0 {
		return "", false
	}
	return parts[0] + ":" + parts[1], true
}

func aspectRatioNumber(value string) (float64, bool) {
	normalized, ok := normalizedAspectRatio(value)
	if !ok {
		return 0, false
	}
	parts := strings.Split(normalized, ":")
	width := parsePositiveFloat(parts[0])
	height := parsePositiveFloat(parts[1])
	return width / height, true
}

func adjustAspectRatioToRange(value string, minValue float64, maxValue float64, allowed []string) string {
	current, ok := aspectRatioNumber(value)
	if !ok {
		if len(allowed) > 0 {
			return allowed[0]
		}
		return "1:1"
	}
	if len(allowed) > 0 {
		closest := ""
		minDiff := math.Inf(1)
		for _, candidate := range allowed {
			ratio, ok := aspectRatioNumber(candidate)
			if !ok || ratio < minValue || ratio > maxValue {
				continue
			}
			diff := math.Abs(ratio - current)
			if diff < minDiff {
				minDiff = diff
				closest = candidate
			}
		}
		if closest != "" {
			return closest
		}
	}
	if current < minValue {
		return ratioString(minValue)
	}
	return ratioString(maxValue)
}

func closestAspectRatio(value string, allowed []string) string {
	if len(allowed) == 0 {
		return value
	}
	current, ok := aspectRatioNumber(value)
	if !ok {
		return allowed[0]
	}
	closest := ""
	minDiff := math.Inf(1)
	for _, candidate := range allowed {
		ratio, ok := aspectRatioNumber(candidate)
		if !ok {
			continue
		}
		diff := math.Abs(ratio - current)
		if diff < minDiff {
			minDiff = diff
			closest = candidate
		}
	}
	if closest != "" {
		return closest
	}
	return allowed[0]
}

func ratioString(value float64) string {
	if value <= 0 {
		return "1:1"
	}
	return strings.TrimRight(strings.TrimRight(strconv.FormatFloat(value, 'f', 6, 64), "0"), ".") + ":1"
}

func parsePositiveFloat(value string) float64 {
	for _, r := range strings.TrimSpace(value) {
		if r < '0' || r > '9' {
			if r != '.' {
				return 0
			}
		}
	}
	out, _ := strconv.ParseFloat(strings.TrimSpace(value), 64)
	return out
}

func positiveFloatFromAny(value any) float64 {
	switch typed := value.(type) {
	case int:
		return float64(typed)
	case int64:
		return float64(typed)
	case float64:
		return typed
	case string:
		return parsePositiveFloat(typed)
	default:
		return 0
	}
}

func positiveIntegerFromAny(value any) int {
	number := positiveFloatFromAny(value)
	if number <= 0 {
		return 0
	}
	return int(math.Round(number))
}

func parsePixelSizeString(value string) (int, int, bool) {
	value = strings.TrimSpace(strings.ToLower(value))
	if value == "" || isEmptyParamString(value) {
		return 0, 0, false
	}
	parts := strings.Split(value, "x")
	if len(parts) != 2 {
		return 0, 0, false
	}
	width := positiveIntegerFromAny(parts[0])
	height := positiveIntegerFromAny(parts[1])
	if width <= 0 || height <= 0 {
		return 0, 0, false
	}
	return width, height, true
}

func isEmptyParamString(value string) bool {
	normalized := strings.ToLower(strings.TrimSpace(value))
	return normalized == "null" || normalized == "undefined"
}

func isImageResolution(modelType string, value string) bool {
	if modelType != "image_generate" && modelType != "image_edit" {
		return false
	}
	return normalizeImageResolution(value) != ""
}

func isVideoResolution(modelType string, value string) bool {
	return isVideoModelType(modelType) && normalizeVideoResolution(value) != ""
}

func normalizeMediaResolution(modelType string, value string) string {
	if modelType == "image_generate" || modelType == "image_edit" {
		return normalizeImageResolution(value)
	}
	if isVideoModelType(modelType) {
		return normalizeVideoResolution(value)
	}
	return ""
}

func normalizeImageResolution(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	if len(value) < 2 || value[len(value)-1] != 'k' {
		return ""
	}
	magnitude, err := strconv.Atoi(value[:len(value)-1])
	if err != nil || magnitude <= 0 {
		return ""
	}
	return strconv.Itoa(magnitude) + "K"
}

func normalizeVideoResolution(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "480p":
		return "480p"
	case "720p":
		return "720p"
	case "1080p":
		return "1080p"
	case "1440p":
		return "1440p"
	case "2160p", "4k":
		return "2160p"
	default:
		return ""
	}
}

func isVideoModelType(modelType string) bool {
	return modelType == "video_generate" || modelType == "text_to_video" || modelType == "image_to_video" || modelType == "video_edit" || modelType == "video_reference" || modelType == "video_first_last_frame" || modelType == "omni_video" || modelType == "omni"
}

func cloneMap(values map[string]any) map[string]any {
	out := map[string]any{}
	for key, value := range values {
		out[key] = cloneAny(value)
	}
	return out
}

func cloneAny(value any) any {
	switch typed := value.(type) {
	case map[string]any:
		return cloneMap(typed)
	case []any:
		out := make([]any, 0, len(typed))
		for _, item := range typed {
			out = append(out, cloneAny(item))
		}
		return out
	case []map[string]any:
		out := make([]any, 0, len(typed))
		for _, item := range typed {
			out = append(out, cloneMap(item))
		}
		return out
	default:
		return value
	}
}
