package api

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

	usersvc2 "github.com/tidbcloud/account/internel/svc/user"

	"github.com/tidbcloud/account/internel/svc/admin"

	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/commons/metrics"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/transport/http/tigin"

	sentrygin "github.com/getsentry/sentry-go/gin"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"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/account/admin/docs"
	"github.com/tidbcloud/account/admin/errs"
	"github.com/tidbcloud/account/admin/forms"
	"github.com/tidbcloud/account/admin/models"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	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
	fakeUser          string
	fakeRole          *entity.Role
	RbacPathWhitelist = []string{
		`/db-users/:cluster_id/jwt`,
		`/db-users/:cluster_id/test-db-user`,
		`/db-users/:cluster_id/list`,
		`/api/v1/admin/db-users/feature/whitelist`,
	}
)

// @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 := tigin.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)
				case "id":
					url = strings.Replace(url, p.Value, ":id", 1)
				case "customer_id":
					url = strings.Replace(url, p.Value, ":customer_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

	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.GET("/orgs", ListTenants)
		adminGroup.GET("/orgs/:org_id", GetTenantById)
		adminGroup.GET("/orgs/:org_id/data-resource", GetTenantResource)
		adminGroup.PATCH("/orgs/:org_id", PatchTenant)
		adminGroup.DELETE("/orgs/:org_id", DeleteTenant)
		adminGroup.GET("/orgs/user-org", GetUserOrgs)

		// org user
		adminGroup.GET("/orgs/:org_id/users", GetOrgUsers)
		// only call by devops
		adminGroup.DELETE("/orgs/:org_id/users/:user_id", RemoveUserFromOrg)
		// only call by devops
		adminGroup.POST("/orgs/:org_id/users/:user_id", AddUserToOrg)

		adminGroup.POST("/orgs/:org_id/impersonation", AddImpersonation)
		adminGroup.DELETE("/orgs/:org_id/impersonation", DeleteImpersonation)
		adminGroup.POST("/orgs/:org_id/salesforce", SalesforceSyncLeads)

		adminGroup.GET("/orgs/:org_id/projects", GetOrgProjects)
		// only call by devops
		adminGroup.DELETE("/orgs/:org_id/projects/:project_id", DeleteProject)
		// project user
		adminGroup.GET("/orgs/:org_id/projects/:project_id/users", GetProjectUsers)

		// only call by devops
		adminGroup.POST("/orgs/:org_id/projects/:project_id/users/:user_id", AddUserToProject)
		// only call by devops
		adminGroup.DELETE("/orgs/:org_id/projects/:project_id/users/:user_id", RemoveUserFromeProject)
		adminGroup.GET("/orgs/:org_id/projects/:project_id/data-resource", GetProjectResource)

		// only call by devops
		adminGroup.DELETE("/users/:user_id", DeleteUserFromAllOrg)
		adminGroup.GET("/users", ListUsers)

		adminGroup.GET("/access-logs", ListAccessLogs)
		adminGroup.GET("/orgs/:org_id/access-logs", TenantAccessLogs)

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

		// rbac role
		adminGroup.POST("/rbac/role/create", CreateRbacRole)
		adminGroup.GET("/rbac/role/list", ListRbacRoles)
		adminGroup.GET("/rbac/role/get/:id", GetRbacRoleById)
		adminGroup.POST("/rbac/role/update", UpdateRbacRole)
		adminGroup.POST("/rbac/role/delete/:id", DeleteRbacRole)

		// rbac privilege
		adminGroup.POST("/rbac/privilege/create", CreateRbacPrivilege)
		adminGroup.GET("/rbac/privilege/list", ListRbacPrivileges)
		adminGroup.GET("/rbac/privilege/get/:id", GetRbacPrivilegeById)
		adminGroup.POST("/rbac/privilege/update", UpdateRbacPrivilege)
		adminGroup.POST("/rbac/privilege/delete/:id", DeleteRbacPrivilege)
		adminGroup.GET("/rbac/privilege/system-list", ListPrivilegeSystems)

		// rbac role-privilege
		adminGroup.POST("/rbac/role-privilege/create", CreateRbacRolePrivilege)
		adminGroup.POST("/rbac/role-privilege/add", AddRbacRolePrivilege)
		adminGroup.POST("/rbac/role-privilege/delete", DeleteRbacRolePrivilege)
		adminGroup.GET("/rbac/role-privilege/list/:role_id", ListRbacRoleRelatePrivileges)

		adminGroup.POST("/rbac/role-binding/create", CreateRbacRoleBinding)
		adminGroup.POST("/rbac/role-binding/delete", DeleteRbacRoleBinding)

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

		// msp new
		adminGroup.POST("/partner/msp/upload-logo", UploadMspLogo)
		adminGroup.POST("/partner/msp/create", CreateMspOrg)
		adminGroup.POST("/partner/msp/update", UpdateMspOrg)
		adminGroup.GET("/partner/msp/:org_id", GetMspByOrgId)
		adminGroup.GET("/partner/msp/:org_id/signup-url", GetSignupUrl)
		adminGroup.POST("/partner/msp/:org_id/create-apikey", CreateMspApiKey)

		// resale
		adminGroup.POST("/partner/resale/create", CreateResalePartnerOrg)
		adminGroup.GET("/partner/resale/:org_id/signup-url", GetResaleCustomerSignupUrl)

		// query partner's customers
		adminGroup.GET("/partner/:org_id/customers", GetCustomersByPartnerId)

		// partner  customer
		adminGroup.POST("/partner/partner-customer/create", CreateCustomer) // add partner_type
		adminGroup.DELETE("/partner/partner-customer/delete", DeleteCustomer)
		adminGroup.POST("/partner/partner-customer/cancel", CancelCustomer)
		adminGroup.GET("/partner/partner-customer/:org_id", GetCustomerByCustomerOrgId)

		// cppo
		adminGroup.POST("/partner/cppo/create", CreateCppoOrg)
		adminGroup.POST("/partner/mcpo/create", CreateMCPOResellerOrg)

		// test db user
		adminGroup.GET("/db-users/:cluster_id/list", ListDbUsers)
		adminGroup.POST("/db-users/:cluster_id/test", TestDbUser)
		adminGroup.POST("/db-users/:cluster_id/jwt", TestDbUserJwt)
		adminGroup.GET("/db-users/:cluster_id/jwt", GetDbUserJwt)

		adminGroup.POST("/db-users/feature/whitelist", ConfigFeatureWhitelist)

		// test api
		//adminGroup.GET("/users/test", TestCrudUser)

	}

	return r
}

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

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

	c.PureJSON(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 err == errs.ErrForbidden {
			code = http.StatusForbidden
		} else if isUnauthorized(err) {
			code = http.StatusUnauthorized
		} else if isBadRequest(err) {
			code = http.StatusBadRequest
		} else if errors.IsNotFound(err) {
			code = http.StatusNotFound
		}
	}

	tilog.Info(ctx, "got error", 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 {
		tilog.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 GetUserEmail(c *gin.Context) (string, bool) {
	if config.IsLocalEnv() {
		email := "letian.wang@pingcap.com"
		return email, true
	}
	v, ok := c.Get(string(auth0.AuthTokenClaimKey))

	if !ok {
		return "", false
	}
	email := v.(string)

	return email, true
}
func GetUserFromContext(c *gin.Context) (*entity.User, error) {
	ctx := c.Request.Context()
	//if config.IsLocalEnv() {
	//	email := "letian.wang@pingcap.com"
	//	return email, true
	//}
	v, ok := c.Get(string(auth0.AuthTokenClaimKey))

	if !ok {
		tilog.Info(ctx, "not fount from AuthTokenClaimKey")
		return nil, nil
	}
	tilog.Info(ctx, "get login email from Context", zap.Any("v", v))
	email := v.(string)
	userSvc, _ := usersvc2.GetUserSvc()
	user, err := userSvc.GetUserByEmail(ctx, email)

	return user, err
}

// 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
// decorated your routes by middlewares.MustHaveAdminPermissions
func GetUserContext(c *gin.Context) *admin.UserContext {
	ctx := c.Request.Context()
	//remove when  push prod
	email, ok := GetUserEmail(c)
	if !ok {
		return nil
	}

	m := models.GetAdminModel()
	// wangletian test
	//if email == "" {
	//	email = "letian.wang@pingcap.com"
	//}
	userContext := m.User(ctx, email)

	return &userContext
}

func GetLimitAndOffset(c *gin.Context) forms.LimitAndOffset {
	ctx := c.Request.Context()
	pageReq := forms.PaginationReq{}
	if err := c.ShouldBindQuery(&pageReq); err != nil {
		tilog.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 Pagination(ctx context.Context, items interface{}, limitOffset forms.LimitAndOffset) interface{} {
	typ := reflect.TypeOf(items)
	if typ.Kind() != reflect.Slice {
		tilog.Error(ctx, "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(
		gin.CustomRecovery(func(c *gin.Context, e interface{}) {
			ctx := c.Request.Context()
			//logger := tilog.WithContextL(ctx)
			if err, ok := e.(error); ok && errors.HasStack(err) {
				msg := fmt.Sprintf("panic, err: %s, stack: %s", err, errors.GetStackTracer(err).StackTrace())
				//logger.Sugar().Error("panic, err: %s, stack: %s", err, errors.GetStackTracer(err).StackTrace())
				tilog.Error(ctx, msg, zap.Error(err))
			} else {
				msg := fmt.Sprintf("panic, err: %s, stack: %s", err, debug.Stack())
				tilog.Error(ctx, msg, zap.Error(err))
				//logger.Sugar().Error("panic, err: %s, stack: %s", e, debug.Stack())
			}
			c.AbortWithStatus(http.StatusInternalServerError)
		}),
		// repanic, let it catch by gin.CustomRecovery
		sentrygin.New(sentrygin.Options{Repanic: true}),
	)
	return nil
}
