package gitops

import (
	"encoding/json"
	"fmt"
	"strconv"
)

// DriftEntry is one field where the repo-declared value differs from the live
// stored value. Values are display strings; comparison is done on normalized
// forms so cosmetic differences (default coercion, YAML int vs JSON number)
// don't register as drift.
type DriftEntry struct {
	Field     string `json:"field"`
	RepoValue string `json:"repo_value"`
	LiveValue string `json:"live_value"`
}

// driftFieldOrder is the stable order drift entries are reported in.
var driftFieldOrder = []string{keyPort, keyHealthcheck, keyDeployStrategy}

// Drift compares the declared overlay (the present, source-supported fields)
// against the live source_config and returns the fields that differ. Only
// declared fields are considered — a key the file omits is "unmanaged",
// neither drift nor clean (review C5). Comparison is post-normalization.
func Drift(spec Spec, live json.RawMessage, sourceKind string) ([]DriftEntry, error) {
	liveMap := map[string]any{}
	if len(live) > 0 {
		if err := json.Unmarshal(live, &liveMap); err != nil {
			return nil, fmt.Errorf("gitops: decode live source_config: %w", err)
		}
	}
	allowed := supportedKeys(sourceKind)
	declared := declaredValues(spec)

	var entries []DriftEntry
	for _, k := range driftFieldOrder {
		repoVal, ok := declared[k]
		if !ok || !allowed[k] {
			continue
		}
		liveVal, livePresent := liveMap[k]
		if normalizeField(k, repoVal) == normalizeField(k, liveVal) {
			continue
		}
		entries = append(entries, DriftEntry{
			Field:     k,
			RepoValue: displayField(k, repoVal, true),
			LiveValue: displayField(k, liveVal, livePresent),
		})
	}
	return entries, nil
}

// normalizeField returns the canonical comparison form of a field value.
func normalizeField(key string, v any) string {
	switch key {
	case keyDeployStrategy:
		// "" and "recreate" are the same effective strategy for dockerfile and
		// static (see each source's effectiveStrategy).
		s := toStr(v)
		if s == "" || s == "recreate" {
			return "recreate"
		}
		return s
	case keyPort:
		return canonInt(v)
	default:
		return toStr(v)
	}
}

// displayField renders a value for the UI. present=false means the key is
// absent from the live config.
func displayField(key string, v any, present bool) string {
	if !present {
		return "(unset)"
	}
	if key == keyDeployStrategy {
		if s := toStr(v); s == "" {
			return "recreate (default)"
		}
	}
	switch n := v.(type) {
	case float64:
		// JSON numbers decode as float64; show whole numbers without ".0".
		return strconv.FormatInt(int64(n), 10)
	case nil:
		return "(unset)"
	default:
		return fmt.Sprint(v)
	}
}

// canonInt coerces any numeric representation (YAML int, JSON float64, etc.)
// to a base-10 integer string for value-equality comparison.
func canonInt(v any) string {
	switch n := v.(type) {
	case int:
		return strconv.Itoa(n)
	case int64:
		return strconv.FormatInt(n, 10)
	case float64:
		return strconv.FormatInt(int64(n), 10)
	case json.Number:
		return n.String()
	case nil:
		return "0"
	default:
		return fmt.Sprint(v)
	}
}

func toStr(v any) string {
	if v == nil {
		return ""
	}
	if s, ok := v.(string); ok {
		return s
	}
	return fmt.Sprint(v)
}
