package api

import (
	"encoding/json"
	"reflect"
	"strings"

	"github.com/aws/aws-lambda-go/events"
	"gitlab.com/uafrica/go-utils/errors"
	"gitlab.com/uafrica/go-utils/reflection"
	"gitlab.com/uafrica/go-utils/service"
)

type Context interface {
	service.Context
	Request() events.APIGatewayProxyRequest
	GetRequestParams(paramsStructType reflect.Type) (interface{}, error)
	GetRequestBody(requestStructType reflect.Type) (interface{}, error)
	LogAPIRequestAndResponse(res events.APIGatewayProxyResponse, err error)
}

var contextInterfaceType = reflect.TypeOf((*Context)(nil)).Elem()

type apiContext struct {
	service.Context
	request events.APIGatewayProxyRequest
}

func (ctx apiContext) Request() events.APIGatewayProxyRequest {
	return ctx.request
}

//todo: change to be a ctx method that defer to log so it does not have to be called explicitly
//it should also capture metrics for the handler and automaticlaly write the audit record,
//(but still allow for audit to be suppressed may be in some cases)
func (ctx *apiContext) LogAPIRequestAndResponse(res events.APIGatewayProxyResponse, err error) {
	fields := map[string]interface{}{
		"path":                   ctx.request.Path,
		"method":                 ctx.request.HTTPMethod,
		"status_code":            res.StatusCode,
		"api_gateway_request_id": ctx.RequestID(),
	}

	if ctx.request.HTTPMethod == "GET" {
		fields["req-query"] = ctx.request.QueryStringParameters
	}

	statusOK := res.StatusCode >= 200 && res.StatusCode <= 299
	if err != nil || !statusOK {
		fields["error"] = err
		fields["req-body"] = ctx.request.Body
		fields["req-query"] = ctx.request.QueryStringParameters
		fields["res-body"] = res.Body
	}
	ctx.Context.WithFields(fields).Infof("Request & Response: err=%+v", err)
}

//allocate struct for params, populate it from the URL parameters then validate and return the struct
func (ctx apiContext) GetRequestParams(paramsStructType reflect.Type) (interface{}, error) {
	paramValues := map[string]interface{}{}
	for n, v := range ctx.request.QueryStringParameters {
		paramValues[n] = v
	}
	paramsStructValuePtr := reflect.New(paramsStructType)

	if err := ctx.extract("params", paramsStructType, paramsStructValuePtr.Elem()); err != nil {
		return nil, errors.Wrapf(err, "failed to put query param values into struct")
	}
	if err := ctx.applyClaim("params", paramsStructValuePtr.Interface()); err != nil {
		return nil, errors.Wrapf(err, "failed to fill claims on params")
	}
	if validator, ok := paramsStructValuePtr.Interface().(IValidator); ok {
		if err := validator.Validate(); err != nil {
			return nil, errors.Wrapf(err, "invalid params")
		}
	}
	return paramsStructValuePtr.Elem().Interface(), nil
}

func (ctx apiContext) extract(name string, t reflect.Type, v reflect.Value) error {
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		switch f.Type.Kind() {
		case reflect.Struct:
			if err := ctx.extract(name+"."+f.Name, t.Field(i).Type, v.Field(i)); err != nil {
				return errors.Wrapf(err, "failed to fill sub %s.%s", name, f.Name)
			}
			continue
		default:
		}

		n := (strings.SplitN(f.Tag.Get("json"), ",", 2))[0]
		if n == "" {
			n = strings.ToLower(f.Name)
		}
		if n == "" || n == "-" {
			continue
		}

		//get value(s) from query string
		var paramStrValues []string
		if paramStrValue, isDefined := ctx.request.QueryStringParameters[n]; isDefined {
			if len(paramStrValue) >= 2 && paramStrValue[0] == '[' && paramStrValue[len(paramStrValue)-1] == ']' {
				paramStrValues = strings.Split(paramStrValue[1:len(paramStrValue)-1], ",") //from [CSV]
			} else {
				paramStrValues = []string{paramStrValue} //single value
			}
		} else {
			paramStrValues = ctx.request.MultiValueQueryStringParameters[n]
		}
		if len(paramStrValues) == 0 {
			continue //param has no value specified in URL
		}

		//param is defined >=1 times in URL
		if f.Type.Kind() == reflect.Slice {
			//iterate over all specified values
			for index, paramStrValue := range paramStrValues {
				newValuePtr := reflect.New(f.Type.Elem())
				if err := reflection.SetValue(newValuePtr.Elem(), paramStrValue); err != nil {
					return errors.Wrapf(err, "failed to set %s[%d]=%s", n, index, paramStrValues[0])
				}
				v.Field(i).Set(reflect.Append(v.Field(i), newValuePtr.Elem()))
			}
		} else {
			if len(paramStrValues) > 1 {
				return errors.Errorf("%s does not support >1 values(%v)", n, strings.Join(paramStrValues, ","))
			}
			//single value specified
			if err := reflection.SetValue(v.Field(i), paramStrValues[0]); err != nil {
				return errors.Wrapf(err, "failed to set %s=%s", n, paramStrValues[0])
			}
		}
	} //for each param struct field
	return nil
}

func (ctx apiContext) GetRequestBody(requestStructType reflect.Type) (interface{}, error) {
	requestStructValuePtr := reflect.New(requestStructType)
	err := json.Unmarshal([]byte(ctx.request.Body), requestStructValuePtr.Interface())
	if err != nil {
		return nil, errors.Wrapf(err, "failed to JSON request body")
	}

	if err := ctx.applyClaim("body", requestStructValuePtr.Interface()); err != nil {
		return nil, errors.Wrapf(err, "failed to fill claims on body")
	}
	if validator, ok := requestStructValuePtr.Interface().(IValidator); ok {
		if err := validator.Validate(); err != nil {
			return nil, errors.Wrapf(err, "invalid request body")
		}
	}
	return requestStructValuePtr.Elem().Interface(), nil
}

type IValidator interface {
	Validate() error
}

func (ctx *apiContext) applyClaim(name string, valuePtr interface{}) error {
	t := reflect.TypeOf(valuePtr)
	if t.Kind() != reflect.Ptr {
		return errors.Errorf("%T is not a pointer", valuePtr) //programming error... it must be a pointer to be able to change it
	}
	t = t.Elem()
	if t.Kind() != reflect.Struct {
		//ctx.Debugf("Not setting claims on %T", valuePtr)
		return nil //not a struct - nothing to do - is allowed e.g. for posting a string.
	}
	if err := ctx.setClaim(name, t, reflect.ValueOf(valuePtr).Elem()); err != nil {
		return errors.Wrapf(err, "failed to set claim on %T", valuePtr)
	}
	return nil
}

func (ctx *apiContext) setClaim(name string, structType reflect.Type, structValue reflect.Value) error {
	if len(ctx.Claim()) == 0 {
		ctx.Debugf("NO CLAIM to apply to %s of type (%s)", name, structType.Name())
		return nil
	}

	for fieldName, claimValue := range ctx.Claim() {
		if field := structValue.FieldByName(fieldName); field.IsValid() {
			if err := reflection.SetValue(field, claimValue); err != nil {
				return errors.Errorf("failed to set %s.%s=(%T)%v", structType.Name(), fieldName, claimValue, claimValue)
			}
			ctx.Debugf("defined claim %s.%s=(%T)%v ...", name, fieldName, claimValue, claimValue)
			// } else {
			// 	ctx.Debugf("claim(%s) does not apply to %s", fieldName, structType.Name())
		}
	}

	//recurse into sub-structs and sub struct ptrs (not yet slices)
	for i := 0; i < structType.NumField(); i++ {
		f := structType.Field(i)
		if len(f.Name) > 0 && f.Name[0] >= 'a' && f.Name[0] <= 'z' {
			//private field - do not enter
			continue
		}
		if f.Type.Kind() == reflect.Struct {
			if err := ctx.setClaim(name+"."+structType.Field(i).Name, f.Type, structValue.Field(i)); err != nil {
				return errors.Wrapf(err, "failed to set claim on sub struct %s.%s", structType.Name(), f.Name)
			}
		}
		if f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && !structValue.Field(i).IsNil() {
			if err := ctx.setClaim(name+"."+structType.Field(i).Name, f.Type.Elem(), structValue.Field(i).Elem()); err != nil {
				return errors.Wrapf(err, "failed to set claim on sub &struct %s.%s", structType.Name(), f.Name)
			}
		}
	}
	return nil
}