package cloudconfig

import (
	"fmt"
	"strconv"

	"github.com/tidbcloud/devops/api"
	ccapi "github.com/tidbcloud/devops/api/cloudconfig"
	clusterapi "github.com/tidbcloud/devops/api/cluster"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	ccsvcapi "github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	cmxsvcapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"

	"github.com/gin-gonic/gin"
)

// checkConfigurationChangeScope enforces per-role tenant scope on a loaded
// change, dispatching by which target (cluster / tenant init / template) it
// carries. A global template change (TargetTemplate.TenantID == nil) affects
// every tenant, so we deny it for any restricted caller.
func (c *CloudConfigController) checkConfigurationChangeScope(ctx *gin.Context, change *ccent.ConfigurationChange) error {
	switch {
	case change.TargetCluster != nil:
		return c.checkClusterLocatorScope(ctx, change.TargetCluster.ClusterID)
	case change.TargetTenantInit != nil:
		return ctrl.CheckTenantScope(ctx, change.TargetTenantInit.TenantID)
	case change.TargetTemplate != nil:
		if change.TargetTemplate.TenantID == nil {
			if ctrl.IsTenantRestricted(ctx) {
				return fmt.Errorf("global template change %d is not accessible with this api key", change.ID)
			}
			return nil
		}
		return c.checkTenantLocatorScope(ctx, *change.TargetTemplate.TenantID)
	}
	return nil
}

// GetConfigChange
//
//	@Id				CloudConfigControllerGetConfigChange
//	@Description	get a config change of a cluster
//	@Tags			CloudConfigController
//	@Accept			json
//	@Produce		json
//	@Param			change_id	path		string	true	"change id"
//	@Success		200			{object}	ccapi.GetConfigurationChangeResp
//	@Failure		default		{object}	api.BaseResp
//	@Router			/api/v1/devops/cloudconfig/changes/{change_id} [get]
func (c *CloudConfigController) GetConfigurationChange(ctx *gin.Context) {
	changeIDStr := ctx.Param("change_id")
	if changeIDStr == "" {
		ctrl.FailWithCode(ctx, fmt.Errorf("change_id is required"), api.ErrCodeInvalidArgument)
		return
	}
	changeID, err := strconv.ParseUint(changeIDStr, 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	change, err := c.svc.GetConfigurationChange(ctx, changeID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if err := c.checkConfigurationChangeScope(ctx, change); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	resp := &ccapi.GetConfigurationChangeResp{
		Change: change,
	}
	ctrl.Success(ctx, resp)
}

// ApplyConfigurationChange
//
//	@Id				CloudConfigControllerApplyConfigurationChange
//	@Description	aplly a config change
//	@Tags			CloudConfigController
//	@Accept			json
//	@Produce		json
//	@Param			change_id	path		string								true	"change id"
//	@Param			req			body		ccapi.ApplyConfigurationChangeReq	false	"request param"
//	@Success		200			{object}	api.BaseResp
//	@Failure		default		{object}	api.BaseResp
//	@Router			/api/v1/devops/cloudconfig/changes/{change_id}/apply [post]
func (c *CloudConfigController) ApplyConfigurationChange(ctx *gin.Context) {
	changeID, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	req := &ccapi.ApplyConfigurationChangeReq{}
	err = ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	change, err := c.svc.GetConfigurationChange(ctx, changeID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if err := c.checkConfigurationChangeScope(ctx, change); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	param := ccsvcapi.ApplyConfigurationChangeParam{
		OperatorEmail: ctx.GetString(consts.CtxUserEmail),
	}
	if req.Mode != nil {
		param.Mode = &ccsvcapi.ApplyMode{
			Allow: container.TransformSlice(req.Mode.AllowMethods, container.FromString[ccsvcapi.ApplyMethod]),
			Deny:  container.TransformSlice(req.Mode.DenyMethods, container.FromString[ccsvcapi.ApplyMethod]),
		}
	}
	err = c.svc.ApplyConfigurationChange(ctx, changeID, param)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// CancelConfigurationChange
//
//	@Id				CloudConfigControllerCancelConfigurationChange
//	@Description	cancel a config change
//	@Tags			CloudConfigController
//	@Accept			json
//	@Produce		json
//	@Param			change_id	path		string	true	"change id"
//	@Success		200			{object}	api.BaseResp
//	@Router			/api/v1/devops/cloudconfig/changes/{change_id}/cancel [post]
func (c *CloudConfigController) CancelConfigurationChange(ctx *gin.Context) {
	changeID, err := strconv.ParseUint(ctx.Param("change_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	change, err := c.svc.GetConfigurationChange(ctx, changeID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}
	if err := c.checkConfigurationChangeScope(ctx, change); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	err = c.svc.CancelConfigurationChange(ctx, changeID)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

func BuildParamToPreviewConfigurationChange(req *ccapi.PreviewConfigurationChangeReq) *ccsvcapi.PreviewConfigurationChangeParam {
	param := &ccsvcapi.PreviewConfigurationChangeParam{}
	param.ChangeType = ccsvcapi.ChangeType(req.ChangeType)
	if len(req.Components) != 0 {
		param.Components = container.TransformSlice(req.Components, BuildChangeComponentParam)
	}
	if len(req.Files) != 0 {
		param.Files = container.TransformSlice(req.Files, BuildChangeFileParam)
	}
	if req.Version != nil {
		param.Version = BuildChangeVersionParam(req.Version)
	}

	return param
}

func BuildParamToCreateConfigurationChange(req *ccapi.CreateConfigurationChangeReq, email string) *ccsvcapi.CreateConfigurationChangeParam {
	param := &ccsvcapi.CreateConfigurationChangeParam{
		Reason:      req.Reason,
		Applicant:   email,
		ApplyPolicy: req.ApplyPolicy,
	}
	param.ChangeType = ccsvcapi.ChangeType(req.ChangeType)
	if len(req.Components) != 0 {
		param.Components = container.TransformSlice(req.Components, BuildChangeComponentParam)
	}
	if len(req.Files) != 0 {
		param.Files = container.TransformSlice(req.Files, BuildChangeFileParam)
	}
	if req.Version != nil {
		param.Version = BuildChangeVersionParam(req.Version)
	}
	if req.ExecutionWindow != nil {
		param.ExecutionWindow = BuildCreateOpsExecutionWindowParam(req.ExecutionWindow)
	}

	return param
}

func BuildCreateOpsExecutionWindowParam(req *clusterapi.CreateOpsExecutionWindowReq) *cmxsvcapi.CreateOpsExecutionWindowParam {
	return &cmxsvcapi.CreateOpsExecutionWindowParam{
		StartAt: req.StartAt,
		EndAt:   req.EndAt,
	}
}

func BuildChangeComponentParam(req *ccapi.ChangeComponentReq) *ccsvcapi.ChangeComponentParam {
	param := &ccsvcapi.ChangeComponentParam{
		Component: req.Component,
	}
	if req.OverwriteImageTag != nil {
		param.OverwriteImageTag = &ccsvcapi.ChangeImageTagParam{
			ToImageTag: req.OverwriteImageTag.ToImageTag,
		}
	}
	if req.Version != nil {
		param.Version = BuildChangeVersionParam(req.Version)
	}
	return param
}

func BuildChangeFileParam(req *ccapi.ChangeFileReq) *ccsvcapi.ChangeFileParam {
	param := &ccsvcapi.ChangeFileParam{
		Filename:         req.Filename,
		ConfigType:       req.ConfigType,
		CustomParameters: req.CustomParameters,
		PreviousText:     req.PreviousText,
	}
	if req.ConfigType == "" {
		req.ConfigType = req.Filename // FIXME: to compatible with old version
	}
	if req.Template != nil {
		param.Template = &ccsvcapi.ChangeTemplateParam{
			ToTemplateID: req.Template.ToTemplateID,
		}
	}
	return param
}

func BuildChangeVersionParam(req *ccapi.ChangeVersionReq) *ccsvcapi.ChangeVersionParam {
	param := &ccsvcapi.ChangeVersionParam{
		ToVersion: req.ToVersion,
	}
	return param
}
