package api

import (
	"context"
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/core-portal/internal/admin/consts"
	"github.com/tidbcloud/core-portal/internal/admin/forms"
	"github.com/tidbcloud/core-portal/internal/central/config"
	"github.com/tidbcloud/core-portal/internal/central/grpc/grpcli"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	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"
	"google.golang.org/grpc/metadata"
)

var (
	deletedClusterStatuses = []core.ClusterStatus{
		core.ClusterStatus_CLUSTER_STATUS_DELETED, core.ClusterStatus_CLUSTER_STATUS_DELETECOMPLETE,
		core.ClusterStatus_CLUSTER_STATUS_CLEARED,
	}
)

func forbidM2mRequest(c *gin.Context) error {
	logger := log.WithContextS(c.Request.Context())
	v, ok := c.Get(consts.CONTEXT_CURRENT_USER)
	if !ok {
		logger.Warnf("can't get user")
		return nil
	}
	email, ok := v.(string)
	if !ok {
		logger.Warnf("invalid user type")
		return nil
	}

	logger.Infof("user(email=%s)", email)
	if strings.HasSuffix(email, "@m2m-client") &&
		config.Global().ServerConfig.ForbidM2mRequest {
		logger.Warnf("forbid request, because this request is from M2M")
		return fmt.Errorf("forbidden")
	}
	return nil
}

// @Summary List Tenant Cluster
// @tags TenantCuster
// @Accept  json
// @Produce json
// @Param org_id path string true "org_id"
// @Param page query forms.PaginationReq  true "pagination"
// @Success 200 {object} forms.ListTenantClustersResp
// @Router /orgs/{org_id}/clusters [get]
func ListTenantClusters(c *gin.Context) {
	ctx := c.Request.Context()
	req := forms.GetOrgIDReq{}
	if err := c.ShouldBindUri(&req); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}
	logger := log.WithContextS(ctx).With(zap.Uint64("tenantID", req.OrgID))
	logger.Info("get clusters of tenant, begin")
	if err := forbidM2mRequest(c); err != nil {
		Fail(c, err, http.StatusForbidden)
		return
	}

	total, clusters, err := listTenantClusters(ctx, req.OrgID, GetPager(c))
	resp := forms.ListTenantClustersResp{}
	statusCode := http.StatusOK
	// add a middleware
	if err != nil {
		if err.IsException() {
			logger.Errorf("failed: %s", err)
		} else {
			logger.Infof("failed: %s", err)
		}
		err = err.Redact()
		resp.Code = int64(err.ErrCode())
		resp.Msg = err.ErrMsg()
		statusCode = err.HttpStatus()
	} else {
		resp.Total = total
		resp.Items = clusters
	}
	c.JSON(statusCode, resp)
}

func listTenantClusters(ctx context.Context, tenantID uint64, pager *base.Pager) (int64, []forms.TenantCluster, bizerr.BizError) {
	fitler := &core.ClusterFilter{
		TenantIds:       []uint64{tenantID},
		ExcludeStatuses: deletedClusterStatuses,
	}
	sorter := &base.Sorter{
		OrderBy: "created_at",
		Sort:    base.Sort_SORT_DESC,
	}
	listReq := &core.ListClustersReq{
		Filter: fitler,
		Pager:  pager,
		Sorter: sorter,
	}
	countReq := &core.CountClustersReq{
		Filter: fitler,
	}

	var (
		listResp  *core.ListClustersResp
		countResp *core.CountClustersResp
		err       bizerr.BizError
	)

	listResp, err = grpcli.ListClusters(ctx, listReq)
	if err != nil {
		return 0, nil, err
	}
	countResp, err = grpcli.CountClusters(ctx, countReq)
	if err != nil {
		return 0, nil, err
	}

	id2project := map[uint64]*account.Project{}
	projectFilter := &account.ProjectFilter{
		OrgIds: []uint64{tenantID},
		Status: []string{string(entity.ProjectStatusActive)},
	}
	projectsRes, err := grpcli.ListProjects(ctx, projectFilter)
	if err != nil {
		return 0, nil, err
	}
	for _, each := range projectsRes.Projects {
		project := each
		id2project[each.Id] = project
	}

	out := make([]forms.TenantCluster, 0, len(listResp.Clusters))
	for _, id := range listResp.OrderedClusterIds {
		item := listResp.Clusters[id]
		out = append(out, buildAPITenantClusterItem(item, id2project[item.ProjectId]))
	}
	return countResp.Count, out, nil
}

// TODO(chenfei): NOTE, this function is customized for config-service and should be REMOVED when the gitops v1 offline.
// @Summary List Clusters for config service
// @tags TenantCuster
// @Accept  json
// @Produce json
// @Param org_id path string true "org_id"
// @Param page query forms.PaginationReq  true "pagination"
// @Success 200 {object} forms.ListTenantClustersResp
// @Router /clusters4configservice [get]
func ListClustersForConfigService(c *gin.Context) {
	ctx := c.Request.Context()
	logger := log.WithContextS(ctx)
	logger.Info("list clusters for config-service, begin")
	if err := forbidM2mRequest(c); err != nil {
		Fail(c, err, http.StatusForbidden)
		return
	}

	clusters, err := listClustersForConfigService(ctx)
	resp := forms.ListTenantClustersResp{}
	statusCode := http.StatusOK
	// add a middleware
	if err != nil {
		if err.IsException() {
			logger.Errorf("failed: %s", err)
		} else {
			logger.Infof("failed: %s", err)
		}
		err = err.Redact()
		resp.Code = int64(err.ErrCode())
		resp.Msg = err.ErrMsg()
		statusCode = err.HttpStatus()
	} else {
		resp.Items = clusters
	}
	logger.Info("list clusters for config-service, end")
	c.JSON(statusCode, resp)
}

func listClustersForConfigService(ctx context.Context) ([]forms.TenantCluster, bizerr.BizError) {
	// As the config-service is offline and all dedicated clusters have been migrated to the 3-layer architecture.
	return []forms.TenantCluster{}, nil

	// filter := &core.ClusterFilter{
	//	Statuses:  []core.ClusterStatus{core.ClusterStatus_CLUSTER_STATUS_NORMAL},
	//	IsDevTier: pointer.ToBool(false),
	//}
	// listReq := &core.ListClustersReq{
	//	Filter: filter,
	//}
	// listResp, err := grpcli.ListClusters(ctx, listReq)
	// if err != nil {
	//	return nil, err
	// }
	//
	// out := make([]forms.TenantCluster, 0, len(listResp.Clusters))
	// for _, id := range listResp.OrderedClusterIds {
	//	item := listResp.Clusters[id]
	//	out = append(out, buildAPITenantClusterItem(item, nil))
	// }
	// return out, nil
}

// @Summary Delete Tenant Cluster
// @tags TenantCuster
// @Accept  json
// @Produce json
// @Param org_id path string true "org_id"
// @Param cluster_id path string true "cluster_id"
// @Success 200 {object} forms.EmptyResponse
// @Router /orgs/{org_id}/clusters/{cluster_id} [delete]
func DeleteTenantCluster(c *gin.Context) {
	req := forms.GetOrgIDAndClusterIDReq{}
	if err := c.ShouldBindUri(&req); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}

	userContext := GetUserContext(c)
	if err := userContext.DeleteTenantCluster(c.Request.Context(), req.OrgID, req.ClusterID); err != nil {
		Fail(c, err, http.StatusInternalServerError)
		return
	}

	Success(c, nil, http.StatusOK)
}

// @Summary Pause Tenant Cluster
// @tags TenantCuster
// @Accept  json
// @Produce json
// @Param org_id path string true "org_id"
// @Param cluster_id path string true "cluster_id"
// @Success 200 {object} forms.CodeMsgResponse
// @Router /orgs/{org_id}/clusters/{cluster_id}/pause [post]
func PauseTenantCluster(c *gin.Context) {
	req := forms.GetOrgIDAndClusterIDReq{}
	if err := c.ShouldBindUri(&req); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}

	userContext := GetUserContext(c)
	if err := userContext.PauseTenantCluster(c.Request.Context(), req.OrgID, req.ClusterID); err != nil {
		FailFromBizErr(c, err)
		return
	}

	Success(c, &forms.CodeMsgResponse{}, http.StatusOK)
}

// @Summary Resume Tenant Cluster
// @tags TenantCuster
// @Accept  json
// @Produce json
// @Param org_id path string true "org_id"
// @Param cluster_id path string true "cluster_id"
// @Success 200 {object} forms.CodeMsgResponse
// @Router /orgs/{org_id}/clusters/{cluster_id}/resume [post]
func ResumeTenantCluster(c *gin.Context) {
	req := forms.GetOrgIDAndClusterIDReq{}
	if err := c.ShouldBindUri(&req); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}

	userContext := GetUserContext(c)
	if err := userContext.ResumeTenantCluster(c.Request.Context(), req.OrgID, req.ClusterID); err != nil {
		FailFromBizErr(c, err)
		return
	}

	Success(c, &forms.CodeMsgResponse{}, http.StatusOK)
}

// @Summary Scale Tenant Cluster
// @tags TenantCuster
// @Accept  json
// @Produce json
// @Param org_id path string true "org_id"
// @Param cluster_id path string true "cluster_id"
// @Param scale body forms.ScaleComponentReq true "scale cluster component"
// @Success 200 {object} forms.EmptyResponse
// @Router /orgs/{org_id}/clusters/{cluster_id}/components [put]
func ScaleTenantCluster(c *gin.Context) {
	ctx := c.Request.Context()
	orgIDAndClusterID := forms.GetOrgIDAndClusterIDReq{}
	if err := c.ShouldBindUri(&orgIDAndClusterID); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}

	scaleReq := forms.ScaleComponentReq{}
	if err := c.ShouldBindJSON(&scaleReq); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}

	email := MustGetUserFromContext(c)
	user, err := grpcli.GetUserByEmail(ctx, email)
	if err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}
	err = scaleTenantCluster(ctx, user.Id, orgIDAndClusterID.OrgID, orgIDAndClusterID.ClusterID, &scaleReq)
	resp := forms.CodeMsgResponse{}
	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)
}

func scaleTenantCluster(ctx context.Context, userId uint64, orgID uint64, clusterID uint64, scaleReq *forms.ScaleComponentReq) bizerr.BizError {
	components := make([]*core.ClusterComponent, 0, len(scaleReq.Components))
	for _, c := range scaleReq.Components {
		components = append(components, &core.ClusterComponent{
			ComponentType:   core.ComponentType(core.ComponentType_value[c.ComponentType]),
			NodeProfileName: c.NodeProfileName,
			Replicas:        c.Replicas,
			StorageSizeGi:   c.StorageSizeGi,
		})
	}
	coreReq := &core.ScaleClusterReq{
		ClusterId:           clusterID,
		NewComponents:       components,
		AllowScaleIn:        true,
		AllowScaleSecondary: false,
		AllowScaleDown:      false,
		OperatorId:          userId,
		BaseReq:             nil,
	}

	_, err := grpcli.GetClusterWithTenantID(ctx, orgID, clusterID)
	if err != nil {
		return err
	}

	ctx = metadata.AppendToOutgoingContext(ctx,
		grpcli.ContextKeyTenantID, strconv.FormatUint(orgID, 10),
		grpcli.ContextKeyClusterID, strconv.FormatUint(clusterID, 10),
	)
	_, err = grpcli.ScaleCluster(ctx, coreReq)
	return err
}

// @Summary ListTenantClusterLifeCycle
// @tags Tenant
// @Accept  json
// @Produce json
// @Param org_id path string  true "org_id"
// @Param search query forms.ListTenantClusterLifeCycleReq  true "search"
// @Success 200 {object} forms.ListTenantClusterLifeCycleResp
// @Router /orgs/{org_id}/cluster-lifecycles [get]
func ListTenantClusterLifeCycle(c *gin.Context) {
	reqUri := forms.GetOrgIDReq{}
	if err := c.ShouldBindUri(&reqUri); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}
	req := forms.ListTenantClusterLifeCycleReq{}
	if err := c.ShouldBindQuery(&req); err != nil {
		Fail(c, err, http.StatusBadRequest)
		return
	}

	userContext := GetUserContext(c)

	cf, err := userContext.ListTenantClusterLifeCycle(reqUri.OrgID, req.Start, req.End)
	if err != nil {
		Fail(c, err, http.StatusInternalServerError)
		return
	}

	resp := forms.ListTenantClusterLifeCycleResp{Total: int64(len(cf))}
	for _, item := range cf {
		resp.Items = append(resp.Items, forms.TenantClusterLifecycle{
			Id:        item.ID,
			ProjectID: item.ProjectID,
			CreatedAt: item.CreatedAt,
			DeletedAt: item.DeletedAt,
		})
	}
	Success(c, resp, http.StatusOK)
}
