package api

import (
	"context"
	"net/http"
	"reflect"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/core-portal/internal/admin/admin"
	"github.com/tidbcloud/core-portal/internal/admin/consts"
	"github.com/tidbcloud/core-portal/internal/admin/errs"
	"github.com/tidbcloud/core-portal/internal/admin/forms"
	"github.com/tidbcloud/core-portal/internal/admin/models"
	"github.com/tidbcloud/core-portal/internal/central/db/tenant"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/base"
	entity "github.com/tidbcloud/core-svc/model"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// @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 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 FailFromBizErr(c *gin.Context, err bizerr.BizError) {
	if err == nil {
		return
	}

	logger := log.WithContextS(c.Request.Context())
	logger.Infof("request failed,err=%s", err)

	resp := &forms.CodeMsgResponse{}
	err = err.Redact()
	resp.Code = int64(err.ErrCode())
	resp.Msg = err.ErrMsg()
	c.JSON(err.HttpStatus(), resp)
}

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 InitAdminRouter(ginEng *gin.Engine) {
	// NOTE: we add cors middleware in the top level of the gin engine.
	externalOrgGroup := ginEng.Group("/external-api/v1/orgs")
	externalOrgGroup.Use(MustHaveAPIKey())
	{
		externalOrgGroup.POST("/profile/replace_node_and_sku", ReplaceNodeAndSKUByYaml)
	}

	adminGroup := ginEng.Group("/api/v1/admin")
	adminGroup.Use(MustHaveAdminPermissions())
	{
		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.POST("/orgs/:org_id/clusters/:cluster_id/pause", PauseTenantCluster)
		adminGroup.POST("/orgs/:org_id/clusters/:cluster_id/resume", ResumeTenantCluster)
		adminGroup.GET("/orgs/:org_id/clusters/:cluster_id/dashboard_links", GetDashboardLinks)
		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.GET("/providers/:provider/regions/:region_id", GetRegionByID)
		adminGroup.POST("/profile/query", CalcNodeProfiles)

	}
}
