package server

import (
	"context"
	"fmt"
	"slices"

	"github.com/samber/lo"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow/dag"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type UpdateBackupGcSettingInput struct {
	*apiv1.UpdateBackupGcSettingRequest
}

type UpdateBackupGcSettingOutput struct {
	*apiv1.BackupGcSetting
}

func (s *serverImpl) UpdateBackupGcSetting(ctx context.Context, req *UpdateBackupGcSettingInput) (*UpdateBackupGcSettingOutput, error) {
	bs := req.GetBackupGcSetting()
	sharedPoolId := bs.GetSharedPoolId()
	if sharedPoolId == "" {
		return nil, status.Errorf(codes.InvalidArgument, "shared pool id is empty")
	}
	sharedPool, err := s.guardSharedPoolCleanupMutationByID(ctx, sharedPoolId)
	if err != nil {
		return nil, err
	}

	// check if there is update workflow running
	dagIns, err := s.getRunningDagIns(ctx, []string{dag.PlanUpdateBackupGcSetting}, []string{sharedPoolId}, nil, nil, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get running update backup gc setting dag instance, shared pool id: %s, err: %w", sharedPoolId, err)
	}
	if len(dagIns) > 0 {
		return nil, status.Errorf(codes.AlreadyExists, "exist one updating not finished, please try again later")
	}

	backupGcSetting, err := s.backupGcSettingRepo.Load(ctx, sharedPoolId)
	if err != nil {
		return nil, fmt.Errorf("failed to load backup gc setting, shared pool id: %s, err: %w", sharedPoolId, err)
	}

	if err := doUpdateBackupGcSettingValidation(backupGcSetting, req.BackupGcSetting, sharedPool.DeletedAt != nil); err != nil {
		return nil, err
	}

	backupGcSetting.HasManualBackup = lo.ToPtr(bs.GetHasManualBackup())
	backupGcSetting.MinT2DataAgeDays = bs.GetMinT2DataAgeDays()
	backupGcSetting.MinT3DataAgeDays = bs.GetMinT3DataAgeDays()
	backupGcSetting.MaxT1MetaLifetimeDays = lo.ToPtr(bs.GetMaxT1MetaLifetimeDays())
	backupGcSetting.MaxT1WalLifetimeDays = lo.ToPtr(bs.GetMaxT1WalLifetimeDays())
	backupGcSetting.MaxT1PackedLifetimeDays = lo.ToPtr(bs.GetMaxT1PackedLifetimeDays())
	backupGcSetting.MaxT2SstLifetimeDays = lo.ToPtr(bs.GetMaxT2SstLifetimeDays())
	backupGcSetting.MaxT3DataLifetimeDays = lo.ToPtr(bs.GetMaxT3DataLifetimeDays())
	// respect static config for test environment
	if s.bizCfg.DefaultTiBRConfig.MaxRetainT1AfterSPDeletedDays > 0 {
		backupGcSetting.MaxT1MetaLifetimeDays = lo.ToPtr(s.bizCfg.DefaultTiBRConfig.MaxRetainT1AfterSPDeletedDays)
		backupGcSetting.MaxT1WalLifetimeDays = lo.ToPtr(s.bizCfg.DefaultTiBRConfig.MaxRetainT1AfterSPDeletedDays)
	}
	if s.bizCfg.DefaultTiBRConfig.MaxRetainT2AfterSPDeletedDays > 0 {
		backupGcSetting.MaxT2SstLifetimeDays = lo.ToPtr(s.bizCfg.DefaultTiBRConfig.MaxRetainT2AfterSPDeletedDays)
	}
	if s.bizCfg.DefaultTiBRConfig.MaxRetainT3AfterSPDeletedDays > 0 {
		backupGcSetting.MaxT3DataLifetimeDays = lo.ToPtr(s.bizCfg.DefaultTiBRConfig.MaxRetainT3AfterSPDeletedDays)
	}

	var dagExecId string
	var dagSubmitted bool
	err = s.transactioner.Transaction(ctx, func(ctx context.Context) error {
		ierr := s.backupGcSettingRepo.Save(ctx, backupGcSetting)
		if ierr != nil {
			return fmt.Errorf("failed to save backup gc setting, shared pool id: %s, err: %w", sharedPoolId, ierr)
		}
		dagExecId = utils.GeneratedDagExecId(ctx, fmt.Sprintf("update-gc-setting-%s", sharedPoolId))
		ierr = s.dagExecutor.Submit(ctx,
			dag.PlanUpdateBackupGcSetting,
			actions.ActionArgs{workflow.SharedPoolIdKey: actions.ActionArgString(sharedPoolId)},
			dagExecId)
		if ierr != nil {
			return fmt.Errorf("failed to submit update backup gc setting dag, shared pool id: %s, err: %w", sharedPoolId, ierr)
		}
		dagSubmitted = true
		return nil
	})
	if err != nil {
		if dagSubmitted {
			if abortErr := s.dagExecutor.Abort(ctx, dagExecId); abortErr != nil {
				tilog.Error(ctx, "failed to abort dag after transaction failure",
					zap.String("dagExecId", dagExecId),
					zap.Error(abortErr))
			}
		}
		tilog.Error(ctx, "failed to update backup gc setting", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
		return nil, err
	}

	bs.Status = convertBackupGcSettingStatus(backupGcSetting.Status)
	return &UpdateBackupGcSettingOutput{
		BackupGcSetting: bs,
	}, nil
}

func doUpdateBackupGcSettingValidation(
	curSetting *entity.BackupGcSetting,
	req *apiv1.BackupGcSetting,
	sharedPoolDeleted bool,
) error {
	// no allow changes if it's in internal changing status
	if slices.Contains(schema.BackupGcStatusesOfInternalChanging, curSetting.Status) {
		return status.Errorf(codes.FailedPrecondition, "sharedpool is creating or deleting, no allow to change backup gc setting")
	}

	// no allow to change MinT2DataAgeDays or MinT3DataAgeDays if sharedpool is not running, tibrgc can only be updated when sharedpool is running
	if sharedPoolDeleted || curSetting.Status != schema.BackupGcStatusSPRunning {
		if curSetting.MinT2DataAgeDays != req.GetMinT2DataAgeDays() || curSetting.MinT3DataAgeDays != req.GetMinT3DataAgeDays() {
			return status.Errorf(codes.FailedPrecondition, "sharedpool is not running, no allow to change MinT2DataAgeDays or MinT3DataAgeDays")
		}
	}

	if (curSetting.T2StrategyEnabled == nil && req.GetT2StrategyEnabled()) ||
		(curSetting.T2StrategyEnabled != nil && *curSetting.T2StrategyEnabled != req.GetT2StrategyEnabled()) {
		return status.Errorf(codes.InvalidArgument, "not allow to change T2StrategyEnabled")
	}
	if (curSetting.T3StrategyEnabled == nil && req.GetT3StrategyEnabled()) ||
		(curSetting.T3StrategyEnabled != nil && *curSetting.T3StrategyEnabled != req.GetT3StrategyEnabled()) {
		return status.Errorf(codes.InvalidArgument, "not allow to change T3StrategyEnabled")
	}

	if req.GetMinT2DataAgeDays() == 0 {
		return status.Errorf(codes.InvalidArgument, "min t2 data age days is 1")
	}
	if req.GetMinT3DataAgeDays() == 0 {
		return status.Errorf(codes.InvalidArgument, "min t3 data age days is 1")
	}

	return nil
}
