package api

import (
	"context"
	"fmt"
	"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/config"
	entity "github.com/tidbcloud/core-svc/model"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"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 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
		}
	}

	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()
	v, ok := c.Get(consts.CONTEXT_CURRENT_USER)
	if !ok {
		return nil
	}

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

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

	return &userContext
}

func MustGetUserFromContext(c *gin.Context) string {
	v, ok := c.Get(consts.CONTEXT_CURRENT_USER)
	if !ok {
		panic("no user found in gin context")
	}

	email, ok := v.(string)
	if !ok {
		panic("the user is not auth.User")
	}
	return email
}

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/upsert", degradationCheckWrapper(UpsertProfileByYaml))
		externalOrgGroup.POST("/profile/sync_infra_profile", degradationCheckWrapper(SyncInfraProfile))
		externalOrgGroup.GET("/profile/sku_synced", CheckSkusSynced)
	}

	adminGroup := ginEng.Group("/api/v1/admin")
	adminGroup.Use(MustHaveAdminPermissions())
	{
		adminGroup.GET("/orgs/:org_id/clusters", ListTenantClusters)
		adminGroup.GET("/clusters4configservice", ListClustersForConfigService)
		adminGroup.DELETE("/orgs/:org_id/clusters/:cluster_id", degradationCheckWrapper(DeleteTenantCluster))
		adminGroup.PUT("/orgs/:org_id/clusters/:cluster_id/components", degradationCheckWrapper(ScaleTenantCluster))
		adminGroup.POST("/orgs/:org_id/clusters/:cluster_id/pause", degradationCheckWrapper(PauseTenantCluster))
		adminGroup.POST("/orgs/:org_id/clusters/:cluster_id/resume", degradationCheckWrapper(ResumeTenantCluster))
		adminGroup.GET("/orgs/:org_id/clusters/:cluster_id/dashboard_links", GetDashboardLinks)
		adminGroup.GET("/orgs/:org_id/cluster-lifecycles", ListTenantClusterLifeCycle)

		// region
		adminGroup.POST("/providers", degradationCheckWrapper(AddProvider))
		adminGroup.GET("/providers", ListProviders)
		adminGroup.POST("/providers/:provider/regions", degradationCheckWrapper(AddRegion))
		adminGroup.PUT("/providers/:provider/regions/:region_id", degradationCheckWrapper(UpdateRegion))
		adminGroup.GET("/providers/:provider/regions", ListRegions)
		adminGroup.GET("/providers/:provider/regions/:region_id", GetRegionByID)
		adminGroup.POST("/profile/query", CalcNodeProfiles)

		// maintenance window
		adminGroup.POST("/maintenance-tasks", degradationCheckWrapper(AddMaintenanceTask))
	}
}

// isDegraded is used degrade all WRITE API in `admin`. To enable it only need to set `enable` to true.
func isDegraded() bool {
	return config.Global().Degradation.Enable
}

func degradationCheckWrapper(handler gin.HandlerFunc) gin.HandlerFunc {
	return func(c *gin.Context) {
		if isDegraded() {
			Fail(c, fmt.Errorf("system is under maintenance, please retry later"), http.StatusInternalServerError)
			return
		}

		handler(c)
	}
}
