package merge

import (
	"errors"
	"fmt"

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

// OverrideVisitor is visiting the changes during the override process
// and allows to control what changes are allowed, or update the effective
// value.
//
// For instance, it can disallow changes outside the specific path(s), or update
// the location of the effective value.
//
// Values returned by 'VisitInsert' and 'VisitUpdate' are used as the final value
// of the node. 'VisitDelete' can return ErrOverrideUndoDelete to undo delete.
//
// 'VisitDelete' is called when a value is removed from mapping or sequence
// 'VisitInsert' is called when a new value is added to mapping or sequence
// 'VisitUpdate' is called when a leaf value is updated
type OverrideVisitor struct {
	VisitDelete func(valuePath dyn.Path, left dyn.Value) error
	VisitInsert func(valuePath dyn.Path, right dyn.Value) (dyn.Value, error)
	VisitUpdate func(valuePath dyn.Path, left dyn.Value, right dyn.Value) (dyn.Value, error)
}

var ErrOverrideUndoDelete = errors.New("undo delete operation")

// Override overrides value 'leftRoot' with 'rightRoot', keeping 'location' if values
// haven't changed. Preserving 'location' is important to preserve the original source of the value
// for error reporting.
func Override(leftRoot dyn.Value, rightRoot dyn.Value, visitor OverrideVisitor) (dyn.Value, error) {
	return override(dyn.EmptyPath, leftRoot, rightRoot, visitor)
}

func override(basePath dyn.Path, left dyn.Value, right dyn.Value, visitor OverrideVisitor) (dyn.Value, error) {
	if left.Kind() != right.Kind() {
		return visitor.VisitUpdate(basePath, left, right)
	}

	// NB: we only call 'VisitUpdate' on leaf values, and for sequences and mappings
	// we don't know if value was updated or not

	switch left.Kind() {
	case dyn.KindMap:
		merged, err := overrideMapping(basePath, left.MustMap(), right.MustMap(), visitor)

		if err != nil {
			return dyn.InvalidValue, err
		}

		return dyn.NewValue(merged, left.Locations()), nil

	case dyn.KindSequence:
		// some sequences are keyed, and we can detect which elements are added/removed/updated,
		// but we don't have this information
		merged, err := overrideSequence(basePath, left.MustSequence(), right.MustSequence(), visitor)

		if err != nil {
			return dyn.InvalidValue, err
		}

		return dyn.NewValue(merged, left.Locations()), nil

	case dyn.KindString:
		if left.MustString() == right.MustString() {
			return left, nil
		} else {
			return visitor.VisitUpdate(basePath, left, right)
		}

	case dyn.KindFloat:
		// TODO consider comparison with epsilon if normalization doesn't help, where do we use floats?

		if left.MustFloat() == right.MustFloat() {
			return left, nil
		} else {
			return visitor.VisitUpdate(basePath, left, right)
		}

	case dyn.KindBool:
		if left.MustBool() == right.MustBool() {
			return left, nil
		} else {
			return visitor.VisitUpdate(basePath, left, right)
		}

	case dyn.KindTime:
		if left.MustTime() == right.MustTime() {
			return left, nil
		} else {
			return visitor.VisitUpdate(basePath, left, right)
		}

	case dyn.KindInt:
		if left.MustInt() == right.MustInt() {
			return left, nil
		} else {
			return visitor.VisitUpdate(basePath, left, right)
		}
	case dyn.KindNil:
		return left, nil
	}

	return dyn.InvalidValue, fmt.Errorf("unexpected kind %s at %s", left.Kind(), basePath.String())
}

func overrideMapping(basePath dyn.Path, leftMapping dyn.Mapping, rightMapping dyn.Mapping, visitor OverrideVisitor) (dyn.Mapping, error) {
	out := dyn.NewMapping()

	for _, leftPair := range leftMapping.Pairs() {
		// detect if key was removed
		if _, ok := rightMapping.GetPair(leftPair.Key); !ok {
			path := basePath.Append(dyn.Key(leftPair.Key.MustString()))

			err := visitor.VisitDelete(path, leftPair.Value)

			// if 'delete' was undone, add it back
			if errors.Is(err, ErrOverrideUndoDelete) {
				err := out.Set(leftPair.Key, leftPair.Value)
				if err != nil {
					return dyn.NewMapping(), err
				}
			} else if err != nil {
				return dyn.NewMapping(), err
			}
		}
	}

	// iterating only right mapping will remove keys not present anymore
	// and insert new keys

	for _, rightPair := range rightMapping.Pairs() {
		if leftPair, ok := leftMapping.GetPair(rightPair.Key); ok {
			path := basePath.Append(dyn.Key(rightPair.Key.MustString()))
			newValue, err := override(path, leftPair.Value, rightPair.Value, visitor)

			if err != nil {
				return dyn.NewMapping(), err
			}

			// key was there before, so keep its location
			err = out.Set(leftPair.Key, newValue)

			if err != nil {
				return dyn.NewMapping(), err
			}
		} else {
			path := basePath.Append(dyn.Key(rightPair.Key.MustString()))

			newValue, err := visitor.VisitInsert(path, rightPair.Value)

			if err != nil {
				return dyn.NewMapping(), err
			}

			err = out.Set(rightPair.Key, newValue)

			if err != nil {
				return dyn.NewMapping(), err
			}
		}
	}

	return out, nil
}

func overrideSequence(basePath dyn.Path, left []dyn.Value, right []dyn.Value, visitor OverrideVisitor) ([]dyn.Value, error) {
	minLen := min(len(left), len(right))
	var values []dyn.Value

	for i := 0; i < minLen; i++ {
		path := basePath.Append(dyn.Index(i))
		merged, err := override(path, left[i], right[i], visitor)

		if err != nil {
			return nil, err
		}

		values = append(values, merged)
	}

	if len(right) > len(left) {
		for i := minLen; i < len(right); i++ {
			path := basePath.Append(dyn.Index(i))
			newValue, err := visitor.VisitInsert(path, right[i])

			if err != nil {
				return nil, err
			}

			values = append(values, newValue)
		}
	} else if len(left) > len(right) {
		for i := minLen; i < len(left); i++ {
			path := basePath.Append(dyn.Index(i))
			err := visitor.VisitDelete(path, left[i])

			// if 'delete' was undone, add it back
			if errors.Is(err, ErrOverrideUndoDelete) {
				values = append(values, left[i])
			} else if err != nil {
				return nil, err
			}
		}
	}

	return values, nil
}
