// Copyright 2022-2025 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"
	"strconv"
	"time"

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

type (
	// Stream contains CRUD methods on a consumer via [ConsumerManager], as well
	// as operations on an existing stream. It allows fetching and removing
	// messages from a stream, as well as purging a stream.
	Stream interface {
		ConsumerManager

		// Info returns StreamInfo from the server.
		Info(ctx context.Context, opts ...StreamInfoOpt) (*StreamInfo, error)

		// CachedInfo returns ConsumerInfo currently cached on this stream.
		// This method does not perform any network requests. The cached
		// StreamInfo is updated on every call to Info and Update.
		CachedInfo() *StreamInfo

		// Purge removes messages from a stream. It is a destructive operation.
		// Use with caution. See StreamPurgeOpt for available options.
		Purge(ctx context.Context, opts ...StreamPurgeOpt) error

		// GetMsg retrieves a raw stream message stored in JetStream by sequence number.
		GetMsg(ctx context.Context, seq uint64, opts ...GetMsgOpt) (*RawStreamMsg, error)

		// GetLastMsgForSubject retrieves the last raw stream message stored in
		// JetStream on a given subject.
		GetLastMsgForSubject(ctx context.Context, subject string) (*RawStreamMsg, error)

		// DeleteMsg deletes a message from a stream.
		// On the server, the message is marked as erased, but not overwritten.
		DeleteMsg(ctx context.Context, seq uint64) error

		// SecureDeleteMsg deletes a message from a stream. The deleted message
		// is overwritten with random data. As a result, this operation is slower
		// than DeleteMsg.
		SecureDeleteMsg(ctx context.Context, seq uint64) error
	}

	// ConsumerManager provides CRUD API for managing consumers. It is
	// available as a part of [Stream] interface. CreateConsumer,
	// UpdateConsumer, CreateOrUpdateConsumer and Consumer methods return a
	// [Consumer] interface, allowing to operate on a consumer (e.g. consume
	// messages).
	ConsumerManager interface {
		// CreateOrUpdateConsumer creates a pull 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, cfg ConsumerConfig) (Consumer, error)

		// CreateConsumer creates a pull 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, cfg ConsumerConfig) (Consumer, error)

		// UpdateConsumer updates an existing pull 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, 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, cfg OrderedConsumerConfig) (Consumer, error)

		// Consumer returns an interface to an existing consumer, allowing processing
		// of messages. If consumer does not exist, ErrConsumerNotFound is
		// returned.
		//
		// It returns ErrNotPullConsumer if the consumer is not a pull consumer (deliver subject is not set).
		Consumer(ctx context.Context, 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, consumer string) error

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

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

		// ListConsumers returns ConsumerInfoLister enabling iterating over a
		// channel of consumer infos.
		ListConsumers(context.Context) ConsumerInfoLister

		// ConsumerNames returns a ConsumerNameLister enabling iterating over a
		// channel of consumer names.
		ConsumerNames(context.Context) ConsumerNameLister

		// UnpinConsumer unpins the currently pinned client for a consumer for the given group name.
		// If consumer does not exist, ErrConsumerNotFound is returned.
		UnpinConsumer(ctx context.Context, consumer string, group string) 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, 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, 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, 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, consumer string) (PushConsumer, error)
	}

	RawStreamMsg struct {
		Subject  string
		Sequence uint64
		Header   nats.Header
		Data     []byte
		Time     time.Time
	}

	stream struct {
		name string
		info *StreamInfo
		js   *jetStream
	}

	// StreamInfoOpt is a function setting options for [Stream.Info]
	StreamInfoOpt func(*streamInfoRequest) error

	streamInfoRequest struct {
		apiPagedRequest
		DeletedDetails bool   `json:"deleted_details,omitempty"`
		SubjectFilter  string `json:"subjects_filter,omitempty"`
	}

	consumerInfoResponse struct {
		apiResponse
		*ConsumerInfo
	}

	// StreamPurgeOpt is a function setting options for [Stream.Purge]
	StreamPurgeOpt func(*StreamPurgeRequest) error

	// StreamPurgeRequest is an API request body to purge a stream.

	StreamPurgeRequest struct {
		// Purge up to but not including sequence.
		Sequence uint64 `json:"seq,omitempty"`
		// Subject to match against messages for the purge command.
		Subject string `json:"filter,omitempty"`
		// Number of messages to keep.
		Keep uint64 `json:"keep,omitempty"`
	}

	streamPurgeResponse struct {
		apiResponse
		Success bool   `json:"success,omitempty"`
		Purged  uint64 `json:"purged"`
	}

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

	consumerPauseRequest struct {
		PauseUntil *time.Time `json:"pause_until,omitempty"`
	}

	ConsumerPauseResponse struct {
		// Paused is true if the consumer is paused.
		Paused bool `json:"paused"`
		// PauseUntil is the time until the consumer is paused.
		PauseUntil time.Time `json:"pause_until"`
		// PauseRemaining is the time remaining until the consumer is paused.
		PauseRemaining time.Duration `json:"pause_remaining,omitempty"`
	}

	consumerPauseApiResponse struct {
		apiResponse
		ConsumerPauseResponse
	}

	// GetMsgOpt is a function setting options for [Stream.GetMsg]
	GetMsgOpt func(*apiMsgGetRequest) error

	apiMsgGetRequest struct {
		Seq     uint64 `json:"seq,omitempty"`
		LastFor string `json:"last_by_subj,omitempty"`
		NextFor string `json:"next_by_subj,omitempty"`
	}

	// apiMsgGetResponse is the response for a Stream get request.
	apiMsgGetResponse struct {
		apiResponse
		Message *storedMsg `json:"message,omitempty"`
	}

	// storedMsg is a raw message stored in JetStream.
	storedMsg struct {
		Subject  string    `json:"subject"`
		Sequence uint64    `json:"seq"`
		Header   []byte    `json:"hdrs,omitempty"`
		Data     []byte    `json:"data,omitempty"`
		Time     time.Time `json:"time"`
	}

	msgDeleteRequest struct {
		Seq     uint64 `json:"seq"`
		NoErase bool   `json:"no_erase,omitempty"`
	}

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

	// ConsumerInfoLister is used to iterate over a channel of consumer 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.
	ConsumerInfoLister interface {
		Info() <-chan *ConsumerInfo
		Err() error
	}

	// ConsumerNameLister is used to iterate over a channel of consumer 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.
	ConsumerNameLister interface {
		Name() <-chan string
		Err() error
	}

	consumerLister struct {
		js       *jetStream
		offset   int
		pageInfo *apiPaged

		consumers chan *ConsumerInfo
		names     chan string
		err       error
	}

	consumerListResponse struct {
		apiResponse
		apiPaged
		Consumers []*ConsumerInfo `json:"consumers"`
	}

	consumerNamesResponse struct {
		apiResponse
		apiPaged
		Consumers []string `json:"consumers"`
	}

	consumerUnpinRequest struct {
		Group string `json:"group"`
	}
)

// 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 (s *stream) CreateOrUpdateConsumer(ctx context.Context, cfg ConsumerConfig) (Consumer, error) {
	return upsertPullConsumer(ctx, s.js, s.name, 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 (s *stream) CreateConsumer(ctx context.Context, cfg ConsumerConfig) (Consumer, error) {
	return upsertPullConsumer(ctx, s.js, s.name, 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 (s *stream) UpdateConsumer(ctx context.Context, cfg ConsumerConfig) (Consumer, error) {
	return upsertPullConsumer(ctx, s.js, s.name, cfg, consumerActionUpdate)
}

// CreateOrUpdatePushConsumer 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 consume messages.
func (s *stream) CreateOrUpdatePushConsumer(ctx context.Context, cfg ConsumerConfig) (PushConsumer, error) {
	return upsertPushConsumer(ctx, s.js, s.name, cfg, consumerActionCreateOrUpdate)
}

// CreatePushConsumer 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 consume messages.
func (s *stream) CreatePushConsumer(ctx context.Context, cfg ConsumerConfig) (PushConsumer, error) {
	return upsertPushConsumer(ctx, s.js, s.name, cfg, consumerActionCreate)
}

// UpdatePushConsumer updates an existing consumer. If consumer does not
// exist, ErrConsumerDoesNotExist is returned. Consumer interface is
// returned, allowing to consume messages.
func (s *stream) UpdatePushConsumer(ctx context.Context, cfg ConsumerConfig) (PushConsumer, error) {
	return upsertPushConsumer(ctx, s.js, s.name, 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 (s *stream) OrderedConsumer(ctx context.Context, cfg OrderedConsumerConfig) (Consumer, error) {
	namePrefix := cfg.NamePrefix
	if namePrefix == "" {
		namePrefix = nuid.Next()
	}
	oc := &orderedConsumer{
		js:         s.js,
		cfg:        &cfg,
		stream:     s.name,
		namePrefix: namePrefix,
		doReset:    make(chan struct{}, 1),
	}
	consCfg := oc.getConsumerConfig()
	cons, err := s.CreateOrUpdateConsumer(ctx, *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 (s *stream) Consumer(ctx context.Context, name string) (Consumer, error) {
	return getConsumer(ctx, s.js, s.name, name)
}

func (s *stream) PushConsumer(ctx context.Context, name string) (PushConsumer, error) {
	return getPushConsumer(ctx, s.js, s.name, name)
}

// DeleteConsumer removes a consumer with given name from a stream.
// If consumer does not exist, ErrConsumerNotFound is returned.
func (s *stream) DeleteConsumer(ctx context.Context, name string) error {
	return deleteConsumer(ctx, s.js, s.name, name)
}

// PauseConsumer pauses a consumer.
func (s *stream) PauseConsumer(ctx context.Context, name string, pauseUntil time.Time) (*ConsumerPauseResponse, error) {
	return pauseConsumer(ctx, s.js, s.name, name, &pauseUntil)
}

// ResumeConsumer resumes a consumer.
func (s *stream) ResumeConsumer(ctx context.Context, name string) (*ConsumerPauseResponse, error) {
	return resumeConsumer(ctx, s.js, s.name, name)
}

// Info returns StreamInfo from the server.
func (s *stream) Info(ctx context.Context, opts ...StreamInfoOpt) (*StreamInfo, error) {
	ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}
	var infoReq *streamInfoRequest
	for _, opt := range opts {
		if infoReq == nil {
			infoReq = &streamInfoRequest{}
		}
		if err := opt(infoReq); err != nil {
			return nil, err
		}
	}
	var req []byte
	var err error
	var subjectMap map[string]uint64
	var offset int

	infoSubject := fmt.Sprintf(apiStreamInfoT, s.name)
	var info *StreamInfo
	for {
		if infoReq != nil {
			if infoReq.SubjectFilter != "" {
				if subjectMap == nil {
					subjectMap = make(map[string]uint64)
				}
				infoReq.Offset = offset
			}
			req, err = json.Marshal(infoReq)
			if err != nil {
				return nil, err
			}
		}
		var resp streamInfoResponse
		if _, err = s.js.apiRequestJSON(ctx, infoSubject, &resp, req); err != nil {
			return nil, err
		}
		if resp.Error != nil {
			if resp.Error.ErrorCode == JSErrCodeStreamNotFound {
				return nil, ErrStreamNotFound
			}
			return nil, resp.Error
		}
		info = resp.StreamInfo
		var total int
		if resp.Total != 0 {
			total = resp.Total
		}
		if len(resp.StreamInfo.State.Subjects) > 0 {
			for subj, msgs := range resp.StreamInfo.State.Subjects {
				subjectMap[subj] = msgs
			}
			offset = len(subjectMap)
		}
		if total == 0 || total <= offset {
			info.State.Subjects = nil
			// we don't want to store subjects in cache
			cached := *info
			s.info = &cached
			info.State.Subjects = subjectMap
			break
		}
	}

	return info, nil
}

// CachedInfo returns ConsumerInfo currently cached on this stream.
// This method does not perform any network requests. The cached
// StreamInfo is updated on every call to Info and Update.
func (s *stream) CachedInfo() *StreamInfo {
	return s.info
}

// Purge removes messages from a stream. It is a destructive operation.
// Use with caution. See StreamPurgeOpt for available options.
func (s *stream) Purge(ctx context.Context, opts ...StreamPurgeOpt) error {
	ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}
	var purgeReq StreamPurgeRequest
	for _, opt := range opts {
		if err := opt(&purgeReq); err != nil {
			return err
		}
	}
	var req []byte
	var err error
	req, err = json.Marshal(purgeReq)
	if err != nil {
		return err
	}

	purgeSubject := fmt.Sprintf(apiStreamPurgeT, s.name)

	var resp streamPurgeResponse
	if _, err = s.js.apiRequestJSON(ctx, purgeSubject, &resp, req); err != nil {
		return err
	}
	if resp.Error != nil {
		return resp.Error
	}

	return nil
}

// GetMsg retrieves a raw stream message stored in JetStream by sequence number.
func (s *stream) GetMsg(ctx context.Context, seq uint64, opts ...GetMsgOpt) (*RawStreamMsg, error) {
	req := &apiMsgGetRequest{Seq: seq}
	for _, opt := range opts {
		if err := opt(req); err != nil {
			return nil, err
		}
	}
	return s.getMsg(ctx, req)
}

// GetLastMsgForSubject retrieves the last raw stream message stored in
// JetStream on a given subject subject.
func (s *stream) GetLastMsgForSubject(ctx context.Context, subject string) (*RawStreamMsg, error) {
	return s.getMsg(ctx, &apiMsgGetRequest{LastFor: subject})
}

func (s *stream) getMsg(ctx context.Context, mreq *apiMsgGetRequest) (*RawStreamMsg, error) {
	ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}
	req, err := json.Marshal(mreq)
	if err != nil {
		return nil, err
	}

	var gmSubj string

	// handle direct gets
	if s.info.Config.AllowDirect {
		if mreq.LastFor != "" {
			gmSubj = fmt.Sprintf(apiDirectMsgGetLastBySubjectT, s.name, mreq.LastFor)
			r, err := s.js.apiRequest(ctx, gmSubj, nil)
			if err != nil {
				return nil, err
			}
			return convertDirectGetMsgResponseToMsg(r.msg)
		}
		gmSubj = fmt.Sprintf(apiDirectMsgGetT, s.name)
		r, err := s.js.apiRequest(ctx, gmSubj, req)
		if err != nil {
			return nil, err
		}
		return convertDirectGetMsgResponseToMsg(r.msg)
	}

	var resp apiMsgGetResponse
	dsSubj := fmt.Sprintf(apiMsgGetT, s.name)
	_, err = s.js.apiRequestJSON(ctx, dsSubj, &resp, req)
	if err != nil {
		return nil, err
	}

	if resp.Error != nil {
		if resp.Error.ErrorCode == JSErrCodeMessageNotFound {
			return nil, ErrMsgNotFound
		}
		return nil, resp.Error
	}

	msg := resp.Message

	var hdr nats.Header
	if len(msg.Header) > 0 {
		hdr, err = nats.DecodeHeadersMsg(msg.Header)
		if err != nil {
			return nil, err
		}
	}

	return &RawStreamMsg{
		Subject:  msg.Subject,
		Sequence: msg.Sequence,
		Header:   hdr,
		Data:     msg.Data,
		Time:     msg.Time,
	}, nil
}

func convertDirectGetMsgResponseToMsg(r *nats.Msg) (*RawStreamMsg, error) {
	// Check for 404/408. We would get a no-payload message and a "Status" header
	if len(r.Data) == 0 {
		val := r.Header.Get(statusHdr)
		if val != "" {
			switch val {
			case statusNoMsgs:
				return nil, ErrMsgNotFound
			default:
				desc := r.Header.Get("Description")
				if desc == "" {
					desc = "unable to get message"
				}
				return nil, fmt.Errorf("nats: %s", desc)
			}
		}
	}

	// Check for headers that give us the required information to
	// reconstruct the message.
	if len(r.Header) == 0 {
		return nil, errors.New("nats: response should have headers")
	}
	stream := r.Header.Get(StreamHeader)
	if stream == "" {
		return nil, errors.New("nats: missing stream header")
	}

	seqStr := r.Header.Get(SequenceHeader)
	if seqStr == "" {
		return nil, errors.New("nats: missing sequence header")
	}
	seq, err := strconv.ParseUint(seqStr, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("nats: invalid sequence header '%s': %v", seqStr, err)
	}
	timeStr := r.Header.Get(TimeStampHeaer)
	if timeStr == "" {
		return nil, errors.New("nats: missing timestamp header")
	}

	tm, err := time.Parse(time.RFC3339Nano, timeStr)
	if err != nil {
		return nil, fmt.Errorf("nats: invalid timestamp header '%s': %v", timeStr, err)
	}
	subj := r.Header.Get(SubjectHeader)
	if subj == "" {
		return nil, errors.New("nats: missing subject header")
	}
	return &RawStreamMsg{
		Subject:  subj,
		Sequence: seq,
		Header:   r.Header,
		Data:     r.Data,
		Time:     tm,
	}, nil
}

// DeleteMsg deletes a message from a stream.
// On the server, the message is marked as erased, but not overwritten.
func (s *stream) DeleteMsg(ctx context.Context, seq uint64) error {
	return s.deleteMsg(ctx, &msgDeleteRequest{Seq: seq, NoErase: true})
}

// SecureDeleteMsg deletes a message from a stream. The deleted message
// is overwritten with random data. As a result, this operation is slower
// than DeleteMsg.
func (s *stream) SecureDeleteMsg(ctx context.Context, seq uint64) error {
	return s.deleteMsg(ctx, &msgDeleteRequest{Seq: seq})
}

func (s *stream) deleteMsg(ctx context.Context, req *msgDeleteRequest) error {
	ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
	if cancel != nil {
		defer cancel()
	}
	r, err := json.Marshal(req)
	if err != nil {
		return err
	}
	subj := fmt.Sprintf(apiMsgDeleteT, s.name)
	var resp msgDeleteResponse
	if _, err = s.js.apiRequestJSON(ctx, subj, &resp, r); err != nil {
		return err
	}
	if !resp.Success {
		return fmt.Errorf("%w: %s", ErrMsgDeleteUnsuccessful, resp.Error.Error())
	}
	return nil
}

// ListConsumers returns ConsumerInfoLister enabling iterating over a
// channel of consumer infos.
func (s *stream) ListConsumers(ctx context.Context) ConsumerInfoLister {
	l := &consumerLister{
		js:        s.js,
		consumers: make(chan *ConsumerInfo),
	}
	go func() {
		defer close(l.consumers)
		ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
		if cancel != nil {
			defer cancel()
		}
		for {
			page, err := l.consumerInfos(ctx, s.name)
			if err != nil && !errors.Is(err, ErrEndOfData) {
				l.err = err
				return
			}
			for _, info := range page {
				select {
				case <-ctx.Done():
					l.err = ctx.Err()
					return
				default:
				}
				if info != nil {
					l.consumers <- info
				}
			}
			if errors.Is(err, ErrEndOfData) {
				return
			}
		}
	}()

	return l
}

func (s *consumerLister) Info() <-chan *ConsumerInfo {
	return s.consumers
}

func (s *consumerLister) Err() error {
	return s.err
}

// ConsumerNames returns a ConsumerNameLister enabling iterating over a
// channel of consumer names.
func (s *stream) ConsumerNames(ctx context.Context) ConsumerNameLister {
	l := &consumerLister{
		js:    s.js,
		names: make(chan string),
	}
	go func() {
		defer close(l.names)
		ctx, cancel := s.js.wrapContextWithoutDeadline(ctx)
		if cancel != nil {
			defer cancel()
		}
		for {
			page, err := l.consumerNames(ctx, s.name)
			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
}

func (s *consumerLister) Name() <-chan string {
	return s.names
}

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

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

	slSubj := fmt.Sprintf(apiConsumerListT, stream)
	var resp consumerListResponse
	_, err = s.js.apiRequestJSON(ctx, slSubj, &resp, req)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		return nil, resp.Error
	}

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

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

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

	slSubj := fmt.Sprintf(apiConsumerNamesT, stream)
	var resp consumerNamesResponse
	_, err = s.js.apiRequestJSON(ctx, slSubj, &resp, req)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		return nil, resp.Error
	}

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

// UnpinConsumer unpins the currently pinned client for a consumer for the given group name.
// If consumer does not exist, ErrConsumerNotFound is returned.
func (s *stream) UnpinConsumer(ctx context.Context, consumer string, group string) error {
	return unpinConsumer(ctx, s.js, s.name, consumer, group)
}
