package instance

import (
	"fmt"
	"slices"

	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
)

type actorCreateInstance struct {
	*baseActor
}

func (a *actorCreateInstance) Preview(param CreateInstanceParam) ([]opent.Advisory, error) {
	ctx := a.Context

	if err := param.Validate(); err != nil {
		return nil, err
	}

	operation := a.OperationRegistry.Get(param.Operation)
	if operation == nil {
		return nil, fmt.Errorf("unknown operation '%s'", param.Operation)
	}
	if param.Type != operation.Target {
		return nil, fmt.Errorf("type '%s' is mismatch with operation", param.Type)
	}

	newIns, err := a.newInstance(param, operation)
	if err != nil {
		return nil, err
	}

	var advisories []opent.Advisory
	for _, actionInIns := range param.Actions {
		action, exist := a.ActionRegistry.Get(actionInIns.Name)
		if !exist {
			return nil, fmt.Errorf("miss a action '%s' in action registry", actionInIns.Name)
		}
		if err := action.PreCheck(ctx, newIns); err != nil {
			return nil, fmt.Errorf("action '%s' precheck failed: %w", actionInIns.Name, err)
		}
		if aa, ok := action.(actions.AdvisoryAction); ok {
			adv, err := aa.Advisory(ctx, newIns)
			if err != nil {
				a.Logger.Error(err, "advisory check failed", "action", actionInIns.Name)
				return nil, fmt.Errorf("advisory check for action '%s' failed: %w", actionInIns.Name, err)
			}
			a.Logger.Info("advisory check result", "action", actionInIns.Name, "count", len(adv), "advisories", adv)
			advisories = append(advisories, adv...)
		}
	}

	return advisories, nil
}

func (a *actorCreateInstance) Create(param CreateInstanceParam) error {
	ctx := a.Context

	if err := param.Validate(); err != nil {
		return err
	}

	// get the dependent operation
	operation := a.OperationRegistry.Get(param.Operation)
	if operation == nil {
		return fmt.Errorf("unknown operation '%s'", param.Operation)
	}
	if param.Type != operation.Target {
		return fmt.Errorf("type '%s' is mismatch with operation", param.Type)
	}

	// validate actions is valid
	for _, actionInOP := range operation.Actions {
		index := slices.IndexFunc(param.Actions, func(ciap CreateInstanceActionParam) bool {
			return ciap.Name == actionInOP.Name
		})

		if index == -1 {
			if !actionInOP.Optional {
				return fmt.Errorf("miss a required action '%s'", actionInOP.Name)
			}

			continue // skip a optional action
		}
	}

	newIns, err := a.newInstance(param, operation)
	if err != nil {
		return err
	}

	// action precheck
	for _, actionInIns := range param.Actions {
		action, exist := a.ActionRegistry.Get(actionInIns.Name)
		if !exist {
			return fmt.Errorf("miss a action '%s' in action registry", actionInIns.Name)
		}
		err := action.PreCheck(ctx, newIns)
		if err != nil {
			return fmt.Errorf("action '%s' precheck failed: %w", actionInIns.Name, err)
		}
	}

	if param.ExecutionWindow != nil {
		window, err := a.MaintenanceSvc.CreateOpsExecutionWindow(ctx, *param.ExecutionWindow)
		if err != nil {
			return fmt.Errorf("create ops execution window failed: %w", err)
		}
		newIns.ExecutionWindowID = pointer.Pointer(window.ID)
		newIns.ExecutionWindow = window
	}

	// save a new instance to repo
	createdIns, err := a.OperationRepo.CreateInstance(ctx, newIns)
	if err != nil {
		return err
	}

	a.Instance = createdIns
	return nil
}

func (a *actorCreateInstance) newInstance(param CreateInstanceParam, operation *opent.Operation) (*opent.Instance, error) {
	newInstance := &opent.Instance{
		ID:             0,
		Type:           param.Type,
		Operation:      param.Operation,
		Reason:         param.Reason,
		Applicant:      param.Applicant,
		OperatorEntity: param.OperatorEntity,
		Params:         param.Params,

		Phase: opent.InstancePhaseCreated,
	}
	if param.IsDraft {
		newInstance.Phase = opent.InstancePhaseDraft
	}

	// build instance actions
	for order, actionParam := range param.Actions {
		action, exist := a.ActionRegistry.Get(actionParam.Name)
		if !exist {
			return nil, fmt.Errorf("miss a action '%s' in action registry", actionParam.Name)
		}

		newAction := &opent.InstanceAction{
			ID:              0,
			Order:           opent.ActionOrder(order), // generate order
			Name:            actionParam.Name,
			Type:            action.Metadata().Type,
			StartPolicy:     actionParam.StartPolicy,
			StartScheduleAt: actionParam.StartScheduleAt,

			StartedAt:  nil,
			FinishedAt: nil,
			Status:     opent.ActionStatusCreated,
		}
		newInstance.Actions = append(newInstance.Actions, newAction)
	}

	err := a.normalizeParams(newInstance, operation)
	if err != nil {
		return nil, fmt.Errorf("normalize params failed: %w", err)
	}

	return newInstance, nil
}

func (a *actorCreateInstance) normalizeParams(ins *opent.Instance, op *opent.Operation) error {
	if ins.Params == nil {
		ins.Params = make(map[opent.ParamName]string)
	}

	// Normalize operation-defined params before action prechecks.
	for name, def := range op.Params {
		_, exist := ins.GetParam(name)
		if !exist {
			if def.Const != nil {
				ins.Params[name] = *def.Const
				continue
			}
			if def.Default != nil {
				ins.Params[name] = *def.Default
				continue
			}
			if def.Optional {
				continue
			}
			return fmt.Errorf("param '%s' is required", name)
		}

	}

	return nil
}
