package backup

import (
	"context"
	"strconv"
	"strings"

	"github.com/samber/lo"

	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	openapingv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/nextgen/v1beta2"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/app/openapi/nextgen/common"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository"
	pkgcommon "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
	regionutil "github.com/tidbcloud/tidb-management-service/utilities/region"
)

func (s serverImpl) ListBackups(
	ctx context.Context,
	req *openapingv1beta2.ListBackupsRequest,
) (*openapingv1beta2.ListBackupsResponse, error) {
	ctx, span := tracer.Start(ctx, "ListBackups")
	defer span.End()

	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	clusterIds, err := s.validateListBackupsRequest(ctx, req, reqMeta)
	if err != nil {
		return nil, err
	}
	if clusterIds != nil && len(clusterIds) == 0 {
		return &openapingv1beta2.ListBackupsResponse{
			Backups:       []*openapingv1beta2.Backup{},
			TotalSize:     0,
			NextPageToken: "",
		}, nil
	}

	resp, err := s.nextGenCli.ListBackups(ctx, listBackupsConvertToNextgenRequest(req, reqMeta, clusterIds))
	if err != nil {
		return nil, err
	}

	return listBackupsConvertToOpenapiResponse(resp), nil
}

func (s serverImpl) validateListBackupsRequest(
	ctx context.Context,
	req *openapingv1beta2.ListBackupsRequest,
	reqMeta *metadata.RequestMeta,
) ([]uint64, error) {
	if err := common.ValidateRegionIds(req.RegionIds); err != nil {
		return nil, err
	}

	clusterIds, err := parseListBackupClusterIds(req.TidbIds)
	if err != nil {
		return nil, err
	}

	allScope, projectIDs, instanceIDs, err := s.accountCli.GetInstancePermissionScopeOfUser(ctx, reqMeta.OrgID, reqMeta.UserID)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodePermission, "failed to get instance permission scope of user")
	}
	permissionFilter := &repository.PermissionFilter{
		All:        allScope,
		ProjectIds: projectIDs,
		ClusterIds: instanceIDs,
	}

	permittedClusterIds, err := s.listPermittedBackupClusterIds(ctx, reqMeta, clusterIds, permissionFilter)
	if err != nil {
		return nil, err
	}

	return permittedClusterIds, nil
}

func parseListBackupClusterIds(tidbIds []string) ([]uint64, error) {
	clusterIds := make([]uint64, 0, len(tidbIds))
	for _, tidbId := range tidbIds {
		id, err := strconv.ParseUint(tidbId, 10, 64)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeParams, "invalid tidb id: %s", tidbId)
		}
		clusterIds = append(clusterIds, id)
	}
	return clusterIds, nil
}

func (s serverImpl) listPermittedBackupClusterIds(
	ctx context.Context,
	reqMeta *metadata.RequestMeta,
	requestedClusterIds []uint64,
	permissionFilter *repository.PermissionFilter,
) ([]uint64, error) {
	if permissionFilter.All {
		if len(requestedClusterIds) == 0 {
			return nil, nil
		}
		return s.listBackupClusterIds(ctx, reqMeta, requestedClusterIds, permissionFilter)
	}
	if len(permissionFilter.ProjectIds) == 0 && len(permissionFilter.ClusterIds) == 0 {
		return []uint64{}, nil
	}
	return s.listBackupClusterIds(ctx, reqMeta, requestedClusterIds, permissionFilter)
}

func (s serverImpl) listBackupClusterIds(
	ctx context.Context,
	reqMeta *metadata.RequestMeta,
	requestedClusterIds []uint64,
	permissionFilter *repository.PermissionFilter,
) ([]uint64, error) {
	filter := &repository.ClusterFilter{
		OrganizationIds:  []uint64{reqMeta.OrgID},
		ClusterIds:       requestedClusterIds,
		View:             entity.ClusterViewClusterViewLocal,
		QueryDeleted:     true,
		DeployType:       entity.DeployTypeNextGen,
		PermissionFilter: permissionFilter,
	}

	pageSize := int64(len(requestedClusterIds))
	if len(requestedClusterIds) == 0 {
		count, err := s.clusterSvc.CountClusters(ctx, filter)
		if err != nil {
			return nil, bizerr.WrapError(err, "failed to count clusters")
		}
		if count == 0 {
			return []uint64{}, nil
		}
		pageSize = count
	}

	clusters, err := s.clusterSvc.ListClusters(ctx, filter, &pkgcommon.Page{Offset: 0, PageSize: pageSize})
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to list clusters")
	}
	if permissionFilter.All && len(clusters) != len(requestedClusterIds) {
		missingClusterIds := make([]string, 0, len(requestedClusterIds)-len(clusters))
		for _, id := range requestedClusterIds {
			if !lo.ContainsBy(clusters, func(c *entity.Cluster) bool { return c.ClusterId == id }) {
				missingClusterIds = append(missingClusterIds, strconv.FormatUint(id, 10))
			}
		}
		return nil, bizerr.New(bizerr.ErrCodeParams, "clusters not found: %s", strings.Join(missingClusterIds, ", "))
	}

	return lo.Map(clusters, func(cluster *entity.Cluster, _ int) uint64 {
		return cluster.ClusterId
	}), nil
}

func listBackupsConvertToNextgenRequest(
	req *openapingv1beta2.ListBackupsRequest,
	reqMeta *metadata.RequestMeta,
	clusterIds []uint64,
) *globalngv1.ListBackupsRequest {
	clusterIdStrs := req.TidbIds
	if clusterIds != nil {
		clusterIdStrs = lo.Map(clusterIds, func(id uint64, _ int) string {
			return strconv.FormatUint(id, 10)
		})
	}
	return &globalngv1.ListBackupsRequest{
		ClusterIds:      clusterIdStrs,
		OrganizationIds: []string{strconv.FormatUint(reqMeta.OrgID, 10)},
		States:          common.ConvertToNextgenBackupStates(req.States),
		ServicePlans:    common.ConvertToNextgenServicePlans(req.ServicePlans),
		RegionIds:       regionutil.ToEnumFormats(req.RegionIds),
		ClusterStates:   common.ConvertToNextgenClusterStates(req.TidbStates),
		TriggerTypes:    common.ConvertToNextgenBackupTriggerTypes(req.TriggerTypes),
		StartTime:       req.StartTime,
		EndTime:         req.EndTime,
		PageSize:        req.PageSize,
		PageToken:       req.PageToken,
		Skip:            req.Skip,
	}
}

func listBackupsConvertToOpenapiResponse(resp *globalngv1.ListBackupsResponse) *openapingv1beta2.ListBackupsResponse {
	return &openapingv1beta2.ListBackupsResponse{
		Backups:       common.ConvertToOpenapiBackups(resp.Backups),
		TotalSize:     resp.TotalSize,
		NextPageToken: resp.NextPageToken,
	}
}
