package schema

import (
	"fmt"
	"reflect"

	"github.com/Workiva/go-datastructures/queue"
)

// ErrorContainer represents a container where we can add errors and retrieve them.
type ErrorContainer interface {
	Push(err error)
	PushWarning(err error)
	HasErrors() bool
	HasWarnings() bool
	Errors() []error
	Warnings() []error
}

// Validator represents the validator interface.
type Validator struct {
	errors map[string][]error
}

// NewValidator create a validator.
func NewValidator() *Validator {
	validator := new(Validator)
	validator.errors = make(map[string][]error)

	return validator
}

// QueueItem an item representing a struct field and its path.
type QueueItem struct {
	value reflect.Value
	path  string
}

func (v *Validator) validateOne(item QueueItem, q *queue.Queue) error { //nolint:unparam
	if item.value.Type().Kind() == reflect.Ptr {
		if item.value.IsNil() {
			return nil
		}

		elem := item.value.Elem()

		q.Put(QueueItem{ //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
			value: elem,
			path:  item.path,
		})
	} else if item.value.Kind() == reflect.Struct {
		numFields := item.value.Type().NumField()

		validateFn := item.value.Addr().MethodByName("Validate")

		if validateFn.IsValid() {
			structValidator := NewStructValidator()
			validateFn.Call([]reflect.Value{reflect.ValueOf(structValidator)})
			v.errors[item.path] = structValidator.Errors()
		}

		for i := 0; i < numFields; i++ {
			field := item.value.Type().Field(i)
			value := item.value.Field(i)

			q.Put(QueueItem{ //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
				value: value,
				path:  item.path + "." + field.Name,
			})
		}
	}

	return nil
}

// Validate validate a struct.
func (v *Validator) Validate(s interface{}) error {
	q := queue.New(40)
	q.Put(QueueItem{value: reflect.ValueOf(s), path: "root"}) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.

	for !q.Empty() {
		val, err := q.Get(1)
		if err != nil {
			return err
		}

		item, ok := val[0].(QueueItem)
		if !ok {
			return fmt.Errorf("Cannot convert item into QueueItem")
		}

		v.validateOne(item, q) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
	}

	return nil
}

// PrintErrors display the errors thrown during validation.
func (v *Validator) PrintErrors() {
	for path, errs := range v.errors {
		fmt.Printf("Errors at %s:\n", path)

		for _, err := range errs {
			fmt.Printf("--> %s\n", err)
		}
	}
}

// Errors return the errors thrown during validation.
func (v *Validator) Errors() map[string][]error {
	return v.errors
}

// StructValidator is a validator for structs.
type StructValidator struct {
	errors   []error
	warnings []error
}

// NewStructValidator is a constructor of struct validator.
func NewStructValidator() *StructValidator {
	val := new(StructValidator)
	val.errors = make([]error, 0)
	val.warnings = make([]error, 0)

	return val
}

// Push an error to the validator.
func (v *StructValidator) Push(err error) {
	v.errors = append(v.errors, err)
}

// PushWarning error to the validator.
func (v *StructValidator) PushWarning(err error) {
	v.warnings = append(v.warnings, err)
}

// HasErrors checks whether the validator contains errors.
func (v *StructValidator) HasErrors() bool {
	return len(v.errors) > 0
}

// HasWarnings checks whether the validator contains warning errors.
func (v *StructValidator) HasWarnings() bool {
	return len(v.warnings) > 0
}

// Errors returns the errors.
func (v *StructValidator) Errors() []error {
	return v.errors
}

// Warnings returns the warnings.
func (v *StructValidator) Warnings() []error {
	return v.warnings
}

// Clear errors and warnings.
func (v *StructValidator) Clear() {
	v.errors = []error{}
	v.warnings = []error{}
}
