// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pubsub

import (
	"context"
	"errors"
	"fmt"
	"log"
	"runtime"
	"strings"
	"sync"
	"time"

	"cloud.google.com/go/iam"
	"cloud.google.com/go/internal/optional"
	ipubsub "cloud.google.com/go/internal/pubsub"
	vkit "cloud.google.com/go/pubsub/apiv1"
	pb "cloud.google.com/go/pubsub/apiv1/pubsubpb"
	"cloud.google.com/go/pubsub/internal/scheduler"
	gax "github.com/googleapis/gax-go/v2"
	"go.opencensus.io/stats"
	"go.opencensus.io/tag"
	"go.opentelemetry.io/otel/attribute"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
	"go.opentelemetry.io/otel/trace"
	"google.golang.org/api/support/bundler"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/encoding/gzip"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/durationpb"
	fmpb "google.golang.org/protobuf/types/known/fieldmaskpb"
	"google.golang.org/protobuf/types/known/timestamppb"
)

const (
	// MaxPublishRequestCount is the maximum number of messages that can be in
	// a single publish request, as defined by the PubSub service.
	MaxPublishRequestCount = 1000

	// MaxPublishRequestBytes is the maximum size of a single publish request
	// in bytes, as defined by the PubSub service.
	MaxPublishRequestBytes = 1e7
)

const (
	// TODO: math.MaxInt was added in Go 1.17. We should use that once 1.17
	// becomes the minimum supported version of Go.
	intSize = 32 << (^uint(0) >> 63)
	maxInt  = 1<<(intSize-1) - 1
)

// ErrOversizedMessage indicates that a message's size exceeds MaxPublishRequestBytes.
var ErrOversizedMessage = bundler.ErrOversizedItem

// Topic is a reference to a PubSub topic.
//
// The methods of Topic are safe for use by multiple goroutines.
type Topic struct {
	c *Client
	// The fully qualified identifier for the topic, in the format "projects/<projid>/topics/<name>"
	name string

	// Settings for publishing messages. All changes must be made before the
	// first call to Publish. The default is DefaultPublishSettings.
	PublishSettings PublishSettings

	mu        sync.RWMutex
	stopped   bool
	scheduler *scheduler.PublishScheduler

	flowController

	// EnableMessageOrdering enables delivery of ordered keys.
	EnableMessageOrdering bool

	// enableTracing enables OTel tracing of Pub/Sub messages on this topic.
	// This is configured at client instantiation, and allows
	// disabling tracing even when a tracer provider is detectd.
	enableTracing bool
}

// PublishSettings control the bundling of published messages.
type PublishSettings struct {

	// Publish a non-empty batch after this delay has passed.
	DelayThreshold time.Duration

	// Publish a batch when it has this many messages. The maximum is
	// MaxPublishRequestCount.
	CountThreshold int

	// Publish a batch when its size in bytes reaches this value.
	ByteThreshold int

	// The number of goroutines used in each of the data structures that are
	// involved along the the Publish path. Adjusting this value adjusts
	// concurrency along the publish path.
	//
	// Defaults to a multiple of GOMAXPROCS.
	NumGoroutines int

	// The maximum time that the client will attempt to publish a bundle of messages.
	Timeout time.Duration

	// The maximum number of bytes that the Bundler will keep in memory before
	// returning ErrOverflow. This is now superseded by FlowControlSettings.MaxOutstandingBytes.
	// If MaxOutstandingBytes is set, that value will override BufferedByteLimit.
	//
	// Defaults to DefaultPublishSettings.BufferedByteLimit.
	// Deprecated: Set `Topic.PublishSettings.FlowControlSettings.MaxOutstandingBytes` instead.
	BufferedByteLimit int

	// FlowControlSettings defines publisher flow control settings.
	FlowControlSettings FlowControlSettings

	// EnableCompression enables transport compression for Publish operations
	EnableCompression bool

	// CompressionBytesThreshold defines the threshold (in bytes) above which messages
	// are compressed for transport. Only takes effect if EnableCompression is true.
	CompressionBytesThreshold int
}

func (ps *PublishSettings) shouldCompress(batchSize int) bool {
	return ps.EnableCompression && batchSize > ps.CompressionBytesThreshold
}

// DefaultPublishSettings holds the default values for topics' PublishSettings.
var DefaultPublishSettings = PublishSettings{
	DelayThreshold: 10 * time.Millisecond,
	CountThreshold: 100,
	ByteThreshold:  1e6,
	Timeout:        60 * time.Second,
	// By default, limit the bundler to 10 times the max message size. The number 10 is
	// chosen as a reasonable amount of messages in the worst case whilst still
	// capping the number to a low enough value to not OOM users.
	BufferedByteLimit: 10 * MaxPublishRequestBytes,
	FlowControlSettings: FlowControlSettings{
		MaxOutstandingMessages: 1000,
		MaxOutstandingBytes:    -1,
		LimitExceededBehavior:  FlowControlIgnore,
	},
	// Publisher compression defaults matches Java's defaults
	// https://github.com/googleapis/java-pubsub/blob/7d33e7891db1b2e32fd523d7655b6c11ea140a8b/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/Publisher.java#L717-L718
	EnableCompression:         false,
	CompressionBytesThreshold: 240,
}

// CreateTopic creates a new topic.
//
// The specified topic ID must start with a letter, and contain only letters
// ([A-Za-z]), numbers ([0-9]), dashes (-), underscores (_), periods (.),
// tildes (~), plus (+) or percent signs (%). It must be between 3 and 255
// characters in length, and must not start with "goog". For more information,
// see: https://cloud.google.com/pubsub/docs/admin#resource_names
//
// If the topic already exists an error will be returned.
func (c *Client) CreateTopic(ctx context.Context, topicID string) (*Topic, error) {
	t := c.Topic(topicID)
	_, err := c.pubc.CreateTopic(ctx, &pb.Topic{Name: t.name})
	if err != nil {
		return nil, err
	}
	return t, nil
}

// CreateTopicWithConfig creates a topic from TopicConfig.
//
// The specified topic ID must start with a letter, and contain only letters
// ([A-Za-z]), numbers ([0-9]), dashes (-), underscores (_), periods (.),
// tildes (~), plus (+) or percent signs (%). It must be between 3 and 255
// characters in length, and must not start with "goog". For more information,
// see: https://cloud.google.com/pubsub/docs/admin#resource_names.
//
// If the topic already exists, an error will be returned.
func (c *Client) CreateTopicWithConfig(ctx context.Context, topicID string, tc *TopicConfig) (*Topic, error) {
	t := c.Topic(topicID)
	topic := tc.toProto()
	topic.Name = t.name
	_, err := c.pubc.CreateTopic(ctx, topic)
	if err != nil {
		return nil, err
	}
	return t, nil
}

// Topic creates a reference to a topic in the client's project.
//
// If a Topic's Publish method is called, it has background goroutines
// associated with it. Clean them up by calling Topic.Stop.
//
// Avoid creating many Topic instances if you use them to publish.
func (c *Client) Topic(id string) *Topic {
	return c.TopicInProject(id, c.projectID)
}

// TopicInProject creates a reference to a topic in the given project.
//
// If a Topic's Publish method is called, it has background goroutines
// associated with it. Clean them up by calling Topic.Stop.
//
// Avoid creating many Topic instances if you use them to publish.
func (c *Client) TopicInProject(id, projectID string) *Topic {
	return newTopic(c, fmt.Sprintf("projects/%s/topics/%s", projectID, id))
}

func newTopic(c *Client, name string) *Topic {
	return &Topic{
		c:               c,
		name:            name,
		PublishSettings: DefaultPublishSettings,
		enableTracing:   c.enableTracing,
	}
}

// TopicState denotes the possible states for a topic.
type TopicState int

const (
	// TopicStateUnspecified is the default value. This value is unused.
	TopicStateUnspecified = iota

	// TopicStateActive means the topic does not have any persistent errors.
	TopicStateActive

	// TopicStateIngestionResourceError means ingestion from the data source
	// has encountered a permanent error.
	// See the more detailed error state in the corresponding ingestion
	// source configuration.
	TopicStateIngestionResourceError
)

// TopicConfig describes the configuration of a topic.
type TopicConfig struct {
	// The fully qualified identifier for the topic, in the format "projects/<projid>/topics/<name>"
	name string

	// The set of labels for the topic.
	Labels map[string]string

	// The topic's message storage policy.
	MessageStoragePolicy MessageStoragePolicy

	// The name of the Cloud KMS key to be used to protect access to messages
	// published to this topic, in the format
	// "projects/P/locations/L/keyRings/R/cryptoKeys/K".
	KMSKeyName string

	// Schema defines the schema settings upon topic creation.
	SchemaSettings *SchemaSettings

	// RetentionDuration configures the minimum duration to retain a message
	// after it is published to the topic. If this field is set, messages published
	// to the topic in the last `RetentionDuration` are always available to subscribers.
	// For instance, it allows any attached subscription to [seek to a
	// timestamp](https://cloud.google.com/pubsub/docs/replay-overview#seek_to_a_time)
	// that is up to `RetentionDuration` in the past. If this field is
	// not set, message retention is controlled by settings on individual
	// subscriptions. Cannot be more than 31 days or less than 10 minutes.
	//
	// For more information, see https://cloud.google.com/pubsub/docs/replay-overview#topic_message_retention.
	RetentionDuration optional.Duration

	// State is an output-only field indicating the state of the topic.
	State TopicState

	// IngestionDataSourceSettings are settings for ingestion from a
	// data source into this topic.
	IngestionDataSourceSettings *IngestionDataSourceSettings
}

// String returns the printable globally unique name for the topic config.
// This method only works when the topic config is returned from the server,
// such as when calling `client.Topic` or `client.Topics`.
// Otherwise, this will return an empty string.
func (t *TopicConfig) String() string {
	return t.name
}

// ID returns the unique identifier of the topic within its project.
// This method only works when the topic config is returned from the server,
// such as when calling `client.Topic` or `client.Topics`.
// Otherwise, this will return an empty string.
func (t *TopicConfig) ID() string {
	slash := strings.LastIndex(t.name, "/")
	if slash == -1 {
		return ""
	}
	return t.name[slash+1:]
}

func (tc *TopicConfig) toProto() *pb.Topic {
	var retDur *durationpb.Duration
	if tc.RetentionDuration != nil {
		retDur = durationpb.New(optional.ToDuration(tc.RetentionDuration))
	}
	pbt := &pb.Topic{
		Labels:                      tc.Labels,
		MessageStoragePolicy:        messageStoragePolicyToProto(&tc.MessageStoragePolicy),
		KmsKeyName:                  tc.KMSKeyName,
		SchemaSettings:              schemaSettingsToProto(tc.SchemaSettings),
		MessageRetentionDuration:    retDur,
		IngestionDataSourceSettings: tc.IngestionDataSourceSettings.toProto(),
	}
	return pbt
}

// TopicConfigToUpdate describes how to update a topic.
type TopicConfigToUpdate struct {
	// If non-nil, the current set of labels is completely
	// replaced by the new set.
	Labels map[string]string

	// If non-nil, the existing policy (containing the list of regions)
	// is completely replaced by the new policy.
	//
	// Use the zero value &MessageStoragePolicy{} to reset the topic back to
	// using the organization's Resource Location Restriction policy.
	//
	// If nil, the policy remains unchanged.
	//
	// This field has beta status. It is not subject to the stability guarantee
	// and may change.
	MessageStoragePolicy *MessageStoragePolicy

	// If set to a positive duration between 10 minutes and 31 days, RetentionDuration is changed.
	// If set to a negative value, this clears RetentionDuration from the topic.
	// If nil, the retention duration remains unchanged.
	RetentionDuration optional.Duration

	// Schema defines the schema settings upon topic creation.
	//
	// Use the zero value &SchemaSettings{} to remove the schema from the topic.
	SchemaSettings *SchemaSettings

	// IngestionDataSourceSettings are settings for ingestion from a
	// data source into this topic.
	//
	// When changing this value, the entire data source settings object must be applied,
	// rather than just the differences. This includes the source and logging settings.
	//
	// Use the zero value &IngestionDataSourceSettings{} to remove the ingestion settings from the topic.
	IngestionDataSourceSettings *IngestionDataSourceSettings
}

func protoToTopicConfig(pbt *pb.Topic) TopicConfig {
	tc := TopicConfig{
		name:                        pbt.Name,
		Labels:                      pbt.Labels,
		MessageStoragePolicy:        protoToMessageStoragePolicy(pbt.MessageStoragePolicy),
		KMSKeyName:                  pbt.KmsKeyName,
		SchemaSettings:              protoToSchemaSettings(pbt.SchemaSettings),
		State:                       TopicState(pbt.State),
		IngestionDataSourceSettings: protoToIngestionDataSourceSettings(pbt.IngestionDataSourceSettings),
	}
	if pbt.GetMessageRetentionDuration() != nil {
		tc.RetentionDuration = pbt.GetMessageRetentionDuration().AsDuration()
	}
	return tc
}

// DetachSubscriptionResult is the response for the DetachSubscription method.
// Reserved for future use.
type DetachSubscriptionResult struct{}

// DetachSubscription detaches a subscription from its topic. All messages
// retained in the subscription are dropped. Subsequent `Pull` and `StreamingPull`
// requests will return FAILED_PRECONDITION. If the subscription is a push
// subscription, pushes to the endpoint will stop.
func (c *Client) DetachSubscription(ctx context.Context, sub string) (*DetachSubscriptionResult, error) {
	_, err := c.pubc.DetachSubscription(ctx, &pb.DetachSubscriptionRequest{
		Subscription: sub,
	})
	if err != nil {
		return nil, err
	}
	return &DetachSubscriptionResult{}, nil
}

// MessageStoragePolicy constrains how messages published to the topic may be stored. It
// is determined when the topic is created based on the policy configured at
// the project level.
type MessageStoragePolicy struct {
	// AllowedPersistenceRegions is the list of GCP regions where messages that are published
	// to the topic may be persisted in storage. Messages published by publishers running in
	// non-allowed GCP regions (or running outside of GCP altogether) will be
	// routed for storage in one of the allowed regions.
	//
	// If empty, it indicates a misconfiguration at the project or organization level, which
	// will result in all Publish operations failing. This field cannot be empty in updates.
	//
	// If nil, then the policy is not defined on a topic level. When used in updates, it resets
	// the regions back to the organization level Resource Location Restriction policy.
	//
	// For more information, see
	// https://cloud.google.com/pubsub/docs/resource-location-restriction#pubsub-storage-locations.
	AllowedPersistenceRegions []string
}

func protoToMessageStoragePolicy(msp *pb.MessageStoragePolicy) MessageStoragePolicy {
	if msp == nil {
		return MessageStoragePolicy{}
	}
	return MessageStoragePolicy{AllowedPersistenceRegions: msp.AllowedPersistenceRegions}
}

func messageStoragePolicyToProto(msp *MessageStoragePolicy) *pb.MessageStoragePolicy {
	if msp == nil || msp.AllowedPersistenceRegions == nil {
		return nil
	}
	return &pb.MessageStoragePolicy{AllowedPersistenceRegions: msp.AllowedPersistenceRegions}
}

// IngestionDataSourceSettings enables ingestion from a data source into this topic.
type IngestionDataSourceSettings struct {
	Source IngestionDataSource

	PlatformLogsSettings *PlatformLogsSettings
}

// IngestionDataSource is the kind of ingestion source to be used.
type IngestionDataSource interface {
	isIngestionDataSource() bool
}

// AWSKinesisState denotes the possible states for ingestion from Amazon Kinesis Data Streams.
type AWSKinesisState int

const (
	// AWSKinesisStateUnspecified is the default value. This value is unused.
	AWSKinesisStateUnspecified = iota

	// AWSKinesisStateActive means ingestion is active.
	AWSKinesisStateActive

	// AWSKinesisStatePermissionDenied means encountering an error while consumign data from Kinesis.
	// This can happen if:
	//   - The provided `aws_role_arn` does not exist or does not have the
	//     appropriate permissions attached.
	//   - The provided `aws_role_arn` is not set up properly for Identity
	//     Federation using `gcp_service_account`.
	//   - The Pub/Sub SA is not granted the
	//     `iam.serviceAccounts.getOpenIdToken` permission on
	//     `gcp_service_account`.
	AWSKinesisStatePermissionDenied

	// AWSKinesisStatePublishPermissionDenied means permission denied encountered while publishing to the topic.
	// This can happen due to Pub/Sub SA has not been granted the appropriate publish
	// permissions https://cloud.google.com/pubsub/docs/access-control#pubsub.publisher
	AWSKinesisStatePublishPermissionDenied

	// AWSKinesisStateStreamNotFound means the Kinesis stream does not exist.
	AWSKinesisStateStreamNotFound

	// AWSKinesisStateConsumerNotFound means the Kinesis consumer does not exist.
	AWSKinesisStateConsumerNotFound
)

// IngestionDataSourceAWSKinesis are ingestion settings for Amazon Kinesis Data Streams.
type IngestionDataSourceAWSKinesis struct {
	// State is an output-only field indicating the state of the kinesis connection.
	State AWSKinesisState

	// StreamARN is the Kinesis stream ARN to ingest data from.
	StreamARN string

	// ConsumerARn is the Kinesis consumer ARN to used for ingestion in Enhanced
	// Fan-Out mode. The consumer must be already created and ready to be used.
	ConsumerARN string

	// AWSRoleARn is the AWS role ARN to be used for Federated Identity authentication
	// with Kinesis. Check the Pub/Sub docs for how to set up this role and the
	// required permissions that need to be attached to it.
	AWSRoleARN string

	// GCPServiceAccount is the GCP service account to be used for Federated Identity
	// authentication with Kinesis (via a `AssumeRoleWithWebIdentity` call for
	// the provided role). The `aws_role_arn` must be set up with
	// `accounts.google.com:sub` equals to this service account number.
	GCPServiceAccount string
}

var _ IngestionDataSource = (*IngestionDataSourceAWSKinesis)(nil)

func (i *IngestionDataSourceAWSKinesis) isIngestionDataSource() bool {
	return true
}

// CloudStorageIngestionState denotes the possible states for ingestion from Cloud Storage.
type CloudStorageIngestionState int

const (
	// CloudStorageIngestionStateUnspecified is the default value. This value is unused.
	CloudStorageIngestionStateUnspecified = iota

	// CloudStorageIngestionStateActive means ingestion is active.
	CloudStorageIngestionStateActive

	// CloudStorageIngestionPermissionDenied means encountering an error while calling the Cloud Storage API.
	// This can happen if the Pub/Sub SA has not been granted the
	// [appropriate permissions](https://cloud.google.com/storage/docs/access-control/iam-permissions):
	// - storage.objects.list: to list the objects in a bucket.
	// - storage.objects.get: to read the objects in a bucket.
	// - storage.buckets.get: to verify the bucket exists.
	CloudStorageIngestionPermissionDenied

	// CloudStorageIngestionPublishPermissionDenied means encountering an error when publishing to the topic.
	// This can happen if the Pub/Sub SA has not been granted the [appropriate publish
	// permissions](https://cloud.google.com/pubsub/docs/access-control#pubsub.publisher)
	CloudStorageIngestionPublishPermissionDenied

	// CloudStorageIngestionBucketNotFound means the provided bucket doesn't exist.
	CloudStorageIngestionBucketNotFound

	// CloudStorageIngestionTooManyObjects means the bucket has too many objects, ingestion will be paused.
	CloudStorageIngestionTooManyObjects
)

// IngestionDataSourceCloudStorage are ingestion settings for Cloud Storage.
type IngestionDataSourceCloudStorage struct {
	// State is an output-only field indicating the state of the Cloud storage ingestion source.
	State CloudStorageIngestionState

	// Bucket is the Cloud Storage bucket. The bucket name must be without any
	// prefix like "gs://". See the bucket naming requirements (https://cloud.google.com/storage/docs/buckets#naming).
	Bucket string

	// InputFormat is the format of objects in Cloud Storage.
	// Defaults to TextFormat.
	InputFormat ingestionDataSourceCloudStorageInputFormat

	// MinimumObjectCreateTime means objects with a larger or equal creation timestamp will be
	// ingested.
	MinimumObjectCreateTime time.Time

	// MatchGlob is the pattern used to match objects that will be ingested. If
	// empty, all objects will be ingested. See the [supported
	// patterns](https://cloud.google.com/storage/docs/json_api/v1/objects/list#list-objects-and-prefixes-using-glob).
	MatchGlob string
}

var _ IngestionDataSource = (*IngestionDataSourceCloudStorage)(nil)

func (i *IngestionDataSourceCloudStorage) isIngestionDataSource() bool {
	return true
}

type ingestionDataSourceCloudStorageInputFormat interface {
	isCloudStorageIngestionInputFormat() bool
}

var _ ingestionDataSourceCloudStorageInputFormat = (*IngestionDataSourceCloudStorageTextFormat)(nil)
var _ ingestionDataSourceCloudStorageInputFormat = (*IngestionDataSourceCloudStorageAvroFormat)(nil)
var _ ingestionDataSourceCloudStorageInputFormat = (*IngestionDataSourceCloudStoragePubSubAvroFormat)(nil)

// IngestionDataSourceCloudStorageTextFormat means Cloud Storage data will be interpreted as text.
type IngestionDataSourceCloudStorageTextFormat struct {
	Delimiter string
}

func (i *IngestionDataSourceCloudStorageTextFormat) isCloudStorageIngestionInputFormat() bool {
	return true
}

// IngestionDataSourceCloudStorageAvroFormat means Cloud Storage data will be interpreted in Avro format.
type IngestionDataSourceCloudStorageAvroFormat struct{}

func (i *IngestionDataSourceCloudStorageAvroFormat) isCloudStorageIngestionInputFormat() bool {
	return true
}

// IngestionDataSourceCloudStoragePubSubAvroFormat is used assuming the data was written using Cloud
// Storage subscriptions https://cloud.google.com/pubsub/docs/cloudstorage.
type IngestionDataSourceCloudStoragePubSubAvroFormat struct{}

func (i *IngestionDataSourceCloudStoragePubSubAvroFormat) isCloudStorageIngestionInputFormat() bool {
	return true
}

// EventHubsState denotes the possible states for ingestion from Event Hubs.
type EventHubsState int

const (
	// EventHubsStateUnspecified is the default value. This value is unused.
	EventHubsStateUnspecified = iota

	// EventHubsStateActive means the state is active.
	EventHubsStateActive

	// EventHubsStatePermissionDenied indicates encountered permission denied error
	// while consuming data from Event Hubs.
	// This can happen when `client_id`, or `tenant_id` are invalid. Or the
	// right permissions haven't been granted.
	EventHubsStatePermissionDenied

	// EventHubsStatePublishPermissionDenied indicates permission denied encountered
	// while publishing to the topic.
	EventHubsStatePublishPermissionDenied

	// EventHubsStateNamespaceNotFound indicates the provided Event Hubs namespace couldn't be found.
	EventHubsStateNamespaceNotFound

	// EventHubsStateNotFound indicates the provided Event Hub couldn't be found.
	EventHubsStateNotFound

	// EventHubsStateSubscriptionNotFound indicates the provided Event Hubs subscription couldn't be found.
	EventHubsStateSubscriptionNotFound

	// EventHubsStateResourceGroupNotFound indicates the provided Event Hubs resource group couldn't be found.
	EventHubsStateResourceGroupNotFound
)

// IngestionDataSourceAzureEventHubs are ingestion settings for Azure Event Hubs.
type IngestionDataSourceAzureEventHubs struct {
	// Output only field that indicates the state of the Event Hubs ingestion source.
	State EventHubsState

	// Name of the resource group within the Azure subscription
	ResourceGroup string

	// Name of the Event Hubs namespace
	Namespace string

	// Rame of the Event Hub.
	EventHub string

	// Client ID of the Azure application that is being used to authenticate Pub/Sub.
	ClientID string

	// Tenant ID of the Azure application that is being used to authenticate Pub/Sub.
	TenantID string

	// The Azure subscription ID
	SubscriptionID string

	// GCPServiceAccount is the GCP service account to be used for Federated Identity
	// authentication.
	GCPServiceAccount string
}

var _ IngestionDataSource = (*IngestionDataSourceAzureEventHubs)(nil)

func (i *IngestionDataSourceAzureEventHubs) isIngestionDataSource() bool {
	return true
}

// AmazonMSKState denotes the possible states for ingestion from Amazon MSK.
type AmazonMSKState int

const (
	// AmazonMSKStateUnspecified is the default value. This value is unused.
	AmazonMSKStateUnspecified = iota

	// AmazonMSKActive indicates MSK topic is active.
	AmazonMSKActive

	// AmazonMSKPermissionDenied indicates permission denied encountered while consuming data from Amazon MSK.
	AmazonMSKPermissionDenied

	// AmazonMSKPublishPermissionDenied indicates permission denied encountered while publishing to the topic.
	AmazonMSKPublishPermissionDenied

	// AmazonMSKClusterNotFound indicates the provided Msk cluster wasn't found.
	AmazonMSKClusterNotFound

	// AmazonMSKTopicNotFound indicates the provided topic wasn't found.
	AmazonMSKTopicNotFound
)

// IngestionDataSourceAmazonMSK are ingestion settings for Amazon MSK.
type IngestionDataSourceAmazonMSK struct {
	// An output-only field that indicates the state of the Amazon
	// MSK ingestion source.
	State AmazonMSKState

	// The Amazon Resource Name (ARN) that uniquely identifies the
	// cluster.
	ClusterARN string

	// The name of the topic in the Amazon MSK cluster that Pub/Sub
	// will import from.
	Topic string

	// AWS role ARN to be used for Federated Identity authentication
	// with Amazon MSK. Check the Pub/Sub docs for how to set up this role and
	// the required permissions that need to be attached to it.
	AWSRoleARN string

	// The GCP service account to be used for Federated Identity
	// authentication with Amazon MSK (via a `AssumeRoleWithWebIdentity` call
	// for the provided role). The `aws_role_arn` must be set up with
	// `accounts.google.com:sub` equals to this service account number.
	GCPServiceAccount string
}

var _ IngestionDataSource = (*IngestionDataSourceAmazonMSK)(nil)

func (i *IngestionDataSourceAmazonMSK) isIngestionDataSource() bool {
	return true
}

// ConfluentCloudState denotes state of ingestion topic with confluent cloud
type ConfluentCloudState int

const (
	// ConfluentCloudStateUnspecified is the default value. This value is unused.
	ConfluentCloudStateUnspecified = iota

	// ConfluentCloudActive indicates the state is active.
	ConfluentCloudActive = 1

	// ConfluentCloudPermissionDenied indicates permission denied encountered
	// while consuming data from Confluent Cloud.
	ConfluentCloudPermissionDenied = 2

	// ConfluentCloudPublishPermissionDenied indicates permission denied encountered
	// while publishing to the topic.
	ConfluentCloudPublishPermissionDenied = 3

	// ConfluentCloudUnreachableBootstrapServer indicates the provided bootstrap
	// server address is unreachable.
	ConfluentCloudUnreachableBootstrapServer = 4

	// ConfluentCloudClusterNotFound indicates the provided cluster wasn't found.
	ConfluentCloudClusterNotFound = 5

	// ConfluentCloudTopicNotFound indicates the provided topic wasn't found.
	ConfluentCloudTopicNotFound = 6
)

// IngestionDataSourceConfluentCloud are ingestion settings for confluent cloud.
type IngestionDataSourceConfluentCloud struct {
	// An output-only field that indicates the state of the
	// Confluent Cloud ingestion source.
	State ConfluentCloudState

	// The address of the bootstrap server. The format is url:port.
	BootstrapServer string

	// The id of the cluster.
	ClusterID string

	// The name of the topic in the Confluent Cloud cluster that
	// Pub/Sub will import from.
	Topic string

	// The id of the identity pool to be used for Federated Identity
	// authentication with Confluent Cloud. See
	// https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/identity-providers/oauth/identity-pools.html#add-oauth-identity-pools.
	IdentityPoolID string

	// The GCP service account to be used for Federated Identity
	// authentication with `identity_pool_id`.
	GCPServiceAccount string
}

var _ IngestionDataSource = (*IngestionDataSourceConfluentCloud)(nil)

func (i *IngestionDataSourceConfluentCloud) isIngestionDataSource() bool {
	return true
}

func protoToIngestionDataSourceSettings(pbs *pb.IngestionDataSourceSettings) *IngestionDataSourceSettings {
	if pbs == nil {
		return nil
	}

	s := &IngestionDataSourceSettings{}
	if k := pbs.GetAwsKinesis(); k != nil {
		s.Source = &IngestionDataSourceAWSKinesis{
			State:             AWSKinesisState(k.State),
			StreamARN:         k.GetStreamArn(),
			ConsumerARN:       k.GetConsumerArn(),
			AWSRoleARN:        k.GetAwsRoleArn(),
			GCPServiceAccount: k.GetGcpServiceAccount(),
		}
	} else if cs := pbs.GetCloudStorage(); cs != nil {
		var format ingestionDataSourceCloudStorageInputFormat
		switch t := cs.InputFormat.(type) {
		case *pb.IngestionDataSourceSettings_CloudStorage_TextFormat_:
			format = &IngestionDataSourceCloudStorageTextFormat{
				Delimiter: *t.TextFormat.Delimiter,
			}
		case *pb.IngestionDataSourceSettings_CloudStorage_AvroFormat_:
			format = &IngestionDataSourceCloudStorageAvroFormat{}
		case *pb.IngestionDataSourceSettings_CloudStorage_PubsubAvroFormat:
			format = &IngestionDataSourceCloudStoragePubSubAvroFormat{}
		}
		s.Source = &IngestionDataSourceCloudStorage{
			State:                   CloudStorageIngestionState(cs.GetState()),
			Bucket:                  cs.GetBucket(),
			InputFormat:             format,
			MinimumObjectCreateTime: cs.GetMinimumObjectCreateTime().AsTime(),
			MatchGlob:               cs.GetMatchGlob(),
		}
	} else if e := pbs.GetAzureEventHubs(); e != nil {
		s.Source = &IngestionDataSourceAzureEventHubs{
			State:             EventHubsState(e.GetState()),
			ResourceGroup:     e.GetResourceGroup(),
			Namespace:         e.GetNamespace(),
			EventHub:          e.GetEventHub(),
			ClientID:          e.GetClientId(),
			TenantID:          e.GetTenantId(),
			SubscriptionID:    e.GetSubscriptionId(),
			GCPServiceAccount: e.GetGcpServiceAccount(),
		}
	} else if m := pbs.GetAwsMsk(); m != nil {
		s.Source = &IngestionDataSourceAmazonMSK{
			State:             AmazonMSKState(m.GetState()),
			ClusterARN:        m.GetClusterArn(),
			Topic:             m.GetTopic(),
			AWSRoleARN:        m.GetAwsRoleArn(),
			GCPServiceAccount: m.GetGcpServiceAccount(),
		}
	} else if c := pbs.GetConfluentCloud(); c != nil {
		s.Source = &IngestionDataSourceConfluentCloud{
			State:             ConfluentCloudState(c.GetState()),
			BootstrapServer:   c.GetBootstrapServer(),
			ClusterID:         c.GetClusterId(),
			Topic:             c.GetTopic(),
			IdentityPoolID:    c.GetIdentityPoolId(),
			GCPServiceAccount: c.GetGcpServiceAccount(),
		}
	}

	if pbs.PlatformLogsSettings != nil {
		s.PlatformLogsSettings = &PlatformLogsSettings{
			Severity: PlatformLogsSeverity(pbs.PlatformLogsSettings.Severity),
		}
	}

	return s
}

func (i *IngestionDataSourceSettings) toProto() *pb.IngestionDataSourceSettings {
	if i == nil {
		return nil
	}
	// An empty/zero-valued config is treated the same as nil and clearing this setting.
	if (IngestionDataSourceSettings{}) == *i {
		return nil
	}
	pbs := &pb.IngestionDataSourceSettings{}
	if i.PlatformLogsSettings != nil {
		pbs.PlatformLogsSettings = &pb.PlatformLogsSettings{
			Severity: pb.PlatformLogsSettings_Severity(i.PlatformLogsSettings.Severity),
		}
	}
	if out := i.Source; out != nil {
		if k, ok := out.(*IngestionDataSourceAWSKinesis); ok {
			pbs.Source = &pb.IngestionDataSourceSettings_AwsKinesis_{
				AwsKinesis: &pb.IngestionDataSourceSettings_AwsKinesis{
					State:             pb.IngestionDataSourceSettings_AwsKinesis_State(k.State),
					StreamArn:         k.StreamARN,
					ConsumerArn:       k.ConsumerARN,
					AwsRoleArn:        k.AWSRoleARN,
					GcpServiceAccount: k.GCPServiceAccount,
				},
			}
		}
		if cs, ok := out.(*IngestionDataSourceCloudStorage); ok {
			switch format := cs.InputFormat.(type) {
			case *IngestionDataSourceCloudStorageTextFormat:
				pbs.Source = &pb.IngestionDataSourceSettings_CloudStorage_{
					CloudStorage: &pb.IngestionDataSourceSettings_CloudStorage{
						State:  pb.IngestionDataSourceSettings_CloudStorage_State(cs.State),
						Bucket: cs.Bucket,
						InputFormat: &pb.IngestionDataSourceSettings_CloudStorage_TextFormat_{
							TextFormat: &pb.IngestionDataSourceSettings_CloudStorage_TextFormat{
								Delimiter: &format.Delimiter,
							},
						},
						MinimumObjectCreateTime: timestamppb.New(cs.MinimumObjectCreateTime),
						MatchGlob:               cs.MatchGlob,
					},
				}
			case *IngestionDataSourceCloudStorageAvroFormat:
				pbs.Source = &pb.IngestionDataSourceSettings_CloudStorage_{
					CloudStorage: &pb.IngestionDataSourceSettings_CloudStorage{
						Bucket: cs.Bucket,
						InputFormat: &pb.IngestionDataSourceSettings_CloudStorage_AvroFormat_{
							AvroFormat: &pb.IngestionDataSourceSettings_CloudStorage_AvroFormat{},
						},
						MinimumObjectCreateTime: timestamppb.New(cs.MinimumObjectCreateTime),
						MatchGlob:               cs.MatchGlob,
					},
				}
			case *IngestionDataSourceCloudStoragePubSubAvroFormat:
				pbs.Source = &pb.IngestionDataSourceSettings_CloudStorage_{
					CloudStorage: &pb.IngestionDataSourceSettings_CloudStorage{
						State:  pb.IngestionDataSourceSettings_CloudStorage_State(cs.State),
						Bucket: cs.Bucket,
						InputFormat: &pb.IngestionDataSourceSettings_CloudStorage_PubsubAvroFormat{
							PubsubAvroFormat: &pb.IngestionDataSourceSettings_CloudStorage_PubSubAvroFormat{},
						},
						MinimumObjectCreateTime: timestamppb.New(cs.MinimumObjectCreateTime),
						MatchGlob:               cs.MatchGlob,
					},
				}
			}
		}
		if e, ok := out.(*IngestionDataSourceAzureEventHubs); ok {
			pbs.Source = &pb.IngestionDataSourceSettings_AzureEventHubs_{
				AzureEventHubs: &pb.IngestionDataSourceSettings_AzureEventHubs{
					ResourceGroup:     e.ResourceGroup,
					Namespace:         e.Namespace,
					EventHub:          e.EventHub,
					ClientId:          e.ClientID,
					TenantId:          e.TenantID,
					SubscriptionId:    e.SubscriptionID,
					GcpServiceAccount: e.GCPServiceAccount,
				},
			}
		}
		if m, ok := out.(*IngestionDataSourceAmazonMSK); ok {
			pbs.Source = &pb.IngestionDataSourceSettings_AwsMsk_{
				AwsMsk: &pb.IngestionDataSourceSettings_AwsMsk{
					ClusterArn:        m.ClusterARN,
					Topic:             m.Topic,
					AwsRoleArn:        m.AWSRoleARN,
					GcpServiceAccount: m.GCPServiceAccount,
				},
			}
		}
		if c, ok := out.(*IngestionDataSourceConfluentCloud); ok {
			pbs.Source = &pb.IngestionDataSourceSettings_ConfluentCloud_{
				ConfluentCloud: &pb.IngestionDataSourceSettings_ConfluentCloud{
					BootstrapServer:   c.BootstrapServer,
					ClusterId:         c.ClusterID,
					Topic:             c.Topic,
					IdentityPoolId:    c.IdentityPoolID,
					GcpServiceAccount: c.GCPServiceAccount,
				},
			}
		}
	}
	return pbs
}

// PlatformLogsSettings configures logging produced by Pub/Sub.
// Currently only valid on Cloud Storage ingestion topics.
type PlatformLogsSettings struct {
	Severity PlatformLogsSeverity
}

// PlatformLogsSeverity are the severity levels of Platform Logs.
type PlatformLogsSeverity int32

const (
	// PlatformLogsSeverityUnspecified is the default value. Logs level is unspecified. Logs will be disabled.
	PlatformLogsSeverityUnspecified PlatformLogsSeverity = iota
	// PlatformLogsSeverityDisabled means logs will be disabled.
	PlatformLogsSeverityDisabled
	// PlatformLogsSeverityDebug means debug logs and higher-severity logs will be written.
	PlatformLogsSeverityDebug
	// PlatformLogsSeverityInfo means info logs and higher-severity logs will be written.
	PlatformLogsSeverityInfo
	// PlatformLogsSeverityWarning means warning logs and higher-severity logs will be written.
	PlatformLogsSeverityWarning
	// PlatformLogsSeverityError means only error logs will be written.
	PlatformLogsSeverityError
)

// Config returns the TopicConfig for the topic.
func (t *Topic) Config(ctx context.Context) (TopicConfig, error) {
	pbt, err := t.c.pubc.GetTopic(ctx, &pb.GetTopicRequest{Topic: t.name})
	if err != nil {
		return TopicConfig{}, err
	}
	return protoToTopicConfig(pbt), nil
}

// Update changes an existing topic according to the fields set in cfg. It returns
// the new TopicConfig.
func (t *Topic) Update(ctx context.Context, cfg TopicConfigToUpdate) (TopicConfig, error) {
	req := t.updateRequest(cfg)
	if len(req.UpdateMask.Paths) == 0 {
		return TopicConfig{}, errors.New("pubsub: UpdateTopic call with nothing to update")
	}
	rpt, err := t.c.pubc.UpdateTopic(ctx, req)
	if err != nil {
		return TopicConfig{}, err
	}
	return protoToTopicConfig(rpt), nil
}

func (t *Topic) updateRequest(cfg TopicConfigToUpdate) *pb.UpdateTopicRequest {
	pt := &pb.Topic{Name: t.name}
	var paths []string
	if cfg.Labels != nil {
		pt.Labels = cfg.Labels
		paths = append(paths, "labels")
	}
	if cfg.MessageStoragePolicy != nil {
		pt.MessageStoragePolicy = messageStoragePolicyToProto(cfg.MessageStoragePolicy)
		paths = append(paths, "message_storage_policy")
	}
	if cfg.RetentionDuration != nil {
		r := optional.ToDuration(cfg.RetentionDuration)
		pt.MessageRetentionDuration = durationpb.New(r)
		if r < 0 {
			// Clear MessageRetentionDuration if sentinel value is read.
			pt.MessageRetentionDuration = nil
		}
		paths = append(paths, "message_retention_duration")
	}
	// Updating SchemaSettings' field masks are more complicated here
	// since each field should be able to be independently edited, while
	// preserving the current values for everything else. We also denote
	// the zero value SchemaSetting to mean clearing or removing schema
	// from the topic.
	if cfg.SchemaSettings != nil {
		pt.SchemaSettings = schemaSettingsToProto(cfg.SchemaSettings)
		clearSchema := true
		if pt.SchemaSettings.Schema != "" {
			paths = append(paths, "schema_settings.schema")
			clearSchema = false
		}
		if pt.SchemaSettings.Encoding != pb.Encoding_ENCODING_UNSPECIFIED {
			paths = append(paths, "schema_settings.encoding")
			clearSchema = false
		}
		if pt.SchemaSettings.FirstRevisionId != "" {
			paths = append(paths, "schema_settings.first_revision_id")
			clearSchema = false
		}
		if pt.SchemaSettings.LastRevisionId != "" {
			paths = append(paths, "schema_settings.last_revision_id")
			clearSchema = false
		}
		// Clear the schema if all of its values are equal to the zero value.
		if clearSchema {
			paths = append(paths, "schema_settings")
			pt.SchemaSettings = nil
		}
	}
	if cfg.IngestionDataSourceSettings != nil {
		pt.IngestionDataSourceSettings = cfg.IngestionDataSourceSettings.toProto()
		paths = append(paths, "ingestion_data_source_settings")
	}
	return &pb.UpdateTopicRequest{
		Topic:      pt,
		UpdateMask: &fmpb.FieldMask{Paths: paths},
	}
}

// Topics returns an iterator which returns all of the topics for the client's project.
func (c *Client) Topics(ctx context.Context) *TopicIterator {
	it := c.pubc.ListTopics(ctx, &pb.ListTopicsRequest{Project: c.fullyQualifiedProjectName()})
	return &TopicIterator{
		c:  c,
		it: it,
		next: func() (string, error) {
			topic, err := it.Next()
			if err != nil {
				return "", err
			}
			return topic.Name, nil
		},
	}
}

// TopicIterator is an iterator that returns a series of topics.
type TopicIterator struct {
	c    *Client
	it   *vkit.TopicIterator
	next func() (string, error)
}

// Next returns the next topic. If there are no more topics, iterator.Done will be returned.
func (tps *TopicIterator) Next() (*Topic, error) {
	topicName, err := tps.next()
	if err != nil {
		return nil, err
	}
	return newTopic(tps.c, topicName), nil
}

// NextConfig returns the next topic config. If there are no more topics,
// iterator.Done will be returned.
// This call shares the underlying iterator with calls to `TopicIterator.Next`.
// If you wish to use mix calls, create separate iterator instances for both.
func (t *TopicIterator) NextConfig() (*TopicConfig, error) {
	tpb, err := t.it.Next()
	if err != nil {
		return nil, err
	}
	cfg := protoToTopicConfig(tpb)
	return &cfg, nil
}

// ID returns the unique identifier of the topic within its project.
func (t *Topic) ID() string {
	slash := strings.LastIndex(t.name, "/")
	if slash == -1 {
		// name is not a fully-qualified name.
		panic("bad topic name")
	}
	return t.name[slash+1:]
}

// String returns the printable globally unique name for the topic.
func (t *Topic) String() string {
	return t.name
}

// Delete deletes the topic.
func (t *Topic) Delete(ctx context.Context) error {
	return t.c.pubc.DeleteTopic(ctx, &pb.DeleteTopicRequest{Topic: t.name})
}

// Exists reports whether the topic exists on the server.
func (t *Topic) Exists(ctx context.Context) (bool, error) {
	if t.name == "_deleted-topic_" {
		return false, nil
	}
	_, err := t.c.pubc.GetTopic(ctx, &pb.GetTopicRequest{Topic: t.name})
	if err == nil {
		return true, nil
	}
	if status.Code(err) == codes.NotFound {
		return false, nil
	}
	return false, err
}

// IAM returns the topic's IAM handle.
func (t *Topic) IAM() *iam.Handle {
	return iam.InternalNewHandle(t.c.pubc.Connection(), t.name)
}

// Subscriptions returns an iterator which returns the subscriptions for this topic.
//
// Some of the returned subscriptions may belong to a project other than t.
func (t *Topic) Subscriptions(ctx context.Context) *SubscriptionIterator {
	it := t.c.pubc.ListTopicSubscriptions(ctx, &pb.ListTopicSubscriptionsRequest{
		Topic: t.name,
	})
	return &SubscriptionIterator{
		c:    t.c,
		next: it.Next,
	}
}

// ErrTopicStopped indicates that topic has been stopped and further publishing will fail.
var ErrTopicStopped = errors.New("pubsub: Stop has been called for this topic")

// A PublishResult holds the result from a call to Publish.
//
// Call Get to obtain the result of the Publish call. Example:
//
//	// Get blocks until Publish completes or ctx is done.
//	id, err := r.Get(ctx)
//	if err != nil {
//	    // TODO: Handle error.
//	}
type PublishResult = ipubsub.PublishResult

var errTopicOrderingNotEnabled = errors.New("Topic.EnableMessageOrdering=false, but an OrderingKey was set in Message. Please remove the OrderingKey or turn on Topic.EnableMessageOrdering")

// Publish publishes msg to the topic asynchronously. Messages are batched and
// sent according to the topic's PublishSettings. Publish never blocks.
//
// Publish returns a non-nil PublishResult which will be ready when the
// message has been sent (or has failed to be sent) to the server.
//
// Publish creates goroutines for batching and sending messages. These goroutines
// need to be stopped by calling t.Stop(). Once stopped, future calls to Publish
// will immediately return a PublishResult with an error.
func (t *Topic) Publish(ctx context.Context, msg *Message) *PublishResult {
	var createSpan trace.Span
	if t.enableTracing {
		opts := getPublishSpanAttributes(t.c.projectID, t.ID(), msg)
		opts = append(opts, trace.WithAttributes(semconv.CodeFunction("Publish")))
		ctx, createSpan = startSpan(ctx, createSpanName, t.ID(), opts...)
	}
	ctx, err := tag.New(ctx, tag.Insert(keyStatus, "OK"), tag.Upsert(keyTopic, t.name))
	if err != nil {
		log.Printf("pubsub: cannot create context with tag in Publish: %v", err)
	}

	r := ipubsub.NewPublishResult()
	if !t.EnableMessageOrdering && msg.OrderingKey != "" {
		ipubsub.SetPublishResult(r, "", errTopicOrderingNotEnabled)
		spanRecordError(createSpan, errTopicOrderingNotEnabled)
		return r
	}

	// Calculate the size of the encoded proto message by accounting
	// for the length of an individual PubSubMessage and Data/Attributes field.
	msgSize := proto.Size(&pb.PubsubMessage{
		Data:        msg.Data,
		Attributes:  msg.Attributes,
		OrderingKey: msg.OrderingKey,
	})
	if t.enableTracing {
		createSpan.SetAttributes(semconv.MessagingMessageBodySize(len(msg.Data)))
	}

	t.initBundler()
	t.mu.RLock()
	defer t.mu.RUnlock()
	if t.stopped {
		ipubsub.SetPublishResult(r, "", ErrTopicStopped)
		spanRecordError(createSpan, ErrTopicStopped)
		return r
	}

	var batcherSpan trace.Span
	var fcSpan trace.Span

	if t.enableTracing {
		_, fcSpan = startSpan(ctx, publishFCSpanName, "")
	}
	if err := t.flowController.acquire(ctx, msgSize); err != nil {
		t.scheduler.Pause(msg.OrderingKey)
		ipubsub.SetPublishResult(r, "", err)
		spanRecordError(fcSpan, err)
		return r
	}
	if t.enableTracing {
		fcSpan.End()
	}

	bmsg := &bundledMessage{
		msg:        msg,
		res:        r,
		size:       msgSize,
		createSpan: createSpan,
	}

	if t.enableTracing {
		_, batcherSpan = startSpan(ctx, batcherSpanName, "")
		bmsg.batcherSpan = batcherSpan

		// Inject the context from the first publish span rather than from flow control / batching.
		injectPropagation(ctx, msg)
	}

	if err := t.scheduler.Add(msg.OrderingKey, bmsg, msgSize); err != nil {
		t.scheduler.Pause(msg.OrderingKey)
		ipubsub.SetPublishResult(r, "", err)
		spanRecordError(createSpan, err)
	}

	return r
}

// Stop sends all remaining published messages and stop goroutines created for handling
// publishing. Returns once all outstanding messages have been sent or have
// failed to be sent.
func (t *Topic) Stop() {
	t.mu.Lock()
	noop := t.stopped || t.scheduler == nil
	t.stopped = true
	t.mu.Unlock()
	if noop {
		return
	}
	t.scheduler.FlushAndStop()
}

// Flush blocks until all remaining messages are sent.
func (t *Topic) Flush() {
	if t.stopped || t.scheduler == nil {
		return
	}
	t.scheduler.Flush()
}

type bundledMessage struct {
	msg  *Message
	res  *PublishResult
	size int
	// createSpan is the entire publish createSpan (from user calling Publish to the publish RPC resolving).
	createSpan trace.Span
	// batcherSpan traces the message batching operation in publish scheduler.
	batcherSpan trace.Span
}

func (t *Topic) initBundler() {
	t.mu.RLock()
	noop := t.stopped || t.scheduler != nil
	t.mu.RUnlock()
	if noop {
		return
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	// Must re-check, since we released the lock.
	if t.stopped || t.scheduler != nil {
		return
	}

	timeout := t.PublishSettings.Timeout

	workers := t.PublishSettings.NumGoroutines
	// Unless overridden, allow many goroutines per CPU to call the Publish RPC
	// concurrently. The default value was determined via extensive load
	// testing (see the loadtest subdirectory).
	if t.PublishSettings.NumGoroutines == 0 {
		workers = 25 * runtime.GOMAXPROCS(0)
	}

	t.scheduler = scheduler.NewPublishScheduler(workers, func(bundle interface{}) {
		// Use a context detached from the one passed to NewClient.
		ctx := context.Background()
		if timeout != 0 {
			var cancel func()
			ctx, cancel = context.WithTimeout(ctx, timeout)
			defer cancel()
		}
		bmsgs := bundle.([]*bundledMessage)
		if t.enableTracing {
			for _, m := range bmsgs {
				m.batcherSpan.End()
				m.createSpan.AddEvent(eventPublishStart, trace.WithAttributes(semconv.MessagingBatchMessageCount(len(bmsgs))))
			}
		}
		t.publishMessageBundle(ctx, bmsgs)
		if t.enableTracing {
			for _, m := range bmsgs {
				m.createSpan.AddEvent(eventPublishEnd)
				m.createSpan.End()
			}
		}
	})
	t.scheduler.DelayThreshold = t.PublishSettings.DelayThreshold
	t.scheduler.BundleCountThreshold = t.PublishSettings.CountThreshold
	if t.scheduler.BundleCountThreshold > MaxPublishRequestCount {
		t.scheduler.BundleCountThreshold = MaxPublishRequestCount
	}
	t.scheduler.BundleByteThreshold = t.PublishSettings.ByteThreshold

	fcs := DefaultPublishSettings.FlowControlSettings
	fcs.LimitExceededBehavior = t.PublishSettings.FlowControlSettings.LimitExceededBehavior
	if t.PublishSettings.FlowControlSettings.MaxOutstandingBytes > 0 {
		b := t.PublishSettings.FlowControlSettings.MaxOutstandingBytes
		fcs.MaxOutstandingBytes = b

		// If MaxOutstandingBytes is set, disable BufferedByteLimit by setting it to maxint.
		// This is because there's no way to set "unlimited" for BufferedByteLimit,
		// and simply setting it to MaxOutstandingBytes occasionally leads to issues where
		// BufferedByteLimit is reached even though there are resources available.
		t.PublishSettings.BufferedByteLimit = maxInt
	}
	if t.PublishSettings.FlowControlSettings.MaxOutstandingMessages > 0 {
		fcs.MaxOutstandingMessages = t.PublishSettings.FlowControlSettings.MaxOutstandingMessages
	}

	t.flowController = newTopicFlowController(fcs)

	bufferedByteLimit := DefaultPublishSettings.BufferedByteLimit
	if t.PublishSettings.BufferedByteLimit > 0 {
		bufferedByteLimit = t.PublishSettings.BufferedByteLimit
	}
	t.scheduler.BufferedByteLimit = bufferedByteLimit

	// Calculate the max limit of a single bundle. 5 comes from the number of bytes
	// needed to be reserved for encoding the PubsubMessage repeated field.
	t.scheduler.BundleByteLimit = MaxPublishRequestBytes - calcFieldSizeString(t.name) - 5
}

// ErrPublishingPaused is a custom error indicating that the publish paused for the specified ordering key.
type ErrPublishingPaused struct {
	OrderingKey string
}

func (e ErrPublishingPaused) Error() string {
	return fmt.Sprintf("pubsub: Publishing for ordering key, %s, paused due to previous error. Call topic.ResumePublish(orderingKey) before resuming publishing", e.OrderingKey)

}

func (t *Topic) publishMessageBundle(ctx context.Context, bms []*bundledMessage) {
	ctx, err := tag.New(ctx, tag.Insert(keyStatus, "OK"), tag.Upsert(keyTopic, t.name))
	if err != nil {
		log.Printf("pubsub: cannot create context with tag in publishMessageBundle: %v", err)
	}
	numMsgs := len(bms)
	pbMsgs := make([]*pb.PubsubMessage, numMsgs)
	var orderingKey string
	if numMsgs != 0 {
		// extract the ordering key for this batch. since
		// messages in the same batch share the same ordering
		// key, it doesn't matter which we read from.
		orderingKey = bms[0].msg.OrderingKey
	}

	if t.enableTracing {
		links := make([]trace.Link, 0, numMsgs)
		for _, bm := range bms {
			if bm.createSpan.SpanContext().IsSampled() {
				links = append(links, trace.Link{SpanContext: bm.createSpan.SpanContext()})
			}
		}

		projectID, topicID := parseResourceName(t.name)
		var pSpan trace.Span
		opts := getCommonOptions(projectID, topicID)
		// Add link to publish RPC span of createSpan(s).
		opts = append(opts, trace.WithLinks(links...))
		opts = append(
			opts,
			trace.WithAttributes(
				semconv.MessagingBatchMessageCount(numMsgs),
				semconv.CodeFunction("publishMessageBundle"),
			),
		)
		ctx, pSpan = startSpan(ctx, publishRPCSpanName, topicID, opts...)
		defer pSpan.End()

		// Add the reverse link to createSpan(s) of publish RPC span.
		if pSpan.SpanContext().IsSampled() {
			for _, bm := range bms {
				bm.createSpan.AddLink(trace.Link{
					SpanContext: pSpan.SpanContext(),
					Attributes: []attribute.KeyValue{
						semconv.MessagingOperationName(publishRPCSpanName),
					},
				})
			}
		}
	}
	var batchSize int
	for i, bm := range bms {
		pbMsgs[i] = &pb.PubsubMessage{
			Data:        bm.msg.Data,
			Attributes:  bm.msg.Attributes,
			OrderingKey: bm.msg.OrderingKey,
		}
		batchSize = batchSize + proto.Size(pbMsgs[i])
		bm.msg = nil // release bm.msg for GC
	}

	var res *pb.PublishResponse
	start := time.Now()
	if orderingKey != "" && t.scheduler.IsPaused(orderingKey) {
		err = ErrPublishingPaused{OrderingKey: orderingKey}
	} else {
		// Apply custom publish retryer on top of user specified retryer and
		// default retryer.
		opts := t.c.pubc.CallOptions.Publish
		var settings gax.CallSettings
		for _, opt := range opts {
			opt.Resolve(&settings)
		}
		r := &publishRetryer{defaultRetryer: settings.Retry()}
		gaxOpts := []gax.CallOption{
			gax.WithGRPCOptions(grpc.MaxCallSendMsgSize(maxSendRecvBytes)),
			gax.WithRetry(func() gax.Retryer { return r }),
		}
		if t.PublishSettings.shouldCompress(batchSize) {
			gaxOpts = append(gaxOpts, gax.WithGRPCOptions(grpc.UseCompressor(gzip.Name)))
		}
		res, err = t.c.pubc.Publish(ctx, &pb.PublishRequest{
			Topic:    t.name,
			Messages: pbMsgs,
		}, gaxOpts...)
	}
	end := time.Now()
	if err != nil {
		t.scheduler.Pause(orderingKey)
		// Update context with error tag for OpenCensus,
		// using same stats.Record() call as success case.
		ctx, _ = tag.New(ctx, tag.Upsert(keyStatus, "ERROR"),
			tag.Upsert(keyError, err.Error()))
	}
	stats.Record(ctx,
		PublishLatency.M(float64(end.Sub(start)/time.Millisecond)),
		PublishedMessages.M(int64(len(bms))))
	for i, bm := range bms {
		t.flowController.release(ctx, bm.size)
		if err != nil {
			ipubsub.SetPublishResult(bm.res, "", err)
			spanRecordError(bm.createSpan, err)
		} else {
			ipubsub.SetPublishResult(bm.res, res.MessageIds[i], nil)
			if t.enableTracing {
				bm.createSpan.SetAttributes(semconv.MessagingMessageIDKey.String(res.MessageIds[i]))
			}
		}
	}
}

// ResumePublish resumes accepting messages for the provided ordering key.
// Publishing using an ordering key might be paused if an error is
// encountered while publishing, to prevent messages from being published
// out of order.
func (t *Topic) ResumePublish(orderingKey string) {
	t.mu.RLock()
	noop := t.scheduler == nil
	t.mu.RUnlock()
	if noop {
		return
	}

	t.scheduler.Resume(orderingKey)
}
