package defaultconfig

import (
	"context"
	"fmt"
	"slices"

	ccproto "github.com/tidbcloud/config-center/proto/config_center"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/repo"
)

type actorCreate struct {
	*baseActor
}

func (a *actorCreate) Create(ctx context.Context, param api.CreateDefaultConfigurationChangeParam) error {
	err := a.creatable(ctx, param)
	if err != nil {
		return err
	}

	builder := a.newDefaultConfigurationChangeBuilder(param)

	err = builder.Assemble(ctx)
	if err != nil {
		return err
	}

	createdChange, err := a.Repo.CreateDefaultConfigurationChange(ctx, builder.Change)
	if err != nil {
		return err
	}

	err = a.Repo.BatchUpsertDefaultConfigurationChangeItems(ctx, createdChange.ID, builder.ChangeItems)
	if err != nil {
		return err
	}

	a.Change = createdChange
	return nil
}

func (a *actorCreate) creatable(ctx context.Context, param api.CreateDefaultConfigurationChangeParam) error {
	tmplCache := map[uint64]*ccent.Template{}

	inProgressChanges, err := a.Repo.ListDefaultConfigurationChanges(ctx,
		repo.DefaultConfigurationChangeOpts.PhaseNotIn(ccent.DefaultConfigurationChangeClosedValues),
	)
	if err != nil {
		return fmt.Errorf("failed to list in-progress changes: %w", err)
	}
	inProgressChangeIDs := container.TransformSlice(inProgressChanges, func(chg *ccent.DefaultConfigurationChange) uint64 {
		return chg.ID
	})

	for _, bindingInParam := range param.TemplateDefaultCaseBindingChange {
		if len(inProgressChangeIDs) != 0 {
			// check if there are some in-progress changes with the same default case.
			count, err := a.Repo.CountDefaultConfigurationChangeItems(ctx,
				repo.DefaultConfigurationChangeItemsOpts.OwnByChanges(inProgressChangeIDs),
				repo.DefaultConfigurationChangeItemsOpts.TemplateDefaultCaseBindingIs(bindingInParam.ClusterType, bindingInParam.DefaultCase),
			)
			if err != nil {
				return fmt.Errorf("count template change with default case %s failed: %s", bindingInParam.DefaultCase.HumanReadString(), err)
			}
			if count > 0 {
				return fmt.Errorf("there are some in-progress template changes with default case %s", bindingInParam.DefaultCase.HumanReadString())
			}
		}

		// check if template id and default case is mapped correctly.
		tmpl, exist := tmplCache[bindingInParam.TemplateID]
		if !exist {
			tmpl, err = a.ConfigService.GetTemplate(ctx, bindingInParam.ClusterType, bindingInParam.TemplateID, api.GetTemplateParam{
				WithDefaultCases: true,
			})
			if err != nil {
				return fmt.Errorf("get template %d failed: %s", bindingInParam.TemplateID, err)
			}
			tmplCache[bindingInParam.TemplateID] = tmpl
		}

		index := slices.IndexFunc(tmpl.DefaultCases, func(def *ccent.TemplateDefaultCase) bool {
			return def.IsEqual(bindingInParam.DefaultCase)
		})
		if index < 0 {
			return fmt.Errorf("template %d does not have default case %s", bindingInParam.TemplateID, bindingInParam.DefaultCase.HumanReadString())
		}
	}

	if param.VersionRelease != nil {
		allVersions, err := a.ConfigService.ListAvailableClusterVersions(ctx, param.VersionRelease.ClusterType)
		if err != nil {
			return fmt.Errorf("list available versions failed: %s", err)
		}
		foundSource, foundTarget := false, false
		for _, version := range allVersions {
			if version.Version == param.VersionRelease.SourceVersion {
				foundSource = true
			}
			if version.Version == param.VersionRelease.TargetVersion {
				foundTarget = true
			}
		}
		if !foundSource {
			return fmt.Errorf("source version '%s' is not existing", param.VersionRelease.SourceVersion)
		}
		if foundTarget {
			return fmt.Errorf("target version '%s' is already existing", param.VersionRelease.TargetVersion)
		}
	}

	return nil
}

type DefaultConfigurationChangeBuilder struct {
	// Inputs
	Comps Components
	Param api.CreateDefaultConfigurationChangeParam

	// Outputs
	Change      *ccent.DefaultConfigurationChange
	ChangeItems []*ccent.DefaultConfigurationChangeItem
}

func (a *actorCreate) newDefaultConfigurationChangeBuilder(param api.CreateDefaultConfigurationChangeParam) *DefaultConfigurationChangeBuilder {
	b := &DefaultConfigurationChangeBuilder{
		Comps: a.Components,
		Param: param,
	}

	b.Change = &ccent.DefaultConfigurationChange{
		ID:           0,
		Applicant:    param.Applicant,
		Reason:       param.Reason,
		DisplayName:  param.DisplayName,
		Phase:        ccent.DefaultConfigurationChangePhaseInProgress,
		PhaseReason:  "",
		Environments: append(slices.Clone(a.Conf.SubEnvironments), string(consts.GetEnv())),
	}
	b.ChangeItems = []*ccent.DefaultConfigurationChangeItem{}

	return b
}

func (b *DefaultConfigurationChangeBuilder) Assemble(ctx context.Context) error {
	var err error

	err = b.AssembleVersionRelease(ctx)
	if err != nil {
		return fmt.Errorf("assemble version release failed: %s", err)
	}

	err = b.AssembleTemplateDefaultCaseBindingChange(ctx)
	if err != nil {
		return fmt.Errorf("assemble template default case binding change failed: %s", err)
	}

	err = b.AssembleDefaultComponentChange(ctx)
	if err != nil {
		return fmt.Errorf("assemble default component change failed: %s", err)
	}

	return nil
}

// AssembleVersionRelease modify `b.Change` by the version release
//
// NOTE: It will cause change for template default case bindings and default component, so call it firstly.
func (b *DefaultConfigurationChangeBuilder) AssembleVersionRelease(ctx context.Context) error {
	if b.Param.VersionRelease == nil {
		return nil
	}

	verRelease := b.Param.VersionRelease

	// list all template default case bindings from source version to change.
	allBindings := []*ccent.TemplateDefaultCaseBinding{}
	coveringInstanceTypes := container.NewSet[string]()
	param := api.ListTemplateDefaultCaseBindingsParam{
		DefaultForVersion: &verRelease.SourceVersion,
	}
	err := travelAllTemplateDefaultCaseBindings(ctx, b.Comps.ConfigService, verRelease.ClusterType, param, func(binding *ccent.TemplateDefaultCaseBinding) {
		binding.DefaultCase.Version = verRelease.TargetVersion // upgrade version by default.
		allBindings = append(allBindings, binding)
		coveringInstanceTypes.Insert(ccent.FormatCloudInstanceType(binding.DefaultCase.CloudInstanceType))
	})
	if err != nil {
		return fmt.Errorf("list template default case bindings for version '%s' failed: %s", verRelease.SourceVersion, err)
	}

	// list all components to build the new default components.
	allDefComps := []*ccent.DefaultComponentConfig{}
	comps := ccproto.ClusterType_DEDICATED_TIDB.AvailableComponents() // TODO: use List Images api in the future

	for _, comp := range comps {
		if comp == "tiproxy" { // FIXME: remove it when using List Images api in the future
			continue
		}

		// copy the default component config from source version.
		defComp, err := b.Comps.ConfigService.GetDefaultComponent(ctx, &ccent.ComponentDefaultCase{
			ClusterType: verRelease.ClusterType,
			Component:   comp,
			Version:     verRelease.SourceVersion,
		})
		if err != nil {
			return fmt.Errorf("get default component of '%s' failed: %s", comp, err)
		}

		defComp.DefaultCase.Version = verRelease.TargetVersion
		allDefComps = append(allDefComps, defComp)
	}

	// update change
	b.Change.ItemTypes = append(b.Change.ItemTypes, ccent.DefaultConfigurationChangeItemTypeVersionRelease)
	b.ChangeItems = append(b.ChangeItems, &ccent.DefaultConfigurationChangeItem{
		ID:       0,
		ItemType: ccent.DefaultConfigurationChangeItemTypeVersionRelease,
		Data: &ccent.DefaultConfigurationChangeItemDataUnion{
			VersionRelease: &ccent.VersionRelease{
				ClusterType:           verRelease.ClusterType,
				SourceVersion:         verRelease.SourceVersion,
				TargetVersion:         verRelease.TargetVersion,
				CoveringInstanceTypes: coveringInstanceTypes.Slice(),
			},
		},
		ParentID: 0,
	})
	// update b.Param
	b.Param.TemplateDefaultCaseBindingChange = allBindings
	b.Param.DefaultComponentConfigChange = allDefComps

	return nil
}

func (b *DefaultConfigurationChangeBuilder) AssembleTemplateDefaultCaseBindingChange(ctx context.Context) error {
	for _, binding := range b.Param.TemplateDefaultCaseBindingChange {
		bindingChg := &ccent.TemplateDefaultCaseBindingChange{
			Origin: *binding,
		}
		bindingChg.Modified = *bindingChg.ToReleaseData(nil)

		b.Change.ItemTypes = append(b.Change.ItemTypes, ccent.DefaultConfigurationChangeItemTypeTemplateDefaultCaseBinding)
		b.ChangeItems = append(b.ChangeItems, &ccent.DefaultConfigurationChangeItem{
			ID:       0,
			ItemType: ccent.DefaultConfigurationChangeItemTypeTemplateDefaultCaseBinding,
			Data: &ccent.DefaultConfigurationChangeItemDataUnion{
				TemplateDefaultCaseBinding: bindingChg,
			},
			ParentID: 0,
		})
	}

	return nil
}

func (b *DefaultConfigurationChangeBuilder) AssembleDefaultComponentChange(ctx context.Context) error {
	for _, comp := range b.Param.DefaultComponentConfigChange {
		compChg := &ccent.DefaultComponentConfigChange{
			Origin: *comp,
		}
		compChg.Modified = *compChg.ToReleaseData(nil)

		b.Change.ItemTypes = append(b.Change.ItemTypes, ccent.DefaultConfigurationChangeItemTypeDefaultComponentConfig)
		b.ChangeItems = append(b.ChangeItems, &ccent.DefaultConfigurationChangeItem{
			ID:       0,
			ItemType: ccent.DefaultConfigurationChangeItemTypeDefaultComponentConfig,
			Data: &ccent.DefaultConfigurationChangeItemDataUnion{
				DefaultComponentConfig: compChg,
			},
			ParentID: 0,
		})

	}

	return nil
}

func travelAllTemplateDefaultCaseBindings(ctx context.Context, svc api.ConfigurationService, clusterType ccent.ClusterType, param api.ListTemplateDefaultCaseBindingsParam, fn func(binding *ccent.TemplateDefaultCaseBinding)) error {
	param.Page = 1
	param.PerPage = 50

	for {
		bindings, _, err := svc.ListTemplateDefaultCaseBindings(ctx, clusterType, param)
		if err != nil {
			return err
		}

		if len(bindings) == 0 {
			break
		}

		for _, binding := range bindings {
			fn(binding)
		}
		param.Page++
	}

	return nil
}
