package api

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/http"
	"reflect"
	"time"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambdacontext"
	"gitlab.com/uafrica/go-utils/errors"
	"gitlab.com/uafrica/go-utils/logger"
)

func (api Api) NewContext(baseCtx context.Context, requestID string, request events.APIGatewayProxyRequest) (Context, error) {
	serviceContext, err := api.Service.NewContext(baseCtx, requestID, nil)
	if err != nil {
		return nil, err
	}

	return &apiContext{
		Context: serviceContext,
		request: request,
	}, nil
}

//this is native handler for lambda passed into lambda.Start()
//to run locally, this is called from app.ServeHTTP()
func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGatewayProxyRequest) (res events.APIGatewayProxyResponse, err error) {
	res = events.APIGatewayProxyResponse{
		StatusCode: http.StatusInternalServerError,
		Body:       "undefined response",
		Headers:    map[string]string{},
	}

	// Replace the proxy resource with the path, has some edge cases but works for our current API implementation
	// Edge case being that if have path params then specify those routes explicitly
	if apiGatewayProxyReq.Resource == "/{proxy+}" {
		apiGatewayProxyReq.Resource = apiGatewayProxyReq.Path
	}

	//get request-id from HTTP headers (used when making internal service calls)
	//if not defined in header, get the AWS request id from the AWS context
	requestID, ok := apiGatewayProxyReq.Headers[api.requestIDHeaderKey]
	if !ok || requestID == "" {
		if lambdaContext, ok := lambdacontext.FromContext(baseCtx); ok && lambdaContext != nil {
			requestID = lambdaContext.AwsRequestID
		}
	}

	// service context invoke the starters and could fail, e.g. if cannot connect to db
	Ctx, err = api.NewContext(baseCtx, requestID, apiGatewayProxyReq)
	if err != nil {
		return res, err
	}
	

	//report handler crashes
	if api.crashReporter != nil {
		defer api.crashReporter.Catch(Ctx)
	}

	defer func() {
		//set CORS headers on every response
		if api.cors != nil {
			for n, v := range api.cors.CORS() {
				res.Headers[n] = v
			}
		}
	}()

	defer func() {
		Ctx.LogAPIRequestAndResponse(res, err)
		if err != nil {
			Ctx.Errorf("failed: %+v", err)

			//try to retrieve HTTP code from error
			if withCause, ok := err.(errors.ErrorWithCause); ok && withCause.Code() != 0 {
				res.StatusCode = withCause.Code()
				err = withCause.Cause() //drop the http layers + up
			} else {
				//no HTTP code indicate in err,
				//see if there are SQL errors that could indicate code
				if code, ok := StatusCodeFromSQLError(err); ok {
					res.StatusCode = code
				}
			}

			errorMessage := fmt.Sprintf("%c", err)
			jsonError, _ := json.Marshal(map[string]interface{}{"message": errorMessage})
			res.Headers["Content-Type"] = "application/json"
			res.Body = string(jsonError)
			err = nil //never pass error back to lambda or http server
		}
		if api.requestIDHeaderKey != "" {
			res.Headers[api.requestIDHeaderKey] = Ctx.RequestID()
		}
		if err := api.Service.WriteValues(Ctx.StartTime(), time.Now(), Ctx.RequestID(), map[string]interface{}{
			"direction":  "incoming",
			"type":       "api",
			"request_id": Ctx.RequestID(),
			"request":    Ctx.Request(),
			"response":   res},
		); err != nil {
			Ctx.Errorf("failed to audit: %+v", err)
		}
	}()

	//Early return OPTIONS call
	if apiGatewayProxyReq.HTTPMethod == "OPTIONS" {
		res.StatusCode = http.StatusNoContent
		err = nil
		return
	}

	rand.Seed(time.Now().Unix())

	for checkName, check := range api.checks {
		var checkData interface{}
		checkData, err = check.Check(Ctx)
		if err != nil {
			err = errors.Wrapf(err, "%s", checkName)
			return
		}
		if err = Ctx.Set(checkName, checkData); err != nil {
			err = errors.Wrapf(err, "failed to set check(%s) data=(%T)%+v", checkName, checkData, checkData)
			return
		}
	}

	Ctx.Debugf("HTTP %s %s ...\n", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
	Ctx.WithFields(map[string]interface{}{
		"http_method":                Ctx.Request().HTTPMethod,
		"path":                       Ctx.Request().Path,
		"api_gateway_request_id":     Ctx.Request().RequestContext.RequestID,
		"user_cognito_auth_provider": Ctx.Request().RequestContext.Identity.CognitoAuthenticationProvider,
		"user_arn":                   Ctx.Request().RequestContext.Identity.UserArn,
	}).Infof("Start API Handler")

	//TODO:
	// // Get claims and check the status of the user
	// Ctx.Claims, err = api.RetrieveClaims(&apiGatewayProxyReq)
	// if err != nil {
	// 	return events.APIGatewayProxyResponse{
	// 		StatusCode: http.StatusBadRequest,
	// 		Body:       fmt.Sprintf("%v\n", err),
	// 		Headers:    utils.CorsHeaders(),
	// 	}, nil
	// }

	// if Ctx.Claims.UserID != nil {
	// 	userStatusResponse := checkUserStatus(Ctx.Claims)
	// 	if userStatusResponse != nil {
	// 		return *userStatusResponse, nil
	// 	}
	// }

	// permissionString := fmt.Sprintf("API_%s%s:%s", os.Getenv("MICRO_SERVICE_API_BASE_PATH"), apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod)
	// if !permissions.HasPermission(Ctx.Claims.Role, permissionString) {
	// 	response, _ := apierr.ClientError(http.StatusUnauthorized, fmt.Sprintf("You do not have access to the requested resource: %s", permissionString))
	// 	if Ctx.Claims.Role == nil {
	// 		Ctx.Errorf("%d :: %s: %v", *Ctx.Claims.RoleID, permissionString, fmt.Errorf("you have no role"))
	// 	} else if Ctx.Claims.RoleID == nil {
	// 		Ctx.Errorf("%s: you have no role ID", permissionString)
	// 	}
	// 	return response, nil
	// }

	//route on method and path
	resourceHandler, err := api.router.Route(apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod)
	if err != nil {
		err = errors.Wrapf(err, "invalid route")
		return
	}

	if legacyHandlerFunc, ok := resourceHandler.(func(req events.APIGatewayProxyRequest) (response events.APIGatewayProxyResponse, err error)); ok {
		Ctx.Debugf("Calling legacy handler...")
		return legacyHandlerFunc(apiGatewayProxyReq)
	}

	handler, ok := resourceHandler.(handler)
	if !ok {
		//should not get here if validateAPIEndpoints() is properly checking!
		err = errors.HTTP(http.StatusInternalServerError, errors.Errorf("invalid handler function %T", resourceHandler), "invalid routing")
	}

	//new type of handler function
	//allocate, populate and validate params struct
	paramsStruct, paramsErr := Ctx.GetRequestParams(handler.RequestParamsType)
	if paramsErr != nil {
		err = errors.HTTP(http.StatusBadRequest, paramsErr, "invalid parameters")
		return
	}
	//apply claims to params struct - TODO: Removed - see if cannot force to get claims from context always
	// if err = Ctx.Claims.FillOnObject(Ctx.request, &paramsStruct); err != nil {
	// 	err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on parameters")
	// 	return
	// }
	Ctx.Debugf("Params: (%T) %+v", paramsStruct, paramsStruct)

	args := []reflect.Value{
		reflect.ValueOf(paramsStruct),
	}

	var bodyStruct interface{}
	if handler.RequestBodyType != nil {
		//allocate, populate and validate request struct
		bodyStruct, err = Ctx.GetRequestBody(handler.RequestBodyType)
		if err != nil {
			err = errors.HTTP(http.StatusBadRequest, err, "invalid body")
			return
		}

		//apply claims to request struct - TODO: Removed - see if cannot force to get claims from context always
		// if err = Ctx.Claims.FillOnObject(Ctx.request, &bodyStruct); err != nil {
		// 	err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on body")
		// 	return
		// }

		Ctx.Debugf("Body: (%T) %+v", bodyStruct, bodyStruct)
		args = append(args, reflect.ValueOf(bodyStruct))
	}

	//call handler in a func with defer to catch potential crash
	Ctx.Infof("Calling handle %s %s ...", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
	var results []reflect.Value
	results, err = func() (results []reflect.Value, err error) {
		defer func() {
			if crashErr := recover(); crashErr != nil {
				stack := logger.CallStack()
				err = errors.Errorf("handler function crashed: %v, with stack: %+v", crashErr, stack)
				return
			}
		}()
		results = handler.FuncValue.Call(args)
		return results, nil
	}()
	if err != nil {
		err = errors.Wrapf(err, "handler failed")
		return
	}

	//Ctx.Debugf("handler -> results: %v", results)
	//see if handler failed using last result of type error
	lastResultValue := results[len(results)-1]
	if !lastResultValue.IsNil() {
		err = lastResultValue.Interface().(error)
		if err != nil {
			return
		}
	}

	//handler succeeded, some handler does not have a response data (typically post/put/patch/delete)
	err = nil
	switch apiGatewayProxyReq.HTTPMethod {
	case http.MethodDelete:
		res.StatusCode = http.StatusNoContent
	default:
		res.StatusCode = http.StatusOK
	}

	if len(results) > 1 {
		responseStruct := results[0].Interface()
		Ctx.Debugf("Response type: %T", responseStruct)

		var bodyBytes []byte
		bodyBytes, err = json.Marshal(responseStruct)
		if err != nil {
			err = errors.Wrapf(err, "failed to encode response content")
			return
		}
		res.Headers["Content-Type"] = "application/json"
		res.Body = string(bodyBytes)
	} else {
		//no content
		delete(res.Headers, "Content-Type")
		res.Body = ""
	}
	return
}

//look for SQL errors in the error stack
func StatusCodeFromSQLError(err error) (int, bool) {
	if err == sql.ErrNoRows {
		return http.StatusNotFound, true
	}

	if errWithCause, ok := err.(errors.ErrorWithCause); ok {
		return StatusCodeFromSQLError(errWithCause.Cause())
	}

	//could not determine known SQL error
	return 0, false
}