package convert

import (
	"fmt"
	"reflect"
	"slices"
	"strconv"

	"github.com/databricks/cli/libs/diag"
	"github.com/databricks/cli/libs/dyn"
	"github.com/databricks/cli/libs/dyn/dynvar"
)

// NormalizeOption is the type for options that can be passed to Normalize.
type NormalizeOption int

const (
	// IncludeMissingFields causes the normalization to include fields that defined on the given
	// type but are missing in the source value. They are included with their zero values.
	IncludeMissingFields NormalizeOption = iota
)

type normalizeOptions struct {
	includeMissingFields bool
}

func Normalize(dst any, src dyn.Value, opts ...NormalizeOption) (dyn.Value, diag.Diagnostics) {
	var n normalizeOptions
	for _, opt := range opts {
		switch opt {
		case IncludeMissingFields:
			n.includeMissingFields = true
		}
	}

	return n.normalizeType(reflect.TypeOf(dst), src, []reflect.Type{}, dyn.EmptyPath)
}

func (n normalizeOptions) normalizeType(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
	for typ.Kind() == reflect.Pointer {
		typ = typ.Elem()
	}

	switch typ.Kind() {
	case reflect.Struct:
		return n.normalizeStruct(typ, src, append(seen, typ), path)
	case reflect.Map:
		return n.normalizeMap(typ, src, append(seen, typ), path)
	case reflect.Slice:
		return n.normalizeSlice(typ, src, append(seen, typ), path)
	case reflect.String:
		return n.normalizeString(typ, src, path)
	case reflect.Bool:
		return n.normalizeBool(typ, src, path)
	case reflect.Int, reflect.Int32, reflect.Int64:
		return n.normalizeInt(typ, src, path)
	case reflect.Float32, reflect.Float64:
		return n.normalizeFloat(typ, src, path)
	case reflect.Interface:
		return n.normalizeInterface(typ, src, path)
	}

	return dyn.InvalidValue, diag.Errorf("unsupported type: %s", typ.Kind())
}

func nullWarning(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnostic {
	return diag.Diagnostic{
		Severity:  diag.Warning,
		Summary:   fmt.Sprintf("expected a %s value, found null", expected),
		Locations: []dyn.Location{src.Location()},
		Paths:     []dyn.Path{path},
	}
}

func typeMismatch(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnostic {
	return diag.Diagnostic{
		Severity:  diag.Warning,
		Summary:   fmt.Sprintf("expected %s, found %s", expected, src.Kind()),
		Locations: []dyn.Location{src.Location()},
		Paths:     []dyn.Path{path},
	}
}

func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
	var diags diag.Diagnostics

	switch src.Kind() {
	case dyn.KindMap:
		out := dyn.NewMapping()
		info := getStructInfo(typ)
		for _, pair := range src.MustMap().Pairs() {
			pk := pair.Key
			pv := pair.Value

			index, ok := info.Fields[pk.MustString()]
			if !ok {
				if !pv.IsAnchor() {
					diags = diags.Append(diag.Diagnostic{
						Severity: diag.Warning,
						Summary:  fmt.Sprintf("unknown field: %s", pk.MustString()),
						// Show all locations the unknown field is defined at.
						Locations: pk.Locations(),
						Paths:     []dyn.Path{path},
					})
				}
				continue
			}

			// Normalize the value according to the field type.
			nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen, path.Append(dyn.Key(pk.MustString())))
			if err != nil {
				diags = diags.Extend(err)
				// Skip the element if it cannot be normalized.
				if !nv.IsValid() {
					continue
				}
			}

			out.Set(pk, nv)
		}

		// Return the normalized value if missing fields are not included.
		if !n.includeMissingFields {
			return dyn.NewValue(out, src.Locations()), diags
		}

		// Populate missing fields with their zero values.
		for k, index := range info.Fields {
			if _, ok := out.GetByString(k); ok {
				continue
			}

			// Optionally dereference pointers to get the underlying field type.
			ftyp := typ.FieldByIndex(index).Type
			for ftyp.Kind() == reflect.Pointer {
				ftyp = ftyp.Elem()
			}

			// Skip field if we have already seen its type to avoid infinite recursion
			// when filling in the zero value of a recursive type.
			if slices.Contains(seen, ftyp) {
				continue
			}

			var v dyn.Value
			switch ftyp.Kind() {
			case reflect.Struct, reflect.Map:
				v, _ = n.normalizeType(ftyp, dyn.V(map[string]dyn.Value{}), seen, path.Append(dyn.Key(k)))
			case reflect.Slice:
				v, _ = n.normalizeType(ftyp, dyn.V([]dyn.Value{}), seen, path.Append(dyn.Key(k)))
			case reflect.String:
				v, _ = n.normalizeType(ftyp, dyn.V(""), seen, path.Append(dyn.Key(k)))
			case reflect.Bool:
				v, _ = n.normalizeType(ftyp, dyn.V(false), seen, path.Append(dyn.Key(k)))
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
				v, _ = n.normalizeType(ftyp, dyn.V(int64(0)), seen, path.Append(dyn.Key(k)))
			case reflect.Float32, reflect.Float64:
				v, _ = n.normalizeType(ftyp, dyn.V(float64(0)), seen, path.Append(dyn.Key(k)))
			default:
				// Skip fields for which we do not have a natural [dyn.Value] equivalent.
				// For example, we don't handle reflect.Complex* and reflect.Uint* types.
				continue
			}
			if v.IsValid() {
				out.Set(dyn.V(k), v)
			}
		}

		return dyn.NewValue(out, src.Locations()), diags
	case dyn.KindNil:
		return src, diags

	case dyn.KindString:
		// Return verbatim if it's a pure variable reference.
		if dynvar.IsPureVariableReference(src.MustString()) {
			return src, nil
		}
	}

	// Cannot interpret as a struct.
	return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src, path))
}

func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
	var diags diag.Diagnostics

	switch src.Kind() {
	case dyn.KindMap:
		out := dyn.NewMapping()
		for _, pair := range src.MustMap().Pairs() {
			pk := pair.Key
			pv := pair.Value

			// Normalize the value according to the map element type.
			nv, err := n.normalizeType(typ.Elem(), pv, seen, path.Append(dyn.Key(pk.MustString())))
			if err != nil {
				diags = diags.Extend(err)
				// Skip the element if it cannot be normalized.
				if !nv.IsValid() {
					continue
				}
			}

			out.Set(pk, nv)
		}

		return dyn.NewValue(out, src.Locations()), diags
	case dyn.KindNil:
		return src, diags

	case dyn.KindString:
		// Return verbatim if it's a pure variable reference.
		if dynvar.IsPureVariableReference(src.MustString()) {
			return src, nil
		}
	}

	// Cannot interpret as a map.
	return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src, path))
}

func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
	var diags diag.Diagnostics

	switch src.Kind() {
	case dyn.KindSequence:
		out := make([]dyn.Value, 0, len(src.MustSequence()))
		for _, v := range src.MustSequence() {
			// Normalize the value according to the slice element type.
			v, err := n.normalizeType(typ.Elem(), v, seen, path.Append(dyn.Index(len(out))))
			if err != nil {
				diags = diags.Extend(err)
				// Skip the element if it cannot be normalized.
				if !v.IsValid() {
					continue
				}
			}

			out = append(out, v)
		}

		return dyn.NewValue(out, src.Locations()), diags
	case dyn.KindNil:
		return src, diags

	case dyn.KindString:
		// Return verbatim if it's a pure variable reference.
		if dynvar.IsPureVariableReference(src.MustString()) {
			return src, nil
		}
	}

	// Cannot interpret as a slice.
	return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindSequence, src, path))
}

func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
	var diags diag.Diagnostics
	var out string

	switch src.Kind() {
	case dyn.KindString:
		out = src.MustString()
	case dyn.KindBool:
		out = strconv.FormatBool(src.MustBool())
	case dyn.KindInt:
		out = strconv.FormatInt(src.MustInt(), 10)
	case dyn.KindFloat:
		out = strconv.FormatFloat(src.MustFloat(), 'f', -1, 64)
	case dyn.KindTime:
		out = src.MustTime().String()
	case dyn.KindNil:
		// Return a warning if the field is present but has a null value.
		return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindString, src, path))
	default:
		return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src, path))
	}

	return dyn.NewValue(out, src.Locations()), diags
}

func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
	var diags diag.Diagnostics
	var out bool

	switch src.Kind() {
	case dyn.KindBool:
		out = src.MustBool()
	case dyn.KindString:
		// See https://github.com/go-yaml/yaml/blob/f6f7691b1fdeb513f56608cd2c32c51f8194bf51/decode.go#L684-L693.
		switch src.MustString() {
		case "true", "y", "Y", "yes", "Yes", "YES", "on", "On", "ON":
			out = true
		case "false", "n", "N", "no", "No", "NO", "off", "Off", "OFF":
			out = false
		default:
			// Return verbatim if it's a pure variable reference.
			if dynvar.IsPureVariableReference(src.MustString()) {
				return src, nil
			}

			// Cannot interpret as a boolean.
			return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path))
		}
	case dyn.KindNil:
		// Return a warning if the field is present but has a null value.
		return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindBool, src, path))
	default:
		return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path))
	}

	return dyn.NewValue(out, src.Locations()), diags
}

func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
	var diags diag.Diagnostics
	var out int64

	switch src.Kind() {
	case dyn.KindInt:
		out = src.MustInt()
	case dyn.KindFloat:
		out = int64(src.MustFloat())
		if src.MustFloat() != float64(out) {
			return dyn.InvalidValue, diags.Append(diag.Diagnostic{
				Severity:  diag.Warning,
				Summary:   fmt.Sprintf(`cannot accurately represent "%g" as integer due to precision loss`, src.MustFloat()),
				Locations: []dyn.Location{src.Location()},
				Paths:     []dyn.Path{path},
			})
		}
	case dyn.KindString:
		var err error
		out, err = strconv.ParseInt(src.MustString(), 10, 64)
		if err != nil {
			// Return verbatim if it's a pure variable reference.
			if dynvar.IsPureVariableReference(src.MustString()) {
				return src, nil
			}

			return dyn.InvalidValue, diags.Append(diag.Diagnostic{
				Severity:  diag.Warning,
				Summary:   fmt.Sprintf("cannot parse %q as an integer", src.MustString()),
				Locations: []dyn.Location{src.Location()},
				Paths:     []dyn.Path{path},
			})
		}
	case dyn.KindNil:
		// Return a warning if the field is present but has a null value.
		return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindInt, src, path))
	default:
		return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src, path))
	}

	return dyn.NewValue(out, src.Locations()), diags
}

func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
	var diags diag.Diagnostics
	var out float64

	switch src.Kind() {
	case dyn.KindFloat:
		out = src.MustFloat()
	case dyn.KindInt:
		out = float64(src.MustInt())
		if src.MustInt() != int64(out) {
			return dyn.InvalidValue, diags.Append(diag.Diagnostic{
				Severity:  diag.Warning,
				Summary:   fmt.Sprintf(`cannot accurately represent "%d" as floating point number due to precision loss`, src.MustInt()),
				Locations: []dyn.Location{src.Location()},
				Paths:     []dyn.Path{path},
			})
		}
	case dyn.KindString:
		var err error
		out, err = strconv.ParseFloat(src.MustString(), 64)
		if err != nil {
			// Return verbatim if it's a pure variable reference.
			if dynvar.IsPureVariableReference(src.MustString()) {
				return src, nil
			}

			return dyn.InvalidValue, diags.Append(diag.Diagnostic{
				Severity:  diag.Warning,
				Summary:   fmt.Sprintf("cannot parse %q as a floating point number", src.MustString()),
				Locations: []dyn.Location{src.Location()},
				Paths:     []dyn.Path{path},
			})
		}
	case dyn.KindNil:
		// Return a warning if the field is present but has a null value.
		return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindFloat, src, path))
	default:
		return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src, path))
	}

	return dyn.NewValue(out, src.Locations()), diags
}

func (n normalizeOptions) normalizeInterface(_ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
	// Deal with every [dyn.Kind] here to ensure completeness.
	switch src.Kind() {
	case dyn.KindMap:
		// Fall through
	case dyn.KindSequence:
		// Fall through
	case dyn.KindString:
		// Fall through
	case dyn.KindBool:
		// Fall through
	case dyn.KindInt:
		// Fall through
	case dyn.KindFloat:
		// Fall through
	case dyn.KindTime:
		// Conversion of a time value to an interface{}.
		// The [dyn.Value.AsAny] equivalent for this kind is the [time.Time] struct.
		// If we convert to a typed representation and back again, we cannot distinguish
		// a [time.Time] struct from any other struct.
		//
		// Therefore, we normalize the time value to a string.
		return dyn.NewValue(src.MustTime().String(), src.Locations()), nil
	case dyn.KindNil:
		// Fall through
	default:
		return dyn.InvalidValue, diag.Errorf("unsupported kind: %s", src.Kind())
	}

	return src, nil
}
