package experiment

import (
	"context"
	"strconv"

	"github.com/tidbcloud/chaos-svc/idl/pbgen/proto/chaos"
	experiment_query "github.com/tidbcloud/chaos-svc/pkg/application/query/experiment"
	"github.com/tidbcloud/chaos-svc/pkg/domain/constant"
	"github.com/tidbcloud/chaos-svc/pkg/domain/entity"
	"github.com/tidbcloud/chaos-svc/pkg/domain/facade"
	"github.com/tidbcloud/chaos-svc/pkg/domain/repo"
	workflowSvc "github.com/tidbcloud/chaos-svc/pkg/domain/service/workflow"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
)

type CreateExperimentSvc struct {
	trans             db.TransProvider
	infraFacade       facade.Infra
	createWorkflowSvc workflowSvc.CreateWorkflowSvc
	specConverter     workflowSvc.SpecConverter
	workflowLockRepo  repo.WorkflowLockRepo
	workflowRepo      repo.WorkflowRepo
	backendRepo       repo.BackendRepo
}

func NewCreateExperimentSvc(
	trans db.TransProvider,
	infraFacade facade.Infra,
	createWorkflowSvc workflowSvc.CreateWorkflowSvc,
	specConverter workflowSvc.SpecConverter,
	workflowLockRepo repo.WorkflowLockRepo,
	workflowRepo repo.WorkflowRepo,
	backendRepo repo.BackendRepo,
) *CreateExperimentSvc {
	return &CreateExperimentSvc{
		trans:             trans,
		infraFacade:       infraFacade,
		createWorkflowSvc: createWorkflowSvc,
		specConverter:     specConverter,
		workflowLockRepo:  workflowLockRepo,
		workflowRepo:      workflowRepo,
		backendRepo:       backendRepo,
	}
}

func (svc *CreateExperimentSvc) Execute(ctx context.Context, req *chaos.CreateExperimentRequest) (*chaos.Experiment, bizerr.BizError) {
	logger := log.WithContextS(ctx).With("clusterID", req.ClusterId).With("projectID", req.ProjectId)
	logger.Info("create experiment")

	tidbCluster, err := svc.infraFacade.GetTiDBCluster(ctx, req.ProjectId, req.ClusterId)
	if err != nil {
		return nil, err
	}
	backend, err := svc.backendRepo.Load(ctx, tidbCluster.ProjectID, tidbCluster.NetworkName)
	if err != nil {
		return nil, err
	}
	if err := ValidateChaosBackendReady(backend); err != nil {
		return nil, err
	}

	var experimentPb *chaos.Experiment
	if err := svc.trans.Transaction(ctx, func(ctx context.Context) error {
		identifier := strconv.FormatUint(req.ClusterId, 10)
		if err := svc.workflowLockRepo.Lock(ctx, constant.LockSceneCreateWorkflow, identifier); err != nil {
			return err.Chain("workflow lock")
		}
		defer svc.unlockWorkflow(ctx, identifier)

		if err := svc.check(ctx, req); err != nil {
			return err.Chain("check")
		}

		params, err := svc.buildCreateWorkflowParams(req, *tidbCluster)
		if err != nil {
			return err
		}

		workflowEntity, err := svc.createWorkflowSvc.Execute(ctx, params)
		if err != nil {
			return err
		}
		experimentPb, err = experiment_query.WorkflowEntity2Pb(svc.specConverter, workflowEntity)
		if err != nil {
			return err
		}
		return nil
	}); err != nil {
		return nil, bizerr.WrapError(err, "transaction")
	}
	return experimentPb, nil
}

func ValidateChaosBackendReady(backend *entity.Backend) bizerr.BizError {
	if backend.Status != chaos.ChaosBackendStatus_CHAOS_BACKEND_STATUS_READY {
		return bizerr.New(bizerr.ErrCodeGeneralBusiness, "chaos backend is not ready")
	}
	return nil
}

func (svc *CreateExperimentSvc) buildCreateWorkflowParams(req *chaos.CreateExperimentRequest, cluster facade.TiDBCluster) (*workflowSvc.CreateWorkflowParam, bizerr.BizError) {
	spec, err := svc.specConverter.Pb2Entity(req.Action)
	if err != nil {
		return nil, err
	}
	summary := ""
	if req.Summary != nil {
		summary = *req.Summary
	}
	return &workflowSvc.CreateWorkflowParam{
		TiDBCluster: cluster,
		UserID:      req.UserId,
		Draft:       req.Draft,
		Summary:     summary,
		Spec:        *spec,
	}, nil
}

func (svc *CreateExperimentSvc) check(ctx context.Context, req *chaos.CreateExperimentRequest) bizerr.BizError {
	// TODO: check if chaos server is ready

	// check if there is no running/draft workflow in the same cluster
	count, err := svc.workflowRepo.Count(ctx, repo.WorkflowFilter{
		ClusterIDs: []uint64{req.ClusterId},
		Statuses:   []chaos.ExperimentStatus{chaos.ExperimentStatus_EXPERIMENT_STATUS_DRAFT, chaos.ExperimentStatus_EXPERIMENT_STATUS_RUNNING},
	})
	if err != nil {
		return bizerr.WrapError(err, "count workflow")
	}
	if count > 0 {
		return bizerr.New(bizerr.ErrCodeGeneralBusiness, "there is a running/draft workflow in the same cluster")
	}
	return nil
}

func (svc *CreateExperimentSvc) unlockWorkflow(ctx context.Context, identifier string) {
	err := svc.workflowLockRepo.Unlock(ctx, constant.LockSceneCreateWorkflow, identifier)
	if err != nil {
		log.WithContextL(ctx).Error("failed to unlock cm lock", zap.Error(err))
	}
}

func ActionTypePb2Entity(actionType chaos.ActionKind) (constant.WorkflowTemplateType, bizerr.BizError) {
	switch actionType {
	case chaos.ActionKind_ACTION_KIND_POD_SHUTDOWN:
		return constant.WorkflowTemplateTypePodShutdown, nil
	default:
		return "", bizerr.New(bizerr.ErrCodeGeneralBusiness, "action type not supported")
	}
}
