// Copyright 2022-2024 The NATS Authors
// 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 jetstream

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"slices"
	"strings"
	"time"

	"github.com/nats-io/nats.go"
	"github.com/nats-io/nuid"
)

type (

	// JetStream is the top-level interface for interacting with JetStream.
	// The capabilities of JetStream include:
	//
	// - Publishing messages to a stream using [Publisher].
	// - Managing streams using [StreamManager].
	// - Managing consumers using [StreamConsumerManager]. Those are the same
	//   methods as on [Stream], but are available as a shortcut to a consumer
	//   bypassing stream lookup.
	// - Managing KeyValue stores using [KeyValueManager].
	// - Managing Object Stores using [ObjectStoreManager].
	//
	// JetStream can be created using [New], [NewWithAPIPrefix] or
	// [NewWithDomain] methods.
	JetStream interface {
		// AccountInfo fetches account information from the server, containing details
		// about the account associated with this JetStream connection. If account is
		// not enabled for JetStream, ErrJetStreamNotEnabledForAccount is returned. If
		// the server does not have JetStream enabled, ErrJetStreamNotEnabled is
		// returned.
		AccountInfo(ctx context.Context) (*AccountInfo, error)

		// Conn returns the underlying NATS connection.
		Conn() *nats.Conn

		// Options returns read-only JetStreamOptions used
		// when making requests to JetStream.
		Options() JetStreamOptions

		StreamConsumerManager
		StreamManager
		Publisher
		KeyValueManager
		ObjectStoreManager
	}

	// Publisher provides methods for publishing messages to a stream.
	// It is available as a part of [JetStream] interface.
	// The behavior of Publisher can be customized using [PublishOpt] options.
	Publisher interface {
		// Publish performs a synchronous publish to a stream and waits for ack
		// from server. It accepts subject name (which must be bound to a stream)
		// and message payload.
		Publish(ctx context.Context, subject string, payload []byte, opts ...PublishOpt) (*PubAck, error)

		// PublishMsg performs a synchronous publish to a stream and waits for
		// ack from server. It accepts subject name (which must be bound to a
		// stream) and nats.Message.
		PublishMsg(ctx context.Context, msg *nats.Msg, opts ...PublishOpt) (*PubAck, error)

		// PublishAsync performs a publish to a stream and returns
		// [PubAckFuture] interface, not blocking while waiting for an
		// acknowledgement. It accepts subject name (which must be bound to a
		// stream) and message payload.
		//
		// PublishAsync does not guarantee that the message has been
		// received by the server. It only guarantees that the message has been
		// sent to the server and thus messages can be stored in the stream
		// out of order in case of retries.
		PublishAsync(subject string, payload []byte, opts ...PublishOpt) (PubAckFuture, error)

		// PublishMsgAsync performs a publish to a stream and returns
		// [PubAckFuture] interface, not blocking while waiting for an
		// acknowledgement. It accepts subject name (which must
		// be bound to a stream) and nats.Message.
		//
		// PublishMsgAsync does not guarantee that the message has been
		// sent to the server and thus messages can be stored in the stream
		// received by the server. It only guarantees that the message has been
		// out of order in case of retries.
		PublishMsgAsync(msg *nats.Msg, opts ...PublishOpt) (PubAckFuture, error)

		// PublishAsyncPending returns the number of async publishes outstanding
		// for this context. An outstanding publish is one that has been
		// sent by the publisher but has not yet received an ack.
		PublishAsyncPending() int

		// PublishAsyncComplete returns a channel that will be closed when all
		// outstanding asynchronously published messages are acknowledged by the
		// server.
		PublishAsyncComplete() <-chan struct{}

		// CleanupPublisher will cleanup the publishing side of JetStreamContext.
		//
		// This will unsubscribe from the internal reply subject if needed.
		// All pending async publishes will fail with ErrJetStreamContextClosed.
		//
		// If an error handler was provided, it will be called for each pending async
		// publish and PublishAsyncComplete will be closed.
		//
		// After completing JetStreamContext is still usable - internal subscription
		// will be recreated on next publish, but the acks from previous publishes will
		// be lost.
		CleanupPublisher()
	}

	// StreamManager provides CRUD API for managing streams. It is available as
	// a part of [JetStream] interface. CreateStream, UpdateStream,
	// CreateOrUpdateStream and Stream methods return a [Stream] interface, allowing
	// to operate on a stream.
	StreamManager interface {
		// CreateStream creates a new stream with given config and returns an
		// interface to operate on it. If stream with given name already exists
		// and its configuration differs from the provided one,
		// ErrStreamNameAlreadyInUse is returned.
		CreateStream(ctx context.Context, cfg StreamConfig) (Stream, error)

		// UpdateStream updates an existing stream. If stream does not exist,
		// ErrStreamNotFound is returned.
		UpdateStream(ctx context.Context, cfg StreamConfig) (Stream, error)

		// CreateOrUpdateStream creates a stream with given config. If stream
		// already exists, it will be updated (if possible).
		CreateOrUpdateStream(ctx context.Context, cfg StreamConfig) (Stream, error)

		// Stream fetches [StreamInfo] and returns a [Stream] interface for a given stream name.
		// If stream does not exist, ErrStreamNotFound is returned.
		Stream(ctx context.Context, stream string) (Stream, error)

		// StreamNameBySubject returns a stream name stream listening on given
		// subject. If no stream is bound to given subject, ErrStreamNotFound
		// is returned.
		StreamNameBySubject(ctx context.Context, subject string) (string, error)

		// DeleteStream removes a stream with given name. If stream does not
		// exist, ErrStreamNotFound is returned.
		DeleteStream(ctx context.Context, stream string) error

		// ListStreams returns StreamInfoLister, enabling iterating over a
		// channel of stream infos.
		ListStreams(context.Context, ...StreamListOpt) StreamInfoLister

		// StreamNames returns a  StreamNameLister, enabling iterating over a
		// channel of stream names.
		StreamNames(context.Context, ...StreamListOpt) StreamNameLister
	}

	// StreamConsumerManager provides CRUD API for managing consumers. It is
	// available as a part of [JetStream] interface. This is an alternative to
	// [Stream] interface, allowing to bypass stream lookup. CreateConsumer,
	// UpdateConsumer, CreateOrUpdateConsumer and Consumer methods return a
	// [Consumer] interface, allowing to operate on a consumer (e.g. consume
	// messages).
	StreamConsumerManager interface {
		// CreateOrUpdateConsumer creates a consumer on a given stream with
		// given config. If consumer already exists, it will be updated (if
		// possible). Consumer interface is returned, allowing to operate on a
		// consumer (e.g. fetch messages).
		CreateOrUpdateConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (Consumer, error)

		// CreateConsumer creates a consumer on a given stream with given
		// config. If consumer already exists and the provided configuration
		// differs from its configuration, ErrConsumerExists is returned. If the
		// provided configuration is the same as the existing consumer, the
		// existing consumer is returned. Consumer interface is returned,
		// allowing to operate on a consumer (e.g. fetch messages).
		CreateConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (Consumer, error)

		// UpdateConsumer updates an existing consumer. If consumer does not
		// exist, ErrConsumerDoesNotExist is returned. Consumer interface is
		// returned, allowing to operate on a consumer (e.g. fetch messages).
		UpdateConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (Consumer, error)

		// OrderedConsumer returns an OrderedConsumer instance. OrderedConsumer
		// are managed by the library and provide a simple way to consume
		// messages from a stream. Ordered consumers are ephemeral in-memory
		// pull consumers and are resilient to deletes and restarts.
		OrderedConsumer(ctx context.Context, stream string, cfg OrderedConsumerConfig) (Consumer, error)

		// Consumer returns an interface to an existing consumer, allowing processing
		// of messages. If consumer does not exist, ErrConsumerNotFound is
		// returned.
		Consumer(ctx context.Context, stream string, consumer string) (Consumer, error)

		// DeleteConsumer removes a consumer with given name from a stream.
		// If consumer does not exist, ErrConsumerNotFound is returned.
		DeleteConsumer(ctx context.Context, stream string, consumer string) error

		// PauseConsumer pauses a consumer until the given time.
		PauseConsumer(ctx context.Context, stream string, consumer string, pauseUntil time.Time) (*ConsumerPauseResponse, error)

		// ResumeConsumer resumes a paused consumer.
		ResumeConsumer(ctx context.Context, stream string, consumer string) (*ConsumerPauseResponse, error)

		// CreateOrUpdatePushConsumer creates a push consumer on a given stream with
		// given config. If consumer already exists, it will be updated (if
		// possible). Consumer interface is returned, allowing to consume messages.
		CreateOrUpdatePushConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (PushConsumer, error)

		// CreatePushConsumer creates a push consumer on a given stream with given
		// config. If consumer already exists and the provided configuration
		// differs from its configuration, ErrConsumerExists is returned. If the
		// provided configuration is the same as the existing consumer, the
		// existing consumer is returned. Consumer interface is returned,
		// allowing to consume messages.
		CreatePushConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (PushConsumer, error)

		// UpdatePushConsumer updates an existing push consumer. If consumer does not
		// exist, ErrConsumerDoesNotExist is returned. Consumer interface is
		// returned, allowing to consume messages.
		UpdatePushConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (PushConsumer, error)

		// PushConsumer returns an interface to an existing push consumer, allowing processing
		// of messages. If consumer does not exist, ErrConsumerNotFound is
		// returned.
		//
		// It returns ErrNotPushConsumer if the consumer is not a push consumer (deliver subject is not set).
		PushConsumer(ctx context.Context, stream string, consumer string) (PushConsumer, error)
	}

	// StreamListOpt is a functional option for [StreamManager.ListStreams] and
	// [StreamManager.StreamNames] methods.
	StreamListOpt func(*streamsRequest) error

	// AccountInfo contains information about the JetStream usage from the
	// current account.
	AccountInfo struct {
		// Tier is the current account usage tier.
		Tier

		// Domain is the domain name associated with this account.
		Domain string `json:"domain"`

		// API is the API usage statistics for this account.
		API APIStats `json:"api"`

		// Tiers is the list of available tiers for this account.
		Tiers map[string]Tier `json:"tiers"`
	}

	// Tier represents a JetStream account usage tier.
	Tier struct {
		// Memory is the memory storage being used for Stream Message storage.
		Memory uint64 `json:"memory"`

		// Store is the disk storage being used for Stream Message storage.
		Store uint64 `json:"storage"`

		// ReservedMemory is the number of bytes reserved for memory usage by
		// this account on the server
		ReservedMemory uint64 `json:"reserved_memory"`

		// ReservedStore is the number of bytes reserved for disk usage by this
		// account on the server
		ReservedStore uint64 `json:"reserved_storage"`

		// Streams is the number of streams currently defined for this account.
		Streams int `json:"streams"`

		// Consumers is the number of consumers currently defined for this
		// account.
		Consumers int `json:"consumers"`

		// Limits are the JetStream limits for this account.
		Limits AccountLimits `json:"limits"`
	}

	// APIStats reports on API calls to JetStream for this account.
	APIStats struct {
		// Level is the API level for this account.
		Level int `json:"level"`

		// Total is the total number of API calls.
		Total uint64 `json:"total"`

		// Errors is the total number of API errors.
		Errors uint64 `json:"errors"`

		// Inflight is the number of API calls currently in flight.
		Inflight uint64 `json:"inflight,omitempty"`
	}

	// AccountLimits includes the JetStream limits of the current account.
	AccountLimits struct {
		// MaxMemory is the maximum amount of memory available for this account.
		MaxMemory int64 `json:"max_memory"`

		// MaxStore is the maximum amount of disk storage available for this
		// account.
		MaxStore int64 `json:"max_storage"`

		// MaxStreams is the maximum number of streams allowed for this account.
		MaxStreams int `json:"max_streams"`

		// MaxConsumers is the maximum number of consumers allowed for this
		// account.
		MaxConsumers int `json:"max_consumers"`
	}

	jetStream struct {
		conn *nats.Conn
		opts JetStreamOptions

		publisher *jetStreamClient
	}

	// JetStreamOpt is a functional option for [New], [NewWithAPIPrefix] and
	// [NewWithDomain] methods.
	JetStreamOpt func(*JetStreamOptions) error

	// JetStreamOptions are used to configure JetStream.
	JetStreamOptions struct {
		// APIPrefix is the prefix used for JetStream API requests.
		APIPrefix string

		// Domain is the domain name token used when sending JetStream requests.
		Domain string

		// DefaultTimeout is the default timeout used for JetStream API requests.
		// This applies when the context passed to JetStream methods does not have
		// a deadline set.
		DefaultTimeout time.Duration

		// ClientTrace enables request/response API calls tracing.
		ClientTrace *ClientTrace

		publisherOpts asyncPublisherOpts

		// this is the actual prefix used in the API requests
		// it is either APIPrefix or a domain specific prefix
		apiPrefix      string
		replyPrefix    string
		replyPrefixLen int
	}

	// ClientTrace can be used to trace API interactions for [JetStream].
	ClientTrace struct {
		// RequestSent is called when an API request is sent to the server.
		RequestSent func(subj string, payload []byte)

		// ResponseReceived is called when a response is received from the
		// server.
		ResponseReceived func(subj string, payload []byte, hdr nats.Header)
	}
	streamInfoResponse struct {
		apiResponse
		apiPaged
		*StreamInfo
	}

	accountInfoResponse struct {
		apiResponse
		AccountInfo
	}

	streamDeleteResponse struct {
		apiResponse
		Success bool `json:"success,omitempty"`
	}

	// StreamInfoLister is used to iterate over a channel of stream infos.
	// Err method can be used to check for errors encountered during iteration.
	// Info channel is always closed and therefore can be used in a range loop.
	StreamInfoLister interface {
		Info() <-chan *StreamInfo
		Err() error
	}

	// StreamNameLister is used to iterate over a channel of stream names.
	// Err method can be used to check for errors encountered during iteration.
	// Name channel is always closed and therefore can be used in a range loop.
	StreamNameLister interface {
		Name() <-chan string
		Err() error
	}

	apiPagedRequest struct {
		Offset int `json:"offset"`
	}

	streamLister struct {
		js       *jetStream
		offset   int
		pageInfo *apiPaged

		streams chan *StreamInfo
		names   chan string
		err     error
	}

	streamListResponse struct {
		apiResponse
		apiPaged
		Streams []*StreamInfo `json:"streams"`
	}

	streamNamesResponse struct {
		apiResponse
		apiPaged
		Streams []string `json:"streams"`
	}

	streamsRequest struct {
		apiPagedRequest
		Subject string `json:"subject,omitempty"`
	}
)

// defaultAPITimeout is used if context.Background() or context.TODO() is passed to API calls.
const defaultAPITimeout = 5 * time.Second

var subjectRegexp = regexp.MustCompile(`^[^ >]*[>]?$`)

// New returns a new JetStream instance.
// It uses default API prefix ($JS.API) for JetStream API requests.
// If a custom API prefix is required, use [NewWithAPIPrefix] or [NewWithDomain].
//
// Available options:
//   - [WithClientTrace] - enables request/response tracing.
//   - [WithPublishAsyncErrHandler] - sets error handler for async message publish.
//   - [WithPublishAsyncMaxPending] - sets the maximum outstanding async publishes
//     that can be inflight at one time.
func New(nc *nats.Conn, opts ...JetStreamOpt) (JetStream, error) {
	jsOpts := JetStreamOptions{
		apiPrefix: DefaultAPIPrefix,
		publisherOpts: asyncPublisherOpts{
			maxpa: defaultAsyncPubAckInflight,
		},
		DefaultTimeout: defaultAPITimeout,
	}
	setReplyPrefix(nc, &jsOpts)
	for _, opt := range opts {
		if err := opt(&jsOpts); err != nil {
			return nil, err
		}
	}
	js := &jetStream{
		conn:      nc,
		opts:      jsOpts,
		publisher: &jetStreamClient{asyncPublisherOpts: jsOpts.publisherOpts},
	}

	return js, nil
}

const (
	// defaultAsyncPubAckInflight is the number of async pub acks inflight.
	defaultAsyncPubAckInflight = 4000
)

func setReplyPrefix(nc *nats.Conn, jsOpts *JetStreamOptions) {
	jsOpts.replyPrefix = nats.InboxPrefix
	if nc.Opts.InboxPrefix != "" {
		jsOpts.replyPrefix = nc.Opts.InboxPrefix + "."
	}
	// Add 1 for the dot separator.
	jsOpts.replyPrefixLen = len(jsOpts.replyPrefix) + aReplyTokensize + 1

}

// NewWithAPIPrefix returns a new JetStream instance and sets the API prefix to be used in requests to JetStream API.
// The API prefix will be used in API requests to JetStream, e.g. <prefix>.STREAM.INFO.<stream>.
//
// Available options:
//   - [WithClientTrace] - enables request/response tracing.
//   - [WithPublishAsyncErrHandler] - sets error handler for async message publish.
//   - [WithPublishAsyncMaxPending] - sets the maximum outstanding async publishes
//     that can be inflight at one time.
func NewWithAPIPrefix(nc *nats.Conn, apiPrefix string, opts ...JetStreamOpt) (JetStream, error) {
	jsOpts := JetStreamOptions{
		publisherOpts: asyncPublisherOpts{
			maxpa: defaultAsyncPubAckInflight,
		},
		APIPrefix:      apiPrefix,
		DefaultTimeout: defaultAPITimeout,
	}
	setReplyPrefix(nc, &jsOpts)
	for _, opt := range opts {
		if err := opt(&jsOpts); err != nil {
			return nil, err
		}
	}
	if apiPrefix == "" {
		return nil, errors.New("API prefix cannot be empty")
	}
	if !strings.HasSuffix(apiPrefix, ".") {
		jsOpts.apiPrefix = fmt.Sprintf("%s.", apiPrefix)
	} else {
		jsOpts.apiPrefix = apiPrefix
	}
	js := &jetStream{
		conn:      nc,
		opts:      jsOpts,
		publisher: &jetStreamClient{asyncPublisherOpts: jsOpts.publisherOpts},
	}
	return js, nil
}

// NewWithDomain returns a new JetStream instance and sets the domain name token used when sending JetStream requests.
// The domain name token will be used in API requests to JetStream, e.g. $JS.<domain>.API.STREAM.INFO.<stream>.
//
// Available options:
//   - [WithClientTrace] - enables request/response tracing.
//   - [WithPublishAsyncErrHandler] - sets error handler for async message publish.
//   - [WithPublishAsyncMaxPending] - sets the maximum outstanding async publishes
//     that can be inflight at one time.
func NewWithDomain(nc *nats.Conn, domain string, opts ...JetStreamOpt) (JetStream, error) {
	jsOpts := JetStreamOptions{
		publisherOpts: asyncPublisherOpts{
			maxpa: defaultAsyncPubAckInflight,
		},
		Domain:         domain,
		DefaultTimeout: defaultAPITimeout,
	}
	setReplyPrefix(nc, &jsOpts)
	for _, opt := range opts {
		if err := opt(&jsOpts); err != nil {
			return nil, err
		}
	}
	if domain == "" {
		return nil, errors.New("domain cannot be empty")
	}
	jsOpts.apiPrefix = fmt.Sprintf(jsDomainT, domain)
	js := &jetStream{
		conn:      nc,
		opts:      jsOpts,
		publisher: &jetStreamClient{asyncPublisherOpts: jsOpts.publisherOpts},
	}
	return js, nil
}

// Conn returns the underlying NATS connection.
func (js *jetStream) Conn() *nats.Conn {
	return js.conn
}

func (js *jetStream) Options() JetStreamOptions {
	opts := js.opts
	// Return a copy of ClientTrace to prevent modification
	if opts.ClientTrace != nil {
		clientTraceCopy := *opts.ClientTrace
		opts.ClientTrace = &clientTraceCopy
	}
	return opts
}

// CreateStream creates a new stream with given config and returns an
// interface to operate on it. If stream with given name already exists,
// ErrStreamNameAlreadyInUse is returned.
func (js *jetStream) CreateStream(ctx context.Context, cfg StreamConfig) (Stream, error) {
	if err := validateStreamName(cfg.Name); err != nil {
		return nil, err
	}
	ctx, cancel := js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}

	ncfg, err := convertStreamConfigDomains(cfg)
	if err != nil {
		return nil, err
	}

	req, err := json.Marshal(ncfg)
	if err != nil {
		return nil, err
	}

	createSubject := fmt.Sprintf(apiStreamCreateT, cfg.Name)
	var resp streamInfoResponse

	if _, err = js.apiRequestJSON(ctx, createSubject, &resp, req); err != nil {
		return nil, err
	}
	if resp.Error != nil {
		if resp.Error.ErrorCode == JSErrCodeStreamNameInUse {
			return nil, ErrStreamNameAlreadyInUse
		}
		return nil, resp.Error
	}

	// check that input subject transform (if used) is reflected in the returned StreamInfo
	if cfg.SubjectTransform != nil && resp.StreamInfo.Config.SubjectTransform == nil {
		return nil, ErrStreamSubjectTransformNotSupported
	}

	if len(cfg.Sources) != 0 {
		if len(cfg.Sources) != len(resp.Config.Sources) {
			return nil, ErrStreamSourceNotSupported
		}

		// the sources list in the response is not ordered
		cfgNumTransforms := make([]int, len(cfg.Sources))
		respNumTransforms := make([]int, len(resp.Config.Sources))
		for i, cfgSource := range cfg.Sources {
			cfgNumTransforms[i] = len(cfgSource.SubjectTransforms)
			respNumTransforms[i] = len(resp.Config.Sources[i].SubjectTransforms)
		}
		slices.Sort(cfgNumTransforms)
		slices.Sort(respNumTransforms)
		if !slices.Equal(cfgNumTransforms, respNumTransforms) {
			return nil, ErrStreamSubjectTransformNotSupported
		}
	}

	return &stream{
		js:   js,
		name: cfg.Name,
		info: resp.StreamInfo,
	}, nil
}

// If we have a Domain, convert to the appropriate ext.APIPrefix.
// This will change the stream source, so should be a copy passed in.
func (ss *StreamSource) convertDomain() error {
	if ss.Domain == "" {
		return nil
	}
	if ss.External != nil {
		return errors.New("nats: domain and external are both set")
	}
	ss.External = &ExternalStream{APIPrefix: fmt.Sprintf(jsExtDomainT, ss.Domain)}
	return nil
}

// Helper for copying when we do not want to change user's version.
func (ss *StreamSource) copy() *StreamSource {
	nss := *ss
	// Check pointers
	if ss.OptStartTime != nil {
		t := *ss.OptStartTime
		nss.OptStartTime = &t
	}
	if ss.External != nil {
		ext := *ss.External
		nss.External = &ext
	}
	return &nss
}

// convertStreamConfigDomains converts domain configurations to external configurations
// in both mirror and sources of a StreamConfig. It creates a copy of the config to avoid
// modifying the caller's version.
func convertStreamConfigDomains(cfg StreamConfig) (StreamConfig, error) {
	ncfg := cfg
	// If we have a mirror and an external domain, convert to ext.APIPrefix.
	if ncfg.Mirror != nil && ncfg.Mirror.Domain != "" {
		// Copy so we do not change the caller's version.
		ncfg.Mirror = ncfg.Mirror.copy()
		if err := ncfg.Mirror.convertDomain(); err != nil {
			return StreamConfig{}, err
		}
	}

	// Check sources for the same.
	if len(ncfg.Sources) > 0 {
		ncfg.Sources = append([]*StreamSource(nil), ncfg.Sources...)
		for i, ss := range ncfg.Sources {
			if ss.Domain != "" {
				ncfg.Sources[i] = ss.copy()
				if err := ncfg.Sources[i].convertDomain(); err != nil {
					return StreamConfig{}, err
				}
			}
		}
	}

	return ncfg, nil
}

// UpdateStream updates an existing stream. If stream does not exist,
// ErrStreamNotFound is returned.
func (js *jetStream) UpdateStream(ctx context.Context, cfg StreamConfig) (Stream, error) {
	if err := validateStreamName(cfg.Name); err != nil {
		return nil, err
	}
	ctx, cancel := js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}

	ncfg, err := convertStreamConfigDomains(cfg)
	if err != nil {
		return nil, err
	}

	req, err := json.Marshal(ncfg)
	if err != nil {
		return nil, err
	}

	updateSubject := fmt.Sprintf(apiStreamUpdateT, cfg.Name)
	var resp streamInfoResponse

	if _, err = js.apiRequestJSON(ctx, updateSubject, &resp, req); err != nil {
		return nil, err
	}
	if resp.Error != nil {
		if resp.Error.ErrorCode == JSErrCodeStreamNotFound {
			return nil, ErrStreamNotFound
		}
		return nil, resp.Error
	}

	// check that input subject transform (if used) is reflected in the returned StreamInfo
	if cfg.SubjectTransform != nil && resp.StreamInfo.Config.SubjectTransform == nil {
		return nil, ErrStreamSubjectTransformNotSupported
	}

	if len(cfg.Sources) != 0 {
		if len(cfg.Sources) != len(resp.Config.Sources) {
			return nil, ErrStreamSourceNotSupported
		}

		// the sources list in the response is not ordered
		cfgNumTransforms := make([]int, len(cfg.Sources))
		respNumTransforms := make([]int, len(resp.Config.Sources))
		for i, cfgSource := range cfg.Sources {
			cfgNumTransforms[i] = len(cfgSource.SubjectTransforms)
			respNumTransforms[i] = len(resp.Config.Sources[i].SubjectTransforms)
		}
		slices.Sort(cfgNumTransforms)
		slices.Sort(respNumTransforms)
		if !slices.Equal(cfgNumTransforms, respNumTransforms) {
			return nil, ErrStreamSubjectTransformNotSupported
		}
	}

	return &stream{
		js:   js,
		name: cfg.Name,
		info: resp.StreamInfo,
	}, nil
}

// CreateOrUpdateStream creates a stream with given config. If stream
// already exists, it will be updated (if possible).
func (js *jetStream) CreateOrUpdateStream(ctx context.Context, cfg StreamConfig) (Stream, error) {
	s, err := js.UpdateStream(ctx, cfg)
	if err != nil {
		if !errors.Is(err, ErrStreamNotFound) {
			return nil, err
		}
		return js.CreateStream(ctx, cfg)
	}

	return s, nil
}

// Stream fetches [StreamInfo] and returns a [Stream] interface for a given stream name.
// If stream does not exist, ErrStreamNotFound is returned.
func (js *jetStream) Stream(ctx context.Context, name string) (Stream, error) {
	if err := validateStreamName(name); err != nil {
		return nil, err
	}
	ctx, cancel := js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}
	infoSubject := fmt.Sprintf(apiStreamInfoT, name)

	var resp streamInfoResponse

	if _, err := js.apiRequestJSON(ctx, infoSubject, &resp); err != nil {
		return nil, err
	}
	if resp.Error != nil {
		if resp.Error.ErrorCode == JSErrCodeStreamNotFound {
			return nil, ErrStreamNotFound
		}
		return nil, resp.Error
	}
	return &stream{
		js:   js,
		name: name,
		info: resp.StreamInfo,
	}, nil
}

// DeleteStream removes a stream with given name
func (js *jetStream) DeleteStream(ctx context.Context, name string) error {
	if err := validateStreamName(name); err != nil {
		return err
	}
	ctx, cancel := js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}
	deleteSubject := fmt.Sprintf(apiStreamDeleteT, name)
	var resp streamDeleteResponse

	if _, err := js.apiRequestJSON(ctx, deleteSubject, &resp); err != nil {
		return err
	}
	if resp.Error != nil {
		if resp.Error.ErrorCode == JSErrCodeStreamNotFound {
			return ErrStreamNotFound
		}
		return resp.Error
	}
	return nil
}

// CreateOrUpdateConsumer creates a consumer on a given stream with
// given config. If consumer already exists, it will be updated (if
// possible). Consumer interface is returned, allowing to operate on a
// consumer (e.g. fetch messages).
func (js *jetStream) CreateOrUpdateConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (Consumer, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return upsertPullConsumer(ctx, js, stream, cfg, consumerActionCreateOrUpdate)
}

// CreateConsumer creates a consumer on a given stream with given
// config. If consumer already exists and the provided configuration
// differs from its configuration, ErrConsumerExists is returned. If the
// provided configuration is the same as the existing consumer, the
// existing consumer is returned. Consumer interface is returned,
// allowing to operate on a consumer (e.g. fetch messages).
func (js *jetStream) CreateConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (Consumer, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return upsertPullConsumer(ctx, js, stream, cfg, consumerActionCreate)
}

// UpdateConsumer updates an existing consumer. If consumer does not
// exist, ErrConsumerDoesNotExist is returned. Consumer interface is
// returned, allowing to operate on a consumer (e.g. fetch messages).
func (js *jetStream) UpdateConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (Consumer, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return upsertPullConsumer(ctx, js, stream, cfg, consumerActionUpdate)
}

// OrderedConsumer returns an OrderedConsumer instance. OrderedConsumer
// are managed by the library and provide a simple way to consume
// messages from a stream. Ordered consumers are ephemeral in-memory
// pull consumers and are resilient to deletes and restarts.
func (js *jetStream) OrderedConsumer(ctx context.Context, stream string, cfg OrderedConsumerConfig) (Consumer, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	namePrefix := cfg.NamePrefix
	if namePrefix == "" {
		namePrefix = nuid.Next()
	}
	oc := &orderedConsumer{
		js:         js,
		cfg:        &cfg,
		stream:     stream,
		namePrefix: namePrefix,
		doReset:    make(chan struct{}, 1),
	}
	consCfg := oc.getConsumerConfig()
	cons, err := js.CreateOrUpdateConsumer(ctx, stream, *consCfg)
	if err != nil {
		return nil, err
	}
	oc.currentConsumer = cons.(*pullConsumer)

	return oc, nil
}

// Consumer returns an interface to an existing consumer, allowing processing
// of messages. If consumer does not exist, ErrConsumerNotFound is
// returned.
func (js *jetStream) Consumer(ctx context.Context, stream string, name string) (Consumer, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return getConsumer(ctx, js, stream, name)
}

// DeleteConsumer removes a consumer with given name from a stream.
// If consumer does not exist, ErrConsumerNotFound is returned.
func (js *jetStream) DeleteConsumer(ctx context.Context, stream string, name string) error {
	if err := validateStreamName(stream); err != nil {
		return err
	}
	return deleteConsumer(ctx, js, stream, name)
}

// CreateOrUpdatePushConsumer creates a push consumer on a given stream with
// given config. If consumer already exists, it will be updated (if
// possible). Consumer interface is returned, allowing to consume messages.
func (js *jetStream) CreateOrUpdatePushConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (PushConsumer, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return upsertPushConsumer(ctx, js, stream, cfg, consumerActionCreateOrUpdate)
}

// CreatePushConsumer creates a push consumer on a given stream with given
// config. If consumer already exists and the provided configuration
// differs from its configuration, ErrConsumerExists is returned. If the
// provided configuration is the same as the existing consumer, the
// existing consumer is returned. Consumer interface is returned,
// allowing to consume messages.
func (js *jetStream) CreatePushConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (PushConsumer, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return upsertPushConsumer(ctx, js, stream, cfg, consumerActionCreate)
}

// UpdatePushConsumer updates an existing push consumer. If consumer does not
// exist, ErrConsumerDoesNotExist is returned. Consumer interface is
// returned, allowing to consume messages.
func (js *jetStream) UpdatePushConsumer(ctx context.Context, stream string, cfg ConsumerConfig) (PushConsumer, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return upsertPushConsumer(ctx, js, stream, cfg, consumerActionUpdate)
}

// PushConsumer returns an interface to an existing consumer, allowing processing
// of messages. If consumer does not exist, ErrConsumerNotFound is
// returned.
func (js *jetStream) PushConsumer(ctx context.Context, stream string, name string) (PushConsumer, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return getPushConsumer(ctx, js, stream, name)
}

func (js *jetStream) PauseConsumer(ctx context.Context, stream string, consumer string, pauseUntil time.Time) (*ConsumerPauseResponse, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return pauseConsumer(ctx, js, stream, consumer, &pauseUntil)
}

func (js *jetStream) ResumeConsumer(ctx context.Context, stream string, consumer string) (*ConsumerPauseResponse, error) {
	if err := validateStreamName(stream); err != nil {
		return nil, err
	}
	return resumeConsumer(ctx, js, stream, consumer)
}

func validateStreamName(stream string) error {
	if stream == "" {
		return ErrStreamNameRequired
	}
	if strings.ContainsAny(stream, ">*. /\\") {
		return fmt.Errorf("%w: '%s'", ErrInvalidStreamName, stream)
	}
	return nil
}

func validateSubject(subject string) error {
	if subject == "" {
		return fmt.Errorf("%w: %s", ErrInvalidSubject, "subject cannot be empty")
	}
	if subject[0] == '.' || subject[len(subject)-1] == '.' || !subjectRegexp.MatchString(subject) {
		return fmt.Errorf("%w: %s", ErrInvalidSubject, subject)
	}
	return nil
}

// AccountInfo fetches account information from the server, containing details
// about the account associated with this JetStream connection. If account is
// not enabled for JetStream, ErrJetStreamNotEnabledForAccount is returned.
//
// If the server does not have JetStream enabled, ErrJetStreamNotEnabled is
// returned (for a single server setup). For clustered topologies, AccountInfo
// will time out.
func (js *jetStream) AccountInfo(ctx context.Context) (*AccountInfo, error) {
	ctx, cancel := js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}
	var resp accountInfoResponse

	if _, err := js.apiRequestJSON(ctx, apiAccountInfo, &resp); err != nil {
		if errors.Is(err, nats.ErrNoResponders) {
			return nil, ErrJetStreamNotEnabled
		}
		return nil, err
	}
	if resp.Error != nil {
		if resp.Error.ErrorCode == JSErrCodeJetStreamNotEnabledForAccount {
			return nil, ErrJetStreamNotEnabledForAccount
		}
		if resp.Error.ErrorCode == JSErrCodeJetStreamNotEnabled {
			return nil, ErrJetStreamNotEnabled
		}
		return nil, resp.Error
	}

	return &resp.AccountInfo, nil
}

// ListStreams returns StreamInfoLister, enabling iterating over a
// channel of stream infos.
func (js *jetStream) ListStreams(ctx context.Context, opts ...StreamListOpt) StreamInfoLister {
	l := &streamLister{
		js:      js,
		streams: make(chan *StreamInfo),
	}
	var streamsReq streamsRequest
	for _, opt := range opts {
		if err := opt(&streamsReq); err != nil {
			l.err = err
			close(l.streams)
			return l
		}
	}
	go func() {
		defer close(l.streams)
		ctx, cancel := js.wrapContextWithoutDeadline(ctx)
		if cancel != nil {
			defer cancel()
		}
		for {
			page, err := l.streamInfos(ctx, streamsReq)
			if err != nil && !errors.Is(err, ErrEndOfData) {
				l.err = err
				return
			}
			for _, info := range page {
				select {
				case l.streams <- info:
				case <-ctx.Done():
					l.err = ctx.Err()
					return
				}
			}
			if errors.Is(err, ErrEndOfData) {
				return
			}
		}
	}()

	return l
}

// Info returns a channel allowing retrieval of stream infos returned by [ListStreams]
func (s *streamLister) Info() <-chan *StreamInfo {
	return s.streams
}

// Err returns an error channel which will be populated with error from [ListStreams] or [StreamNames] request
func (s *streamLister) Err() error {
	return s.err
}

// StreamNames returns a  StreamNameLister, enabling iterating over a
// channel of stream names.
func (js *jetStream) StreamNames(ctx context.Context, opts ...StreamListOpt) StreamNameLister {
	l := &streamLister{
		js:    js,
		names: make(chan string),
	}
	var streamsReq streamsRequest
	for _, opt := range opts {
		if err := opt(&streamsReq); err != nil {
			l.err = err
			close(l.names)
			return l
		}
	}
	go func() {
		ctx, cancel := js.wrapContextWithoutDeadline(ctx)
		if cancel != nil {
			defer cancel()
		}
		defer close(l.names)
		for {
			page, err := l.streamNames(ctx, streamsReq)
			if err != nil && !errors.Is(err, ErrEndOfData) {
				l.err = err
				return
			}
			for _, info := range page {
				select {
				case l.names <- info:
				case <-ctx.Done():
					l.err = ctx.Err()
					return
				}
			}
			if errors.Is(err, ErrEndOfData) {
				return
			}
		}
	}()

	return l
}

// StreamNameBySubject returns a stream name stream listening on given
// subject. If no stream is bound to given subject, ErrStreamNotFound
// is returned.
func (js *jetStream) StreamNameBySubject(ctx context.Context, subject string) (string, error) {
	ctx, cancel := js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}
	if err := validateSubject(subject); err != nil {
		return "", err
	}

	r := &streamsRequest{Subject: subject}
	req, err := json.Marshal(r)
	if err != nil {
		return "", err
	}
	var resp streamNamesResponse
	_, err = js.apiRequestJSON(ctx, apiStreams, &resp, req)
	if err != nil {
		return "", err
	}
	if resp.Error != nil {
		return "", resp.Error
	}
	if len(resp.Streams) == 0 {
		return "", ErrStreamNotFound
	}

	return resp.Streams[0], nil
}

// Name returns a channel allowing retrieval of stream names returned by [StreamNames]
func (s *streamLister) Name() <-chan string {
	return s.names
}

// infos fetches the next [StreamInfo] page
func (s *streamLister) streamInfos(ctx context.Context, streamsReq streamsRequest) ([]*StreamInfo, error) {
	if s.pageInfo != nil && s.offset >= s.pageInfo.Total {
		return nil, ErrEndOfData
	}

	req := streamsRequest{
		apiPagedRequest: apiPagedRequest{
			Offset: s.offset,
		},
		Subject: streamsReq.Subject,
	}
	reqJSON, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}

	var resp streamListResponse
	_, err = s.js.apiRequestJSON(ctx, apiStreamListT, &resp, reqJSON)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		return nil, resp.Error
	}

	s.pageInfo = &resp.apiPaged
	s.offset += len(resp.Streams)
	return resp.Streams, nil
}

// streamNames fetches the next stream names page
func (s *streamLister) streamNames(ctx context.Context, streamsReq streamsRequest) ([]string, error) {
	if s.pageInfo != nil && s.offset >= s.pageInfo.Total {
		return nil, ErrEndOfData
	}

	req := streamsRequest{
		apiPagedRequest: apiPagedRequest{
			Offset: s.offset,
		},
		Subject: streamsReq.Subject,
	}
	reqJSON, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}

	var resp streamNamesResponse
	_, err = s.js.apiRequestJSON(ctx, apiStreams, &resp, reqJSON)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		return nil, resp.Error
	}

	s.pageInfo = &resp.apiPaged
	s.offset += len(resp.Streams)
	return resp.Streams, nil
}

// wrapContextWithoutDeadline wraps context without deadline with default timeout.
// If deadline is already set, it will be returned as is, and cancel() will be nil.
// Caller should check if cancel() is nil before calling it.
func (js *jetStream) wrapContextWithoutDeadline(ctx context.Context) (context.Context, context.CancelFunc) {
	if _, ok := ctx.Deadline(); ok {
		return ctx, nil
	}
	return context.WithTimeout(ctx, js.opts.DefaultTimeout)
}

// CleanupPublisher will cleanup the publishing side of JetStreamContext.
//
// This will unsubscribe from the internal reply subject if needed.
// All pending async publishes will fail with ErrJetStreamContextClosed.
//
// If an error handler was provided, it will be called for each pending async
// publish and PublishAsyncComplete will be closed.
//
// After completing JetStreamContext is still usable - internal subscription
// will be recreated on next publish, but the acks from previous publishes will
// be lost.
func (js *jetStream) CleanupPublisher() {
	js.cleanupReplySub()
	js.publisher.Lock()
	errCb := js.publisher.aecb
	for id, paf := range js.publisher.acks {
		paf.err = ErrJetStreamPublisherClosed
		if paf.errCh != nil {
			paf.errCh <- paf.err
		}
		if errCb != nil {
			// call error handler after releasing the mutex to avoid contention
			defer errCb(js, paf.msg, ErrJetStreamPublisherClosed)
		}
		delete(js.publisher.acks, id)
	}
	if js.publisher.doneCh != nil {
		close(js.publisher.doneCh)
		js.publisher.doneCh = nil
	}
	js.publisher.Unlock()
}

func (js *jetStream) cleanupReplySub() {
	if js.publisher == nil {
		return
	}
	js.publisher.Lock()
	if js.publisher.replySub != nil {
		js.publisher.replySub.Unsubscribe()
		js.publisher.replySub = nil
	}
	if js.publisher.connStatusCh != nil {
		close(js.publisher.connStatusCh)
		js.publisher.connStatusCh = nil
	}
	js.publisher.Unlock()
}
