package cloudconfig

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

	baseapi "github.com/tidbcloud/devops/api"
	auditent "github.com/tidbcloud/devops/entity/audit"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	entcommon "github.com/tidbcloud/devops/entity/common"
	"github.com/tidbcloud/devops/entity/event"
	maintent "github.com/tidbcloud/devops/entity/maintenance"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	adsvc "github.com/tidbcloud/devops/pkg/service/audit"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	ccact "github.com/tidbcloud/devops/pkg/service/cloudconfig/change/config"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/repo"
	cmxapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"
)

func (s *svc) ListConfigurationChanges(ctx context.Context, param api.ListConfigurationChangesParam) ([]*ccent.ConfigurationChange, int64, error) {
	param, err := param.Normalize()
	if err != nil {
		return nil, 0, errs.APIErrorNew(baseapi.ErrCodeInvalidArgument, err)
	}

	opts := []repoopt.Option{}
	if param.IDOnly {
		opts = append(opts, repo.ConfigurationChangeOpts.SelectIDOnly())
	}
	if param.TargetCluster != nil {
		opts = append(opts, repo.ConfigurationChangeOpts.TargetClusterIs(*param.TargetCluster))
	}
	if param.TargetTenantInit != nil {
		opts = append(opts, repo.ConfigurationChangeOpts.TargetTenantInitIs(*param.TargetTenantInit))
	}
	if param.TargetTemplate != nil {
		opts = append(opts, repo.ConfigurationChangeOpts.TargetTemplateIs(*param.TargetTemplate))
	}
	if param.HaveModifiedParameterLike != "" {
		opts = append(opts, repo.ConfigurationChangeOpts.HaveModifiedParameterLike(param.HaveModifiedParameterLike))
	}
	if param.CreatedAtStartUnixSec != nil {
		opts = append(opts, repo.ConfigurationChangeOpts.CreatedAtStartUnixSec(*param.CreatedAtStartUnixSec))
	}
	if param.CreatedAtEndUnixSec != nil {
		opts = append(opts, repo.ConfigurationChangeOpts.CreatedAtEndUnixSec(*param.CreatedAtEndUnixSec))
	}

	total, err := s.repo.CountConfigurationChange(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	if param.Page > 0 {
		opts = append(opts, repo.ConfigurationChangeOpts.WithPager(param.Page, param.PerPage))
	}
	chgs, err := s.repo.ListConfigurationChanges(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}
	if err := s.fillConfigurationChangeExecutionWindows(ctx, chgs...); err != nil {
		return nil, 0, err
	}

	return chgs, total, nil
}

func (s *svc) GetConfigurationChange(ctx context.Context, id uint64) (*ccent.ConfigurationChange, error) {
	chg, err := s.repo.GetConfigurationChange(ctx, id)
	if err != nil {
		return nil, err
	}
	if err := s.fillConfigurationChangeExecutionWindows(ctx, chg); err != nil {
		return nil, err
	}

	return chg, nil
}

func (s *svc) fillConfigurationChangeExecutionWindows(ctx context.Context, changes ...*ccent.ConfigurationChange) error {
	windowIDs := container.NewSet[uint64]()
	for _, chg := range changes {
		if chg.ExecutionWindowID == nil {
			continue
		}
		windowIDs.Insert(*chg.ExecutionWindowID)
	}
	if windowIDs.Len() == 0 {
		return nil
	}

	windows, _, err := s.maintenanceSvc.ListOpsExecutionWindows(ctx, cmxapi.ListOpsExecutionWindowParam{
		IDs: windowIDs.Slice(),
	})
	if err != nil {
		return err
	}
	windowByID := container.SliceToMap(windows, func(window *maintent.OpsExecutionWindow) uint64 {
		return window.ID
	})

	for _, chg := range changes {
		if chg.ExecutionWindowID == nil {
			continue
		}
		windowID := *chg.ExecutionWindowID
		window, ok := windowByID[windowID]
		if !ok {
			return fmt.Errorf("ops execution window %d not found", windowID)
		}
		chg.ExecutionWindow = window
	}

	return nil
}

func (s *svc) PreviewConfigurationChange(ctx context.Context, param api.PreviewConfigurationChangeParam) (*api.PreviewConfigurationChangeResult, error) {
	actor := ccact.NewConfigurationChangeActor(ctx, s.chgComps(), nil)
	result, err := actor.Preview(param)
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (s *svc) CreateConfigurationChange(ctx context.Context, param api.CreateConfigurationChangeParam) (*uint64, error) {
	param.OperatorEntity = pointer.OrDefault(
		param.OperatorEntity,
		entcommon.BuildOperatorEntity(consts.GetOperatorTypeFromCtx(ctx), consts.GetOperatorFromCtx(ctx, param.Applicant)),
	)
	actor := ccact.NewConfigurationChangeActor(ctx, s.chgComps(), nil)

	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		err := actor.Create(param)
		if err != nil {
			return err
		}

		_, err = s.buildAndRecordConfigChangeEvent(ctx, actor, event.CloudConfigConfigChangeEventTypeCreate)
		if err != nil {
			return err
		}

		return nil
	})

	var id *uint64
	if actor.GetContext().Change != nil {
		id = &actor.GetContext().Change.ID
		s.syncChgChan <- *id // sync the change immediately
	}

	return id, err
}

func (s *svc) CancelConfigurationChange(ctx context.Context, id uint64) error {
	chg, err := s.repo.GetConfigurationChange(ctx, id)
	if err != nil {
		return err
	}
	if chg.PremiumUpgradeStepID != nil {
		return errs.APIErrorf(baseapi.ErrCodeFailedPrecond, "managed configuration change %d must be cancelled through its premium upgrade plan", id)
	}

	actor := ccact.NewConfigurationChangeActor(ctx, s.chgComps(), chg)

	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		err := actor.Cancel()
		if err != nil {
			return err
		}

		// record audit log
		_, err = s.buildAndRecordConfigChangeEvent(ctx, actor, event.CloudConfigConfigChangeEventTypeCancel)
		if err != nil {
			return err
		}

		return nil
	})
}

func (s *svc) ApplyConfigurationChange(ctx context.Context, id uint64, param api.ApplyConfigurationChangeParam) error {
	chg, err := s.repo.GetConfigurationChange(ctx, id)
	if err != nil {
		return err
	}
	if chg.PremiumUpgradeStepID != nil {
		return errs.APIErrorf(baseapi.ErrCodeFailedPrecond, "managed configuration change %d must be applied through its premium upgrade plan", id)
	}
	if chg.Applicant != param.OperatorEmail {
		return errs.APIErrorf(baseapi.ErrCodePermissionDenied, "only the applicant can apply the change")
	}

	actor := ccact.NewConfigurationChangeActor(ctx, s.chgComps(), chg)

	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		err := actor.Apply(param)
		if err != nil {
			return err
		}

		// record audit log
		_, err = s.buildAndRecordConfigChangeEvent(ctx, actor, event.CloudConfigConfigChangeEventTypeApply)
		if err != nil {
			return err
		}

		return nil
	})
}

func (s *svc) CreateManagedConfigurationChange(ctx context.Context, param api.CreateManagedConfigurationChangeParam) (*ccent.ConfigurationChange, error) {
	param.Change.OperatorEntity = pointer.OrDefault(
		param.Change.OperatorEntity,
		entcommon.BuildOperatorEntity(consts.GetOperatorTypeFromCtx(ctx), consts.GetOperatorFromCtx(ctx, param.Change.Applicant)),
	)
	actor := ccact.NewConfigurationChangeActor(ctx, s.chgComps(), nil)
	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		if err := actor.CreateManaged(param.Change, param.Ref); err != nil {
			return err
		}
		_, err := s.buildAndRecordConfigChangeEvent(ctx, actor, event.CloudConfigConfigChangeEventTypeCreate)
		return err
	})
	if err != nil {
		return nil, err
	}
	return actor.GetContext().Change, nil
}

func (s *svc) GetManagedConfigurationChangeByStep(ctx context.Context, stepID uint64) (*ccent.ConfigurationChange, error) {
	changes, err := s.repo.ListConfigurationChanges(ctx, repo.ConfigurationChangeOpts.PremiumUpgradeStepIs(stepID))
	if err != nil {
		return nil, err
	}
	if len(changes) == 0 {
		return nil, nil
	}
	if len(changes) > 1 {
		return nil, fmt.Errorf("found %d managed configuration changes for premium upgrade step %d", len(changes), stepID)
	}
	return changes[0], nil
}

func (s *svc) ApplyManagedConfigurationChange(ctx context.Context, ref api.ManagedConfigurationChangeRef, changeID uint64, param api.ApplyConfigurationChangeParam) error {
	chg, err := s.getManagedConfigurationChange(ctx, ref, changeID)
	if err != nil {
		return err
	}
	actor := ccact.NewConfigurationChangeActor(ctx, s.chgComps(), chg)
	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		if err := actor.Apply(param); err != nil {
			return err
		}
		_, err := s.buildAndRecordConfigChangeEvent(ctx, actor, event.CloudConfigConfigChangeEventTypeApply)
		return err
	})
	if err == nil {
		s.syncChgChan <- changeID
	}
	return err
}

func (s *svc) FailManagedConfigurationChange(ctx context.Context, ref api.ManagedConfigurationChangeRef, changeID uint64) error {
	chg, err := s.getManagedConfigurationChange(ctx, ref, changeID)
	if err != nil {
		return err
	}
	if slices.Contains(ccent.ConfigurationChangePhaseFinishedValues, chg.Phase) {
		return nil
	}
	return s.repo.PatchConfigurationChange(ctx, chg, func(current *ccent.ConfigurationChange) error {
		current.Phase = ccent.ConfigurationChangePhaseFailed
		current.ClosedAt = pointer.Pointer(time.Now().Unix())
		return nil
	})
}

func (s *svc) CancelManagedConfigurationChange(ctx context.Context, ref api.ManagedConfigurationChangeRef, changeID uint64) error {
	chg, err := s.getManagedConfigurationChange(ctx, ref, changeID)
	if err != nil {
		return err
	}
	if slices.Contains(ccent.ConfigurationChangePhaseFinishedValues, chg.Phase) {
		return nil
	}
	actor := ccact.NewConfigurationChangeActor(ctx, s.chgComps(), chg)
	return s.dbp.Transaction(ctx, func(ctx context.Context) error {
		if err := actor.Cancel(); err != nil {
			return err
		}
		_, err := s.buildAndRecordConfigChangeEvent(ctx, actor, event.CloudConfigConfigChangeEventTypeCancel)
		return err
	})
}

func (s *svc) getManagedConfigurationChange(ctx context.Context, ref api.ManagedConfigurationChangeRef, changeID uint64) (*ccent.ConfigurationChange, error) {
	chg, err := s.repo.GetConfigurationChange(ctx, changeID)
	if err != nil {
		return nil, err
	}
	if chg.PremiumUpgradeStepID == nil || *chg.PremiumUpgradeStepID != ref.StepID {
		return nil, errs.APIErrorf(baseapi.ErrCodeFailedPrecond, "configuration change %d is not managed by premium upgrade step %d", changeID, ref.StepID)
	}
	if chg.ApprovalID != nil {
		return nil, errs.APIErrorf(baseapi.ErrCodeFailedPrecond, "managed configuration change %d unexpectedly has approval %d", changeID, *chg.ApprovalID)
	}
	return chg, nil
}

func (s *svc) ClusterConfigurationChangeable(ctx context.Context, loc ccent.ClusterConfigurationLocator) (string, error) {
	return ccact.ClusterConfigurationChangeable(ctx, s.chgComps(), loc)
}

func (s *svc) TenantInitConfigurationChangeable(ctx context.Context, loc ccent.TenantInitConfigurationLocator) (string, error) {
	return ccact.TenantInitConfigurationChangeable(ctx, s.chgComps(), loc)
}

func (s *svc) WatchConfigurationChanges(ctx context.Context) error {
	logger := log.FromContext(ctx)

	phaseToSync := []ccent.ConfigurationChangePhase{
		ccent.ConfigurationChangePhaseCreated,
		ccent.ConfigurationChangePhaseApproving,
		ccent.ConfigurationChangePhaseApplying,
	}

	chgs, err := s.repo.ListConfigurationChanges(ctx,
		repo.ConfigurationChangeOpts.SelectIDOnly(), // only select ID
		repo.ConfigurationChangeOpts.PhaseIn(phaseToSync),
	)
	if err != nil {
		return err
	}

	if len(chgs) != 0 {
		logger.Info("watched some config changes need to sync",
			"change_ids", container.TransformSlice(chgs, func(i *ccent.ConfigurationChange) uint64 { return i.ID }),
		)
	}

	for _, chg := range chgs {
		s.syncChgChan <- chg.ID
	}

	return nil
}

func (s *svc) SyncConfigurationChange(ctx context.Context, id uint64) error {
	chg, err := s.repo.GetConfigurationChange(ctx, id)
	if err != nil {
		return err
	}

	ctx = log.WrapContext(ctx, "change_id", id, "change_phase", chg.Phase)
	actor := ccact.NewConfigurationChangeActor(ctx, s.chgComps(), chg)

	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		var err error
		var evType *event.EventType

		switch chg.Phase {
		case ccent.ConfigurationChangePhaseCreated:
			err = actor.SendApproval()
			evType = pointer.Pointer(event.CloudConfigConfigChangeEventTypeSendApproval)
		case ccent.ConfigurationChangePhaseApproving:
			err = actor.ContinueIfApproved()
			evType = pointer.Pointer(event.CloudConfigConfigChangeEventTypeFinishApproval)
		case ccent.ConfigurationChangePhaseApplying:
			err = actor.CheckResult()
			evType = pointer.Pointer(event.CloudConfigConfigChangeEventTypeApplied)
		default:
			log.FromContext(ctx).Info("ingore a change to sync cause unsupported phase")
		}
		if err != nil {
			return err
		}

		// record audit log
		if evType != nil {
			_, err = s.buildAndRecordConfigChangeEvent(ctx, actor, *evType)
			if err != nil {
				return err
			}
		}

		return nil
	})

	return err
}

func (s *svc) chgComps() ccact.Components {
	return ccact.Components{
		Conf:            s.conf,
		Repo:            s.repo,
		ConfigCenter:    s.cfgCenter,
		CoreSvc:         s.coreSvc,
		Dataflow:        s.dataflow,
		Serverless:      s.serverlessCli,
		ApprovalService: s.apprSvc,
		MaintenanceSvc:  s.maintenanceSvc,
		TenantService:   s.tenantSvc,
		ConfigService:   s,
		ParamDict:       s,
		MgmtHTTP:        s.mgmtHTTP,
		EventBus:        s.eventbus,
	}
}

func (s *svc) buildAndRecordConfigChangeEvent(ctx context.Context, actor ccact.ConfigurationChangeActor, typ event.EventType) (*event.CloudConfigConfigChangeEvent, error) {
	change := actor.GetContext().Change
	detail := actor.GetContext().EventDetail

	ev := &event.CloudConfigConfigChangeEvent{
		BaseEvent: event.BaseEvent{
			ID:            change.ID,
			OperatorEmail: consts.GetOperatorFromCtx(ctx, consts.InternalVirtualEmail),
			OperateAt:     pointer.Pointer(time.Now()),
			EventType:     typ,
		},
	}
	if detail != nil {
		ev.Details = *detail
	}

	auditLog, err := adsvc.EventToAuditLog(ev.BaseEvent, auditent.AuditLogTypeCloudConfigChange, ev.Details)
	if err != nil {
		return ev, err
	}

	err = s.auditSvc.Create(ctx, auditLog)
	if err != nil {
		return ev, fmt.Errorf("create audit log failed: %w", err)
	}

	return ev, nil
}
