package defaultconfig

import (
	"context"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	baseapi "github.com/tidbcloud/devops/api"
	cchttpapi "github.com/tidbcloud/devops/api/cloudconfig"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"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"
	apprapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/cvt"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/repo"

	"github.com/go-resty/resty/v2"
)

const (
	ApprTemplateIDFormat   = "cloudconfig/release-default-config-change-%s"
	ApprParamChangeID      = "change_id"
	ApprParamEnv           = "environment"
	ApprParamReleaseNumber = "release_number"
	ApprParamComponents    = "components" // NOTE: used in approval condition.
)

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

type Components struct {
	Conf            config.CloudConfigServiceConfig
	Repo            repo.CloudConfigRepo
	ConfigService   api.ConfigurationService
	ApprovalService apprapi.ApprovalServiceAPI
	SubEnvironments map[string]*resty.Client
}

type Context struct {
	Components
	Objects
}

type DefaultConfigurationChangeActor interface {
	// GetContext returns the context of the actor.
	GetContext() *Context
	// Create creates a new template change.
	Create(ctx context.Context, param api.CreateDefaultConfigurationChangeParam) error
	// Update create new templates and modify default case binding.
	Update(ctx context.Context, param api.BatchUpdateDefaultConfigurationChangeItemsParam) error
	// ReleaseStart send an approval request to release.
	ReleaseStart(ctx context.Context, env string, description string) error
	// ReleaseIfApproved release the change if the approval request is approved.
	ReleaseIfApproved(ctx context.Context, rid uint64) error
	// ReleaseCancel cancel a release.
	ReleaseCancel(ctx context.Context, rid uint64) error
	// Rollback rollback the change to a release.
	Rollback(ctx context.Context, env string, description string, releaseNr uint64) error
	// Close this change and can't be used anymore.
	Close(ctx context.Context, result ccent.DefaultConfigurationChangePhase, reason string) error
}

func NewDefaultConfigurationChangeActor(comps Components, chg *ccent.DefaultConfigurationChange) DefaultConfigurationChangeActor {
	var a DefaultConfigurationChangeActor
	var err error

	actx := &Context{
		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.DefaultConfigurationChangePhaseInProgress:
		return &actorInProgress{baseActor: base}
	case slices.Contains(ccent.DefaultConfigurationChangeClosedValues, 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) Create(ctx context.Context, param api.CreateDefaultConfigurationChangeParam) error {
	return a.defaultErr
}

func (a *baseActor) Update(ctx context.Context, param api.BatchUpdateDefaultConfigurationChangeItemsParam) error {
	return a.defaultErr
}

func (a *baseActor) ReleaseStart(ctx context.Context, env string, description string) error {
	return a.defaultErr
}

func (a *baseActor) ReleaseIfApproved(ctx context.Context, rid uint64) error {
	return a.defaultErr
}

func (a *baseActor) ReleaseCancel(ctx context.Context, rid uint64) error {
	return a.defaultErr
}

func (a *baseActor) Rollback(ctx context.Context, env string, description string, releaseNr uint64) error {
	return a.defaultErr
}

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

func (a *baseActor) releaseStart(
	ctx context.Context,
	env string,
	description string,
	itemPairs *pair.Pairs[*ccent.DefaultConfigurationChangeItem, *ccent.DefaultConfigurationChangeItemReleaseDataUnion],
) error {
	chg := a.Change

	if !slices.Contains(chg.Environments, env) {
		return fmt.Errorf("environment '%s' is not found in change", env)
	}
	if _, exist := a.Components.SubEnvironments[env]; env != string(consts.GetEnv()) && !exist {
		return fmt.Errorf("environment '%s' is unknown", env)
	}

	// check if there is any release in progress, we only allow one release in progress.
	latestReleases, err := a.Repo.ListLatestDefaultConfigurationChangeReleasePerEnvironment(ctx,
		repo.DefaultConfigurationChangeReleaseOpts.OwnByChange(chg.ID),
	)
	if err != nil {
		return fmt.Errorf("get lastest release failed: %s", err)
	}
	latestReleasesPerEnv := container.SliceToMap(latestReleases, func(e *ccent.DefaultConfigurationChangeRelease) string {
		return e.Environment
	})

	for _, release := range latestReleasesPerEnv {
		if !slices.Contains(ccent.DefaultConfigurationChangeReleaseClosedValues, release.Phase) {
			return fmt.Errorf("there are some releases in progress, please wait for them to finish")
		}
	}

	// check if covered instance types are correct to avoid an instance type is released when version is releasing.
	err = a.checkVersionAndInstanceReleaseConflict(ctx)
	if err != nil {
		return fmt.Errorf("check version and instance release conflict failed: %s", err)
	}

	// calc the release number.
	releaseNr := ccent.ReleaseNumberFirst
	if latestRelease, exist := latestReleasesPerEnv[env]; exist {
		releaseNr = latestRelease.ReleaseNumber + 1
	}

	// list the latest item releases for all items
	curItemReleases, err := a.Repo.ListLastestDefaultConfigurationChangeItemReleasesPerItem(ctx,
		repo.DefaultConfigurationChangeItemReleaseOpts.OwnByChange(chg.ID),
		repo.DefaultConfigurationChangeItemReleaseOpts.EnvironmentIs(env),
	)
	if err != nil {
		return fmt.Errorf("list releases failed: %s", err)
	}

	// collect item releases
	newItemReleasePairs := pair.NewPairs[*ccent.DefaultConfigurationChangeItem, *ccent.DefaultConfigurationChangeItemRelease]()
	for curItem, expReleaseData := range itemPairs.Pairs() {
		var curItemReleaseData *ccent.DefaultConfigurationChangeItemReleaseDataUnion

		// found corresponding item release
		idx := slices.IndexFunc(curItemReleases, func(ritem *ccent.DefaultConfigurationChangeItemRelease) bool {
			return ritem.ItemID == curItem.ID
		})
		if idx != -1 {
			curItemReleaseData = curItemReleases[idx].Data
		}

		// check if the item is released by release data.
		isReleased := !curItem.Data.Releasable() || expReleaseData.IsReleased(curItemReleaseData)
		if isReleased {
			continue
		}

		// collect item releases
		newItemRelease := &ccent.DefaultConfigurationChangeItemRelease{
			ID:            0,
			Environment:   env,
			ItemID:        curItem.ID,
			Data:          expReleaseData,
			ReleaseNumber: releaseNr,
			ParentID:      a.Change.ID,
		}
		newItemReleasePairs.Append(curItem, newItemRelease)
	}

	if newItemReleasePairs.Len() == 0 {
		return fmt.Errorf("no any items need to release")
	}

	// send an approval request
	apprParam := apprapi.CreateApprovalFromTemplateParam{
		CreateApprovalParamBase: apprapi.CreateApprovalParamBase{
			Name:       fmt.Sprintf("Release for '%s'", chg.DisplayName),
			Applicant:  chg.Applicant,
			DetailLink: fmt.Sprintf("https://%s/defaultconfig/change/%d", consts.ConsoleAddr(), chg.ID),
			Parameters: map[string]string{
				ApprParamChangeID:      strconv.FormatUint(chg.ID, 10),
				ApprParamEnv:           env,
				ApprParamReleaseNumber: strconv.FormatUint(releaseNr, 10),
				ApprParamComponents:    "",
			},
			Reason: chg.Reason,
		},
	}

	componentSet := container.NewSet[string]()
	for item := range newItemReleasePairs.Firsts() {
		comp := ""

		if item.Data.TemplateDefaultCaseBinding != nil {
			comp = cvt.To.ConfigCenter.ClusterType(item.Data.TemplateDefaultCaseBinding.Origin.ClusterType).
				ConfigType2Component(item.Data.TemplateDefaultCaseBinding.Origin.DefaultCase.ConfigType)
		}
		if item.Data.DefaultComponentConfig != nil {
			comp = item.Data.DefaultComponentConfig.Origin.DefaultCase.Component
		}

		if comp == "" {
			comp = "unknown"
		}
		componentSet.Insert(comp)
	}

	// NOTE: respect the separator ',',  we will parse this string in the condition of approval.
	apprParam.Parameters[ApprParamComponents] = strings.Join(componentSet.Slice(), ",")

	apprTmpl := fmt.Sprintf(ApprTemplateIDFormat, env)
	apprParam.FromTemplate = apprTmpl
	apprID, err := a.ApprovalService.CreateApprovalFromTemplate(ctx, apprParam)
	if err != nil {
		return fmt.Errorf("send approval failed: %s", err)
	}

	// save
	_, err = a.Repo.CreateDefaultConfigurationChangeRelease(ctx, &ccent.DefaultConfigurationChangeRelease{
		Environment:   env,
		Description:   description,
		ReleaseNumber: releaseNr,
		Phase:         ccent.DefaultConfigurationChangeReleasePhaseApproving,
		ApprovalID:    *apprID,
		ParentID:      chg.ID,
	})
	if err != nil {
		return fmt.Errorf("create change release failed: %s", err)
	}

	err = a.Repo.BatchCreateDefaultConfigurationChangeItemReleases(ctx, chg.ID, releaseNr, slices.Collect(newItemReleasePairs.Seconds()))
	if err != nil {
		return fmt.Errorf("create change item releases failed: %s", err)
	}

	return nil
}

func (a *baseActor) close(ctx context.Context, result ccent.DefaultConfigurationChangePhase, reason string) error {
	if !slices.Contains(ccent.DefaultConfigurationChangeClosedValues, result) {
		return fmt.Errorf("phase '%s' is not a result", result)
	}

	a.Change.Phase = result
	a.Change.PhaseReason = reason
	a.Change.ClosedAt = pointer.Pointer(time.Now().Unix())

	return a.Repo.UpdateDefaultConfigurationChange(ctx, a.Change)
}

func (a *baseActor) releaseTemplateDefaultCaseBinding(ctx context.Context, env string, bindings []*ccent.TemplateDefaultCaseBinding) error {
	if env == string(consts.GetEnv()) { // release to current environment
		err := a.ConfigService.BatchUpdateTemplateDefaultCaseBindings(ctx, bindings)
		if err != nil {
			return fmt.Errorf("batch update default case binding failed: %s", err)
		}
	} else {
		// release to sub environments
		cli, exist := a.SubEnvironments[env]
		if !exist {
			return fmt.Errorf("environment '%s' is not found in sub environments", env)
		}

		req := cchttpapi.UpdateTemplateDefaultCaseBindingsReq{
			DefaultCases: bindings,
		}
		_, err := cli.R().
			SetContext(ctx).
			SetBody(req).
			Patch("/api/v1/devops/cloudconfig/default-config/template-bindings") // MUST match the api
		if err != nil {
			return fmt.Errorf("release template default case bindings to environment '%s' failed: %s", env, err)
		}
	}

	return nil
}

func (a *baseActor) releaseDefaultComponents(ctx context.Context, env string, comps []*ccent.DefaultComponentConfig) error {
	if env == string(consts.GetEnv()) { // release to current environment
		err := a.ConfigService.UpdateDefaultComponents(ctx, comps)
		if err != nil {
			return fmt.Errorf("batch update default case binding failed: %s", err)
		}
	} else {
		// release to sub environments
		cli, exist := a.SubEnvironments[env]
		if !exist {
			return fmt.Errorf("environment '%s' is not found in sub environments", env)
		}

		req := cchttpapi.UpdateDefaultComponentsReq{
			Components: comps,
		}
		_, err := cli.R().
			SetContext(ctx).
			SetBody(req).
			Patch("/api/v1/devops/cloudconfig/default-config/components") // MUST match the api
		if err != nil {
			return fmt.Errorf("release default components to environment '%s' failed: %s", env, err)
		}
	}

	return nil
}

func (a *baseActor) checkVersionAndInstanceReleaseConflict(ctx context.Context) error {
	chg := a.Change

	if slices.Contains(a.Change.ItemTypes, ccent.DefaultConfigurationChangeItemTypeVersionRelease) {
		items, err := a.Repo.ListDefaultConfigurationChangeItems(ctx,
			repo.DefaultConfigurationChangeItemsOpts.OwnByChange(chg.ID),
			repo.DefaultConfigurationChangeItemsOpts.TypeIs(ccent.DefaultConfigurationChangeItemTypeVersionRelease),
		)
		if err != nil {
			return fmt.Errorf("list item of version release failed: %s", err)
		}

		if len(items) == 0 {
			return fmt.Errorf("change contain version release but no corresponding item found")
		}
		verRelease := items[0].Data.VersionRelease

		saved := container.NewSetFromSlice(verRelease.CoveringInstanceTypes)
		current := container.NewSet[string]()
		param := api.ListTemplateDefaultCaseBindingsParam{
			DefaultForVersion: &verRelease.SourceVersion,
		}
		err = travelAllTemplateDefaultCaseBindings(ctx, a.Components.ConfigService, verRelease.ClusterType, param, func(binding *ccent.TemplateDefaultCaseBinding) {
			current.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)
		}

		diff := current.Difference(saved)
		if diff.Len() != 0 {
			return fmt.Errorf("some instance types '%v' are not covered by this change", diff.Slice())
		}

	}

	return nil
}
