package api

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"sort"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/go-playground/validator/v10"
	"github.com/pingcap/errcode"
	"github.com/shopspring/decimal"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	entity "github.com/tidbcloud/core-svc/model"
	"github.com/tidbcloud/internal-portal/core-portal/admin/forms"
	"github.com/tidbcloud/internal-portal/core-portal/central/config"
	"github.com/tidbcloud/internal-portal/core-portal/central/grpc/grpcli"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"gopkg.in/yaml.v2"
)

type (
	SkuType string
)

const (
	SkuTypeNode           SkuType = "node"
	SkuTypeDiskIops       SkuType = "disk-iops"
	SkuTypeDiskThroughput SkuType = "disk-throughput"
	SkuTypeDiskStorage    SkuType = "disk-storage"
	SkuTypeBackupStorage  SkuType = "backup-storage"
	SkuTypeTraffic        SkuType = "traffic"

	DiskTypeLocalStorage = "local-storage"

	DeployTypeShared = "shared"
)

var (
	validate = validator.New()
)

func checkProfileManageApiKey(apiKeyEntity *entity.APIKey) error {
	if apiKeyEntity.Name != config.ProfileMngApiKeyName || apiKeyEntity.TenantID != entity.PingCAPTenantID {
		return errcode.NewNotAuthenticatedErr(fmt.Errorf("invalid api key"))
	}
	return nil
}

// @Summary ReplaceNodeAndSKUByYaml
// @Tags Profile
// @Description ReplaceNodeAndSKUByYaml
// @Accept  multipart/form-data
// @Produce json
// @Param files formData forms.SkuAndNode true "sku yaml file and node yaml file"
// @Success 200 {object} forms.ReplaceProfileResponse
// @Router /external-api/v1/orgs/profile/replace_node_and_sku [post]
func ReplaceNodeAndSKUByYaml(c *gin.Context) {
	ctx := c.Request.Context()

	if apiKey, err := GetAPIKey(c); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	} else if err := checkProfileManageApiKey(apiKey); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}

	versionId, err := replaceNodeAndSKUByYaml(ctx, c)
	resp := forms.ReplaceProfileResponse{
		VersionID: versionId,
	}
	statusCode := http.StatusOK
	if err != nil {
		err = err.Redact()
		resp.Code = int64(err.ErrCode())
		resp.Msg = err.ErrMsg()
		statusCode = err.HttpStatus()
	}
	c.JSON(statusCode, resp)
}

// @Summary CalcNodeProfiles
// @Tags Profile
// @Description CalcNodeProfiles
// @Accept  multipart/form-data
// @Produce json
// @Param files formData forms.CalcNodeProfileReq true "calc node profile"
// @Success 200 {object} forms.CalcNodeProfileResp
// @Router /api/v1/admin/profile/query [post]
func CalcNodeProfiles(c *gin.Context) {
	ctx := c.Request.Context()
	statusCode := http.StatusOK
	resp := &forms.CalcNodeProfileResp{}
	req := &forms.CalcNodeProfileReq{}

	if err := c.ShouldBindJSON(req); err != nil {
		resp.Code = int64(bizerr.ErrCodeParams)
		resp.Msg = fmt.Sprintf("invalid param: %s", err)
		c.JSON(statusCode, resp)
		return
	}

	profiles, err := calcNodeProfiles(ctx, req)
	if err != nil {
		err = err.Redact()
		resp.Code = int64(err.ErrCode())
		resp.Msg = err.ErrMsg()
		statusCode = err.HttpStatus()
	} else {
		resp.ComponentNodeProfiles = profiles
	}
	c.JSON(statusCode, resp)
}

type nodeDisk struct {
	InstanceType string `yaml:"type" validate:"oneof=gp2 gp3 pd-ssd local-storage"`
	Iops         int64  `yaml:"iops" validate:"gte=0"`
	ThroughputMi int64  `yaml:"throughputMBs" validate:"gte=0"`
	MinSizeGi    int64  `yaml:"minSizeGi" validate:"gte=0"`
	MaxSizeGi    int64  `yaml:"maxSizeGi" validate:"gte=0,gtefield=MinSizeGi"`
}

type pdNode struct {
	NodeName string `yaml:"nodeName" validate:"min=1"`
	NodeNum  int    `yaml:"nodeNum" validate:"gt=0"`
}

type nodeDetail struct {
	Name           string    `yaml:"name" validate:"min=1"`
	DisplayName    string    `yaml:"displayName" validate:"min=1"`
	InstanceType   string    `yaml:"type" validate:"min=1"`
	DeployType     string    `yaml:"topology" validate:"oneof=dedicated shared"`
	CpuNum         int64     `yaml:"cpu" validate:"gte=0"`      // cpuNum of devtier will be fixed 0
	MemoryGi       int64     `yaml:"memoryGi" validate:"gte=0"` // cpuNum of devtier will be fixed 0
	DefaultNodeNum int       `yaml:"defaultNodeNum" validate:"gte=0"`
	Disk           *nodeDisk `yaml:"disk" validate:"required"`
	PD             *pdNode   `yaml:"pd"`
}

type tenantInfo struct {
	TenantId   uint64 `yaml:"tenantId" validate:"min=1"`
	TenantName string `yaml:"tenantName" validate:"min=1"`
}

type nodeYaml struct {
	ComponentType        string        `yaml:"component" validate:"oneof=pd tidb tikv tidb tiflash"`
	CloudProvider        string        `yaml:"provider" validate:"oneof=aws gcp"`
	Regions              []string      `yaml:"regions" validate:"gt=0"`
	NonStandardCustomers []*tenantInfo `yaml:"nonStandardCustomers"`
	Annotation           string        `yaml:"annotation"`
	Node                 *nodeDetail   `yaml:"node" validate:"required"`
}

type resourceDetail struct {
	// for node
	ComputeInstanceType string `yaml:"instanceType" validate:"omitempty,min=1"`
	DeployType          string `yaml:"topology" validate:"omitempty,oneof=dedicated shared"`

	// for disk-iops,disk-throughput,disk-storage,
	DiskInstanceType string `yaml:"diskType" validate:"omitempty,oneof=gp2 gp3 pd-ssd local-storage"`
}

type conditionDiscount struct {
	Key       string `yaml:"key" json:"key"`
	Value     string `yaml:"value" json:"value"`
	Condition string `yaml:"condition" json:"condition"`
	Price     string `yaml:"price" json:"price"`
}

type multipleFactor struct {
	Label     string `yaml:"label"`
	Threshold int64  `yaml:"threshold"`
}

type skuExtra struct {
	ConditionDiscount *conditionDiscount `yaml:"conditionDiscount" json:"conditionDiscount"`
	MultipleFactor    *multipleFactor    `yaml:"multipleFactor" json:"multipleFactor"`
	Category          string             `yaml:"category" json:"category"`
	Description       string             `yaml:"description" json:"description"`
}

type skuYaml struct {
	CloudProvider  string          `yaml:"provider" validate:"oneof=aws gcp"`
	Region         string          `yaml:"region" validate:"min=1"`
	SkuType        SkuType         `yaml:"resourceType" validate:"oneof=node disk-iops disk-throughput disk-storage backup-storage traffic ticdc-tcu"`
	Price          string          `yaml:"price" validate:"numeric"`
	ResourceDetail *resourceDetail `yaml:"resourceDetail" validate:"required"`

	skuExtra `yaml:",inline"`
}

func replaceNodeAndSKUByYaml(ctx context.Context, c *gin.Context) (uint64, bizerr.BizError) {
	logger := log.WithContextL(ctx)
	skuYamlFile, _, e := c.Request.FormFile("skus.yaml")
	if e != nil {
		return 0, bizerr.New(bizerr.ErrCodeParams, "get sku.yaml: %s", e)
	}
	defer skuYamlFile.Close() // nolint: errcheck
	nodeYamlFile, _, e := c.Request.FormFile("nodes.yaml")
	if e != nil {
		return 0, bizerr.New(bizerr.ErrCodeParams, "get node.yaml: %s", e)
	}
	defer nodeYamlFile.Close() // nolint: errcheck
	commitId := strings.TrimSpace(c.Request.FormValue("commitId"))
	if commitId == "" {
		return 0, bizerr.New(bizerr.ErrCodeParams, "commitId field is empty")
	}
	dryrun, _ := strconv.ParseBool(c.Request.FormValue("dryrun"))
	logger.Info("params", zap.String("commitId", commitId), zap.Bool("dryrun", dryrun))

	skus, nodes, err := parseNodeAndSkuFiles(ctx, skuYamlFile, nodeYamlFile)
	if err != nil {
		return 0, err
	}
	logger.Info("parse yaml files success")

	req, err := buildReplaceProfileByYamlReq(commitId, skus, nodes, dryrun)
	if err != nil {
		return 0, err
	}
	logger.Info("build core request success")

	versionId, err := callCore(ctx, req)
	if err != nil {
		return 0, err
	}
	logger.Info("call core success")
	return versionId, nil
}

func parseNodeAndSkuFiles(ctx context.Context, skuYamlFile, nodeYamlFile io.ReadCloser) ([]*skuYaml, []*nodeYaml, bizerr.BizError) {
	// read all
	skuYamlContent, err := ioutil.ReadAll(skuYamlFile)
	if err != nil {
		return nil, nil, bizerr.New(bizerr.ErrCodeUnknown, "read skuYaml file: %s", err)
	}
	nodeYamlContent, err := ioutil.ReadAll(nodeYamlFile)
	if err != nil {
		return nil, nil, bizerr.New(bizerr.ErrCodeUnknown, "read nodeYaml file: %s", err)
	}

	// parse
	var (
		skus  []*skuYaml
		nodes []*nodeYaml
	)
	if err := yaml.Unmarshal(skuYamlContent, &skus); err != nil {
		return nil, nil, bizerr.New(bizerr.ErrCodeParams, "parse skuYaml: %s", err)
	}
	if err := yaml.Unmarshal(nodeYamlContent, &nodes); err != nil {
		return nil, nil, bizerr.New(bizerr.ErrCodeParams, "parse nodeYaml: %s", err)
	}

	// validate
	if err := validateNodesAndSkus(ctx, nodes, skus); err != nil {
		return nil, nil, err
	}
	return skus, nodes, nil
}

func validateNodesAndSkus(ctx context.Context, nodes []*nodeYaml, skus []*skuYaml) bizerr.BizError {
	logger := log.WithContextL(ctx)

	for _, sku := range skus {
		skuStr, _ := json.Marshal(sku)
		if err := validate.Struct(sku); err != nil {
			logger.Sugar().Infof("sku is invalid, err=%s,sku=%s", err, skuStr)
			return bizerr.New(bizerr.ErrCodeParams, "sku is invalid,sku=%s", skuStr)
		}

		// check price of sku, must gte 0
		if price, err := decimal.NewFromString(sku.Price); err != nil {
			logger.Sugar().Infof("sku.price is invalid, err=%s,sku=%s", err, skuStr)
			return bizerr.New(bizerr.ErrCodeParams, "sku is invalid,sku=%s", skuStr)
		} else if price.LessThan(decimal.Zero) {
			logger.Sugar().Infof("sku.price is less than 0,sku=%s", skuStr)
			return bizerr.New(bizerr.ErrCodeParams, "sku is invalid,sku=%s", skuStr)
		}

		// check resourceDetail.DiskInstanceType
		if (sku.SkuType == SkuTypeDiskIops || sku.SkuType == SkuTypeDiskStorage || sku.SkuType == SkuTypeDiskThroughput) &&
			sku.ResourceDetail.DiskInstanceType == "" {
			return bizerr.New(bizerr.ErrCodeParams, "must give diskType if resourceType is disk-XXX")
		}
		// check if sku.SkuType is node
		if (sku.SkuType == SkuTypeNode) &&
			(sku.ResourceDetail.ComputeInstanceType == "" || sku.ResourceDetail.DeployType == "") {
			return bizerr.New(bizerr.ErrCodeParams, "must give instanceType and topology if resourceType is node")
		}
	}
	for _, node := range nodes {
		if err := validate.Struct(node); err != nil {
			nodeStr, _ := json.Marshal(node)
			logger.Sugar().Infof("node is invalid, err=%s,node=%s", err, nodeStr)
			return bizerr.New(bizerr.ErrCodeParams, "node is invalid,node=%s", nodeStr)
		}

		// only gp3 support custom iops,throughput
		if node.Node.Disk != nil && node.Node.Disk.InstanceType != "gp3" &&
			(node.Node.Disk.Iops != 0 || node.Node.Disk.ThroughputMi != 0) {
			return bizerr.New(bizerr.ErrCodeParams, "only gp3 support custom iops,throughput")
		}

		// minSizeGi must equal to maxSizeGi if diskType is local-storage
		if node.Node.Disk != nil && node.Node.Disk.InstanceType == DiskTypeLocalStorage &&
			(node.Node.Disk.MinSizeGi != node.Node.Disk.MaxSizeGi) {
			return bizerr.New(bizerr.ErrCodeParams, "minSizeGi must equal to maxSizeGi if diskType is local-storage")
		}

		if node.Node.DeployType != DeployTypeShared && (node.Node.CpuNum == 0 || node.Node.MemoryGi == 0) {
			return bizerr.New(bizerr.ErrCodeParams, "cpu or memoriy must gt 0 for %s", node.Node.DeployType)
		}
	}
	return nil
}

func buildReplaceProfileByYamlReq(commitId string, skus []*skuYaml, nodes []*nodeYaml, dryrun bool) (*core.ReplaceProfileByYamlReq, bizerr.BizError) {
	req := &core.ReplaceProfileByYamlReq{
		CommitId: commitId,
		Dryrun:   dryrun,
	}

	for _, sku := range skus {
		skuType, err := parseSkuType(string(sku.SkuType))
		if err != nil {
			return nil, err
		}
		deployType := core.DeployType_DEPLOY_TYPE_UNSPECIFIED
		if sku.SkuType == SkuTypeNode {
			deployType, err = parseDeployType(sku.ResourceDetail.DeployType)
			if err != nil {
				return nil, err
			}
		}

		if err != nil {
			return nil, err
		}
		skusExtra, e := json.Marshal(sku.skuExtra)
		if e != nil {
			return nil, bizerr.New(bizerr.ErrCodeParams, "unmarshal skuExtra failed: %+v", sku.skuExtra)
		}

		req.Skus = append(req.Skus, &core.ReplaceProfileByYamlReq_SKU{
			CloudProvider: strings.TrimSpace(sku.CloudProvider),
			Region:        strings.TrimSpace(sku.Region),
			ResourceType:  skuType,
			Price:         sku.Price,
			Extra:         string(skusExtra),
			ResourceDetail: &core.ReplaceProfileByYamlReq_ResourceDetail{
				ComputeInstanceType: strings.TrimSpace(sku.ResourceDetail.ComputeInstanceType),
				DiskInstanceType:    strings.TrimSpace(sku.ResourceDetail.DiskInstanceType),
				DeployType:          deployType,
			},
		})
	}
	for _, node := range nodes {
		disk := node.Node.Disk
		componentType, err := parseComponentType(node.ComponentType)
		if err != nil {
			return nil, err
		}
		deployType, err := parseDeployType(node.Node.DeployType)
		if err != nil {
			return nil, err
		}
		nonStandardTenantIDs := make([]uint64, 0, len(node.NonStandardCustomers))
		for _, each := range node.NonStandardCustomers {
			nonStandardTenantIDs = append(nonStandardTenantIDs, each.TenantId)
		}

		reqNode := &core.ReplaceProfileByYamlReq_Node{
			CloudProvider:       strings.TrimSpace(node.CloudProvider),
			Regions:             node.Regions,
			DeployType:          deployType,
			ComponentType:       componentType,
			OpenedTenantIds:     nonStandardTenantIDs,
			Name:                strings.TrimSpace(node.Node.Name),
			DisplayName:         strings.TrimSpace(node.Node.DisplayName),
			ComputeInstanceType: strings.TrimSpace(node.Node.InstanceType),
			Cpu:                 int32(node.Node.CpuNum),
			MemoryGi:            int32(node.Node.MemoryGi),
			DefaultNodeNum:      int32(node.Node.DefaultNodeNum),
			DiskInstanceType:    strings.TrimSpace(disk.InstanceType),
			DiskIops:            int32(disk.Iops),
			DiskThroughput:      int32(disk.ThroughputMi),
			MinStorageGi:        int32(disk.MinSizeGi),
			MaxStorageGi:        int32(disk.MaxSizeGi),
			IsPublic:            len(node.NonStandardCustomers) == 0,
			Annotation:          strings.TrimSpace(node.Annotation),
		}

		if componentType == core.ComponentType_COMPONENT_TYPE_TIKV && node.Node.PD != nil {
			reqNode.PdNode = &core.ReplaceProfileByYamlReq_PDNode{
				NodeName: strings.TrimSpace(node.Node.PD.NodeName),
				NodeNum:  int32(node.Node.PD.NodeNum),
			}
		}
		req.Nodes = append(req.Nodes, reqNode)
	}

	return req, nil
}

func newCoreCnn(ctx context.Context) (*grpc.ClientConn, bizerr.BizError) {
	conn, err := grpc.DialContext(ctx, config.Global().GrpcConfig.CoreSvcAddr, grpc.WithInsecure(), grpc.WithUnaryInterceptor(logid.UnaryClientInterceptor()))
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeGRPC, "grpc dial: %s", err)
	}
	return conn, nil
}

func callCore(ctx context.Context, req *core.ReplaceProfileByYamlReq) (uint64, bizerr.BizError) {
	conn, err := newCoreCnn(ctx)
	if err != nil {
		return 0, err
	}

	client := core.NewCoreServiceClient(conn)
	resp, e := client.ReplaceProfileByYaml(ctx, req)
	if e != nil {
		return 0, bizerr.WrapGrpcError(e, "ReplaceProfileByYaml")
	}
	if resp.BaseResp.ErrCode != 0 {
		return 0, bizerr.Restore(bizerr.ErrCode(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}
	return resp.VersionId, nil
}

func parseDeployType(tp string) (core.DeployType, bizerr.BizError) {
	switch tp {
	case "hybrid":
		return core.DeployType_DEPLOY_TYPE_HYBRID, nil
	case "dedicated":
		return core.DeployType_DEPLOY_TYPE_DEDICATED, nil
	case "shared":
		return core.DeployType_DEPLOY_TYPE_SHARED, nil
	default:
		return core.DeployType_DEPLOY_TYPE_UNSPECIFIED, bizerr.New(bizerr.ErrCodeParams, "invalid deploy type: %s", tp)
	}
}

func parseSkuType(tp string) (core.SKUType, bizerr.BizError) {
	switch tp {
	case "node":
		return core.SKUType_SKU_TYPE_NODE, nil
	case "disk-iops":
		return core.SKUType_SKU_TYPE_DISK_IOPS, nil
	case "disk-throughput":
		return core.SKUType_SKU_TYPE_DISK_THROUGHPUT, nil
	case "disk-storage":
		return core.SKUType_SKU_TYPE_DISK_STORAGE, nil
	case "ticdc-tcu":
		return core.SKUType_SKU_TYPE_TICDC_TCU, nil
	case "backup-storage":
		return core.SKUType_SKU_TYPE_BACKUP_STORAGE, nil
	default:
		return core.SKUType_SKU_TYPE_UNSPECIFIED, bizerr.New(bizerr.ErrCodeParams, "invalid sku type: %s", tp)
	}
}

func parseComponentType(tp string) (core.ComponentType, bizerr.BizError) {
	switch tp {
	case "tidb":
		return core.ComponentType_COMPONENT_TYPE_TIDB, nil
	case "tikv":
		return core.ComponentType_COMPONENT_TYPE_TIKV, nil
	case "tiflash":
		return core.ComponentType_COMPONENT_TYPE_TIFLASH, nil
	case "pd":
		return core.ComponentType_COMPONENT_TYPE_PD, nil
	case "ticdc":
		return core.ComponentType_COMPONENT_TYPE_TICDC, nil
	default:
		return core.ComponentType_COMPONENT_TYPE_UNSPECIFIED, bizerr.New(bizerr.ErrCodeParams, "invalid component type: %s", tp)
	}
}

func calcNodeProfiles(ctx context.Context, req *forms.CalcNodeProfileReq) ([]*forms.NodeProfilesWithComponentType, bizerr.BizError) {
	listNodeProfiles4TenantReq := &core.ListNodeProfiles4TenantReq{
		TenantId:          req.OrgId,
		ProjectId:         req.ProjectId,
		CloudProviderName: req.Provider,
		RegionName:        req.Region,
		DeployType:        getClusterDeployType(req.IsDevTier),
	}
	resp, err := grpcli.ListNodeProfiles4Tenant(ctx, listNodeProfiles4TenantReq)
	if err != nil {
		return nil, err
	}

	nodeProfileMap := make(map[core.ComponentType][]*core.NodeProfile4Tenant)
	for _, nodeProfile := range resp.Profiles {
		nodeProfileMap[nodeProfile.ComponentType] = append(nodeProfileMap[nodeProfile.ComponentType], nodeProfile)
	}

	var nodeProfiles []*forms.NodeProfilesWithComponentType
	for k, each := range nodeProfileMap {
		profiles := make([]*forms.NodeProfile, 0, len(each))
		for _, nodeProfile := range each {
			profiles = append(profiles, &forms.NodeProfile{
				Id:               nodeProfile.Id,
				Name:             nodeProfile.Name,
				DefaultNodeNum:   int(nodeProfile.DefaultNodeNum),
				MinStorageSizeGi: int(nodeProfile.MinStorageSizeGi),
				MaxStorageSizeGi: int(nodeProfile.MaxStorageSizeGi),
			})
		}
		sort.Slice(profiles, func(i, j int) bool {
			return profiles[i].Name < profiles[j].Name
		})

		nodeProfiles = append(nodeProfiles, &forms.NodeProfilesWithComponentType{
			ComponentType: k.String(),
			Profiles:      profiles, //fill later
		})
	}

	return nodeProfiles, nil
}

func getClusterDeployType(isDevTier bool) core.DeployType {
	if isDevTier {
		return core.DeployType_DEPLOY_TYPE_SHARED
	}
	return core.DeployType_DEPLOY_TYPE_DEDICATED
}
