package ginutils

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/utils/semaphore"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/sentry"
	"go.uber.org/zap"
)

func ErrorMiddleWare(c *gin.Context) {
	defer func() {
		var err error = nil
		if recover_err := recover(); recover_err != nil {
			if re, ok := recover_err.(error); ok {
				err = billing_errors.NewSystemError(re)
			} else {
				err = billing_errors.NewSystemError(fmt.Errorf("%v", recover_err))
			}
		}
		if err == nil {
			new_err := c.Errors.Last()
			c.Errors = nil
			if new_err == nil {
				return
			}
			err = new_err
		}
		billingError := &billing_errors.BaseError{}
		if errors.As(err, billingError) {
			msg := ""
			if billingError.Is(billing_errors.SystemError) {
				sentry.CaptureError(err)
				log.Info("gin return system server error", zap.String("request", c.Request.URL.Path), zap.Error(err))
				msg = "System Error"
			} else {
				log.Info("gin return error", zap.String("request", c.Request.URL.Path), zap.Error(err))
				msg = billingError.Error()
			}
			if config.Get().GetBool(config.EnableAPIStackTrace) {
				c.JSON(billingError.Status, gin.H{
					"error":      billingError.Error(),
					"stacktrace": fmt.Sprintf("%+v", billingError.Unwrap()),
					"code":       billingError.Code,
					"message":    msg,
				})
			} else {
				c.JSON(billingError.Status, gin.H{
					"error":   billingError.Error(),
					"code":    billingError.Code,
					"message": msg,
				})
			}

		} else {
			log.Error("gin return generic error", zap.String("request", c.Request.URL.Path), zap.Error(err))
			c.JSON(http.StatusInternalServerError, gin.H{
				"error": err.Error(),
			})
		}
	}()
	c.Next()
}

// so ugly for conflict in golang lint and gin standard error handle.
func ReturnError(ctx *gin.Context, err error) {
	ctx.Error(err) // nolint: errcheck
}

type HandlerFuncResult interface{}

type HandlerFunc func(c *gin.Context) (HandlerFuncResult, error)

type Route gin.IRoutes

type MyRoute struct {
	Route

	DisableFilterByQueryMiddleWare bool
}

type WrapHandler struct {
	handlerFunc HandlerFunc

	DisableFilterByQueryMiddleWare bool
}

func (handler *WrapHandler) Handle(c *gin.Context) {
	res, err := handler.handlerFunc(c)
	if err != nil {
		ReturnError(c, err)
		return
	}
	if !handler.DisableFilterByQueryMiddleWare {
		res = FilterByQueryMiddlwWare(c, res)
	}

	c.JSON(http.StatusOK, res)
}

func (r MyRoute) HandleWithError(method string, path string, handlerFunc HandlerFunc) {
	r.Handle(method, path, (&WrapHandler{handlerFunc: handlerFunc, DisableFilterByQueryMiddleWare: r.DisableFilterByQueryMiddleWare}).Handle)
}

func (r MyRoute) HandleWithErrorWithoutQueryFilter(method string, path string, handlerFunc HandlerFunc) {
	r.Handle(method, path, (&WrapHandler{handlerFunc: handlerFunc, DisableFilterByQueryMiddleWare: true}).Handle)
}

type FilterResult map[string]json.RawMessage

func FilterByQuery(results []FilterResult, filters map[string]string) []FilterResult {
	res := []FilterResult{}
	for _, result := range results {
		filtered := false
		for filter, value := range filters {
			valueStr := string(result[filter])
			_ = json.Unmarshal(result[filter], &valueStr)

			if valueStr != value {
				filtered = true
				continue
			}
		}
		if !filtered {
			res = append(res, result)
		}
	}
	return res
}

var (
	QueryWhiteList = []string{"orderby", "sortby"}
)

func FilterByQueryMiddlwWare(c *gin.Context, res HandlerFuncResult) HandlerFuncResult {
	query := c.Request.URL.Query()
	filter := map[string]string{}
	for k, v := range query {
		if len(v) != 1 || v[0] == "" {
			continue
		}
		filter[k] = v[0]
	}

	if len(query) == 0 {
		return res
	}

	bytes, err := json.Marshal(res)
	if err != nil {
		return res
	}

	filterListRes := []FilterResult{}

	err = json.Unmarshal(bytes, &filterListRes)
	if err != nil {
		return res
	}

	result := FilterByQuery(filterListRes, filter)
	return result
}

func (r MyRoute) HandleWithSemaphore(method string, path string, handlerFunc gin.HandlerFunc, sem *semaphore.Semaphore) {
	r.Handle(method, path, func(c *gin.Context) {
		semaphore.ExecutionWithSemaphore(sem, func() { handlerFunc(c) })
	})
}

func (r MyRoute) HandleWithErrorAndSemaphore(method string, path string, handlerFunc HandlerFunc, sem *semaphore.Semaphore) {
	r.HandleWithSemaphore(method, path, (&WrapHandler{handlerFunc: handlerFunc}).Handle, sem)
}
