package operation

import (
	"context"
	"fmt"

	"github.com/tidbcloud/devops/api"
	entcommon "github.com/tidbcloud/devops/entity/common"
	"github.com/tidbcloud/devops/entity/event"
	maintent "github.com/tidbcloud/devops/entity/maintenance"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	cmxapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"
	"github.com/tidbcloud/devops/pkg/service/operation/instance"
	"github.com/tidbcloud/devops/pkg/service/operation/repo"
)

var (
	phaseToSync = []opent.InstancePhase{
		opent.InstancePhaseCreated,
		opent.InstancePhaseApproving,
		opent.InstancePhasePending,
		opent.InstancePhaseRunning,
	}
)

func (s *svc) ListInstances(ctx context.Context, param ListInstanceParam) ([]*opent.Instance, int64, error) {
	param, err := param.Normalize()
	if err != nil {
		return nil, 0, errs.APIErrorNew(api.ErrCodeInvalidArgument, err)
	}

	opts := []repoopt.Option{}
	if param.Type != nil {
		opts = append(opts, repo.InstanceOpts.TypeIs(*param.Type))
	}
	if param.CreateBefore != nil {
		opts = append(opts, repo.InstanceOpts.CreateBefore(*param.CreateBefore))
	}
	if param.CreateAfter != nil {
		opts = append(opts, repo.InstanceOpts.CreateAfter(*param.CreateAfter))
	}
	if param.TargetIDsIn != nil {
		if param.Type == nil {
			return nil, 0, errs.APIErrorNew(api.ErrCodeInvalidArgument, fmt.Errorf("when filtering by target IDs, target type must be specified"))
		}
		opts = append(opts, repo.InstanceOpts.TargetIDIn(*param.Type, param.TargetIDsIn))
	}
	if param.Phases != nil {
		opts = append(opts, repo.InstanceOpts.PhaseIn(param.Phases))
	}
	if param.PhasesNot != nil {
		opts = append(opts, repo.InstanceOpts.PhaseNotIn(param.PhasesNot))
	}
	if param.Applicants != nil {
		for _, app := range param.Applicants {
			if len(app) > 0 {
				opts = append(opts, repo.InstanceOpts.Search(app))
			}
		}
	}
	if param.Operations != nil {
		for _, op := range param.Operations {
			if len(op) > 0 {
				opts = append(opts, repo.InstanceOpts.Search(op))
			}
		}
	}
	if param.ParamSearches != nil {
		opts = append(opts, repo.InstanceOpts.ParamsLike(param.ParamSearches))
	}

	total, err := s.opRepo.CountInstance(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	if param.Page > 0 {
		opts = append(opts, repo.InstanceOpts.WithPager(param.Page, param.PerPage))
	}
	instances, err := s.opRepo.ListInstances(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}
	if err := s.fillInstanceExecutionWindows(ctx, instances...); err != nil {
		return nil, 0, err
	}

	return instances, total, nil
}

func (s *svc) GetInstance(ctx context.Context, id uint64) (*opent.Instance, error) {
	ins, err := s.opRepo.GetInstance(ctx, id)
	if err != nil {
		return nil, err
	}
	if err := s.fillInstanceExecutionWindows(ctx, ins); err != nil {
		return nil, err
	}

	return ins, nil
}

func (s *svc) fillInstanceExecutionWindows(ctx context.Context, instances ...*opent.Instance) error {
	windowIDs := container.NewSet[uint64]()
	for _, ins := range instances {
		if ins.ExecutionWindowID == nil {
			continue
		}
		windowIDs.Insert(*ins.ExecutionWindowID)
	}
	if windowIDs.Len() == 0 {
		return nil
	}

	windows, _, err := s.maintSvc.ListOpsExecutionWindows(ctx, cmxapi.ListOpsExecutionWindowParam{
		IDs: windowIDs.Slice(),
	})
	if err != nil {
		return err
	}
	windowByID := container.SliceToMap(windows, func(window *maintent.OpsExecutionWindow) uint64 {
		return window.ID
	})

	for _, ins := range instances {
		if ins.ExecutionWindowID == nil {
			continue
		}
		windowID := *ins.ExecutionWindowID
		window, ok := windowByID[windowID]
		if !ok {
			return fmt.Errorf("ops execution window %d not found", windowID)
		}
		ins.ExecutionWindow = window
	}

	return nil
}

func (s *svc) PreviewInstance(ctx context.Context, param CreateInstanceParam) (*PreviewInstanceResult, error) {
	actor := instance.NewInstanceActor(ctx, s.insComps(ctx), nil)
	advisories, err := actor.Preview(instance.CreateInstanceParam(param))
	if err != nil {
		return nil, err
	}
	return &PreviewInstanceResult{Advisories: advisories}, nil
}

func (s *svc) CreateInstance(ctx context.Context, param CreateInstanceParam) (*uint64, error) {
	var id *uint64
	param.OperatorEntity = pointer.OrDefault(
		param.OperatorEntity,
		entcommon.BuildOperatorEntity(consts.GetOperatorTypeFromCtx(ctx), consts.GetOperatorFromCtx(ctx, param.Applicant)),
	)

	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := instance.NewInstanceActor(ctx, s.insComps(ctx), nil)
		err := actor.Create(instance.CreateInstanceParam(param))
		if err != nil {
			return err
		}
		newIns := actor.GetContext().Instance

		ev := buildEvent(ctx, actor.GetContext(), event.OperationEventTypeCreate)
		err = s.eventbus.OperationEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %w", err)
		}

		id = &newIns.ID
		return nil
	})

	if id != nil {
		s.syncInsCh <- *id
	}

	return id, err
}

func (s *svc) SubmitInstance(ctx context.Context, id uint64) error {
	ins, err := s.opRepo.GetInstance(ctx, id)
	if err != nil {
		return err
	}

	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := instance.NewInstanceActor(ctx, s.insComps(ctx), ins)
		err := actor.Submit()
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.OperationEventTypeSubmit)
		err = s.eventbus.OperationEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %w", err)
		}

		return nil
	})
}

func (s *svc) UpdateInstance(ctx context.Context, id uint64, param UpdateInstanceParam) error {
	ins, err := s.opRepo.GetInstance(ctx, id)
	if err != nil {
		return err
	}

	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := instance.NewInstanceActor(ctx, s.insComps(ctx), ins)
		err := actor.Update(instance.UpdateInstanceParam(param))
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.OperationEventTypeUpdate)
		err = s.eventbus.OperationEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %w", err)
		}

		return nil
	})
}

func (s *svc) CancelInstance(ctx context.Context, id uint64) error {
	ins, err := s.opRepo.GetInstance(ctx, id)
	if err != nil {
		return err
	}

	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := instance.NewInstanceActor(ctx, s.insComps(ctx), ins)
		err := actor.Cancel()
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.OperationEventTypeCancel)
		err = s.eventbus.OperationEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %s", err)
		}

		return nil
	})
}

func (s *svc) StartInstanceAction(ctx context.Context, id uint64, order opent.ActionOrder, operatorEmail string) error {
	ins, err := s.opRepo.GetInstance(ctx, id)
	if err != nil {
		return err
	}

	for _, action := range ins.Actions {
		if action.Order != order {
			continue // skip if not match order
		}
		actionMeta, err := s.GetAction(ctx, action.Name)
		if err != nil {
			return err
		}
		if !actionMeta.AllowAnyoneApply && operatorEmail != ins.Applicant {
			return errs.APIErrorNew(api.ErrCodePermissionDenied, fmt.Errorf("only the applicant can start the action"))
		}
	}

	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		return s.startInstanceAction(ctx, ins, order, true)
	})
}

func (s *svc) startInstanceAction(ctx context.Context, ins *opent.Instance, order opent.ActionOrder, manual bool) error {
	actor := instance.NewInstanceActor(ctx, s.insComps(ctx), ins)
	err := actor.StartAction(order, manual)
	if err != nil {
		return err
	}

	ev := buildEvent(ctx, actor.GetContext(), event.OperationEventTypeStartAction)
	err = s.eventbus.OperationEvent.Publish(ev)
	if err != nil {
		return fmt.Errorf("publish event failed: %s", err)
	}

	return nil
}

func (s *svc) FinishInstanceAction(ctx context.Context, id uint64, order opent.ActionOrder, result opent.ActionStatus, output string) error {
	ins, err := s.opRepo.GetInstance(ctx, id)
	if err != nil {
		return err
	}

	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		param := instance.FinishInstanceActionParam{
			AllowManual: true,
			Result:      result,
			Output:      output,
		}
		return s.finishInstanceAction(ctx, ins, order, param)
	})
}

func (s *svc) finishInstanceAction(ctx context.Context, ins *opent.Instance, order opent.ActionOrder, param instance.FinishInstanceActionParam) error {
	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := instance.NewInstanceActor(ctx, s.insComps(ctx), ins)

		err := actor.FinishAction(order, param)
		if err != nil {
			return err
		}

		ev := buildEvent(ctx, actor.GetContext(), event.OperationEventTypeFinishAction)
		err = s.eventbus.OperationEvent.Publish(ev)
		if err != nil {
			return fmt.Errorf("publish event failed: %s", err)
		}

		return nil
	})
}

func (s *svc) WatchInstances(ctx context.Context) error {
	logger := log.FromContextOrDef(ctx, s.Logger(ctx))

	instances, err := s.opRepo.ListInstances(ctx, repo.InstanceOpts.PhaseIn(phaseToSync))
	if err != nil {
		return err
	}

	if len(instances) != 0 {
		logger.Info("watched instances",
			"instance_ids", container.TransformSlice(instances, func(i *opent.Instance) uint64 { return i.ID }),
		)
	}

	for _, ins := range instances {
		s.syncInsCh <- ins.ID
	}

	return nil
}

func (s *svc) SyncInstance(ctx context.Context, id uint64) error {
	ins, err := s.opRepo.GetInstance(ctx, id)
	if err != nil {
		return err
	}

	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		actor := instance.NewInstanceActor(ctx, s.insComps(ctx), ins)
		var err error
		var evType event.EventType

		switch ins.Phase {
		case opent.InstancePhaseCreated:
			err = actor.SendApproval()
			evType = event.OperationEventTypeSendApproval
		case opent.InstancePhaseApproving:
			err = actor.ContinueIfApproved()
			evType = event.OperationEventTypeFinishApproval
		case opent.InstancePhasePending, opent.InstancePhaseRunning:
			// choose a action to handle
			for _, insAction := range ins.Actions {
				if insAction.Status == opent.ActionStatusSucceeded || insAction.Status == opent.ActionStatusSkipped {
					continue // goto next action
				}

				// found a action to start or result
				switch insAction.Status {
				case opent.ActionStatusCreated:
					err = s.startInstanceAction(ctx, ins, insAction.Order, false)
				case opent.ActionStatusRunning:
					param := instance.FinishInstanceActionParam{
						AllowManual: false, // don't allow to update manual action
					}
					err = s.finishInstanceAction(ctx, ins, insAction.Order, param)
				case opent.ActionStatusFailed,
					opent.ActionStatusUnknown:
					return errs.RetriableErrorf("status of action '%s' is %s", insAction.Name, insAction.Status)
				}

				evType = "" // don't need here, send event in sub function
				break
			}
		}

		if err == nil && evType != "" {
			ev := buildEvent(ctx, actor.GetContext(), evType)
			err = s.eventbus.OperationEvent.Publish(ev)
			if err != nil {
				return fmt.Errorf("publish event failed: %w", err)
			}
		}

		return err
	})

	return err
}

func (s *svc) insComps(ctx context.Context) instance.Components {
	return instance.Components{
		Logger:            log.FromContextOrDef(ctx, s.Logger(ctx)),
		Conf:              s.conf,
		UserService:       s.userSvc,
		ApprovalService:   s.apprSvc,
		MaintenanceSvc:    s.maintSvc,
		OperationRepo:     s.opRepo,
		OperationRegistry: s.opRegistry,
		ActionRegistry:    s.actRegistry,
		CoreSvcCli:        s.coresvcCli,
		TenantService:     s.tenantSvc,
	}
}
