package api

import (
	"context"
	"net/http"
	"reflect"
	"runtime/debug"
	"strings"
	"time"

	sentrygin "github.com/getsentry/sentry-go/gin"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/pingcap/dbaas/admin/consts"
	_ "github.com/pingcap/dbaas/admin/docs"
	"github.com/pingcap/dbaas/admin/errs"
	"github.com/pingcap/dbaas/admin/forms"
	"github.com/pingcap/dbaas/admin/models"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/db/tenant"
	"github.com/pingcap/dbaas/central/metrics"
	"github.com/pingcap/dbaas/central/model/admin"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	swaggerFiles "github.com/swaggo/files"     // swagger embed files
	ginSwagger "github.com/swaggo/gin-swagger" // gin-swagger middleware
	"github.com/tidbcloud/idl/pbgen/proto/base"
	httpmiddleware "github.com/tidbcloud/pkgv2/http/middleware"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	ginprometheus "github.com/zsais/go-gin-prometheus"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

var (
	// global variable to indicate if this program run as test
	// NOTE that if this option set to true, all the check for user/api key is shutdown, and will
	// return fake api key & user below.
	isUnitTest = false
	fakeAPIKey *entity.APIKey
	fakeUser   *tenant.User
	fakeRole   *entity.Role
)

// @title Management Portal API
// @version 1.0
// @description This is a management portal server

// @host 127.0.0.1:8080
// @BasePath /api/v1/admin
func InitRouter() *gin.Engine {
	r := gin.Default()
	addMiddlewares(r)

	if !isUnitTest {
		metrics.RegisterMetrics()
		// prometheus
		p := ginprometheus.NewPrometheus("gin")
		p.ReqCntURLLabelMappingFn = func(c *gin.Context) string {
			url := c.Request.URL.Path
			for _, p := range c.Params {
				switch p.Key {
				case "cluster_id":
					url = strings.Replace(url, p.Value, ":cluster_id", 1)
				case "org_id":
					url = strings.Replace(url, p.Value, ":org_id", 1)
				case "user_id":
					url = strings.Replace(url, p.Value, ":user_id", 1)
				case "role_id":
					url = strings.Replace(url, p.Value, ":role_id", 1)
				case "provider":
					url = strings.Replace(url, p.Value, ":provider", 1)
				case "region_id":
					url = strings.Replace(url, p.Value, ":region_id", 1)
				case "profile_id":
					url = strings.Replace(url, p.Value, ":profile_id", 1)
				case "disk_id":
					url = strings.Replace(url, p.Value, ":disk_id", 1)
				case "discount_id":
					url = strings.Replace(url, p.Value, ":discount_id", 1)
				case "machine_id":
					url = strings.Replace(url, p.Value, ":machine_id", 1)
				}
			}
			return url
		}
		p.Use(r)

		r.Use(cors.New(cors.Config{
			AllowOrigins:     []string{"*"},
			AllowMethods:     []string{"PUT", "PATCH", "GET", "POST", "DELETE"},
			AllowHeaders:     []string{"Origin", "Authorization", "X-Requested-With", "Content-Type", "Accept"},
			ExposeHeaders:    []string{"Content-Length"},
			AllowCredentials: true,
			AllowOriginFunc: func(origin string) bool {
				return true
			},
			MaxAge: 12 * time.Hour,
		}))
	}

	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
	r.GET("/ping", Ping)    // for health check in k8s
	r.GET("/healthz", Ping) // for health check in k8s

	externalOrgGroup := r.Group("/external-api/v1/orgs")
	if isUnitTest {
		externalOrgGroup.Use(SetFakeUnitTestAPIKey(fakeAPIKey))
	} else {
		externalOrgGroup.Use(MustHaveAPIKey())
	}
	{
		externalOrgGroup.PUT("/:org_id/poc", UpdateOrganizationPocStatus)
		externalOrgGroup.POST("/profile/replace_node_and_sku", ReplaceNodeAndSKUByYaml)
	}

	// this is a public API
	//r.GET("/external-api/v1/cluster_profiles/:provider/profiles", GetPublicClusterProfiles)

	adminGroup := r.Group("/api/v1/admin")
	if isUnitTest {
		adminGroup.Use(SetFakeUnitTestUser(fakeUser))
	} else {
		adminGroup.Use(MustHaveAdminPermissions())
	}
	{
		adminGroup.GET("/system-settings", GetSystemSettings)
		adminGroup.PUT("/system-settings", UpdateSystemSettings)
		adminGroup.GET("/dashboard/stats", GetDashboardStats)

		adminGroup.DELETE("/orgs/:org_id/projects/:project_id", DeleteProject)
		adminGroup.GET("/orgs/:org_id/users", ListTenantUsers)
		adminGroup.POST("/orgs/:org_id/salesforce", SalesforceSyncLeads)
		adminGroup.POST("/orgs/:org_id/users", AddTenantUser)
		adminGroup.PUT("/orgs/:org_id/users/:user_id", UpdateTenantUser)
		adminGroup.DELETE("/orgs/:org_id/users/:user_id", DeleteTenantUser)
		adminGroup.GET("/orgs/:org_id/users/:user_id", GetTenantUser)
		adminGroup.GET("/orgs/:org_id/clusters", ListTenantClusters)
		adminGroup.DELETE("/orgs/:org_id/clusters/:cluster_id", DeleteTenantCluster)
		adminGroup.PUT("/orgs/:org_id/clusters/:cluster_id/components", ScaleTenantCluster)
		adminGroup.GET("/orgs/:org_id/clusters/:cluster_id/dashboard_links", GetDashboardLinks)
		adminGroup.GET("/orgs/:org_id/clusters/:cluster_id/tcu", GetClusterTCU)
		adminGroup.PUT("/orgs/:org_id/clusters/:cluster_id/tcu", UpdateClusterTCU)

		adminGroup.POST("/users", AddCentralUser)
		adminGroup.GET("/users", ListCentralUsers)
		adminGroup.DELETE("/users/:user_id", DeleteCentralUser)
		adminGroup.GET("/users/:user_id", GetCentralUser)
		adminGroup.PUT("/users/:user_id", UpdateCentralUser)
		adminGroup.POST("/roles", AddCentralRole)
		adminGroup.GET("/roles", ListCentralRoles)
		adminGroup.DELETE("/roles/:role_id", DeleteCentralRole)
		adminGroup.GET("/roles/:role_id", GetCentralRole)
		adminGroup.PUT("/roles/:role_id", UpdateCentralRole)
		adminGroup.PATCH("/orgs/:org_id", PatchTenant)
		adminGroup.POST("/orgs/:org_id/impersonation", AddImpersonation)
		adminGroup.GET("/orgs/:org_id/access-logs", TenantAccessLogs)
		adminGroup.DELETE("/orgs/:org_id/impersonation", DeleteImpersonation)
		adminGroup.DELETE("/orgs/:org_id", DeleteTenant)
		adminGroup.GET("/access-logs", ListAccessLogs)
		adminGroup.GET("/me", GetCentralUserProfile)
		adminGroup.POST("/api_key", CreateAPIKey)

		adminGroup.GET("/orgs", ListTenants)
		adminGroup.GET("/orgs/:org_id", GetTenantById)
		adminGroup.GET("/orgs/:org_id/cluster-lifecycles", ListTenantClusterLifeCycle)

		// region
		adminGroup.POST("/providers", AddProvider)
		adminGroup.GET("/providers", ListProviders)

		adminGroup.POST("/providers/:provider/regions", AddRegion)
		adminGroup.PUT("/providers/:provider/regions/:region_id", UpdateRegion)
		adminGroup.GET("/providers/:provider/regions", ListRegions)
		//adminGroup.DELETE("/providers/:provider/regions/:region_id", DeleteRegion) // disable delete region
		adminGroup.GET("/providers/:provider/regions/:region_id", GetRegionByID)
		adminGroup.POST("/profile/query", CalcNodeProfiles)

		// file upload
		adminGroup.POST("/upload", UploadFile)
		adminGroup.GET("/uploaded/*file_path", DownloadFile)

		// billing apis
		adminGroup.Any("/api_proxy/billing/*any", BillingProxy)
		RegisterBillingAPI(adminGroup)
	}

	return r
}

func Success(c *gin.Context, result interface{}, code int) {
	if result == nil {
		result = gin.H{}
	}

	c.JSON(code, result)
}

func isBadRequest(err error) bool {
	if err == nil {
		return false
	}

	msg := err.Error()
	// have a look at github.com/pingcap/errors/juju_adaptor.go
	if strings.Contains(msg, " bad request") {
		return true
	} else if strings.Contains(msg, " not valid") {
		return true
	} else if strings.Contains(msg, "already exists") {
		return true
	} else if strings.Contains(msg, " not supported") {
		return true
	}

	return false
}

func isUnauthorized(err error) bool {
	if err == nil {
		return false
	}

	switch err {
	case errs.ErrInvalidToken, errs.ErrUnauthorized, errs.ErrTokenNotFound:
		return true
	default:
		return false
	}
}

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

	if err != nil {
		if err == gorm.ErrRecordNotFound {
			code = http.StatusNotFound
			msg = "record not found"
		} else if isUnauthorized(err) {
			code = http.StatusUnauthorized
		} else if isBadRequest(err) {
			code = http.StatusBadRequest
		} else if errors.IsNotFound(err) {
			code = http.StatusNotFound
		}
	}

	log.Info(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 {
			msg = e.Error()

			c.JSON(code, gin.H{"code": code, "message": msg})
			return
		}
	}

	if code >= 500 {
		log.Info(ctx, "internal server error", zap.Error(err))
		msg = "internal server error"
	} else {
		if msg == "" && err != nil {
			msg = err.Error()
		}
	}

	c.JSON(code, gin.H{"code": code, "message": msg})
}

func GetAPIKey(c *gin.Context) (*entity.APIKey, error) {
	api, ok := c.Get(consts.CONTEXT_API_KEY)
	if !ok {
		return nil, errs.ErrAPIKeyNotFound
	}

	apiKey, ok := api.(*entity.APIKey)
	if !ok {
		return nil, errs.ErrAPIKeyNotFound
	}

	return apiKey, nil
}

// GetUserContext NOTE THAT *MAKE SURE* the api you visit *HAD ALREADY* set current user in context
// or this function will return a nil pointer, which means this function should use only if you've
// decorate your routes by middlewares.MustHaveAdminPermissions
func GetUserContext(c *gin.Context) *admin.UserContext {
	ctx := c.Request.Context()
	user, ok := c.Get(consts.CONTEXT_CURRENT_USER)
	if !ok {
		return nil
	}

	tenantUser, ok := user.(*tenant.User)
	if !ok {
		return nil
	}

	m := models.GetAdminModel()
	userContext := m.User(ctx, tenantUser)

	return &userContext
}

func GetLimitAndOffset(c *gin.Context) forms.LimitAndOffset {
	ctx := c.Request.Context()
	pageReq := forms.PaginationReq{}
	if err := c.ShouldBindQuery(&pageReq); err != nil {
		log.Error(ctx, "failed to bind pageReq", zap.Error(err))
	}

	limit := int(pageReq.PerPage)
	offset := int(pageReq.Page-1) * limit

	return forms.LimitAndOffset{Limit: limit, Offset: offset}
}

func GetPager(c *gin.Context) *base.Pager {
	ctx := c.Request.Context()
	pageReq := forms.PaginationReq{}
	if err := c.ShouldBindQuery(&pageReq); err != nil {
		log.Error(ctx, "failed to bind pageReq", zap.Error(err))
	}

	return &base.Pager{
		Page:     int64(pageReq.Page),
		PageSize: int64(pageReq.PerPage),
	}
}

func Pagination(items interface{}, limitOffset forms.LimitAndOffset) interface{} {
	typ := reflect.TypeOf(items)
	if typ.Kind() != reflect.Slice {
		log.Error(context.TODO(), "items is not slice")
		return items
	}

	v := reflect.ValueOf(items)

	length := v.Len()
	if length == 0 {
		return items
	}

	left := limitOffset.Offset
	if left >= length {
		return reflect.MakeSlice(typ, 0, 0).Interface()
	}

	right := left + limitOffset.Limit
	if right > length {
		right = length
	}

	result := reflect.MakeSlice(typ, 0, right-left)
	for i := left; i < right; i++ {
		result = reflect.Append(result, v.Index(i))
	}

	return result.Interface()
}

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
}
