package defaultconfig

import (
	"context"
	"fmt"
	"slices"
	"time"

	apprent "github.com/tidbcloud/devops/entity/approval"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	"github.com/tidbcloud/devops/entity/event"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/container/pair"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/repo"
)

type actorInProgress struct {
	*baseActor
}

func (a *actorInProgress) Update(ctx context.Context, param api.BatchUpdateDefaultConfigurationChangeItemsParam) error {
	updater := a.newDefaultConfigurationChangeUpdater(param)
	err := updater.Update(ctx)
	if err != nil {
		return err
	}

	err = a.Repo.UpdateDefaultConfigurationChange(ctx, updater.Change)
	if err != nil {
		return err
	}
	err = a.Repo.BatchUpsertDefaultConfigurationChangeItems(ctx, updater.Change.ID, updater.ChangeItems)
	if err != nil {
		return err
	}

	a.EventDetail = param

	return nil
}

func (a *actorInProgress) ReleaseStart(ctx context.Context, env string, description string) error {
	chg := a.Change

	curItems, err := a.Repo.ListDefaultConfigurationChangeItems(ctx,
		repo.DefaultConfigurationChangeItemsOpts.OwnByChange(chg.ID),
	)
	if err != nil {
		return fmt.Errorf("list change items failed: %s", err)
	}

	// use data in items as expected data
	expItemPairs := pair.NewPairs[*ccent.DefaultConfigurationChangeItem, *ccent.DefaultConfigurationChangeItemReleaseDataUnion]()
	for _, item := range curItems {
		expItemPairs.Append(item, item.Data.ToRelease())
	}

	return a.releaseStart(ctx, env, fmt.Sprintf("[RELEASE] %s", description), expItemPairs)
}

func (a *actorInProgress) ReleaseIfApproved(ctx context.Context, rid uint64) error {
	chg := a.Change

	release, err := a.Repo.GetDefaultConfigurationChangeRelease(ctx, rid)
	if err != nil {
		return fmt.Errorf("get release failed: %s", err)
	}

	if release.Phase != ccent.DefaultConfigurationChangeReleasePhaseApproving {
		return fmt.Errorf("release is not approving")
	}

	// check if approved
	approval, err := a.ApprovalService.GetApproval(ctx, release.ApprovalID)
	if err != nil {
		return fmt.Errorf("get approval failed: %s", err)
	}
	if approval.Status != apprent.ApprovalStatusApproved {
		return fmt.Errorf("release is not approved")
	}

	// list items and releases
	itemReleases, err := a.Repo.ListDefaultConfigurationChangeItemReleases(ctx,
		repo.DefaultConfigurationChangeItemReleaseOpts.OwnByChange(chg.ID),
		repo.DefaultConfigurationChangeItemReleaseOpts.ReleaseNumberIs(release.ReleaseNumber),
	)
	if err != nil {
		return fmt.Errorf("list item releases failed: %s", err)
	}

	itemIDs := container.TransformSlice(itemReleases, func(e *ccent.DefaultConfigurationChangeItemRelease) uint64 {
		return e.ItemID
	})
	items, err := a.Repo.ListDefaultConfigurationChangeItems(ctx,
		repo.DefaultConfigurationChangeItemsOpts.OwnByChange(chg.ID),
		repo.DefaultConfigurationChangeItemsOpts.IDIn(itemIDs),
	)
	if err != nil {
		return fmt.Errorf("list items failed: %s", err)
	}

	// collect entity to release
	bindingsToRelease := []*ccent.TemplateDefaultCaseBinding{}
	defCompsToRelease := []*ccent.DefaultComponentConfig{}
	for _, itemRelease := range itemReleases {
		idx := slices.IndexFunc(items, func(e *ccent.DefaultConfigurationChangeItem) bool {
			return e.ID == itemRelease.ItemID
		})
		if idx == -1 {
			return fmt.Errorf("the item of release '%d' is not found", itemRelease.ID)
		}
		item := items[idx]

		switch item.ItemType {
		case ccent.DefaultConfigurationChangeItemTypeTemplateDefaultCaseBinding:
			binding, err := collectTemplateDefaultCaseBinding(&item.Data.TemplateDefaultCaseBinding.Origin, itemRelease.Data.TemplateDefaultCaseBinding)
			if err != nil {
				return err
			}
			bindingsToRelease = append(bindingsToRelease, binding)
		case ccent.DefaultConfigurationChangeItemTypeDefaultComponentConfig:
			defComp, err := collectDefaultComponentConfig(&item.Data.DefaultComponentConfig.Origin, itemRelease.Data.DefaultComponentConfig)
			if err != nil {
				return err
			}
			defCompsToRelease = append(defCompsToRelease, defComp)
		default:
			return fmt.Errorf("unknown item type '%s'", item.ItemType)
		}
	}

	// release
	if len(bindingsToRelease) != 0 {
		err = a.releaseTemplateDefaultCaseBinding(ctx, release.Environment, bindingsToRelease)
		if err != nil {
			return fmt.Errorf("release template default case binding failed: %s", err)
		}
	}
	if len(defCompsToRelease) != 0 {
		err = a.releaseDefaultComponents(ctx, release.Environment, defCompsToRelease)
		if err != nil {
			return fmt.Errorf("release default component configs failed: %s", err)
		}
	}

	// save in db
	release.ReleasedAt = pointer.Pointer(time.Now().Unix())
	release.Phase = ccent.DefaultConfigurationChangeReleasePhaseReleased
	_, err = a.Repo.UpdateDefaultConfigurationChangeRelease(ctx, release)
	if err != nil {
		return fmt.Errorf("update change release failed: %s", err)
	}

	a.EventDetail = event.CloudConfigDefaultConfigChangeEventRelease{
		Env:           release.Environment,
		ReleaseNumber: release.ReleaseNumber,
	}

	return nil
}

func (a *actorInProgress) ReleaseCancel(ctx context.Context, rid uint64) error {
	release, err := a.Repo.GetDefaultConfigurationChangeRelease(ctx, rid)
	if err != nil {
		return fmt.Errorf("get release failed: %s", err)
	}

	if release.Phase != ccent.DefaultConfigurationChangeReleasePhaseApproving {
		return fmt.Errorf("release is not approving")
	}

	// save in db
	release.Phase = ccent.DefaultConfigurationChangeReleasePhaseCancelled
	_, err = a.Repo.UpdateDefaultConfigurationChangeRelease(ctx, release)
	if err != nil {
		return fmt.Errorf("update change release failed: %s", err)
	}

	a.EventDetail = event.CloudConfigDefaultConfigChangeEventRelease{
		Env:           release.Environment,
		ReleaseNumber: release.ReleaseNumber,
	}

	return nil
}

func (a *actorInProgress) Rollback(ctx context.Context, env string, description string, releaseNr uint64) error {
	chg := a.Change

	curItems, err := a.Repo.ListDefaultConfigurationChangeItems(ctx,
		repo.DefaultConfigurationChangeItemsOpts.OwnByChange(chg.ID),
	)
	if err != nil {
		return fmt.Errorf("list change items failed: %s", err)
	}

	itemReleasesToRB := []*ccent.DefaultConfigurationChangeItemRelease{}
	if releaseNr > ccent.ReleaseNumberNull {
		itemReleases, err := a.Repo.ListLastestDefaultConfigurationChangeItemReleasesPerItem(ctx,
			repo.DefaultConfigurationChangeItemReleaseOpts.OwnByChange(chg.ID),
			repo.DefaultConfigurationChangeItemReleaseOpts.EnvironmentIs(env),
			repo.DefaultConfigurationChangeItemReleaseOpts.ReleaseNumberLessOrEqual(releaseNr),
		)
		if err != nil {
			return fmt.Errorf("list releases used by rollback failed: %s", err)
		}
		itemReleasesToRB = itemReleases
	}

	// use data in latest release before rollbacked released as expected data
	expItemPairs := pair.NewPairs[*ccent.DefaultConfigurationChangeItem, *ccent.DefaultConfigurationChangeItemReleaseDataUnion]()
	for _, item := range curItems {
		var itemReleaseDataToRB *ccent.DefaultConfigurationChangeItemReleaseDataUnion

		idx := slices.IndexFunc(itemReleasesToRB, func(itemRelease *ccent.DefaultConfigurationChangeItemRelease) bool {
			return itemRelease.ItemID == item.ID
		})
		if idx != -1 {
			itemReleaseDataToRB = itemReleasesToRB[idx].Data
		}

		expItemPairs.Append(item, item.Data.ToRollback(itemReleaseDataToRB))
	}

	return a.releaseStart(ctx, env, fmt.Sprintf("[ROLLBACK] %s", description), expItemPairs)
}

func (a *actorInProgress) Close(ctx context.Context, result ccent.DefaultConfigurationChangePhase, reason string) error {
	return a.close(ctx, result, reason)
}

func (a *actorInProgress) newDefaultConfigurationChangeUpdater(param api.BatchUpdateDefaultConfigurationChangeItemsParam) *DefaultConfigurationChangeUpdater {
	return &DefaultConfigurationChangeUpdater{
		Comps:       a.Components,
		Param:       param,
		Change:      a.Change,
		ChangeItems: []*ccent.DefaultConfigurationChangeItem{},
	}
}

type DefaultConfigurationChangeUpdater struct {
	// Inputs
	Comps Components
	Param api.BatchUpdateDefaultConfigurationChangeItemsParam

	// Inputs && Outputs
	Change *ccent.DefaultConfigurationChange

	// Outputs
	ChangeItems []*ccent.DefaultConfigurationChangeItem
}

func (a *DefaultConfigurationChangeUpdater) Update(ctx context.Context) error {
	err := a.UpdateDefaultComponentConfigs(ctx)
	if err != nil {
		return err
	}

	err = a.BatchUpdateTemplate(ctx)
	if err != nil {
		return err
	}

	if len(a.ChangeItems) == 0 {
		return fmt.Errorf("not update anything")
	}

	return nil
}

func (a *DefaultConfigurationChangeUpdater) UpdateDefaultComponentConfigs(ctx context.Context) error {
	if len(a.Param.UpdateDefaultComponents) == 0 {
		return nil
	}

	items, err := a.Comps.Repo.ListDefaultConfigurationChangeItems(ctx,
		repo.DefaultConfigurationChangeItemsOpts.OwnByChange(a.Change.ID),
		repo.DefaultConfigurationChangeItemsOpts.TypeIs(ccent.DefaultConfigurationChangeItemTypeDefaultComponentConfig),
	)
	if err != nil {
		return fmt.Errorf("list default component changes failed: %s", err)
	}

	for _, compChgParam := range a.Param.UpdateDefaultComponents {
		idx := slices.IndexFunc(items, func(item *ccent.DefaultConfigurationChangeItem) bool {
			return item.Data.DefaultComponentConfig.Origin.DefaultCase.IsEqual(compChgParam.DefaultCase)
		})
		if idx == -1 {
			return fmt.Errorf("component '%s' in param is not found in change", compChgParam.DefaultCase.HumanReadString())
		}
		item := items[idx]

		if compChgParam.BaseImage != "" {
			item.Data.DefaultComponentConfig.Modified.BaseImage = compChgParam.BaseImage
		}
		if compChgParam.ImageTag != "" {
			item.Data.DefaultComponentConfig.Modified.ImageTag = compChgParam.ImageTag
		}

		a.ChangeItems = append(a.ChangeItems, item)
	}

	return nil
}

type templateToDefaultCases struct {
	Template     *ccent.Template
	DefaultCases []ccent.TemplateDefaultCase
}

// batchUpdateTemplate create new templates based on current templates and update the
// default case bindings of the change.
func (a *DefaultConfigurationChangeUpdater) BatchUpdateTemplate(ctx context.Context) error {
	param := a.Param.BatchUpdateTemplate

	if param == nil {
		return nil
	}
	if len(param.Items) == 0 {
		return fmt.Errorf("unchange any default case binding")
	}
	if len(param.ParamsToSet) == 0 && len(param.ParamsToUnset) == 0 {
		return fmt.Errorf("unchange any parameters")
	}

	// use config type to reduce the number of queries
	configTypeSet := container.NewSet[string]()
	for _, itemInParam := range param.Items {
		configTypeSet.Insert(itemInParam.Origin.DefaultCase.ConfigType)
	}

	items, err := a.Comps.Repo.ListDefaultConfigurationChangeItems(ctx,
		repo.DefaultConfigurationChangeItemsOpts.OwnByChange(a.Change.ID),
		repo.DefaultConfigurationChangeItemsOpts.TypeIs(ccent.DefaultConfigurationChangeItemTypeTemplateDefaultCaseBinding),
		repo.DefaultConfigurationChangeItemsOpts.TemplateDefaultCaseBindingHasConfigType(configTypeSet.Slice()),
	)
	if err != nil {
		return fmt.Errorf("list template default case binding changes failed: %s", err)
	}

	// validate
	for _, itemInParam := range param.Items {
		var itemInChange *ccent.TemplateDefaultCaseBindingChange
		for _, item := range items {
			if item.Data.TemplateDefaultCaseBinding.Origin.DefaultCase.IsEqual(itemInParam.Origin.DefaultCase) {
				itemInChange = item.Data.TemplateDefaultCaseBinding
				break
			}
		}

		if itemInChange == nil {
			return fmt.Errorf("default case '%s' is not found in change", itemInParam.Origin.DefaultCase.HumanReadString())
		}

		if itemInChange.Modified.TemplateID != itemInParam.Modified.TemplateID ||
			itemInChange.Origin.TemplateID != itemInParam.Origin.TemplateID {
			return fmt.Errorf("template id of param is unmatched with current item")
		}
	}

	// collect templates to replace
	curTemplates := map[uint64]*templateToDefaultCases{} // key: template id
	for _, itemInParam := range param.Items {
		curTemplateID := itemInParam.Modified.TemplateID

		tmplToDef, exist := curTemplates[curTemplateID]
		if exist {
			tmplToDef.DefaultCases = append(tmplToDef.DefaultCases, itemInParam.Origin.DefaultCase)
		} else {
			tmpl, err := a.Comps.ConfigService.GetTemplate(ctx, itemInParam.Origin.ClusterType, curTemplateID, api.GetTemplateParam{
				WithDefaultCases: false,
			})
			if err != nil {
				return fmt.Errorf("collect template '%d' failed: %s", curTemplateID, err)
			}
			curTemplates[curTemplateID] = &templateToDefaultCases{
				Template:     tmpl,
				DefaultCases: []ccent.TemplateDefaultCase{itemInParam.Origin.DefaultCase},
			}
		}
	}

	// build new templates
	newTemplates := []*templateToDefaultCases{}
	for _, tmplToDef := range curTemplates {
		// copy data from current templates but id is 0
		newTemplate, err := pointer.DeepCopy(tmplToDef.Template)
		if err != nil {
			return fmt.Errorf("deep copy template '%d' failed: %s", tmplToDef.Template.ID, err)
		}
		if slices.Contains(a.Change.ItemTypes, ccent.DefaultConfigurationChangeItemTypeVersionRelease) {
			newTemplate.Name = a.Change.DisplayName
		}
		newTemplate.ID = 0

		// modify templates
		for _, paramToSet := range param.ParamsToSet {
			_, exist := newTemplate.Parameters[paramToSet.Name]
			if !exist {
				newTemplate.Parameters[paramToSet.Name] = &ccent.NamedParameter{
					Name: paramToSet.Name,
				}
			}

			newTemplate.Parameters[paramToSet.Name].Value = paramToSet.Value
			newTemplate.Parameters[paramToSet.Name].Comment = paramToSet.Comment
		}

		for _, paramToUnset := range param.ParamsToUnset {
			delete(newTemplate.Parameters, paramToUnset)
		}

		// record new templates
		newTemplates = append(newTemplates, &templateToDefaultCases{
			Template:     newTemplate,
			DefaultCases: tmplToDef.DefaultCases,
		})
	}

	// create new templates
	for idx, tmplToDef := range newTemplates {
		tmpl, err := a.Comps.ConfigService.CreateTemplate(ctx, tmplToDef.Template, api.CreateTemplateParam{AllowAssignID: false})
		if err != nil {
			return fmt.Errorf("create a new template failed: %s", err)
		}

		newTemplates[idx].Template = tmpl
	}

	// update the change.
	for _, tmplToDef := range newTemplates {
		newTmpl := tmplToDef.Template

		// update DefaultCaseBindings change
		for _, defCase := range tmplToDef.DefaultCases {
			idx := slices.IndexFunc(items, func(item *ccent.DefaultConfigurationChangeItem) bool {
				return item.Data.TemplateDefaultCaseBinding.Origin.DefaultCase.IsEqual(defCase)
			})
			if idx == -1 {
				return fmt.Errorf("default case '%s' is not found in change", defCase.HumanReadString())
			}
			item := items[idx]

			item.Data.TemplateDefaultCaseBinding.Modified.TemplateID = newTmpl.ID // update to template id
			a.ChangeItems = append(a.ChangeItems, item)
		}
	}

	return nil
}

func collectTemplateDefaultCaseBinding(origin *ccent.TemplateDefaultCaseBinding, exp *ccent.TemplateDefaultCaseBindingChangeData) (*ccent.TemplateDefaultCaseBinding, error) {
	binding, err := pointer.DeepCopy(origin)
	if err != nil {
		return nil, err
	}

	if exp == nil {
		return binding, nil
	}

	binding.TemplateID = exp.TemplateID
	return binding, nil
}

func collectDefaultComponentConfig(origin *ccent.DefaultComponentConfig, exp *ccent.DefaultComponentConfigChangeData) (*ccent.DefaultComponentConfig, error) {
	def, err := pointer.DeepCopy(origin)
	if err != nil {
		return nil, err
	}

	if exp == nil {
		return def, nil
	}

	def.BaseImage = exp.BaseImage
	def.ImageTag = exp.ImageTag

	return def, nil
}
