package operation

import (
	"fmt"
	"strconv"

	"github.com/tidbcloud/devops/api"
	opapi "github.com/tidbcloud/devops/api/operation"
	opent "github.com/tidbcloud/devops/entity/operation"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	cmxsvcapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"
	opsvc "github.com/tidbcloud/devops/pkg/service/operation"
	opsvcins "github.com/tidbcloud/devops/pkg/service/operation/instance"

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

// checkInstanceTenantScope checks tenant scope for an operation instance based on its params.
func (c *OperationController) checkInstanceTenantScope(ctx *gin.Context, ins *opent.Instance) error {
	params := make(map[string]string, len(ins.Params))
	for k, v := range ins.Params {
		params[string(k)] = v
	}
	return ctrl.CheckParamsScope(ctx, c.coreSvcCli, params)
}

// checkInstanceIDTenantScope loads an instance by ID and checks tenant scope.
func (c *OperationController) checkInstanceIDTenantScope(ctx *gin.Context, instanceID uint64) error {
	ins, err := c.opSvc.GetInstance(ctx, instanceID)
	if err != nil {
		return err
	}
	return c.checkInstanceTenantScope(ctx, ins)
}

// checkParamsTenantScope checks tenant scope from request params map before creating an instance.
func (c *OperationController) checkParamsTenantScope(ctx *gin.Context, params map[string]string) error {
	return ctrl.CheckParamsScope(ctx, c.coreSvcCli, params)
}

// ListInstances
//
//	@Id				OperationControllerListInstances
//	@Description	list instances
//	@Tags			OperationController
//	@Accept			json
//	@Produce		json
//	@Param			req	query		opapi.ListInstanceReq	false	"request param"
//	@Success		200	{object}	opapi.ListInstanceResp
//	@Router			/api/v1/devops/operation/instances [get]
func (c *OperationController) ListInstances(ctx *gin.Context) {
	req := &opapi.ListInstanceReq{}
	err := ctx.ShouldBindQuery(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	param := opsvc.ListInstanceParam{
		Page:    int(req.Page),
		PerPage: int(req.PerPage),
	}
	if req.Type != nil {
		t := opent.OperationTarget(*req.Type)
		param.Type = &t
	}
	if req.CreateBefore != nil {
		param.CreateBefore = req.CreateBefore
	}
	if req.CreateAfter != nil {
		param.CreateAfter = req.CreateAfter
	}
	if req.TargetID != nil {
		param.TargetIDsIn = []string{*req.TargetID}
	}
	if req.Phase != nil {
		param.Phases = []opent.InstancePhase{*req.Phase}
	}
	if req.Applicant != nil {
		param.Applicants = []string{*req.Applicant}
	}
	if req.Operation != nil {
		param.Operations = []string{*req.Operation}
	}

	insList, total, err := c.opSvc.ListInstances(ctx, param)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	resp := &opapi.ListInstanceResp{
		Total:     total,
		Instances: insList,
	}
	ctrl.Success(ctx, resp)
}

// GetInstance
//
//	@Id				OperationControllerGetInstance
//	@Description	get a instance
//	@Tags			OperationController
//	@Accept			json
//	@Produce		json
//	@Param			instance_id	path		int						true	"instance id"
//	@Param			req			query		opapi.GetInstanceReq	false	"request param"
//	@Success		200			{object}	opapi.GetInstanceResp
//	@Router			/api/v1/devops/operation/instances/{instance_id} [get]
func (c *OperationController) GetInstance(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("instance_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	req := &opapi.GetInstanceReq{}
	err = ctx.ShouldBindQuery(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	ins, err := c.opSvc.GetInstance(ctx, id)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	if err := c.checkInstanceTenantScope(ctx, ins); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	resp := &opapi.GetInstanceResp{
		Instance: ins,
	}
	ctrl.Success(ctx, resp)
}

// PreviewInstance
//
//	@Id				OperationControllerPreviewInstance
//	@Description	preview a instance: runs PreCheck and returns advisories without creating
//	@Tags			OperationController
//	@Accept			json
//	@Produce		json
//	@Param			req	body		opapi.CreateInstanceReq	true	"request param"
//	@Success		200	{object}	opapi.PreviewInstanceResp
//	@Router			/api/v1/devops/operation/instances/preview [post]
func (c *OperationController) PreviewInstance(ctx *gin.Context) {
	req := &opapi.CreateInstanceReq{}
	err := ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	if err := c.checkParamsTenantScope(ctx, req.Params); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	v, exist := ctx.Get(consts.CtxUserEmail)
	if !exist {
		ctrl.FailWithCode(ctx, fmt.Errorf("account not found in ctx"), api.ErrCodeInternal)
		return
	}
	email := v.(string)

	param := BuildParamToCreateInstance(req, email)

	result, err := c.opSvc.PreviewInstance(ctx, *param)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	advisories := make([]opapi.Advisory, 0, len(result.Advisories))
	for _, a := range result.Advisories {
		advisories = append(advisories, opapi.Advisory{Level: a.Level, Message: a.Message})
	}

	ctrl.Success(ctx, &opapi.PreviewInstanceResp{Advisories: advisories})
}

// CreateInstance
//
//	@Id				OperationControllerCreateInstance
//	@Description	create a instance
//	@Tags			OperationController
//	@Accept			json
//	@Produce		json
//	@Param			req	body		opapi.CreateInstanceReq	true	"request param"
//	@Success		200	{object}	opapi.CreateInstanceResp
//	@Router			/api/v1/devops/operation/instances [post]
func (c *OperationController) CreateInstance(ctx *gin.Context) {
	req := &opapi.CreateInstanceReq{}
	err := ctx.ShouldBindJSON(req)
	if err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodeInvalidArgument)
		return
	}

	if err := c.checkParamsTenantScope(ctx, req.Params); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	v, exist := ctx.Get(consts.CtxUserEmail)
	if !exist {
		ctrl.FailWithCode(ctx, fmt.Errorf("account not found in ctx"), api.ErrCodeInternal)
		return
	}
	email := v.(string)

	param := BuildParamToCreateInstance(req, email)

	id, err := c.opSvc.CreateInstance(ctx, *param)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	resp := &opapi.CreateInstanceResp{InstanceID: id}
	ctrl.Success(ctx, resp)
}

// SubmitInstance
//
//	@Id				OperationControllerSubmitInstance
//	@Description	submit a instance
//	@Tags			OperationController
//	@Accept			json
//	@Produce		json
//	@Param			instance_id	path		int	true	"instance id"
//	@Success		200			{object}	api.BaseResp
//	@Router			/api/v1/devops/operation/instances/{instance_id}/submit [post]
func (c *OperationController) SubmitInstance(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("instance_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	if err := c.checkInstanceIDTenantScope(ctx, id); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	err = c.opSvc.SubmitInstance(ctx, id)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// UpdateInstance
//
//	@Id				OperationControllerUpdateInstance
//	@Description	update a instance
//	@Tags			OperationController
//	@Accept			json
//	@Produce		json
//	@Param			instance_id	path		int						true	"instance id"
//	@Param			req			body		opapi.UpdateInstanceReq	true	"request param"
//	@Success		200			{object}	api.BaseResp
//	@Router			/api/v1/devops/operation/instances/{instance_id} [patch]
func (c *OperationController) UpdateInstance(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("instance_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	if err := c.checkInstanceIDTenantScope(ctx, id); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

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

	param := opsvc.UpdateInstanceParam{
		Reason:  req.Reason,
		Params:  req.Params,
		Actions: nil,
	}
	if req.Actions != nil {
		param.Actions = pointer.Pointer(container.TransformSlice(*req.Actions, BuildParamToCreateInstanceAction))
	}

	err = c.opSvc.UpdateInstance(ctx, id, param)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// CancelInstance
//
//	@Id				OperationControllerCancelInstance
//	@Description	cancel a instance
//	@Tags			OperationController
//	@Accept			json
//	@Produce		json
//	@Param			instance_id	path		int	true	"instance id"
//	@Success		200			{object}	api.BaseResp
//	@Router			/api/v1/devops/operation/instances/{instance_id}/cancel [post]
func (c *OperationController) CancelInstance(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("instance_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	if err := c.checkInstanceIDTenantScope(ctx, id); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	err = c.opSvc.CancelInstance(ctx, id)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// StartInstanceAction
//
//	@Id				OperationControllerStartInstanceAction
//	@Description	start a instance action maunally
//	@Tags			OperationController
//	@Accept			json
//	@Produce		json
//	@Param			instance_id		path		int	true	"instance id"
//	@Param			action_order	path		int	true	"instance action order"
//	@Success		200				{object}	api.BaseResp
//	@Router			/api/v1/devops/operation/instances/{instance_id}/actions/{action_order}/start [post]
func (c *OperationController) StartInstanceAction(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("instance_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	if err := c.checkInstanceIDTenantScope(ctx, id); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	order, err := strconv.ParseInt(ctx.Param("action_order"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("action order is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	err = c.opSvc.StartInstanceAction(ctx, id, opent.ActionOrder(order), ctx.GetString(consts.CtxUserEmail))
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

// FinishInstanceAction
//
//	@Id				OperationControllerFinishInstanceAction
//	@Description	finish a instance action
//	@Tags			OperationController
//	@Accept			json
//	@Produce		json
//	@Param			instance_id		path		int								true	"instance id"
//	@Param			action_order	path		int								true	"instance action order"
//	@Param			req				body		opapi.FinishInstanceActionReq	true	"request param"
//	@Success		200				{object}	api.BaseResp
//	@Router			/api/v1/devops/operation/instances/{instance_id}/actions/{action_order}/finish [post]
func (c *OperationController) FinishInstanceAction(ctx *gin.Context) {
	id, err := strconv.ParseUint(ctx.Param("instance_id"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("id is not a int"), api.ErrCodeInvalidArgument)
		return
	}

	if err := c.checkInstanceIDTenantScope(ctx, id); err != nil {
		ctrl.FailWithCode(ctx, err, api.ErrCodePermissionDenied)
		return
	}

	order, err := strconv.ParseInt(ctx.Param("action_order"), 10, 64)
	if err != nil {
		ctrl.FailWithCode(ctx, fmt.Errorf("action order is not a int"), api.ErrCodeInvalidArgument)
		return
	}

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

	err = c.opSvc.FinishInstanceAction(ctx, id, opent.ActionOrder(order), req.Result, req.Output)
	if err != nil {
		ctrl.Fail(ctx, err)
		return
	}

	ctrl.Success(ctx, nil)
}

func BuildParamToCreateInstance(req *opapi.CreateInstanceReq, applicant string) *opsvc.CreateInstanceParam {
	param := &opsvc.CreateInstanceParam{
		Type:      opent.OperationTarget(req.Type),
		Operation: req.Operation,
		Reason:    req.Reason,
		Applicant: applicant,
		Params:    make(map[opent.ParamName]string),
		IsDraft:   req.IsDraft,
	}
	for k, v := range req.Params {
		param.Params[opent.ParamName(k)] = v
	}
	for _, a := range req.Actions {
		param.Actions = append(param.Actions, BuildParamToCreateInstanceAction(a))
	}
	if req.ExecutionWindow != nil {
		param.ExecutionWindow = &cmxsvcapi.CreateOpsExecutionWindowParam{
			StartAt: req.ExecutionWindow.StartAt,
			EndAt:   req.ExecutionWindow.EndAt,
		}
	}

	return param
}

func BuildParamToCreateInstanceAction(req opapi.CreateInstanceAction) opsvcins.CreateInstanceActionParam {
	return opsvcins.CreateInstanceActionParam{
		Name:            opent.ActionName(req.Name),
		StartPolicy:     req.StartPolicy,
		StartScheduleAt: req.StartScheduleAt,
	}
}
