package api

import (
	"reflect"

	"gitlab.com/uafrica/go-utils/errors"
)

type handler struct {
	RequestParamsType reflect.Type
	RequestBodyType   reflect.Type
	ResponseType      reflect.Type
	FuncValue         reflect.Value
}

func NewHandler(fnc interface{}) (handler, error) {
	h := handler{}

	fncType := reflect.TypeOf(fnc)
	if fncType.NumIn() < 2 || fncType.NumIn() > 3 {
		return h, errors.Errorf("takes %d args instead of (Context, Params[, Body])", fncType.NumIn())
	}
	if fncType.NumOut() < 1 || fncType.NumOut() > 2 {
		return h, errors.Errorf("returns %d results instead of ([Response,] error)", fncType.NumOut())
	}

	//arg[0] must implement interface api.Context
	//if _, ok := reflect.New(fncType.In(0)).Interface().(Context); !ok {

	if fncType.In(0) != contextInterfaceType &&
		!fncType.In(0).Implements(contextInterfaceType) {
		return h, errors.Errorf("first arg %v does not implement %v", fncType.In(0), contextInterfaceType)
	}

	//arg[1] must be a struct for params. It may be an empty struct, but
	//all public fields require a json tag which we will use to math the URL param name
	if err := validateStructType(fncType.In(1)); err != nil {
		return h, errors.Wrapf(err, "second arg %v is not valid params struct type", fncType.In(1))
	}
	h.RequestParamsType = fncType.In(1)

	//arg[2] is optional and must be a struct for request body. It may be an empty struct, but
	//all public fields require a json tag which we will use to unmarshal the request body from JSON
	if fncType.NumIn() >= 3 {
		if fncType.In(2).Kind() == reflect.Slice {
			if err := validateStructType(fncType.In(2).Elem()); err != nil {
				return h, errors.Errorf("third arg %v is not valid body []struct type", fncType.In(2))
			}
		} else {
			if err := validateStructType(fncType.In(2)); err != nil {
				return h, errors.Errorf("third arg %v is not valid body struct type", fncType.In(2))
			}
		}

		//todo: check special fields for claims, and see if also applies to params struct...
		//AccountID must be int64 or *int64 with tag =???
		//UserID must be int64 or *int64 with tag =???
		//Username must be string with tag =???

		h.RequestBodyType = fncType.In(2)
	}

	//last result must be error
	if _, ok := reflect.New(fncType.Out(fncType.NumOut() - 1)).Interface().(*error); !ok {
		return h, errors.Errorf("last result %v is not error type", fncType.Out(fncType.NumOut()-1))
	}

	h.FuncValue = reflect.ValueOf(fnc)
	return h, nil
}

func validateStructType(t reflect.Type) error {
	if t.Kind() != reflect.Struct {
		return errors.Errorf("%v is %v, not a struct", t, t.Kind())
	}
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		if f.Name[0] >= 'a' && f.Name[0] <= 'z' {
			//lowercase fields should not have json tag
			if f.Tag.Get("json") != "" {
				return errors.Errorf("%s.%s must be uppercase because it has a json tag \"%s\"",
					t.Name(),
					f.Name,
					f.Tag.Get("json"))
			}
		}

		// 	if f.... check tags recursively... for now, not too strict ... add checks if we see issues that break the API, to help dev to fix before we deploy, or to prevent bad habits...
	}
	return nil
}