package cmputil

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/google/go-cmp/cmp"
)

type DiffReport []Diff

// GetDiffsForField returns subset of the diffs which path starts with the provided path
func (r DiffReport) GetDiffsForField(path string) DiffReport {
	var result []Diff
	for _, diff := range r {
		if strings.HasPrefix(diff.Path, path) {
			if diff.Path != path {
				char := []rune(diff.Path)[len(path)]
				if char != '.' && char != '[' { // if the following symbol is not a delimiter or bracket then that's not our path
					continue
				}
			}
			result = append(result, diff)
		}
	}
	return result
}

// DiffReporter is a simple custom reporter that only records differences
// detected during comparison. Implements an interface required by cmp.Reporter option
type DiffReporter struct {
	path  cmp.Path
	Diffs DiffReport
}

func (r *DiffReporter) PushStep(ps cmp.PathStep) {
	r.path = append(r.path, ps)
}

func (r *DiffReporter) PopStep() {
	r.path = r.path[:len(r.path)-1]
}

func (r *DiffReporter) Report(rs cmp.Result) {
	if !rs.Equal() {
		vx, vy := r.path.Last().Values()
		r.Diffs = append(r.Diffs, Diff{
			Path:  printPath(r.path),
			Left:  vx,
			Right: vy,
		})
	}
}

func printPath(p cmp.Path) string {
	ss := strings.Builder{}
	for _, s := range p {
		toAdd := ""
		switch v := s.(type) {
		case cmp.StructField:
			toAdd = v.String()
		case cmp.MapIndex:
			toAdd = fmt.Sprintf("[%s]", v.Key())
		case cmp.SliceIndex:
			if v.Key() >= 0 {
				toAdd = fmt.Sprintf("[%d]", v.Key())
			}
		}
		if toAdd == "" {
			continue
		}
		ss.WriteString(toAdd)
	}
	return strings.TrimPrefix(ss.String(), ".")
}

func (r DiffReport) String() string {
	b := strings.Builder{}
	for _, diff := range r {
		b.WriteString(diff.String())
		b.WriteByte('\n')
	}
	return b.String()
}

// Paths returns the slice of paths of the current DiffReport
func (r DiffReport) Paths() []string {
	var result = make([]string, 0, len(r))
	for _, diff := range r {
		result = append(result, diff.Path)
	}
	return result
}

type Diff struct {
	// Path to the field that has difference separated by period. Array index and key are designated by square brackets.
	// For example, Annotations[12345].Data.Fields[0].ID
	Path  string
	Left  reflect.Value
	Right reflect.Value
}

func (d *Diff) String() string {
	return fmt.Sprintf("%v:\n\t-: %+v\n\t+: %+v\n", d.Path, describeReflectValue(d.Left), describeReflectValue(d.Right))
}

func describeReflectValue(v reflect.Value) any {
	// invalid reflect.Value is produced when two collections (slices\maps) are compared and one misses value.
	// This way go-cmp indicates that an element was added\removed from a list.
	if !v.IsValid() {
		return "<none>"
	}
	return v
}

// IsAddOperation returns true when
//   - Left does not have value and Right has
//   - the kind of Left and Right is either reflect.Slice or reflect.Map and the length of Left is less than length of Right
//
// In all other cases it returns false.
// NOTE: this is applicable to diff of Maps and Slices only
func (d *Diff) IsAddOperation() bool {
	return !d.Left.IsValid() && d.Right.IsValid() || (d.Left.Kind() == d.Right.Kind() && // cmp reports adding first element to a nil slice as creation of one and therefore Left is valid and it is nil and right is a new slice
		(d.Left.Kind() == reflect.Slice || d.Left.Kind() == reflect.Map) && d.Left.Len() < d.Right.Len())
}

// IsDeleteOperation returns true when
//   - Right does not have value and Left has
//   - the kind of Left and Right is either reflect.Slice or reflect.Map and the length of Right is less than length of Left
//
// In all other cases it returns false.
// NOTE: this is applicable to diff of Maps and Slices only
func (d *Diff) IsDeleteOperation() bool {
	return d.Left.IsValid() && !d.Right.IsValid() || (d.Left.Kind() == d.Right.Kind() &&
		(d.Left.Kind() == reflect.Slice || d.Left.Kind() == reflect.Map) && d.Left.Len() > d.Right.Len())
}
