package premiumupgrade

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/go-logr/logr"
	"github.com/tidbcloud/devops/api"
	upgradehttp "github.com/tidbcloud/devops/api/premiumupgrade"
	entcommon "github.com/tidbcloud/devops/entity/common"
	prem "github.com/tidbcloud/devops/entity/premiumupgrade"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	upgradeapi "github.com/tidbcloud/devops/pkg/service/premiumupgrade/api"
	"go.uber.org/dig"
)

type ControllerParam struct {
	dig.In

	Logger  logr.Logger
	CoreSvc *coresvccli.CoreSvcCli
	Service upgradeapi.Service
}

type Controller struct {
	logger  logr.Logger
	coreSvc *coresvccli.CoreSvcCli
	service upgradeapi.Service
}

func NewController(p ControllerParam) ctrl.Controller {
	return &Controller{logger: p.Logger.WithName("PremiumUpgradeController"), coreSvc: p.CoreSvc, service: p.Service}
}

func (c *Controller) Name() string { return "PremiumUpgradeController" }

func (c *Controller) RegisterAPI(suite *ctrl.Suite) error {
	group := suite.Router.Group("/premiumUpgradePlans")
	group.GET("/componentPolicies", c.ListComponentPolicies)
	group.POST("/targetConflicts", c.CheckTargetConflicts)
	group.POST("/preview", c.PreviewPlan)
	group.POST("", c.CreatePlan)
	group.GET("", c.ListPlans)
	group.GET("/:plan_id", c.GetPlan)
	group.POST("/:plan_id/apply", c.ApplyPlan)
	group.POST("/:plan_id/cancel", c.CancelPlan)
	group.POST("/:plan_id/steps/:step_id/apply", c.ApplyStep)
	group.POST("/:plan_id/steps/:step_id/retry", c.RetryStep)
	return nil
}

// ListComponentPolicies
//
//	@Id			PremiumUpgradeControllerListComponentPolicies
//	@Tags		PremiumUpgradeService
//	@Produce	json
//	@Success	200	{object}	upgradehttp.ListComponentPoliciesResp
//	@Router		/api/v1/devops/premiumUpgradePlans/componentPolicies [get]
func (c *Controller) ListComponentPolicies(ctx *gin.Context) {
	ctrl.Success(ctx, &upgradehttp.ListComponentPoliciesResp{Items: c.service.ListComponentPolicies(ctx)})
}

// CheckTargetConflicts
//
//	@Id				PremiumUpgradeControllerCheckTargetConflicts
//	@Description	Check whether selected Premium clusters already belong to active upgrade plans
//	@Tags			PremiumUpgradeService
//	@Accept			json
//	@Produce		json
//	@Param			req	body		upgradehttp.CheckTargetConflictsReq	true	"CheckTargetConflictsReq"
//	@Success		200	{object}	upgradehttp.CheckTargetConflictsResp
//	@Router			/api/v1/devops/premiumUpgradePlans/targetConflicts [post]
func (c *Controller) CheckTargetConflicts(ctx *gin.Context) {
	request := &upgradehttp.CheckTargetConflictsReq{}
	if err := ctx.ShouldBindJSON(request); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	if err := c.checkClusterScopes(ctx, request.ClusterIDs); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}
	conflicts, err := c.service.CheckTargetConflicts(ctx, upgradeapi.CheckTargetConflictsParam{
		ClusterIDs: request.ClusterIDs,
	})
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	ctrl.Success(ctx, &upgradehttp.CheckTargetConflictsResp{Conflicts: conflicts})
}

// PreviewPlan
//
//	@Id				PremiumUpgradeControllerPreviewPlan
//	@Description	Resolve Premium cluster locators, components and fixed execution order
//	@Tags			PremiumUpgradeService
//	@Accept			json
//	@Produce		json
//	@Param			req	body		upgradehttp.PreviewPlanReq	true	"PreviewPlanReq"
//	@Success		200	{object}	upgradehttp.PreviewPlanResp
//	@Router			/api/v1/devops/premiumUpgradePlans/preview [post]
func (c *Controller) PreviewPlan(ctx *gin.Context) {
	request := &upgradehttp.PreviewPlanReq{}
	if err := ctx.ShouldBindJSON(request); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	if err := c.checkClusterScopes(ctx, request.ClusterIDs); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}
	result, err := c.service.PreviewPlan(ctx, toPreviewParam(request))
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	ctrl.Success(ctx, &upgradehttp.PreviewPlanResp{Preview: result})
}

// CreatePlan
//
//	@Id				PremiumUpgradeControllerCreatePlan
//	@Description	Create one immutable Premium upgrade plan and one aggregate approval
//	@Tags			PremiumUpgradeService
//	@Accept			json
//	@Produce		json
//	@Param			Idempotency-Key	header		string						true	"Idempotency key"
//	@Param			req				body		upgradehttp.CreatePlanReq	true	"CreatePlanReq"
//	@Success		200				{object}	upgradehttp.CreatePlanResp
//	@Router			/api/v1/devops/premiumUpgradePlans [post]
func (c *Controller) CreatePlan(ctx *gin.Context) {
	request := &upgradehttp.CreatePlanReq{}
	if err := ctx.ShouldBindJSON(request); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	if err := c.checkClusterScopes(ctx, request.ClusterIDs); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}
	idempotencyKey := strings.TrimSpace(ctx.GetHeader("Idempotency-Key"))
	if idempotencyKey == "" {
		ctrl.FailWithCode(ctx, fmt.Errorf("Idempotency-Key header is required"), api.ErrCodeInvalidArgument)
		return
	}
	applicant := consts.GetUserEmailFromCtx(ctx, "")
	if applicant == "" {
		ctrl.FailWithCode(ctx, fmt.Errorf("user email is required to create an approval"), api.ErrCodeUnauthenticated)
		return
	}
	operatorName := consts.GetOperatorFromCtx(ctx, applicant)
	plan, err := c.service.CreatePlan(ctx, upgradeapi.CreatePlanParam{
		PreviewPlanParam:     toPreviewParam(&request.PreviewPlanReq),
		Name:                 request.Name,
		Reason:               request.Reason,
		Applicant:            applicant,
		OperatorEntity:       entcommon.BuildOperatorEntity(consts.GetOperatorTypeFromCtx(ctx), operatorName),
		ExpectedManifestHash: request.ExpectedManifestHash,
		IdempotencyKey:       idempotencyKey,
	})
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	ctrl.Success(ctx, &upgradehttp.CreatePlanResp{Plan: plan})
}

// ListPlans
//
//	@Id			PremiumUpgradeControllerListPlans
//	@Tags		PremiumUpgradeService
//	@Produce	json
//	@Param		req	query		upgradehttp.ListPlansReq	true	"ListPlansReq"
//	@Success	200	{object}	upgradehttp.ListPlansResp
//	@Router		/api/v1/devops/premiumUpgradePlans [get]
func (c *Controller) ListPlans(ctx *gin.Context) {
	request := &upgradehttp.ListPlansReq{}
	if err := ctx.ShouldBindQuery(request); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	statuses := make([]prem.PlanStatus, 0, len(request.Statuses))
	for _, status := range request.Statuses {
		statuses = append(statuses, prem.PlanStatus(status))
	}
	var tenantID *uint64
	if request.TenantID != "" {
		parsed, err := strconv.ParseUint(request.TenantID, 10, 64)
		if err != nil {
			ctrl.FailWithCode(ctx, fmt.Errorf("invalid tenant_id %q: %w", request.TenantID, err), api.ErrCodeInvalidArgument)
			return
		}
		if err := ctrl.CheckTenantScope(ctx, parsed); err != nil {
			ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
			return
		}
		tenantID = &parsed
	} else if ctrl.IsTenantRestricted(ctx) {
		ctrl.FailWithCode(ctx, fmt.Errorf("tenant_id is required for this api key"), api.ErrCodePermissionDenied)
		return
	}
	items, total, err := c.service.ListPlans(ctx, upgradeapi.ListPlansParam{
		Applicant: request.Applicant, TenantID: tenantID, Statuses: statuses, Page: int(request.Page), PerPage: int(request.PerPage),
	})
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	ctrl.Success(ctx, &upgradehttp.ListPlansResp{Total: total, Items: items})
}

// GetPlan
//
//	@Id			PremiumUpgradeControllerGetPlan
//	@Tags		PremiumUpgradeService
//	@Produce	json
//	@Param		plan_id	path		int	true	"Plan ID"
//	@Success	200		{object}	upgradehttp.GetPlanResp
//	@Router		/api/v1/devops/premiumUpgradePlans/{plan_id} [get]
func (c *Controller) GetPlan(ctx *gin.Context) {
	planID, err := parsePlanID(ctx)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	plan, err := c.service.GetPlan(ctx, planID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if err := c.checkClusterScopes(ctx, plan.RequestedClusterIDs); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}
	ctrl.Success(ctx, &upgradehttp.GetPlanResp{Plan: plan})
}

// ApplyPlan
//
//	@Id				PremiumUpgradeControllerApplyPlan
//	@Description	Start an approved plan that is configured to run all steps in sequence
//	@Tags			PremiumUpgradeService
//	@Param			plan_id	path		int	true	"Plan ID"
//	@Success		200		{object}	api.BaseResp
//	@Router			/api/v1/devops/premiumUpgradePlans/{plan_id}/apply [post]
func (c *Controller) ApplyPlan(ctx *gin.Context) {
	planID, err := parsePlanID(ctx)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	plan, err := c.service.GetPlan(ctx, planID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if err := c.checkClusterScopes(ctx, plan.RequestedClusterIDs); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}
	if err := c.service.ApplyPlan(ctx, planID, consts.GetUserEmailFromCtx(ctx, "")); err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	ctrl.Success(ctx, nil)
}

// CancelPlan
//
//	@Id			PremiumUpgradeControllerCancelPlan
//	@Tags		PremiumUpgradeService
//	@Param		plan_id	path		int	true	"Plan ID"
//	@Success	200		{object}	api.BaseResp
//	@Router		/api/v1/devops/premiumUpgradePlans/{plan_id}/cancel [post]
func (c *Controller) CancelPlan(ctx *gin.Context) {
	planID, err := parsePlanID(ctx)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	plan, err := c.service.GetPlan(ctx, planID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if err := c.checkClusterScopes(ctx, plan.RequestedClusterIDs); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}
	if err := c.service.CancelPlan(ctx, planID, consts.GetUserEmailFromCtx(ctx, "")); err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	ctrl.Success(ctx, nil)
}

// ApplyStep
//
//	@Id				PremiumUpgradeControllerApplyStep
//	@Description	Apply one managed ConfigurationChange in a step-by-step plan
//	@Tags			PremiumUpgradeService
//	@Param			plan_id	path		int	true	"Plan ID"
//	@Param			step_id	path		int	true	"Step ID"
//	@Success		200		{object}	api.BaseResp
//	@Router			/api/v1/devops/premiumUpgradePlans/{plan_id}/steps/{step_id}/apply [post]
func (c *Controller) ApplyStep(ctx *gin.Context) {
	planID, err := parsePlanID(ctx)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	stepID, err := strconv.ParseUint(ctx.Param("step_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	plan, err := c.service.GetPlan(ctx, planID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if err := c.checkClusterScopes(ctx, plan.RequestedClusterIDs); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}
	if err := c.service.ApplyStep(ctx, planID, stepID, consts.GetUserEmailFromCtx(ctx, "")); err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	ctrl.Success(ctx, nil)
}

// RetryStep
//
//	@Id			PremiumUpgradeControllerRetryStep
//	@Tags		PremiumUpgradeService
//	@Param		plan_id	path		int	true	"Plan ID"
//	@Param		step_id	path		int	true	"Step ID"
//	@Success	200		{object}	api.BaseResp
//	@Router		/api/v1/devops/premiumUpgradePlans/{plan_id}/steps/{step_id}/retry [post]
func (c *Controller) RetryStep(ctx *gin.Context) {
	planID, err := parsePlanID(ctx)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	stepID, err := strconv.ParseUint(ctx.Param("step_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}
	plan, err := c.service.GetPlan(ctx, planID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if err := c.checkClusterScopes(ctx, plan.RequestedClusterIDs); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}
	if err := c.service.RetryStep(ctx, planID, stepID, consts.GetUserEmailFromCtx(ctx, "")); err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	ctrl.Success(ctx, nil)
}

func (c *Controller) checkClusterScopes(ctx *gin.Context, clusterIDs []string) error {
	for _, raw := range clusterIDs {
		clusterID, err := strconv.ParseUint(raw, 10, 64)
		if err != nil {
			return fmt.Errorf("invalid cluster_id %q", raw)
		}
		if err := ctrl.CheckClusterScope(ctx, c.coreSvc, clusterID); err != nil {
			return err
		}
	}
	return nil
}

func toPreviewParam(request *upgradehttp.PreviewPlanReq) upgradeapi.PreviewPlanParam {
	return upgradeapi.PreviewPlanParam{
		ClusterIDs: request.ClusterIDs, DesiredVersion: request.DesiredVersion,
		ExecutionMode: request.ExecutionMode, Scope: request.Scope,
		ComponentImageTags: request.ComponentImageTags,
	}
}

func parsePlanID(ctx *gin.Context) (uint64, error) {
	return strconv.ParseUint(ctx.Param("plan_id"), 10, 64)
}
