package br

import (
	"context"
	"time"

	"github.com/aws/smithy-go/ptr"
	"github.com/samber/lo"
	"google.golang.org/protobuf/types/known/timestamppb"

	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/br"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/pagination"
)

// ListBackups implements Service.
func (s *serviceImpl) ListBackups(
	ctx context.Context,
	req *globalngv1.ListBackupsRequest,
) (*globalngv1.ListBackupsResponse, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.ListBackups")
	defer span.End()

	if err := validateListBackupsRequest(req); err != nil {
		return nil, err
	}

	backupFilter, berr := s.getBackupFilter(ctx, req)
	if berr != nil {
		return nil, berr
	}

	totalSize, berr := s.backupRepo.UnscopedCount(ctx, backupFilter)
	if berr != nil {
		return nil, berr.Chain("CountBackups")
	}

	if totalSize == 0 {
		return &globalngv1.ListBackupsResponse{
			Backups:       []*globalngv1.Backup{},
			TotalSize:     int32(totalSize),
			NextPageToken: "",
		}, nil
	}

	backups, nextPageToken, paginationErr := pagination.RemotePagination(
		func(page pagination.Page) ([]*entity.Backup, error) {
			cs, berr := s.backupRepo.UnscopedQuery(ctx, backupFilter, &page)
			if berr != nil {
				return nil, berr
			}
			return cs, nil
		}, int(totalSize), req, pagination.ListDefaultPageSize, pagination.ListMaxPageSize)

	if paginationErr != nil {
		return nil, bizerr.WrapError(paginationErr, "RemotePagination")
	}

	// Convert entity.Backup to globalnextgenv1.Backup
	protoBackups := lo.Map(backups, func(backup *entity.Backup, _ int) *globalngv1.Backup {
		return convertBackupToProtoBackup(backup)
	})

	return &globalngv1.ListBackupsResponse{
		Backups:       protoBackups,
		TotalSize:     int32(totalSize),
		NextPageToken: nextPageToken,
	}, nil
}

func validateListBackupsRequest(req *globalngv1.ListBackupsRequest) bizerr.BizError {
	// avoid scan all the backups data accidentally
	if len(req.GetClusterIds()) == 0 && len(req.GetOrganizationIds()) == 0 && len(req.GetBackupIds()) == 0 {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"at least one of cluster ids, organization ids or backup ids must be set",
		)
	}

	// user want to list backups of clusters in specific state
	if len(req.GetClusterStates()) > 0 {
		if len(req.GetClusterStates()) != 1 {
			return bizerr.New(bizerr.ErrCodeParams, "cluster states should only have one element")
		}
		if req.GetClusterStates()[0] != globalngv1.ListBackupsRequest_ACTIVE &&
			req.GetClusterStates()[0] != globalngv1.ListBackupsRequest_DELETED {
			return bizerr.New(bizerr.ErrCodeParams, "cluster state should be set to one of ACTIVE or DELETED")
		}

		if len(req.GetOrganizationIds()) == 0 {
			return bizerr.New(bizerr.ErrCodeParams, "organization ids must be set when cluster states are set")
		}
		if len(req.GetClusterIds()) > 0 {
			return bizerr.New(bizerr.ErrCodeParams, "cluster ids and cluster states cannot be set at the same time")
		}
		if len(req.GetBackupIds()) > 0 {
			return bizerr.New(bizerr.ErrCodeParams, "backup ids and cluster states cannot be set at the same time")
		}
	}

	if req.GetStartTime() != nil && req.GetEndTime() != nil {
		if req.GetStartTime().AsTime().After(req.GetEndTime().AsTime()) {
			return bizerr.New(bizerr.ErrCodeParams, "start time must be before or equal to end time")
		}
	}

	return nil
}

func (s *serviceImpl) getBackupFilter(
	ctx context.Context,
	req *globalngv1.ListBackupsRequest,
) (*br.QueryBackupsFilter, bizerr.BizError) {
	userSetClusterIds, err := convertStringArrayToUint64Array(req.GetClusterIds())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster ids")
	}
	organizationIds, err := convertStringArrayToUint64Array(req.GetOrganizationIds())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid organization ids")
	}
	backupIds, err := convertStringArrayToUint64Array(req.GetBackupIds())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid backup ids")
	}

	// support cluster states filter by findout related cluster ids in specific tidb state
	clusterIds, berr := s.getClusterIds(ctx, req, userSetClusterIds, organizationIds)
	if berr != nil {
		return nil, berr
	}

	backupFilter := &br.QueryBackupsFilter{
		Ids:             backupIds,
		ClusterIds:      clusterIds,
		OrganizationIds: organizationIds,
		States:          convertBackupStatesProtoToSchema(req.GetStates()),
		ServicePlans:    convertServicePlansProtoToSchema(req.GetServicePlans()),
		RegionIds:       req.GetRegionIds(),
		TriggerTypes:    convertBackupTriggerTypesProtoToSchema(req.GetTriggerTypes()),
		BackupTimeLte:   getTimePtr(req.GetEndTime()),
		BackupTimeGte:   getTimePtr(req.GetStartTime()),
		Deleted:         ptr.Bool(false),
	}
	now := time.Now().UTC()
	backupFilter.ExcludeExpiredAutoBackupsAt = &now

	return backupFilter, nil
}

// getClusterIds is used to get the cluster ids from the request,
// if the cluster ids are not set, it will get the cluster ids with specific states in organizations
func (s *serviceImpl) getClusterIds(
	ctx context.Context,
	req *globalngv1.ListBackupsRequest,
	userSetClusterIds, userSetOrganizationIds []uint64,
) ([]uint64, bizerr.BizError) {
	if len(userSetClusterIds) != 0 {
		return userSetClusterIds, nil
	}

	if len(req.GetClusterStates()) != 0 {
		backupSettings, err := s.backupSettingRepo.UnscopedQuery(ctx, &br.QueryBackupSettingFilter{
			States:    convertTiDBStateToBackupSettingStates(req.GetClusterStates()[0]),
			TenantIds: userSetOrganizationIds,
		}, nil)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeDB, "GetClusterIdsByStates")
		}
		clusterIds := lo.Map(backupSettings, func(backupSetting *entity.BackupSetting, _ int) uint64 {
			return backupSetting.ClusterId
		})
		return clusterIds, nil
	}

	return nil, nil
}

func getTimePtr(protoTime *timestamppb.Timestamp) *time.Time {
	if protoTime == nil {
		return nil
	}
	t := protoTime.AsTime()
	return &t
}
