package server

import (
	"context"
	"errors"
	"path"
	"regexp"
	"strings"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	clusteragentv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const (
	defaultPlanReplayerPageSize = uint32(100)
	maxPlanReplayerPageSize     = uint32(1000)
	maxPlanReplayerPageTokenLen = 16384
	maxPlanReplayerFileNameLen  = 1024
)

var planReplayerClusterIDPattern = regexp.MustCompile(`^\d+$`)

// ListPlanReplayerFiles resolves storage from the logical cluster and delegates cloud access to cluster-agent.
func (s *serverImpl) ListPlanReplayerFiles(
	ctx context.Context,
	req *apiv1.ListPlanReplayerFilesRequest,
) (*apiv1.ListPlanReplayerFilesResponse, error) {
	pageSize, err := validateListPlanReplayerFilesRequest(req)
	if err != nil {
		return nil, err
	}
	sharedPool, storageRoot, err := s.resolvePlanReplayerStorage(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	downstream, err := s.clustergatewayCli.ListPlanReplayerFiles(
		ctx,
		sharedPool,
		&clusteragentv1.ListPlanReplayerFilesReq{
			ClusterId:   req.GetClusterId(),
			StorageRoot: storageRoot,
			PageSize:    pageSize,
			PageToken:   req.GetPageToken(),
		},
	)
	if err != nil {
		return nil, err
	}

	files := make([]*apiv1.PlanReplayerFile, 0, len(downstream.GetFiles()))
	for _, file := range downstream.GetFiles() {
		if file == nil {
			continue
		}
		files = append(files, &apiv1.PlanReplayerFile{
			FileName:     file.GetFileName(),
			SizeBytes:    file.GetSizeBytes(),
			LastModified: file.GetLastModified(),
			StorageUri:   file.GetStorageUri(),
		})
	}
	return &apiv1.ListPlanReplayerFilesResponse{
		Files:         files,
		NextPageToken: downstream.GetNextPageToken(),
	}, nil
}

// GeneratePlanReplayerFileDownloadURL resolves storage and delegates object validation and signing to cluster-agent.
func (s *serverImpl) GeneratePlanReplayerFileDownloadURL(
	ctx context.Context,
	req *apiv1.GeneratePlanReplayerFileDownloadURLRequest,
) (*apiv1.GeneratePlanReplayerFileDownloadURLResponse, error) {
	if err := validateGeneratePlanReplayerFileDownloadURLRequest(req); err != nil {
		return nil, err
	}
	sharedPool, storageRoot, err := s.resolvePlanReplayerStorage(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	downstream, err := s.clustergatewayCli.GeneratePlanReplayerFileDownloadURL(
		ctx,
		sharedPool,
		&clusteragentv1.GeneratePlanReplayerFileDownloadURLReq{
			ClusterId:   req.GetClusterId(),
			StorageRoot: storageRoot,
			FileName:    req.GetFileName(),
		},
	)
	if err != nil {
		return nil, err
	}
	return &apiv1.GeneratePlanReplayerFileDownloadURLResponse{
		DownloadUrl: downstream.GetDownloadUrl(),
		ExpireTime:  downstream.GetExpireTime(),
	}, nil
}

func (s *serverImpl) resolvePlanReplayerStorage(
	ctx context.Context,
	clusterID string,
) (*entity.SharedPool, string, error) {
	cluster, err := s.tidbClusterRepo.Load(ctx, clusterID, false)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			return nil, "", status.Errorf(codes.NotFound, "tidb cluster %s not found", clusterID)
		}
		return nil, "", status.Errorf(codes.Internal, "load tidb cluster %s: %v", clusterID, err)
	}
	sharedPool, err := s.sharedPoolRepo.Load(ctx, cluster.GetSharedPoolId(), false)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			return nil, "", status.Errorf(codes.NotFound, "shared pool %s not found", cluster.GetSharedPoolId())
		}
		return nil, "", status.Errorf(codes.Internal, "load shared pool %s: %v", cluster.GetSharedPoolId(), err)
	}
	storageRoot, err := sharedPool.GetPlanReplayerStorageRoot()
	switch {
	case err == nil:
		return sharedPool, storageRoot, nil
	case errors.Is(err, entity.ErrPlanReplayerStorageNotReady):
		return nil, "", status.Errorf(codes.FailedPrecondition, "shared pool %s storage is not ready", sharedPool.GetId())
	case errors.Is(err, entity.ErrPlanReplayerStorageProviderUnsupported):
		return nil, "", status.Errorf(codes.Unimplemented, "shared pool %s provider does not support plan replayer storage", sharedPool.GetId())
	default:
		return nil, "", status.Errorf(codes.Internal, "resolve shared pool %s plan replayer storage: %v", sharedPool.GetId(), err)
	}
}

func validateListPlanReplayerFilesRequest(req *apiv1.ListPlanReplayerFilesRequest) (uint32, error) {
	if req == nil {
		return 0, status.Error(codes.InvalidArgument, "request is required")
	}
	if err := validatePlanReplayerClusterID(req.GetClusterId()); err != nil {
		return 0, err
	}
	if req.GetPageSize() > maxPlanReplayerPageSize {
		return 0, status.Errorf(codes.InvalidArgument, "page_size must not exceed %d", maxPlanReplayerPageSize)
	}
	if len(req.GetPageToken()) > maxPlanReplayerPageTokenLen {
		return 0, status.Errorf(codes.InvalidArgument, "page_token must not exceed %d bytes", maxPlanReplayerPageTokenLen)
	}
	if req.GetPageSize() == 0 {
		return defaultPlanReplayerPageSize, nil
	}
	return req.GetPageSize(), nil
}

func validateGeneratePlanReplayerFileDownloadURLRequest(
	req *apiv1.GeneratePlanReplayerFileDownloadURLRequest,
) error {
	if req == nil {
		return status.Error(codes.InvalidArgument, "request is required")
	}
	if err := validatePlanReplayerClusterID(req.GetClusterId()); err != nil {
		return err
	}
	fileName := req.GetFileName()
	if len(fileName) > maxPlanReplayerFileNameLen {
		return status.Errorf(codes.InvalidArgument, "file_name must not exceed %d bytes", maxPlanReplayerFileNameLen)
	}
	if fileName == "" ||
		fileName != path.Base(fileName) ||
		strings.ContainsAny(fileName, `/\`) ||
		!strings.HasPrefix(fileName, "replayer_") ||
		!strings.HasSuffix(fileName, ".zip") {
		return status.Error(codes.InvalidArgument, "file_name must match replayer_*.zip and be a basename")
	}
	return nil
}

func validatePlanReplayerClusterID(clusterID string) error {
	if !planReplayerClusterIDPattern.MatchString(clusterID) {
		return status.Error(codes.InvalidArgument, "cluster_id must contain only digits")
	}
	return nil
}
