package validation

import (
	"fmt"
	"reflect"

	kapi "k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/meta"
	"k8s.io/kubernetes/pkg/api/validation"
	"k8s.io/kubernetes/pkg/runtime"
	"k8s.io/kubernetes/pkg/util/validation/field"
)

type RuntimeObjectValidator interface {
	Validate(obj runtime.Object) field.ErrorList
	ValidateUpdate(obj, old runtime.Object) field.ErrorList
}

var Validator = &RuntimeObjectsValidator{map[reflect.Type]RuntimeObjectValidatorInfo{}}

type RuntimeObjectsValidator struct {
	typeToValidator map[reflect.Type]RuntimeObjectValidatorInfo
}

type RuntimeObjectValidatorInfo struct {
	Validator     RuntimeObjectValidator
	IsNamespaced  bool
	HasObjectMeta bool
	UpdateAllowed bool
}

func (v *RuntimeObjectsValidator) GetInfo(obj runtime.Object) (RuntimeObjectValidatorInfo, bool) {
	ret, ok := v.typeToValidator[reflect.TypeOf(obj)]
	return ret, ok
}

func (v *RuntimeObjectsValidator) MustRegister(obj runtime.Object, validateFunction interface{}, validateUpdateFunction interface{}) {
	if err := v.Register(obj, validateFunction, validateUpdateFunction); err != nil {
		panic(err)
	}
}

func (v *RuntimeObjectsValidator) Register(obj runtime.Object, validateFunction interface{}, validateUpdateFunction interface{}) error {
	objType := reflect.TypeOf(obj)
	if oldValidator, exists := v.typeToValidator[objType]; exists {
		panic(fmt.Sprintf("%v is already registered with %v", objType, oldValidator))
	}

	validator, err := NewValidationWrapper(validateFunction, validateUpdateFunction)
	if err != nil {
		return err
	}

	isNamespaced, err := GetRequiresNamespace(obj)
	if err != nil {
		return err
	}

	updateAllowed := validateUpdateFunction != nil

	v.typeToValidator[objType] = RuntimeObjectValidatorInfo{validator, isNamespaced, HasObjectMeta(obj), updateAllowed}

	return nil
}

func (v *RuntimeObjectsValidator) Validate(obj runtime.Object) field.ErrorList {
	if obj == nil {
		return field.ErrorList{}
	}

	allErrs := field.ErrorList{}

	specificValidationInfo, err := v.getSpecificValidationInfo(obj)
	if err != nil {
		allErrs = append(allErrs, field.InternalError(nil, err))
		return allErrs
	}

	allErrs = append(allErrs, specificValidationInfo.Validator.Validate(obj)...)
	return allErrs
}

func (v *RuntimeObjectsValidator) ValidateUpdate(obj, old runtime.Object) field.ErrorList {
	if obj == nil && old == nil {
		return field.ErrorList{}
	}
	if newType, oldType := reflect.TypeOf(obj), reflect.TypeOf(old); newType != oldType {
		return field.ErrorList{field.Invalid(field.NewPath("kind"), newType.Kind(), validation.NewInvalidTypeError(oldType.Kind(), newType.Kind(), "runtime.Object").Error())}
	}

	allErrs := field.ErrorList{}

	specificValidationInfo, err := v.getSpecificValidationInfo(obj)
	if err != nil {
		if fieldErr, ok := err.(*field.Error); ok {
			allErrs = append(allErrs, fieldErr)
		} else {
			allErrs = append(allErrs, field.InternalError(nil, err))
		}
		return allErrs
	}

	allErrs = append(allErrs, specificValidationInfo.Validator.ValidateUpdate(obj, old)...)

	// no errors so far, make sure that the new object is actually valid against the original validator
	if len(allErrs) == 0 {
		allErrs = append(allErrs, specificValidationInfo.Validator.Validate(obj)...)
	}

	return allErrs
}

func (v *RuntimeObjectsValidator) getSpecificValidationInfo(obj runtime.Object) (RuntimeObjectValidatorInfo, error) {
	objType := reflect.TypeOf(obj)
	specificValidationInfo, exists := v.typeToValidator[objType]

	if !exists {
		return RuntimeObjectValidatorInfo{}, fmt.Errorf("no validator registered for %v", objType)
	}

	return specificValidationInfo, nil
}

func GetRequiresNamespace(obj runtime.Object) (bool, error) {
	groupVersionKinds, _, err := kapi.Scheme.ObjectKinds(obj)
	if err != nil {
		return false, err
	}

	restMapping, err := kapi.RESTMapper.RESTMapping(groupVersionKinds[0].GroupKind())
	if err != nil {
		return false, err
	}

	return restMapping.Scope.Name() == meta.RESTScopeNameNamespace, nil
}

func HasObjectMeta(obj runtime.Object) bool {
	objValue := reflect.ValueOf(obj).Elem()
	field := objValue.FieldByName("ObjectMeta")

	if !field.IsValid() {
		return false
	}

	return true
}