package api

import (
	"crypto/tls"
	"fmt"
	"net/http"
	"runtime/debug"

	sentrygin "github.com/getsentry/sentry-go/gin"
	"github.com/gin-gonic/gin"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/internal-portal/api/types"
	"github.com/tidbcloud/internal-portal/apiserver/auth"
	infraapi "github.com/tidbcloud/internal-portal/core-portal/central/infra-api"
	"github.com/tidbcloud/internal-portal/temporary/rbac"
	httpmiddleware "github.com/tidbcloud/pkgv2/http/middleware"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"github.com/tidbcloud/pkgv2/secret/encrypt"
	ginprometheus "github.com/zsais/go-gin-prometheus"
	"go.uber.org/zap"
)

var (
	encryptor encrypt.Encryptor
	infraAPI  *infraapi.API
)

func InitRouter(
	a *auth.Auth,
	tlsConfig *tls.Config,
	api *infraapi.API,
	enc encrypt.Encryptor,
) *gin.Engine {
	r := gin.Default()
	addMiddlewares(r)
	r.Use(mwCORS())

	p := ginprometheus.NewPrometheus("gin")
	p.Use(r)

	// set infra api
	infraAPI = api
	encryptor = enc

	// status apis for health check
	r.GET("/ping", Ping)    // for health check in k8s
	r.GET("/healthz", Ping) // for health check in k8s

	// useralert apis
	grpUserAlertV1 := r.Group("/api/useralerts/v1")
	grpUserAlertV1.POST("/alert", FireAlert)

	// userdebug apis
	grpUserDebugV1 := r.Group("/api/userdebug/v1")
	grpUserDebugV1.Use(a.Auth, rbac.Useadapter())

	grpUserDebugV1.GET("/:organization/features",
		getUserDebugFeatures)
	grpUserDebugV1.GET("/:organization/authorizations",
		getUserDebugAuthList)
	grpUserDebugV1.GET("/:organization/:cluster/authorization/details/:auth_id",
		getUserDebugAuthDetail)
	grpUserDebugV1.GET("/:organization/:cluster/authorization",
		getUserDebugAuthCode)
	grpUserDebugV1.POST("/:organization/:cluster/authorization",
		postUserDebugAuth(tlsConfig))
	grpUserDebugV1.DELETE("/:organization/:cluster/authorization",
		deleteUserDebugAuth)
	grpUserDebugV1.GET("/:organization/:cluster/authorization/records",
		getUserDebugAuthRecords)
	grpUserDebugV1.GET("/:organization/:cluster/authorization/records/:auth_id",
		getUserDebugAuthRecordsForAuth)

	return r
}

func addMiddlewares(engine *gin.Engine) *gin.Engine {
	engine.Use(
		logid.InjectLogid4GinMW,
		gin.CustomRecovery(func(c *gin.Context, e interface{}) {
			logger := log.WithContextL(c.Request.Context())
			if err, ok := e.(error); ok && errors.HasStack(err) {
				logger.Sugar().Warnf("panic, err: %s, stack: %s", err, errors.GetStackTracer(err).StackTrace())
			} else {
				logger.Sugar().Warnf("panic, err: %s, stack: %s", e, debug.Stack())
			}
			c.AbortWithStatus(http.StatusInternalServerError)
		}),
		httpmiddleware.TraceRequest,
		// repanic, let it catch by gin.CustomRecovery
		sentrygin.New(sentrygin.Options{Repanic: true}),
	)

	return nil
}

// Auth middleware do not authenticate OPTIONS request, which violates the requirements of tenant proxy, so
// we handle CORS of options in prior
func mwCORS() gin.HandlerFunc {
	return func(c *gin.Context) {
		if c.Request.Method == http.MethodOptions {
			c.Writer.Header().Add("Access-Control-Allow-Headers", "*")
			c.Writer.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
			c.Writer.Header().Add("Access-Control-Allow-Origin", "*")
			c.Writer.Header().Add("Access-Control-Allow-Credentials", "true")
			c.AbortWithStatus(http.StatusOK)
			return
		}
		c.Writer.Header().Add("Access-Control-Allow-Origin", "*")
		c.Next()
	}
}

func Success(c *gin.Context, code int, message string) {
	c.JSON(code, types.ResponseMessage{
		Code:    int64(code),
		Message: message,
	})
}

func Fail(c *gin.Context, msg string, err error, code int) {
	ctx := c.Request.Context()

	log.Error(ctx, "got error", zap.Error(err), zap.Int("code", code))
	if e, ok := err.(errcode.ErrorCode); ok {
		if code := e.Code().HTTPCode(); code >= 400 && code < 500 {
			c.JSON(code, types.ResponseMessage{Code: int64(code), Message: e.Error()})
			return
		}
	}

	if code >= 500 {
		log.Info(ctx, fmt.Sprintf("internal server error: %s", msg), zap.Error(err))
		msg = fmt.Sprintf("%s, please check the cluster status or contact support.", msg)
	}

	c.JSON(code, types.ResponseMessage{Code: int64(code), Message: msg})
}

func Ping(c *gin.Context) {
	shouldFail := c.Query("shouldFail")
	if shouldFail != "" {
		Fail(c, "", nil, http.StatusBadRequest)
		return
	}

	Success(c, http.StatusOK, "pong")
}
