package config

import (
	"context"
	"fmt"
	"slices"

	baseapi "github.com/tidbcloud/devops/api"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	"github.com/tidbcloud/devops/entity/event"
	"github.com/tidbcloud/devops/pkg/infra/clients/configcentercli"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/dataflowcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/mgmthttpcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/serverlesscli"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/eventbus"
	"github.com/tidbcloud/devops/pkg/infra/utils/container/pair"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	apprapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/change/config/applier"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/repo"
	cmxapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"
	"github.com/tidbcloud/devops/pkg/service/tenant"
)

type Objects struct {
	// Origin is the original instance object which is used to patch.
	//
	// NOTE: **DO NOT** modify it.
	Origin *ccent.ConfigurationChange
	// Instance is the copied instance object that is used in action.
	Change *ccent.ConfigurationChange
	// Event is the event detail of the change.
	EventDetail *event.CloudConfigChangeEventDetail
}

type Components struct {
	Conf            config.CloudConfigServiceConfig
	Repo            repo.CloudConfigRepo
	ConfigCenter    *configcentercli.Client
	CoreSvc         *coresvccli.CoreSvcCli
	Dataflow        *dataflowcli.DataflowCli
	Serverless      *serverlesscli.Client
	ApprovalService apprapi.ApprovalServiceAPI
	MaintenanceSvc  cmxapi.ServiceAPI
	TenantService   tenant.TenantServiceAPI
	ConfigService   api.ConfigurationService
	ParamDict       api.ParameterDirectoryService
	MgmtHTTP        mgmthttpcli.MgmtHTTPClient
	EventBus        *eventbus.EventBus
}

type Context struct {
	context.Context
	Components
	Objects
}

func (c *Context) PatchConfigurationChange(ctx context.Context, patch ...repo.PatchConfigurationChange) error {
	return c.Repo.PatchConfigurationChange(ctx, c.Origin, patch...)
}

type ConfigurationChangeActor interface {
	// GetContext returns the context of the actor.
	GetContext() *Context
	// Preview validates the param and returns the result of previewing a cluster config change.
	Preview(param api.PreviewConfigurationChangeParam) (*api.PreviewConfigurationChangeResult, error)
	// Create validates the param and creates a cluster config change in the database.
	Create(param api.CreateConfigurationChangeParam) error
	CreateManaged(param api.CreateConfigurationChangeParam, ref api.ManagedConfigurationChangeRef) error
	// SendApproval create an operation in operation serivce to send an approval.
	SendApproval() error
	// ContinueIfApproved check the approval status and transition the phase.
	ContinueIfApproved() error
	// Apply update configs in config center and call mgmt API to apply the changes.
	Apply(param api.ApplyConfigurationChangeParam) error
	// CheckResult check the result of applying.
	CheckResult() error
	// Cancel close the change and do nothing.
	Cancel() error
}

func NewConfigurationChangeActor(ctx context.Context, comps Components, chg *ccent.ConfigurationChange) ConfigurationChangeActor {
	var a ConfigurationChangeActor
	var err error

	actx := &Context{
		Context:    ctx,
		Components: comps,
		Objects: Objects{
			Origin: chg,
		},
	}
	if actx.Origin == nil {
		return &actorCreate{
			baseActor: &baseActor{
				Context:    actx,
				defaultErr: errs.APIErrorf(baseapi.ErrCodeFailedPrecond, "only support to create a new cluster config change"),
			},
		}
	}

	actx.Change, err = pointer.DeepCopy(actx.Origin) // deep copy to avoid modify origin instance directly
	if err != nil {
		return &baseActor{defaultErr: errs.APIErrorf(baseapi.ErrCodeFailedPrecond, "copy instance failed: %s", err)}
	}

	phase := actx.Origin.Phase
	base := &baseActor{
		Context: actx,
		defaultErr: errs.APIErrorf(baseapi.ErrCodeFailedPrecond,
			"unsupported action when phase is %s", actx.Origin.Phase,
		),
	}
	switch {
	case phase == ccent.ConfigurationChangePhaseCreated:
		a = &actorCreated{baseActor: base}
	case phase == ccent.ConfigurationChangePhaseApproving:
		a = &actorApproving{baseActor: base}
	case phase == ccent.ConfigurationChangePhaseWaitForApply:
		a = &actorWaitForApply{baseActor: base}
	case phase == ccent.ConfigurationChangePhaseApplying:
		a = &actorApplying{baseActor: base}
	case slices.Contains(ccent.ConfigurationChangePhaseFinishedValues, phase):
		// finished phase, can't do anything
		a = base
	default:
		base.defaultErr = fmt.Errorf("unsupported instance phase: %s", actx.Origin.Phase)
		a = base
	}

	return a
}

type baseActor struct {
	*Context

	defaultErr error
}

func (a *baseActor) GetContext() *Context {
	return a.Context
}

func (a *baseActor) Preview(param api.PreviewConfigurationChangeParam) (*api.PreviewConfigurationChangeResult, error) {
	return nil, a.defaultErr
}

func (a *baseActor) Create(param api.CreateConfigurationChangeParam) error {
	return a.defaultErr
}

func (a *baseActor) CreateManaged(param api.CreateConfigurationChangeParam, ref api.ManagedConfigurationChangeRef) error {
	return a.defaultErr
}

func (a *baseActor) SendApproval() error {
	return a.defaultErr
}

func (a *baseActor) ContinueIfApproved() error {
	return a.defaultErr
}

func (a *baseActor) Apply(param api.ApplyConfigurationChangeParam) error {
	return a.defaultErr
}

func (a *baseActor) CheckResult() error {
	return a.defaultErr
}

func (a *baseActor) Cancel() error {
	return a.defaultErr
}

func (a *baseActor) cancel() error {
	ctx := a.Context

	// transition status to 'canceled'
	err := a.PatchConfigurationChange(ctx, PatchConfigurationChangeToClose(ccent.ConfigurationChangePhaseCancelled))
	return err
}

func (a *baseActor) distributeChange(allAppliers []applier.Applier) (*pair.Pairs[applier.Applier, *applier.Input], error) {
	chg := a.Change

	results := pair.NewPairs[applier.Applier, *applier.Input]()

	// build input for every applier
	for _, apler := range allAppliers {
		results.Append(apler, applier.NewInput(chg))
	}

	// accept
	for _, fileChg := range chg.Scope.Files {
		unaccept := true

		for apler, input := range results.Pairs() {
			accepted := apler.AcceptFileChange(fileChg)
			if accepted {
				unaccept = false
				input.Files = append(input.Files, fileChg)
			}
		}

		if unaccept {
			return nil, fmt.Errorf("no applier can handle the file change: %s", fileChg.Filename)
		}
	}
	for _, compChg := range chg.Scope.Components {
		unaccept := true

		for apler, input := range results.Pairs() {
			accepted := apler.AcceptComponentChange(compChg)
			if accepted {
				unaccept = false
				input.Components = append(input.Components, compChg)
			}
		}

		if unaccept {
			return nil, fmt.Errorf("no applier can handle the component change: %s", compChg.Component)
		}
	}
	if chg.Scope.Version != nil {
		unaccept := true

		for apler, input := range results.Pairs() {
			accepted := apler.AcceptVersionChange(chg.Scope.Version, chg.ChangeType)
			if accepted {
				unaccept = false
				input.Version = chg.Scope.Version
			}
		}

		if unaccept {
			return nil, fmt.Errorf("no applier can handle the version change")
		}
	}

	return results, nil
}

func (a *baseActor) candidateAppliers(chg *ccent.ConfigurationChange) ([]applier.Applier, error) {
	switch chg.TargetType {
	case ccent.ConfigurationChangeTargetTypeTenantInit:
		return []applier.Applier{
			applier.NewConfigCenterTenantInitApplier(a.Components.ConfigCenter),
		}, nil

	case ccent.ConfigurationChangeTargetTypeCluster:
		switch chg.ChangeType {
		case string(api.ChangeTypeDm):
			return []applier.Applier{
				applier.NewOnlyConfigCenterClusterApplier(a.Components.ConfigCenter),
			}, nil
		case string(api.ChangeTypeTiproxy):
			return []applier.Applier{
				applier.NewMgmtServiceApplier(a.Components.CoreSvc, a.Components.ConfigCenter),
			}, nil
		case string(api.ChangeTypeLogicalCluster):
			return []applier.Applier{
				applier.NewNextGenApplier(a.Components.CoreSvc, a.Components.MgmtHTTP, a.Components.ConfigCenter, a.EventBus),
			}, nil
		case string(api.ChangeTypeSharedPool):
			return []applier.Applier{
				applier.NewResourcePoolApplier(a.Components.CoreSvc, a.Components.MgmtHTTP, a.Components.ConfigCenter, a.EventBus),
			}, nil
		case string(api.ChangeTypeChangefeedNg), string(api.ChangeTypeTiCDCNg):
			return []applier.Applier{
				applier.NewDataflowNgApplier(a.Components.Dataflow, a.Components.ConfigCenter),
			}, nil
		case string(api.ChangeTypeChangefeedEssential):
			return []applier.Applier{
				applier.NewServerlessChangefeedApplier(a.Components.Serverless),
			}, nil
		default:
			return []applier.Applier{
				applier.NewMgmtServiceApplier(a.Components.CoreSvc, a.Components.ConfigCenter),
				applier.NewDataflowApplier(a.Components.Dataflow, a.Components.ConfigCenter),
				applier.NewOnlyConfigCenterClusterApplier(a.Components.ConfigCenter),
			}, nil
		}

	case ccent.ConfigurationChangeTargetTypeTemplate:
		return []applier.Applier{
			applier.NewConfigurationTemplateApplier(a.Components.ConfigCenter),
		}, nil

	default:
		return nil, fmt.Errorf("apply config change but target type is unknown: %s", a.Change.TargetType)
	}
}
