package service

import (
	"context"
	"errors"
	"regexp"
	"time"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/tidbcloud/cluster-service-ng/internal/cluster-agent/internal/objectstore"
)

const planReplayerDownloadURLTTL = time.Hour

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

// ListPlanReplayerFiles lists one object-storage page for a logical cluster.
func (s *AgentService) ListPlanReplayerFiles(
	ctx context.Context,
	req *apiv1.ListPlanReplayerFilesReq,
) (*apiv1.ListPlanReplayerFilesResp, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "request is required")
	}
	if !logicalClusterIDPattern.MatchString(req.GetClusterId()) {
		return nil, status.Error(codes.InvalidArgument, "cluster_id must contain only digits")
	}
	if s.planReplayerStore == nil {
		return nil, status.Errorf(codes.Unimplemented, "plan replayer storage is unsupported for provider %q", s.provider)
	}

	pageSize := req.GetPageSize()
	if pageSize == 0 {
		pageSize = objectstore.DefaultPageSize
	}
	if pageSize > objectstore.MaxPageSize {
		return nil, status.Errorf(codes.InvalidArgument, "page_size must not exceed %d", objectstore.MaxPageSize)
	}

	location, err := objectstore.ParseLocation(req.GetStorageRoot(), s.provider)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid storage_root: %v", err)
	}
	listPrefix, err := location.ListPrefix(req.GetClusterId())
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid list prefix: %v", err)
	}
	listScope := location.ListScope(listPrefix)
	cursor, err := objectstore.DecodePageToken(req.GetPageToken(), listScope)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, "invalid page_token")
	}

	result, err := s.planReplayerStore.List(ctx, location, listPrefix, int32(pageSize), cursor)
	if err != nil {
		return nil, planReplayerStoreStatus("list plan replayer files", err)
	}

	files := make([]*apiv1.PlanReplayerFile, 0, len(result.Objects))
	for _, object := range result.Objects {
		fileName, ok := location.FileNameFromObjectKey(req.GetClusterId(), object.Key)
		if !ok {
			continue
		}
		lastModified := timestamppb.New(object.LastModified)
		if err := lastModified.CheckValid(); err != nil {
			return nil, status.Errorf(codes.Internal, "invalid last-modified time for object %q: %v", fileName, err)
		}
		files = append(files, &apiv1.PlanReplayerFile{
			FileName:     fileName,
			SizeBytes:    object.SizeBytes,
			LastModified: lastModified,
			StorageUri:   location.CanonicalURI(object.Key),
		})
	}

	nextPageToken := ""
	if result.IsTruncated {
		if result.NextContinuationToken == "" {
			return nil, status.Error(codes.Internal, "object store returned a truncated page without a continuation token")
		}
		nextPageToken, err = objectstore.EncodePageToken(result.NextContinuationToken, listScope)
		if err != nil {
			return nil, status.Errorf(codes.Internal, "encode next_page_token: %v", err)
		}
	}
	return &apiv1.ListPlanReplayerFilesResp{
		Files:         files,
		NextPageToken: nextPageToken,
	}, nil
}

// GeneratePlanReplayerFileDownloadURL generates a temporary GET URL after confirming the object exists.
func (s *AgentService) GeneratePlanReplayerFileDownloadURL(
	ctx context.Context,
	req *apiv1.GeneratePlanReplayerFileDownloadURLReq,
) (*apiv1.GeneratePlanReplayerFileDownloadURLResp, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "request is required")
	}
	if !logicalClusterIDPattern.MatchString(req.GetClusterId()) {
		return nil, status.Error(codes.InvalidArgument, "cluster_id must contain only digits")
	}
	if s.planReplayerStore == nil {
		return nil, status.Errorf(codes.Unimplemented, "plan replayer storage is unsupported for provider %q", s.provider)
	}

	location, err := objectstore.ParseLocation(req.GetStorageRoot(), s.provider)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid storage_root: %v", err)
	}
	objectKey, err := location.ObjectKey(req.GetClusterId(), req.GetFileName())
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid file_name: %v", err)
	}
	if err := s.planReplayerStore.Head(ctx, location, objectKey); err != nil {
		return nil, planReplayerStoreStatus("head plan replayer file", err)
	}
	signed, err := s.planReplayerStore.PresignGet(ctx, location, objectKey, planReplayerDownloadURLTTL)
	if err != nil {
		return nil, planReplayerStoreStatus("presign plan replayer file", err)
	}
	if signed.URL == "" {
		return nil, status.Error(codes.Internal, "object store returned an empty download URL")
	}
	expireTime := timestamppb.New(signed.ExpiresAt)
	if err := expireTime.CheckValid(); err != nil {
		return nil, status.Errorf(codes.Internal, "invalid download URL expiration: %v", err)
	}
	return &apiv1.GeneratePlanReplayerFileDownloadURLResp{
		DownloadUrl: signed.URL,
		ExpireTime:  expireTime,
	}, nil
}

func planReplayerStoreStatus(operation string, err error) error {
	switch {
	case errors.Is(err, objectstore.ErrObjectNotFound):
		return status.Errorf(codes.NotFound, "%s: object not found", operation)
	case errors.Is(err, objectstore.ErrInvalidCursor):
		return status.Errorf(codes.InvalidArgument, "%s: continuation token is invalid", operation)
	case errors.Is(err, objectstore.ErrAccessDenied):
		return status.Errorf(codes.PermissionDenied, "%s: object storage access denied", operation)
	case errors.Is(err, objectstore.ErrUnavailable):
		return status.Errorf(codes.Unavailable, "%s: object storage is temporarily unavailable", operation)
	case errors.Is(err, context.Canceled):
		return status.Error(codes.Canceled, operation+": request canceled")
	case errors.Is(err, context.DeadlineExceeded):
		return status.Error(codes.DeadlineExceeded, operation+": deadline exceeded")
	default:
		return status.Errorf(codes.Internal, "%s: object storage operation failed: %v", operation, err)
	}
}
