// Copyright 2019-2026 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 server

import (
	"archive/tar"
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"math"
	mrand "math/rand"
	"net"
	"os"
	"path/filepath"
	"runtime"
	"slices"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/klauspost/compress/s2"
	"github.com/minio/highwayhash"
	"github.com/nats-io/nats-server/v2/server/ats"
	"github.com/nats-io/nats-server/v2/server/avl"
	"github.com/nats-io/nats-server/v2/server/elastic"
	"github.com/nats-io/nats-server/v2/server/gsl"
	"github.com/nats-io/nats-server/v2/server/stree"
	"github.com/nats-io/nats-server/v2/server/thw"
	"golang.org/x/crypto/chacha20"
	"golang.org/x/crypto/chacha20poly1305"
)

type FileStoreConfig struct {
	// Where the parent directory for all storage will be located.
	StoreDir string
	// BlockSize is the file block size. This also represents the maximum overhead size.
	BlockSize uint64
	// CacheExpire is how long with no activity until we expire the cache.
	CacheExpire time.Duration
	// SubjectStateExpire is how long with no activity until we expire a msg block's subject state.
	SubjectStateExpire time.Duration
	// SyncInterval is how often we sync to disk in the background.
	SyncInterval time.Duration
	// SyncAlways is when the stream should sync all data writes.
	SyncAlways bool
	// AsyncFlush allows async flush to batch write operations.
	AsyncFlush bool
	// Cipher is the cipher to use when encrypting.
	Cipher StoreCipher
	// Compression is the algorithm to use when compressing.
	Compression StoreCompression

	// Internal reference to our server.
	srv *Server
}

// FileStreamInfo allows us to remember created time.
type FileStreamInfo struct {
	Created time.Time
	StreamConfig
}

type StoreCipher int

const (
	ChaCha StoreCipher = iota
	AES
	NoCipher
)

func (cipher StoreCipher) String() string {
	switch cipher {
	case ChaCha:
		return "ChaCha20-Poly1305"
	case AES:
		return "AES-GCM"
	case NoCipher:
		return "None"
	default:
		return "Unknown StoreCipher"
	}
}

type StoreCompression uint8

const (
	NoCompression StoreCompression = iota
	S2Compression
)

func (alg StoreCompression) String() string {
	switch alg {
	case NoCompression:
		return "None"
	case S2Compression:
		return "S2"
	default:
		return "Unknown StoreCompression"
	}
}

func (alg StoreCompression) MarshalJSON() ([]byte, error) {
	var str string
	switch alg {
	case S2Compression:
		str = "s2"
	case NoCompression:
		str = "none"
	default:
		return nil, fmt.Errorf("unknown compression algorithm")
	}
	return json.Marshal(str)
}

func (alg *StoreCompression) UnmarshalJSON(b []byte) error {
	var str string
	if err := json.Unmarshal(b, &str); err != nil {
		return err
	}
	switch str {
	case "s2":
		*alg = S2Compression
	case "none":
		*alg = NoCompression
	default:
		return fmt.Errorf("unknown compression algorithm")
	}
	return nil
}

// File ConsumerInfo is used for creating consumer stores.
type FileConsumerInfo struct {
	Created time.Time
	Name    string
	ConsumerConfig
}

// Default file and directory permissions.
const (
	defaultDirPerms  = os.FileMode(0700)
	defaultFilePerms = os.FileMode(0600)
)

type psi struct {
	total uint64
	fblk  uint32
	lblk  uint32
}

type fileStore struct {
	srv         *Server
	mu          sync.RWMutex
	state       StreamState
	tombs       []uint64
	ld          *LostStreamData
	scb         StorageUpdateHandler
	rmcb        StorageRemoveMsgHandler
	pmsgcb      ProcessJetStreamMsgHandler
	ageChk      *time.Timer // Timer to expire messages.
	ageChkRun   bool        // Whether message expiration is currently running.
	ageChkTime  int64       // When the message expiration is scheduled to run.
	syncTmr     *time.Timer
	cfg         FileStreamInfo
	fcfg        FileStoreConfig
	prf         keyGen
	oldprf      keyGen
	aek         cipher.AEAD
	lmb         *msgBlock
	blks        []*msgBlock
	bim         map[uint32]*msgBlock
	psim        *stree.SubjectTree[psi]
	tsl         int
	adml        int
	hh          *highwayhash.Digest64
	qch         chan struct{}
	fsld        chan struct{}
	cmu         sync.RWMutex
	cfs         []ConsumerStore
	sips        int
	dirty       int
	closing     bool
	closed      atomic.Bool // Atomic to reduce contention on ConsumerStores.
	fip         bool
	receivedAny bool
	firstMoved  bool
	ttls        *thw.HashWheel
	scheduling  *MsgScheduling
	sdm         *SDMMeta
	lpex        time.Time // Last PurgeEx call.
}

// Represents a message store block and its data.
type msgBlock struct {
	// Here for 32bit systems and atomic.
	first      msgId
	last       msgId
	mu         sync.RWMutex
	fs         *fileStore
	aek        cipher.AEAD
	bek        cipher.Stream
	seed       []byte
	nonce      []byte
	mfn        string
	mfd        *os.File
	cmp        StoreCompression // Effective compression at the time of loading the block
	liwsz      int64
	index      uint32
	bytes      uint64 // User visible bytes count.
	rbytes     uint64 // Total bytes (raw) including deleted. Used for rolling to new blk.
	cbytes     uint64 // Bytes count after last compaction. 0 if no compaction happened yet.
	msgs       uint64 // User visible message count.
	fss        *stree.SubjectTree[SimpleState]
	kfn        string
	lwts       int64
	llts       int64
	lrts       int64
	lsts       int64
	llseq      uint64
	hh         *highwayhash.Digest64
	ecache     elastic.Pointer[cache]
	cache      *cache
	cloads     uint64
	cexp       time.Duration
	fexp       time.Duration
	ctmr       *time.Timer
	werr       error
	dmap       avl.SequenceSet
	fch        chan struct{}
	qch        chan struct{}
	lchk       [8]byte
	loading    bool
	flusher    bool
	noTrack    bool
	needSync   bool
	syncAlways bool
	noCompact  bool
	closed     bool
	ttls       uint64 // How many msgs have TTLs?
	schedules  uint64 // How many msgs have schedules?

	// Used to mock write failures.
	mockWriteErr bool
}

// Write through caching layer that is also used on loading messages.
type cache struct {
	buf  []byte
	wp   int
	idx  []uint32
	fseq uint64
	nra  bool
}

type msgId struct {
	seq uint64
	ts  int64
}

const (
	// Magic is used to identify the file store files.
	magic = uint8(22)
	// Version
	version = uint8(1)
	// New IndexInfo Version
	newVersion = uint8(2)
	// hdrLen
	hdrLen = 2
	// This is where we keep the streams.
	streamsDir = "streams"
	// This is where we keep inflight batches for streams.
	batchesDir = "batches"
	// This is where we keep the message store blocks.
	msgDir = "msgs"
	// This is where we temporarily move the messages dir.
	purgeDir = "__msgs__"
	// used to scan blk file names.
	blkScan = "%d.blk"
	// suffix of a block file
	blkSuffix = ".blk"
	// used for compacted blocks that are staged.
	newScan = "%d.new"
	// used to scan index file names.
	indexScan = "%d.idx"
	// used to store our block encryption key.
	keyScan = "%d.key"
	// to look for orphans
	keyScanAll = "*.key"
	// This is where we keep state on consumers.
	consumerDir = "obs"
	// Index file for a consumer.
	consumerState = "o.dat"
	// The suffix that will be given to a new temporary block for compression or when rewriting the full file.
	blkTmpSuffix = ".tmp"
	// This is where we keep state on templates.
	// Deprecated: stream templates are deprecated and will be removed in a future version.
	tmplsDir = "templates"
	// default cache buffer expiration
	defaultCacheBufferExpiration = 10 * time.Second
	// default sync interval
	defaultSyncInterval = 2 * time.Minute
	// default idle timeout to close FDs.
	closeFDsIdle = 30 * time.Second
	// default expiration time for mb.fss when idle.
	defaultFssExpiration = 2 * time.Minute
	// coalesceMinimum
	coalesceMinimum = 16 * 1024
	// maxFlushWait is maximum we will wait to gather messages to flush.
	maxFlushWait = 8 * time.Millisecond

	// Metafiles for streams and consumers.
	JetStreamMetaFile    = "meta.inf"
	JetStreamMetaFileSum = "meta.sum"
	JetStreamMetaFileKey = "meta.key"

	// This is the full snapshotted state for the stream.
	streamStreamStateFile = "index.db"

	// This is the encoded time hash wheel for TTLs.
	ttlStreamStateFile = "thw.db"

	// This is the encoded message scheduling file.
	msgSchedulingStreamStateFile = "sched.db"

	// AEK key sizes
	minMetaKeySize = 64
	minBlkKeySize  = 64

	// Default stream block size.
	defaultLargeBlockSize = 8 * 1024 * 1024 // 8MB
	// Default for workqueue or interest based.
	defaultMediumBlockSize = 4 * 1024 * 1024 // 4MB
	// For smaller reuse buffers. Usually being generated during contention on the lead write buffer.
	// E.g. mirrors/sources etc.
	defaultSmallBlockSize = 1 * 1024 * 1024 // 1MB
	// NOT an actual block size, but used for the sync.Pools, so that we don't allocate huge buffers
	// unnecessarily until there are enough writes to justify it.
	defaultTinyBlockSize = 1 * 1024 * 256 // 256KB
	// Maximum size for the encrypted head block.
	maximumEncryptedBlockSize = 2 * 1024 * 1024 // 2MB
	// Default for KV based
	defaultKVBlockSize = defaultMediumBlockSize
	// max block size for now.
	maxBlockSize = defaultLargeBlockSize
	// Compact minimum threshold.
	compactMinimum = 2 * 1024 * 1024 // 2MB
	// FileStoreMinBlkSize is minimum size we will do for a blk size.
	FileStoreMinBlkSize = 32 * 1000 // 32kib
	// FileStoreMaxBlkSize is maximum size we will do for a blk size.
	FileStoreMaxBlkSize = maxBlockSize
	// Check for bad record length value due to corrupt data.
	rlBadThresh = 32 * 1024 * 1024
	// Checksum size for hash for msg records.
	recordHashSize = 8
)

func newFileStore(fcfg FileStoreConfig, cfg StreamConfig) (*fileStore, error) {
	return newFileStoreWithCreated(fcfg, cfg, time.Now().UTC(), nil, nil)
}

func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created time.Time, prf, oldprf keyGen) (fs *fileStore, err error) {
	if cfg.Name == _EMPTY_ {
		return nil, fmt.Errorf("name required")
	}
	if cfg.Storage != FileStorage {
		return nil, fmt.Errorf("fileStore requires file storage type in config")
	}
	// Default values.
	if fcfg.BlockSize == 0 {
		fcfg.BlockSize = dynBlkSize(cfg.Retention, cfg.MaxBytes, prf != nil)
	}
	if fcfg.BlockSize > maxBlockSize {
		return nil, fmt.Errorf("filestore max block size is %s", friendlyBytes(maxBlockSize))
	}
	if fcfg.CacheExpire == 0 {
		fcfg.CacheExpire = defaultCacheBufferExpiration
	}
	if fcfg.SubjectStateExpire == 0 {
		fcfg.SubjectStateExpire = defaultFssExpiration
	}
	if fcfg.SyncInterval == 0 {
		fcfg.SyncInterval = defaultSyncInterval
	}

	// Check the directory
	if stat, err := os.Stat(fcfg.StoreDir); os.IsNotExist(err) {
		if err := os.MkdirAll(fcfg.StoreDir, defaultDirPerms); err != nil {
			return nil, fmt.Errorf("could not create storage directory - %v", err)
		}
	} else if stat == nil || !stat.IsDir() {
		return nil, fmt.Errorf("storage directory is not a directory")
	}
	tmpfile, err := os.CreateTemp(fcfg.StoreDir, "_test_")
	if err != nil {
		return nil, fmt.Errorf("storage directory is not writable")
	}

	tmpfile.Close()
	<-dios
	os.Remove(tmpfile.Name())
	dios <- struct{}{}

	fs = &fileStore{
		fcfg:   fcfg,
		psim:   stree.NewSubjectTree[psi](),
		bim:    make(map[uint32]*msgBlock),
		cfg:    FileStreamInfo{Created: created, StreamConfig: cfg},
		prf:    prf,
		oldprf: oldprf,
		qch:    make(chan struct{}),
		fsld:   make(chan struct{}),
		srv:    fcfg.srv,
	}

	// Register with access time service.
	ats.Register()

	// If we error before completion make sure to cleanup.
	defer func() {
		if err != nil {
			ats.Unregister()
		}
	}()

	// Only create a THW if we're going to allow TTLs.
	if cfg.AllowMsgTTL {
		fs.ttls = thw.NewHashWheel()
	}
	// Only create scheduling data structure if we're going to allow message schedules.
	if cfg.AllowMsgSchedules {
		fs.scheduling = newMsgScheduling(fs.runMsgScheduling)
	}

	// Set flush in place to AsyncFlush which by default is false.
	fs.fip = !fcfg.AsyncFlush

	// Check if this is a new setup.
	mdir := filepath.Join(fcfg.StoreDir, msgDir)
	odir := filepath.Join(fcfg.StoreDir, consumerDir)
	if err := os.MkdirAll(mdir, defaultDirPerms); err != nil {
		return nil, fmt.Errorf("could not create message storage directory - %v", err)
	}
	if err := os.MkdirAll(odir, defaultDirPerms); err != nil {
		return nil, fmt.Errorf("could not create consumer storage directory - %v", err)
	}

	// Create highway hash for message blocks. Use sha256 of directory as key.
	key := sha256.Sum256([]byte(cfg.Name))
	fs.hh, err = highwayhash.NewDigest64(key[:])
	if err != nil {
		return nil, fmt.Errorf("could not create hash: %v", err)
	}

	keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey)
	_, err = os.Stat(keyFile)
	// Either the file should exist (err=nil), or it shouldn't. Any other error is reported.
	if err != nil && !os.IsNotExist(err) {
		return nil, err
	}
	// Make sure we do not have an encrypted store underneath of us but no main key.
	if fs.prf == nil && err == nil {
		return nil, errNoMainKey
	} else if fs.prf != nil && err == nil {
		// If encryption is configured and the key file exists, recover our keys.
		if err = fs.recoverAEK(); err != nil {
			return nil, err
		}
	}

	// Attempt to recover our state.
	err = fs.recoverFullState()
	if err != nil {
		if !os.IsNotExist(err) {
			fs.warn("Recovering stream state from index errored: %v", err)
		}
		// Hold onto state
		prior := fs.state
		// Reset anything that could have been set from above.
		fs.state = StreamState{}
		fs.psim, fs.tsl = fs.psim.Empty(), 0
		fs.bim = make(map[uint32]*msgBlock)
		fs.blks = nil
		fs.tombs = nil

		// Recover our message state the old way
		if err := fs.recoverMsgs(); err != nil {
			return nil, err
		}

		fs.mu.Lock()
		// Check if our prior state remembers a last sequence past where we can see.
		// Unless we're async flushing, in which case this can happen if some blocks weren't flushed.
		if prior.LastSeq > fs.state.LastSeq && !fs.fcfg.AsyncFlush {
			if mb, err := fs.newMsgBlockForWrite(); err != nil {
				fs.mu.Unlock()
				return nil, err
			} else if err = mb.writeTombstone(prior.LastSeq, prior.LastTime.UnixNano()); err != nil {
				fs.mu.Unlock()
				return nil, err
			}
			fs.state.LastSeq, fs.state.LastTime = prior.LastSeq, prior.LastTime
			if fs.state.Msgs == 0 {
				fs.state.FirstSeq = fs.state.LastSeq + 1
				fs.state.FirstTime = time.Time{}
			}
		}
		// Since we recovered here, make sure to kick ourselves to write out our stream state.
		fs.dirty++
		fs.mu.Unlock()
	}

	// Lock during the remainder of the recovery.
	fs.mu.Lock()
	// Use defer to ensure the lock is released if any of the enforcement operations
	// run into issues to avoid potential deadlocks on exit.
	unlocked := false
	defer func() {
		if !unlocked {
			fs.mu.Unlock()
		}
	}()

	// See if we can bring back our TTL timed hash wheel state from disk.
	if cfg.AllowMsgTTL {
		if err = fs.recoverTTLState(); err != nil && !os.IsNotExist(err) {
			fs.warn("Recovering TTL state from index errored: %v", err)
		}
	}

	// See if we can bring back our message scheduling state from disk.
	if cfg.AllowMsgSchedules {
		if err = fs.recoverMsgSchedulingState(); err != nil && !os.IsNotExist(err) {
			fs.warn("Recovering message scheduling state from index errored: %v", err)
		}
	}

	// Also make sure we get rid of old idx and fss files on return.
	// Do this in separate go routine vs inline and at end of processing.
	defer func() {
		if fs != nil {
			go fs.cleanupOldMeta()
		}
	}()

	// Check if we have any left over tombstones to process.
	if len(fs.tombs) > 0 {
		for _, seq := range fs.tombs {
			fs.removeMsg(seq, false, true, false)
			fs.removeFromLostData(seq)
		}
		// Not needed after this phase.
		fs.tombs = nil
	}

	// Limits checks and enforcement.
	fs.enforceMsgLimit()
	fs.enforceBytesLimit()

	// Do age checks too, make sure to call in place.
	if fs.cfg.MaxAge != 0 {
		err := fs.expireMsgsOnRecover()
		if isPermissionError(err) {
			return nil, err
		}
		fs.startAgeChk()
	}

	// If we have max msgs per subject make sure the is also enforced.
	if fs.cfg.MaxMsgsPer > 0 {
		fs.enforceMsgPerSubjectLimit(false)
	}

	// Grab first sequence for check below while we have lock.
	firstSeq := fs.state.FirstSeq
	fs.mu.Unlock()
	unlocked = true

	// If the stream has an initial sequence number then make sure we
	// have purged up until that point. We will do this only if the
	// recovered first sequence number is before our configured first
	// sequence. Need to do this locked as by now the age check timer
	// has started.
	if cfg.FirstSeq > 0 && firstSeq < cfg.FirstSeq {
		if _, err := fs.purge(cfg.FirstSeq); err != nil {
			return nil, err
		}
	}

	// Write our meta data if it does not exist or is zero'd out.
	meta := filepath.Join(fcfg.StoreDir, JetStreamMetaFile)
	fi, err := os.Stat(meta)
	if err != nil && os.IsNotExist(err) || fi != nil && fi.Size() == 0 {
		if err := fs.writeStreamMeta(); err != nil {
			return nil, err
		}
	}

	// If we expect to be encrypted check that what we are restoring is not plaintext.
	// This can happen on snapshot restores or conversions.
	if fs.prf != nil {
		if _, err := os.Stat(keyFile); err != nil && os.IsNotExist(err) {
			if err := fs.writeStreamMeta(); err != nil {
				return nil, err
			}
		}
	}

	// Setup our sync timer.
	fs.setSyncTimer()

	// Spin up the go routine that will write out our full state stream index.
	go fs.flushStreamStateLoop(fs.qch, fs.fsld)

	return fs, nil
}

// Lock all existing message blocks.
// Lock held on entry.
func (fs *fileStore) lockAllMsgBlocks() {
	for _, mb := range fs.blks {
		mb.mu.Lock()
	}
}

// Unlock all existing message blocks.
// Lock held on entry.
func (fs *fileStore) unlockAllMsgBlocks() {
	for _, mb := range fs.blks {
		mb.mu.Unlock()
	}
}

func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error {
	start := time.Now()
	defer func() {
		if took := time.Since(start); took > time.Minute {
			fs.warn("UpdateConfig took %v", took.Round(time.Millisecond))
		}
	}()

	if fs.isClosed() {
		return ErrStoreClosed
	}
	if cfg.Name == _EMPTY_ {
		return fmt.Errorf("name required")
	}
	if cfg.Storage != FileStorage {
		return fmt.Errorf("fileStore requires file storage type in config")
	}
	if cfg.MaxMsgsPer < -1 {
		cfg.MaxMsgsPer = -1
	}

	fs.mu.Lock()
	new_cfg := FileStreamInfo{Created: fs.cfg.Created, StreamConfig: *cfg}
	old_cfg := fs.cfg
	// The reference story has changed here, so this full msg block lock
	// may not be needed.
	fs.lockAllMsgBlocks()
	fs.cfg = new_cfg
	fs.unlockAllMsgBlocks()
	if err := fs.writeStreamMeta(); err != nil {
		fs.lockAllMsgBlocks()
		fs.cfg = old_cfg
		fs.unlockAllMsgBlocks()
		fs.mu.Unlock()
		return err
	}

	// Create or delete the THW if needed.
	if cfg.AllowMsgTTL && fs.ttls == nil {
		fs.recoverTTLState()
	} else if !cfg.AllowMsgTTL && fs.ttls != nil {
		fs.ttls = nil
	}
	// Create or delete the message scheduling state if needed.
	if cfg.AllowMsgSchedules && fs.scheduling == nil {
		fs.recoverMsgSchedulingState()
	} else if !cfg.AllowMsgSchedules && fs.scheduling != nil {
		fs.scheduling = nil
	}

	// Limits checks and enforcement.
	fs.enforceMsgLimit()
	fs.enforceBytesLimit()

	// Do age timers.
	if fs.ageChk == nil && fs.cfg.MaxAge != 0 {
		fs.startAgeChk()
	}
	if fs.ageChk != nil && fs.cfg.MaxAge == 0 {
		fs.ageChk.Stop()
		fs.ageChk = nil
		fs.ageChkTime = 0
	}

	if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer == 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) {
		fs.enforceMsgPerSubjectLimit(true)
	}

	if lmb := fs.lmb; lmb != nil {
		// Enable/disable async flush depending on if it's supported and already initialized.
		supportsAsyncFlush := !fs.fcfg.SyncAlways && cfg.Replicas > 1

		// Async persist mode opts in to async flushing,
		// sync always would also be disabled if it was configured.
		if cfg.PersistMode == AsyncPersistMode {
			supportsAsyncFlush = true
			fs.fcfg.SyncAlways = false
			lmb.syncAlways = false
		}

		if supportsAsyncFlush && !fs.fcfg.AsyncFlush {
			fs.fcfg.AsyncFlush = true
			lmb.spinUpFlushLoop()
		} else if !supportsAsyncFlush && fs.fcfg.AsyncFlush {
			fs.fcfg.AsyncFlush = false
			lmb.mu.Lock()
			// Quit the flush loop.
			if lmb.qch != nil {
				close(lmb.qch)
				lmb.qch = nil
			}
			lmb.flushPendingMsgsLocked()
			lmb.mu.Unlock()
		}
		// Set flush in place to AsyncFlush which by default is false.
		fs.fip = !fs.fcfg.AsyncFlush
	}
	fs.mu.Unlock()

	if cfg.MaxAge != 0 || cfg.AllowMsgTTL {
		fs.expireMsgs()
	}
	if cfg.AllowMsgSchedules {
		fs.runMsgScheduling()
	}
	return nil
}

func dynBlkSize(retention RetentionPolicy, maxBytes int64, encrypted bool) uint64 {
	if maxBytes > 0 {
		blkSize := (maxBytes / 4) + 1 // (25% overhead)
		// Round up to nearest 100
		if m := blkSize % 100; m != 0 {
			blkSize += 100 - m
		}
		if blkSize <= FileStoreMinBlkSize {
			blkSize = FileStoreMinBlkSize
		} else if blkSize >= FileStoreMaxBlkSize {
			blkSize = FileStoreMaxBlkSize
		} else {
			blkSize = defaultMediumBlockSize
		}
		if encrypted && blkSize > maximumEncryptedBlockSize {
			// Notes on this below.
			blkSize = maximumEncryptedBlockSize
		}
		return uint64(blkSize)
	}

	switch {
	case encrypted:
		// In the case of encrypted stores, large blocks can result in worsened perf
		// since many writes on disk involve re-encrypting the entire block. For now,
		// we will enforce a cap on the block size when encryption is enabled to avoid
		// this.
		return maximumEncryptedBlockSize
	case retention == LimitsPolicy:
		// TODO(dlc) - Make the blocksize relative to this if set.
		return defaultLargeBlockSize
	default:
		// TODO(dlc) - Make the blocksize relative to this if set.
		return defaultMediumBlockSize
	}
}

func genEncryptionKey(sc StoreCipher, seed []byte) (ek cipher.AEAD, err error) {
	if sc == ChaCha {
		ek, err = chacha20poly1305.NewX(seed)
	} else if sc == AES {
		block, e := aes.NewCipher(seed)
		if e != nil {
			return nil, e
		}
		ek, err = cipher.NewGCMWithNonceSize(block, block.BlockSize())
	} else {
		err = errUnknownCipher
	}
	return ek, err
}

// Generate an asset encryption key from the context and server PRF.
func (fs *fileStore) genEncryptionKeys(context string) (aek cipher.AEAD, bek cipher.Stream, seed, encrypted []byte, err error) {
	if fs.prf == nil {
		return nil, nil, nil, nil, errNoEncryption
	}
	// Generate key encryption key.
	rb, err := fs.prf([]byte(context))
	if err != nil {
		return nil, nil, nil, nil, err
	}

	sc := fs.fcfg.Cipher

	kek, err := genEncryptionKey(sc, rb)
	if err != nil {
		return nil, nil, nil, nil, err
	}
	// Generate random asset encryption key seed.

	const seedSize = 32
	seed = make([]byte, seedSize)
	if n, err := rand.Read(seed); err != nil {
		return nil, nil, nil, nil, err
	} else if n != seedSize {
		return nil, nil, nil, nil, fmt.Errorf("not enough seed bytes read (%d != %d", n, seedSize)
	}

	aek, err = genEncryptionKey(sc, seed)
	if err != nil {
		return nil, nil, nil, nil, err
	}

	// Generate our nonce. Use same buffer to hold encrypted seed.
	nonce := make([]byte, kek.NonceSize(), kek.NonceSize()+len(seed)+kek.Overhead())
	if n, err := rand.Read(nonce); err != nil {
		return nil, nil, nil, nil, err
	} else if n != len(nonce) {
		return nil, nil, nil, nil, fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
	}

	bek, err = genBlockEncryptionKey(sc, seed[:], nonce)
	if err != nil {
		return nil, nil, nil, nil, err
	}

	return aek, bek, seed, kek.Seal(nonce, nonce, seed, nil), nil
}

// Will generate the block encryption key.
func genBlockEncryptionKey(sc StoreCipher, seed, nonce []byte) (cipher.Stream, error) {
	if sc == ChaCha {
		return chacha20.NewUnauthenticatedCipher(seed, nonce)
	} else if sc == AES {
		block, err := aes.NewCipher(seed)
		if err != nil {
			return nil, err
		}
		return cipher.NewCTR(block, nonce), nil
	}
	return nil, errUnknownCipher
}

// Lock should be held.
func (fs *fileStore) recoverAEK() error {
	if fs.prf != nil && fs.aek == nil {
		ekey, err := os.ReadFile(filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey))
		if err != nil {
			return err
		}
		rb, err := fs.prf([]byte(fs.cfg.Name))
		if err != nil {
			return err
		}
		kek, err := genEncryptionKey(fs.fcfg.Cipher, rb)
		if err != nil {
			return err
		}
		ns := kek.NonceSize()
		seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil)
		if err != nil {
			return err
		}
		aek, err := genEncryptionKey(fs.fcfg.Cipher, seed)
		if err != nil {
			return err
		}
		fs.aek = aek
	}
	return nil
}

// Lock should be held.
func (fs *fileStore) setupAEK() error {
	if fs.prf != nil && fs.aek == nil {
		key, _, _, encrypted, err := fs.genEncryptionKeys(fs.cfg.Name)
		if err != nil {
			return err
		}
		keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey)
		if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) {
			return err
		}
		err = fs.writeFileWithOptionalSync(keyFile, encrypted, defaultFilePerms)
		if err != nil {
			return err
		}
		// Set our aek.
		fs.aek = key
	}
	return nil
}

// Write out meta and the checksum.
// Lock should be held.
func (fs *fileStore) writeStreamMeta() error {
	if err := fs.setupAEK(); err != nil {
		return err
	}

	meta := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFile)
	if _, err := os.Stat(meta); err != nil && !os.IsNotExist(err) {
		return err
	}
	b, err := json.Marshal(fs.cfg)
	if err != nil {
		return err
	}
	// Encrypt if needed.
	if fs.aek != nil {
		nonce := make([]byte, fs.aek.NonceSize(), fs.aek.NonceSize()+len(b)+fs.aek.Overhead())
		if n, err := rand.Read(nonce); err != nil {
			return err
		} else if n != len(nonce) {
			return fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
		}
		b = fs.aek.Seal(nonce, nonce, b, nil)
	}

	err = fs.writeFileWithOptionalSync(meta, b, defaultFilePerms)
	if err != nil {
		return err
	}
	fs.hh.Reset()
	fs.hh.Write(b)
	var hb [highwayhash.Size64]byte
	checksum := hex.EncodeToString(fs.hh.Sum(hb[:0]))
	sum := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileSum)
	err = fs.writeFileWithOptionalSync(sum, []byte(checksum), defaultFilePerms)
	if err != nil {
		return err
	}
	return nil
}

// Pools to recycle the blocks to help with memory pressure.
var blkPoolTiny = &sync.Pool{
	New: func() any {
		b := [defaultTinyBlockSize]byte{}
		return &b
	},
}
var blkPoolSmall = &sync.Pool{
	New: func() any {
		b := [defaultSmallBlockSize]byte{}
		return &b
	},
}
var blkPoolMedium = &sync.Pool{
	New: func() any {
		b := [defaultMediumBlockSize]byte{}
		return &b
	},
}
var blkPoolBig = &sync.Pool{
	New: func() any {
		b := [defaultLargeBlockSize]byte{}
		return &b
	},
}

// Get a new msg block based on sz estimate.
func getMsgBlockBuf(sz int) (buf []byte) {
	switch {
	case sz <= defaultTinyBlockSize:
		return blkPoolTiny.Get().(*[defaultTinyBlockSize]byte)[:0]
	case sz <= defaultSmallBlockSize:
		return blkPoolSmall.Get().(*[defaultSmallBlockSize]byte)[:0]
	case sz <= defaultMediumBlockSize:
		return blkPoolMedium.Get().(*[defaultMediumBlockSize]byte)[:0]
	case sz <= defaultLargeBlockSize:
		return blkPoolBig.Get().(*[defaultLargeBlockSize]byte)[:0]
	default:
		// Ideally this should not happen, once we return a buffer that's
		// larger than defaultLargeBlockSize then we will refuse to recycle
		// it to stop the pools from bloating excessively.
		return make([]byte, 0, sz)
	}
}

// Recycle the msg block.
func recycleMsgBlockBuf(buf []byte) {
	switch cap(buf) {
	case defaultTinyBlockSize:
		b := (*[defaultTinyBlockSize]byte)(buf[0:defaultTinyBlockSize])
		blkPoolTiny.Put(b)
	case defaultSmallBlockSize:
		b := (*[defaultSmallBlockSize]byte)(buf[0:defaultSmallBlockSize])
		blkPoolSmall.Put(b)
	case defaultMediumBlockSize:
		b := (*[defaultMediumBlockSize]byte)(buf[0:defaultMediumBlockSize])
		blkPoolMedium.Put(b)
	case defaultLargeBlockSize:
		b := (*[defaultLargeBlockSize]byte)(buf[0:defaultLargeBlockSize])
		blkPoolBig.Put(b)
	default:
		// Too large, let the GC collect it instead.
	}
}

const (
	msgHdrSize     = 22
	checksumSize   = 8
	emptyRecordLen = msgHdrSize + checksumSize
)

// Lock should be held.
func (fs *fileStore) noTrackSubjects() bool {
	return !(fs.psim.Size() > 0 || len(fs.cfg.Subjects) > 0 || fs.cfg.Mirror != nil || len(fs.cfg.Sources) > 0)
}

// Will init the basics for a message block.
func (fs *fileStore) initMsgBlock(index uint32) *msgBlock {
	mb := &msgBlock{
		fs:         fs,
		index:      index,
		cexp:       fs.fcfg.CacheExpire,
		fexp:       fs.fcfg.SubjectStateExpire,
		noTrack:    fs.noTrackSubjects(),
		syncAlways: fs.fcfg.SyncAlways,
	}

	mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
	mb.mfn = filepath.Join(mdir, fmt.Sprintf(blkScan, index))

	if mb.hh == nil {
		key := sha256.Sum256(fs.hashKeyForBlock(index))
		mb.hh, _ = highwayhash.NewDigest64(key[:])
	}
	return mb
}

// Check for encryption, we do not load keys on startup anymore so might need to load them here.
// Lock for fs should be held.
func (mb *msgBlock) checkAndLoadEncryption() error {
	if mb.fs != nil && mb.fs.prf != nil && (mb.aek == nil || mb.bek == nil) {
		if err := mb.fs.loadEncryptionForMsgBlock(mb); err != nil {
			return err
		}
	}
	return nil
}

// Lock for fs should be held.
func (fs *fileStore) loadEncryptionForMsgBlock(mb *msgBlock) error {
	if fs.prf == nil {
		return nil
	}

	var createdKeys bool
	mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
	ekey, err := os.ReadFile(filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index)))
	if err != nil {
		// We do not seem to have keys even though we should. Could be a plaintext conversion.
		// Create the keys and we will double check below.
		if err := fs.genEncryptionKeysForBlock(mb); err != nil {
			return err
		}
		createdKeys = true
	} else {
		if len(ekey) < minBlkKeySize {
			return errBadKeySize
		}
		// Recover key encryption key.
		rb, err := fs.prf([]byte(fmt.Sprintf("%s:%d", fs.cfg.Name, mb.index)))
		if err != nil {
			return err
		}

		sc := fs.fcfg.Cipher
		kek, err := genEncryptionKey(sc, rb)
		if err != nil {
			return err
		}
		ns := kek.NonceSize()
		seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil)
		if err != nil {
			// We may be here on a cipher conversion, so attempt to convert.
			if err = mb.convertCipher(); err != nil {
				return err
			}
		} else {
			mb.seed, mb.nonce = seed, ekey[:ns]
		}
		mb.aek, err = genEncryptionKey(sc, mb.seed)
		if err != nil {
			return err
		}
		if mb.bek, err = genBlockEncryptionKey(sc, mb.seed, mb.nonce); err != nil {
			return err
		}
	}

	// If we created keys here, let's check the data and if it is plaintext convert here.
	if createdKeys {
		if err := mb.convertToEncrypted(); err != nil {
			return err
		}
	}

	return nil
}

// Load a last checksum if needed from the block file.
// Lock should be held.
func (mb *msgBlock) ensureLastChecksumLoaded() {
	var empty [8]byte
	if mb.lchk != empty {
		return
	}
	copy(mb.lchk[0:], mb.lastChecksum())
}

// Lock held on entry
func (fs *fileStore) recoverMsgBlock(index uint32) (*msgBlock, error) {
	mb := fs.initMsgBlock(index)
	// Open up the message file, but we will try to recover from the index file.
	// We will check that the last checksums match.
	file, err := mb.openBlock()
	if err != nil {
		return nil, err
	}
	defer file.Close()

	if fi, err := file.Stat(); fi != nil {
		mb.rbytes = uint64(fi.Size())
	} else {
		return nil, err
	}

	// Make sure encryption loaded if needed.
	fs.loadEncryptionForMsgBlock(mb)

	// Grab last checksum from main block file.
	var lchk [8]byte
	if mb.rbytes >= checksumSize {
		if mb.bek != nil {
			// We pass nil, so get a buf from the block pool, we'll need to recycle it afterward.
			buf, _ := mb.loadBlock(nil)
			if len(buf) >= checksumSize {
				mb.bek.XORKeyStream(buf, buf)
				copy(lchk[0:], buf[len(buf)-checksumSize:])
			}
			// We can recycle it now.
			recycleMsgBlockBuf(buf)
		} else {
			file.ReadAt(lchk[:], int64(mb.rbytes)-checksumSize)
		}
	}

	file.Close()

	// Read our index file. Use this as source of truth if possible.
	// This not applicable in >= 2.10 servers. Here for upgrade paths from < 2.10.
	if err := mb.readIndexInfo(); err == nil {
		// Quick sanity check here.
		// Note this only checks that the message blk file is not newer then this file, or is empty and we expect empty.
		if (mb.rbytes == 0 && mb.msgs == 0) || bytes.Equal(lchk[:], mb.lchk[:]) {
			if mb.msgs > 0 && !mb.noTrack && fs.psim != nil {
				fs.populateGlobalPerSubjectInfo(mb)
				// Try to dump any state we needed on recovery.
				mb.tryForceExpireCacheLocked()
			}
			fs.addMsgBlock(mb)
			return mb, nil
		}
	}

	// If we get data loss rebuilding the message block state record that with the fs itself.
	ld, tombs, _ := mb.rebuildState()
	if ld != nil {
		fs.addLostData(ld)
	}
	// Collect all tombstones.
	if len(tombs) > 0 {
		fs.tombs = append(fs.tombs, tombs...)
	}

	if mb.msgs > 0 && !mb.noTrack && fs.psim != nil {
		fs.populateGlobalPerSubjectInfo(mb)
		// Try to dump any state we needed on recovery.
		mb.tryForceExpireCacheLocked()
	}

	mb.closeFDs()
	fs.addMsgBlock(mb)

	return mb, nil
}

func (fs *fileStore) lostData() *LostStreamData {
	fs.mu.RLock()
	defer fs.mu.RUnlock()
	if fs.ld == nil {
		return nil
	}
	nld := *fs.ld
	return &nld
}

// Lock should be held.
func (fs *fileStore) addLostData(ld *LostStreamData) {
	if ld == nil {
		return
	}
	if fs.ld != nil {
		var added bool
		for _, seq := range ld.Msgs {
			if _, found := fs.ld.exists(seq); !found {
				fs.ld.Msgs = append(fs.ld.Msgs, seq)
				added = true
			}
		}
		if added {
			msgs := fs.ld.Msgs
			slices.Sort(msgs)
			fs.ld.Bytes += ld.Bytes
		}
	} else {
		fs.ld = ld
	}
}

// Helper to see if we already have this sequence reported in our lost data.
func (ld *LostStreamData) exists(seq uint64) (int, bool) {
	i := slices.IndexFunc(ld.Msgs, func(i uint64) bool {
		return i == seq
	})
	return i, i > -1
}

func (fs *fileStore) removeFromLostData(seq uint64) {
	if fs.ld == nil {
		return
	}
	if i, found := fs.ld.exists(seq); found {
		fs.ld.Msgs = append(fs.ld.Msgs[:i], fs.ld.Msgs[i+1:]...)
		if len(fs.ld.Msgs) == 0 {
			fs.ld = nil
		}
	}
}

func (fs *fileStore) rebuildState(ld *LostStreamData) {
	fs.mu.Lock()
	defer fs.mu.Unlock()
	fs.rebuildStateLocked(ld)
}

// Lock should be held.
func (fs *fileStore) rebuildStateLocked(ld *LostStreamData) {
	fs.addLostData(ld)

	fs.state.Msgs, fs.state.Bytes = 0, 0
	fs.state.FirstSeq, fs.state.LastSeq = 0, 0

	for _, mb := range fs.blks {
		mb.mu.RLock()
		fs.state.Msgs += mb.msgs
		fs.state.Bytes += mb.bytes
		fseq := atomic.LoadUint64(&mb.first.seq)
		if fs.state.FirstSeq == 0 || (fseq < fs.state.FirstSeq && mb.first.ts != 0) {
			fs.state.FirstSeq = fseq
			if mb.first.ts == 0 {
				fs.state.FirstTime = time.Time{}
			} else {
				fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
			}
		}
		// Preserve last time, could have erased the last message in one block, and then
		// have a tombstone with the proper timestamp afterward in another block
		if lseq := atomic.LoadUint64(&mb.last.seq); lseq >= fs.state.LastSeq {
			fs.state.LastSeq = lseq
			if mb.last.ts == 0 {
				fs.state.LastTime = time.Time{}
			} else {
				fs.state.LastTime = time.Unix(0, mb.last.ts).UTC()
			}
		}
		mb.mu.RUnlock()
	}
}

// Attempt to convert the cipher used for this message block.
func (mb *msgBlock) convertCipher() error {
	fs := mb.fs
	sc := fs.fcfg.Cipher

	var osc StoreCipher
	switch sc {
	case ChaCha:
		osc = AES
	case AES:
		osc = ChaCha
	}

	mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
	ekey, err := os.ReadFile(filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index)))
	if err != nil {
		return err
	}
	if len(ekey) < minBlkKeySize {
		return errBadKeySize
	}
	type prfWithCipher struct {
		keyGen
		StoreCipher
	}
	var prfs []prfWithCipher
	if fs.prf != nil {
		prfs = append(prfs, prfWithCipher{fs.prf, sc})
		prfs = append(prfs, prfWithCipher{fs.prf, osc})
	}
	if fs.oldprf != nil {
		prfs = append(prfs, prfWithCipher{fs.oldprf, sc})
		prfs = append(prfs, prfWithCipher{fs.oldprf, osc})
	}

	for _, prf := range prfs {
		// Recover key encryption key.
		rb, err := prf.keyGen([]byte(fmt.Sprintf("%s:%d", fs.cfg.Name, mb.index)))
		if err != nil {
			continue
		}
		kek, err := genEncryptionKey(prf.StoreCipher, rb)
		if err != nil {
			continue
		}
		ns := kek.NonceSize()
		seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil)
		if err != nil {
			continue
		}
		nonce := ekey[:ns]
		bek, err := genBlockEncryptionKey(prf.StoreCipher, seed, nonce)
		if err != nil {
			return err
		}

		buf, _ := mb.loadBlock(nil)
		bek.XORKeyStream(buf, buf)
		// Check for compression, and make sure we can parse with old cipher and key file.
		if nbuf, err := mb.decompressIfNeeded(buf); err != nil {
			return err
		} else if _, _, err = mb.rebuildStateFromBufLocked(nbuf, false); err != nil {
			return err
		} else if err = mb.indexCacheBuf(nbuf); err != nil {
			return err
		}

		// Reset the cache since we just read everything in.
		mb.cache = nil

		// Generate new keys. If we error for some reason then we will put
		// the old keyfile back.
		if err := fs.genEncryptionKeysForBlock(mb); err != nil {
			keyFile := filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index))
			fs.writeFileWithOptionalSync(keyFile, ekey, defaultFilePerms)
			return err
		}
		mb.bek.XORKeyStream(buf, buf)
		<-dios
		err = os.WriteFile(mb.mfn, buf, defaultFilePerms)
		dios <- struct{}{}
		if err != nil {
			return err
		}
		return nil
	}
	return fmt.Errorf("unable to recover keys")
}

// Convert a plaintext block to encrypted.
func (mb *msgBlock) convertToEncrypted() error {
	if mb.bek == nil {
		return nil
	}
	buf, err := mb.loadBlock(nil)
	if err != nil {
		return err
	}
	// Check for compression.
	if buf, err = mb.decompressIfNeeded(buf); err != nil {
		return err
	} else if _, _, err = mb.rebuildStateFromBufLocked(buf, false); err != nil {
		return err
	} else if err = mb.indexCacheBuf(buf); err != nil {
		// This likely indicates this was already encrypted or corrupt.
		mb.cache = nil
		return err
	}
	// Undo cache from above for later.
	mb.cache = nil
	mb.bek.XORKeyStream(buf, buf)
	<-dios
	err = os.WriteFile(mb.mfn, buf, defaultFilePerms)
	dios <- struct{}{}
	if err != nil {
		return err
	}
	return nil
}

// Return the mb's index.
func (mb *msgBlock) getIndex() uint32 {
	mb.mu.RLock()
	defer mb.mu.RUnlock()
	return mb.index
}

// Rebuild the state of the blk based on what we have on disk in the N.blk file.
// We will return any lost data, and we will return any delete tombstones we encountered.
func (mb *msgBlock) rebuildState() (*LostStreamData, []uint64, error) {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	return mb.rebuildStateLocked()
}

// Rebuild the state of the blk based on what we have on disk in the N.blk file.
// Lock should be held.
func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, []uint64, error) {
	// Remove the .fss file and clear any cache we have set.
	mb.clearCacheAndOffset()

	buf, err := mb.loadBlock(nil)
	defer recycleMsgBlockBuf(buf)

	if err != nil || len(buf) == 0 {
		var ld *LostStreamData
		// No data to rebuild from here.
		if mb.msgs > 0 {
			// We need to declare lost data here.
			ld = &LostStreamData{Msgs: make([]uint64, 0, mb.msgs), Bytes: mb.bytes}
			firstSeq, lastSeq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
			for seq := firstSeq; seq <= lastSeq; seq++ {
				if !mb.dmap.Exists(seq) {
					ld.Msgs = append(ld.Msgs, seq)
				}
			}
			// Clear invalid state. We will let this blk be added in here.
			mb.msgs, mb.bytes, mb.rbytes, mb.fss = 0, 0, 0, nil
			mb.dmap.Empty()
			atomic.StoreUint64(&mb.first.seq, atomic.LoadUint64(&mb.last.seq)+1)
		}
		return ld, nil, err
	}

	// Check if we need to decrypt.
	if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
		return nil, nil, err
	}
	// Check for compression.
	if buf, err = mb.decompressIfNeeded(buf); err != nil {
		return nil, nil, err
	}
	return mb.rebuildStateFromBufLocked(buf, true)
}

// Lock should be held.
func (mb *msgBlock) rebuildStateFromBufLocked(buf []byte, allowTruncate bool) (*LostStreamData, []uint64, error) {
	var err error
	startLastSeq := atomic.LoadUint64(&mb.last.seq)

	// Clear state we need to rebuild.
	mb.msgs, mb.bytes, mb.rbytes, mb.fss = 0, 0, 0, nil
	atomic.StoreUint64(&mb.last.seq, 0)
	mb.last.ts = 0
	firstNeedsSet := true

	mb.rbytes = uint64(len(buf))

	addToDmap := func(seq uint64) {
		if seq == 0 {
			return
		}
		mb.dmap.Insert(seq)
	}

	var le = binary.LittleEndian

	truncate := func(index uint32) {
		// There are cases where we're not allowed to truncate, like for an encrypted or compressed
		// block since the index will be the decrypted and decompressed index.
		if !allowTruncate {
			return
		}

		var fd *os.File
		if mb.mfd != nil {
			fd = mb.mfd
		} else {
			<-dios
			fd, err = os.OpenFile(mb.mfn, os.O_RDWR, defaultFilePerms)
			dios <- struct{}{}
			if err == nil {
				defer fd.Close()
			}
		}
		if fd == nil {
			return
		}
		if err := fd.Truncate(int64(index)); err == nil {
			// Update our checksum.
			if index >= 8 {
				var lchk [8]byte
				fd.ReadAt(lchk[:], int64(index-8))
				copy(mb.lchk[0:], lchk[:])
			}
			fd.Sync()
		}
	}

	gatherLost := func(lb uint32) *LostStreamData {
		var ld LostStreamData
		for seq := atomic.LoadUint64(&mb.last.seq) + 1; seq <= startLastSeq; seq++ {
			ld.Msgs = append(ld.Msgs, seq)
		}
		ld.Bytes = uint64(lb)
		return &ld
	}

	// For tombstones that we find and collect.
	var (
		tombstones      []uint64
		maxTombstoneSeq uint64
		maxTombstoneTs  int64
	)

	// To detect gaps from compaction, and to ensure the sequence keeps moving up.
	var last uint64
	var hb [highwayhash.Size64]byte

	updateLast := func(seq uint64, ts int64) {
		// The sequence needs to only ever move up.
		if seq <= last {
			return
		}

		// Check for any gaps from compaction, meaning no ebit entry.
		if last > 0 && seq != last+1 && mb.msgs != 0 {
			for dseq := last + 1; dseq < seq; dseq++ {
				addToDmap(dseq)
			}
		}
		last = seq
		atomic.StoreUint64(&mb.last.seq, last)
		mb.last.ts = ts
	}

	for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; {
		if index+msgHdrSize > lbuf {
			truncate(index)
			return gatherLost(lbuf - index), tombstones, nil
		}

		hdr := buf[index : index+msgHdrSize]
		rl, slen := le.Uint32(hdr[0:]), int(le.Uint16(hdr[20:]))

		hasHeaders := rl&hbit != 0
		// Clear any headers bit that could be set.
		rl &^= hbit
		shlen := slen
		if hasHeaders {
			shlen += 4
		}
		dlen := int(rl) - msgHdrSize
		// Do some quick sanity checks here.
		if dlen < 0 || shlen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh {
			truncate(index)
			return gatherLost(lbuf - index), tombstones, errBadMsg{mb.mfn, fmt.Sprintf("sanity check failed (dlen %d slen %d rl %d index %d lbuf %d)", dlen, slen, rl, index, lbuf)}
		}

		// Check for checksum failures before additional processing.
		data := buf[index+msgHdrSize : index+rl]
		subj := data[:slen]
		if hh := mb.hh; hh != nil {
			hh.Reset()
			hh.Write(hdr[4:20])
			hh.Write(subj)
			if hasHeaders {
				hh.Write(data[slen+4 : dlen-recordHashSize])
			} else {
				hh.Write(data[slen : dlen-recordHashSize])
			}
			checksum := hh.Sum(hb[:0])
			if !bytes.Equal(checksum, data[len(data)-recordHashSize:]) {
				truncate(index)
				return gatherLost(lbuf - index), tombstones, errBadMsg{mb.mfn, "invalid checksum"}
			}
			copy(mb.lchk[0:], checksum)
		}

		// Grab our sequence and timestamp.
		seq := le.Uint64(hdr[4:])
		ts := int64(le.Uint64(hdr[12:]))

		// Check if this is a delete tombstone.
		if seq&tbit != 0 {
			seq = seq &^ tbit
			// Need to process this here and make sure we have accounted for this properly.
			tombstones = append(tombstones, seq)
			if maxTombstoneSeq == 0 || seq > maxTombstoneSeq {
				maxTombstoneSeq, maxTombstoneTs = seq, ts
			}
			index += rl
			continue
		}

		fseq := atomic.LoadUint64(&mb.first.seq)
		// This is an old erased message, or a new one that we can track.
		if seq == 0 || seq&ebit != 0 || seq < fseq {
			seq = seq &^ ebit
			if seq >= fseq {
				updateLast(seq, ts)
				if mb.msgs == 0 {
					atomic.StoreUint64(&mb.first.seq, seq+1)
					mb.first.ts = 0
				} else if seq != 0 {
					// Only add to dmap if past recorded first seq and non-zero.
					addToDmap(seq)
				}
			}
			index += rl
			continue
		}

		// This is for when we have index info that adjusts for deleted messages
		// at the head. So the first.seq will be already set here. If this is larger
		// replace what we have with this seq.
		if firstNeedsSet && seq >= fseq {
			atomic.StoreUint64(&mb.first.seq, seq)
			firstNeedsSet, mb.first.ts = false, ts
		}

		// The sequence needs to only ever move up.
		if seq <= last {
			// Advance to next record.
			// We've already accounted for this sequence and marked it as deleted.
			index += rl
			continue
		}
		if !mb.dmap.Exists(seq) {
			mb.msgs++
			mb.bytes += uint64(rl)
		}

		updateLast(seq, ts)

		// Advance to next record.
		index += rl
	}

	// For empty msg blocks make sure we recover last seq correctly based off of first.
	// Or if we seem to have no messages but had a tombstone, which we use to remember
	// sequences and timestamps now, use that to properly setup the first and last.
	if mb.msgs == 0 {
		fseq := atomic.LoadUint64(&mb.first.seq)
		if fseq > 0 {
			atomic.StoreUint64(&mb.last.seq, fseq-1)
		} else if fseq == 0 && maxTombstoneSeq > 0 {
			atomic.StoreUint64(&mb.first.seq, maxTombstoneSeq+1)
			mb.first.ts = 0
			if lseq := atomic.LoadUint64(&mb.last.seq); lseq == 0 {
				atomic.StoreUint64(&mb.last.seq, maxTombstoneSeq)
				mb.last.ts = maxTombstoneTs
			}
		}
	}

	return nil, tombstones, nil
}

// For doing warn logging.
// Lock should be held.
func (fs *fileStore) warn(format string, args ...any) {
	// No-op if no server configured.
	if fs.srv == nil {
		return
	}
	fs.srv.Warnf(fmt.Sprintf("Filestore [%s] %s", fs.cfg.Name, format), args...)
}

// For doing debug logging.
// Lock should be held.
func (fs *fileStore) debug(format string, args ...any) {
	// No-op if no server configured.
	if fs.srv == nil {
		return
	}
	fs.srv.Debugf(fmt.Sprintf("Filestore [%s] %s", fs.cfg.Name, format), args...)
}

// Track local state but ignore timestamps here.
func updateTrackingState(state *StreamState, mb *msgBlock) {
	first := atomic.LoadUint64(&mb.first.seq)
	last := atomic.LoadUint64(&mb.last.seq)
	if state.FirstSeq == 0 {
		state.FirstSeq = first
	} else if first < state.FirstSeq && mb.first.ts != 0 {
		state.FirstSeq = first
	}
	if last > state.LastSeq {
		state.LastSeq = last
	}
	state.Msgs += mb.msgs
	state.Bytes += mb.bytes
}

// Determine if our tracking states are the same.
func trackingStatesEqual(fs, mb *StreamState) bool {
	// When a fs is brand new the fs state will have first seq of 0, but tracking mb may have 1.
	// If either has a first sequence that is not 0 or 1 we will check if they are the same, otherwise skip.
	if (fs.FirstSeq > 1 && mb.FirstSeq > 1) || mb.FirstSeq > 1 {
		return fs.Msgs == mb.Msgs && fs.FirstSeq == mb.FirstSeq && fs.LastSeq == mb.LastSeq && fs.Bytes == mb.Bytes
	}
	return fs.Msgs == mb.Msgs && fs.LastSeq == mb.LastSeq && fs.Bytes == mb.Bytes
}

// recoverFullState will attempt to receover our last full state and re-process any state changes
// that happened afterwards.
func (fs *fileStore) recoverFullState() (rerr error) {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	// Check for any left over purged messages.
	<-dios
	pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir)
	if _, err := os.Stat(pdir); err == nil {
		os.RemoveAll(pdir)
	}
	// Grab our stream state file and load it in.
	fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)
	buf, err := os.ReadFile(fn)
	dios <- struct{}{}

	if err != nil {
		if !os.IsNotExist(err) {
			fs.warn("Could not read stream state file: %v", err)
		}
		return err
	}

	const minLen = 32
	if len(buf) < minLen {
		os.Remove(fn)
		fs.warn("Stream state too short (%d bytes)", len(buf))
		return errCorruptState
	}

	// The highwayhash will be on the end. Check that it still matches.
	h := buf[len(buf)-highwayhash.Size64:]
	buf = buf[:len(buf)-highwayhash.Size64]
	fs.hh.Reset()
	fs.hh.Write(buf)
	var hb [highwayhash.Size64]byte
	if !bytes.Equal(h, fs.hh.Sum(hb[:0])) {
		os.Remove(fn)
		fs.warn("Stream state checksum did not match")
		return errCorruptState
	}

	// Decrypt if needed.
	// We can be setup for encryption but if this is a snapshot restore we will be missing the keyfile
	// since snapshots strip encryption.
	if fs.prf != nil && fs.aek != nil {
		ns := fs.aek.NonceSize()
		buf, err = fs.aek.Open(nil, buf[:ns], buf[ns:], nil)
		if err != nil {
			fs.warn("Stream state error reading encryption key: %v", err)
			return err
		}
	}

	version := buf[1]
	if buf[0] != fullStateMagic || version < fullStateMinVersion || version > fullStateVersion {
		os.Remove(fn)
		fs.warn("Stream state magic and version mismatch")
		return errCorruptState
	}

	bi := hdrLen

	readU64 := func() uint64 {
		if bi < 0 {
			return 0
		}
		v, n := binary.Uvarint(buf[bi:])
		if n <= 0 {
			bi = -1
			return 0
		}
		bi += n
		return v
	}
	readI64 := func() int64 {
		if bi < 0 {
			return 0
		}
		v, n := binary.Varint(buf[bi:])
		if n <= 0 {
			bi = -1
			return -1
		}
		bi += n
		return v
	}

	setTime := func(t *time.Time, ts int64) {
		if ts == 0 {
			*t = time.Time{}
		} else {
			*t = time.Unix(0, ts).UTC()
		}
	}

	var state StreamState
	state.Msgs = readU64()
	state.Bytes = readU64()
	state.FirstSeq = readU64()
	baseTime := readI64()
	setTime(&state.FirstTime, baseTime)
	state.LastSeq = readU64()
	setTime(&state.LastTime, readI64())

	// Check for per subject info.
	if numSubjects := int(readU64()); numSubjects > 0 {
		fs.psim, fs.tsl = fs.psim.Empty(), 0
		for i := 0; i < numSubjects; i++ {
			if lsubj := int(readU64()); lsubj > 0 {
				if bi+lsubj > len(buf) {
					os.Remove(fn)
					fs.warn("Stream state bad subject len (%d)", lsubj)
					return errCorruptState
				}
				// If we have lots of subjects this will alloc for each one.
				// We could reference the underlying buffer, but we could guess wrong if
				// number of blocks is large and subjects is low, since we would reference buf.
				subj := buf[bi : bi+lsubj]
				// We had a bug that could cause memory corruption in the PSIM that could have gotten stored to disk.
				// Only would affect subjects, so do quick check.
				if !isValidSubject(bytesToString(subj), true) {
					os.Remove(fn)
					fs.warn("Stream state corrupt subject detected")
					return errCorruptState
				}
				bi += lsubj
				psi := psi{total: readU64(), fblk: uint32(readU64())}
				if psi.total > 1 {
					psi.lblk = uint32(readU64())
				} else {
					psi.lblk = psi.fblk
				}
				fs.psim.Insert(subj, psi)
				fs.tsl += lsubj
			}
		}
	}

	// Track the state as represented by the blocks themselves.
	var mstate StreamState

	if numBlocks := readU64(); numBlocks > 0 {
		lastIndex := int(numBlocks - 1)
		fs.blks = make([]*msgBlock, 0, numBlocks)
		for i := 0; i < int(numBlocks); i++ {
			index, nbytes, fseq, fts, lseq, lts, numDeleted := uint32(readU64()), readU64(), readU64(), readI64(), readU64(), readI64(), readU64()
			var ttls uint64
			if version >= 2 {
				ttls = readU64()
			}
			var schedules uint64
			if version >= 3 {
				schedules = readU64()
			}
			if bi < 0 {
				os.Remove(fn)
				return errCorruptState
			}
			mb := fs.initMsgBlock(index)
			atomic.StoreUint64(&mb.first.seq, fseq)
			atomic.StoreUint64(&mb.last.seq, lseq)
			mb.msgs, mb.bytes = lseq-fseq+1, nbytes
			mb.first.ts, mb.last.ts = fts+baseTime, lts+baseTime
			mb.ttls = ttls
			mb.schedules = schedules
			if numDeleted > 0 {
				dmap, n, err := avl.Decode(buf[bi:])
				if err != nil {
					os.Remove(fn)
					fs.warn("Stream state error decoding avl dmap: %v", err)
					return errCorruptState
				}
				mb.dmap = *dmap
				if mb.msgs > numDeleted {
					mb.msgs -= numDeleted
				} else {
					mb.msgs = 0
				}
				bi += n
			}

			// Pre-emptively mark block as closed, we'll confirm this block
			// still exists on disk and report it as lost if not.
			mb.closed = true

			// Only add in if not empty or the lmb.
			if mb.msgs > 0 || i == lastIndex {
				fs.addMsgBlock(mb)
				updateTrackingState(&mstate, mb)
			} else {
				// Mark dirty to cleanup.
				fs.dirty++
			}
		}
	}

	// Pull in last block index for the block that had last checksum when we wrote the full state.
	blkIndex := uint32(readU64())
	var lchk [8]byte
	if bi+len(lchk) > len(buf) {
		bi = -1
	} else {
		copy(lchk[0:], buf[bi:bi+len(lchk)])
	}

	// Check if we had any errors.
	if bi < 0 {
		os.Remove(fn)
		fs.warn("Stream state has no checksum present")
		return errCorruptState
	}

	// Move into place our state, msgBlks and subject info.
	fs.state = state

	// First let's check the happy path, open the blk file that was the lmb when we created the full state.
	// See if we have the last block available.
	var matched bool
	mb := fs.lmb
	if mb == nil || mb.index != blkIndex {
		os.Remove(fn)
		fs.warn("Stream state block does not exist or index mismatch")
		return errCorruptState
	}
	if _, err := os.Stat(mb.mfn); err != nil && os.IsNotExist(err) {
		// If our saved state is past what we see on disk, fallback and rebuild.
		if ld, _, _ := mb.rebuildState(); ld != nil {
			fs.addLostData(ld)
		}
		fs.warn("Stream state detected prior state, could not locate msg block %d", blkIndex)
		return errPriorState
	}
	if matched = bytes.Equal(mb.lastChecksum(), lchk[:]); !matched {
		// Detected a stale index.db, we didn't write it upon shutdown so can't rely on it being correct.
		fs.warn("Stream state outdated, last block has additional entries, will rebuild")
		return errPriorState
	}

	// We need to see if any blocks exist after our last one even though we matched the last record exactly.
	mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
	var dirs []os.DirEntry

	<-dios
	if f, err := os.Open(mdir); err == nil {
		dirs, _ = f.ReadDir(-1)
		f.Close()
	}
	dios <- struct{}{}

	var index uint32
	for _, fi := range dirs {
		// Ensure it's actually a block file, otherwise fmt.Sscanf also matches %d.blk.tmp
		if !strings.HasSuffix(fi.Name(), blkSuffix) {
			continue
		}
		if n, err := fmt.Sscanf(fi.Name(), blkScan, &index); err == nil && n == 1 {
			if index > blkIndex {
				fs.warn("Stream state outdated, found extra blocks, will rebuild")
				return errPriorState
			} else if mb, ok := fs.bim[index]; ok {
				mb.closed = false
			}
		}
	}

	var rebuild bool
	for _, mb := range fs.blks {
		if mb.closed {
			rebuild = true
			if ld, _, _ := mb.rebuildState(); ld != nil {
				fs.addLostData(ld)
			}
			fs.warn("Stream state detected prior state, could not locate msg block %d", mb.index)
		}
	}
	if rebuild {
		return errPriorState
	}

	// We check first and last seq and number of msgs and bytes. If there is a difference,
	// return and error so we rebuild from the message block state on disk.
	if !trackingStatesEqual(&fs.state, &mstate) {
		os.Remove(fn)
		fs.warn("Stream state encountered internal inconsistency on recover")
		return errCorruptState
	}

	return nil
}

// Lock should be held.
func (fs *fileStore) recoverTTLState() error {
	// See if we have a timed hash wheel for TTLs.
	<-dios
	fn := filepath.Join(fs.fcfg.StoreDir, msgDir, ttlStreamStateFile)
	buf, err := os.ReadFile(fn)
	dios <- struct{}{}

	if err != nil && !os.IsNotExist(err) {
		return err
	}

	fs.ttls = thw.NewHashWheel()

	var ttlseq uint64
	if err == nil {
		ttlseq, err = fs.ttls.Decode(buf)
		if err != nil {
			fs.warn("Error decoding TTL state: %s", err)
			os.Remove(fn)
		}
	}

	if ttlseq < fs.state.FirstSeq {
		ttlseq = fs.state.FirstSeq
	}

	defer fs.resetAgeChk(0)
	if fs.state.Msgs > 0 && ttlseq <= fs.state.LastSeq {
		fs.warn("TTL state is outdated; attempting to recover using linear scan (seq %d to %d)", ttlseq, fs.state.LastSeq)
		var (
			mb     *msgBlock
			sm     StoreMsg
			mblseq uint64
		)
		for seq := ttlseq; seq <= fs.state.LastSeq; seq++ {
		retry:
			if mb == nil {
				if mb = fs.selectMsgBlock(seq); mb == nil {
					// Selecting the message block should return a block that contains this sequence,
					// or a later block if it can't be found.
					// It's an error if we can't find any block within the bounds of first and last seq.
					fs.warn("Error loading msg block with seq %d for recovering TTL", seq)
					continue
				}
				seq = atomic.LoadUint64(&mb.first.seq)
				mblseq = atomic.LoadUint64(&mb.last.seq)
			}
			if mb.ttls == 0 {
				// None of the messages in the block have message TTLs so don't
				// bother doing anything further with this block, skip to the end.
				seq = atomic.LoadUint64(&mb.last.seq) + 1
			}
			if seq > mblseq {
				// We've reached the end of the loaded block, so let's go back to the
				// beginning and process the next block.
				mb.tryForceExpireCache()
				mb = nil
				if seq <= fs.state.LastSeq {
					goto retry
				}
				// Done.
				break
			}
			msg, _, err := mb.fetchMsgNoCopy(seq, &sm)
			if err != nil {
				fs.warn("Error loading msg seq %d for recovering TTL: %s", seq, err)
				continue
			}
			if len(msg.hdr) == 0 {
				continue
			}
			if ttl, _ := getMessageTTL(msg.hdr); ttl > 0 {
				expires := time.Duration(msg.ts) + (time.Second * time.Duration(ttl))
				fs.ttls.Add(seq, int64(expires))
			}
		}
	}
	return nil
}

// Lock should be held.
func (fs *fileStore) recoverMsgSchedulingState() error {
	// See if we have a timed hash wheel for TTLs.
	<-dios
	fn := filepath.Join(fs.fcfg.StoreDir, msgDir, msgSchedulingStreamStateFile)
	buf, err := os.ReadFile(fn)
	dios <- struct{}{}

	if err != nil && !os.IsNotExist(err) {
		return err
	}

	fs.scheduling = newMsgScheduling(fs.runMsgScheduling)

	var schedSeq uint64
	if err == nil {
		schedSeq, err = fs.scheduling.decode(buf)
		if err != nil {
			fs.warn("Error decoding message scheduling state: %s", err)
			os.Remove(fn)
		}
	}

	if schedSeq < fs.state.FirstSeq {
		schedSeq = fs.state.FirstSeq
	}

	defer fs.scheduling.resetTimer()
	if fs.state.Msgs > 0 && schedSeq <= fs.state.LastSeq {
		fs.warn("Message scheduling state is outdated; attempting to recover using linear scan (seq %d to %d)", schedSeq, fs.state.LastSeq)
		var (
			mb     *msgBlock
			sm     StoreMsg
			mblseq uint64
		)
		for seq := schedSeq; seq <= fs.state.LastSeq; seq++ {
		retry:
			if mb == nil {
				if mb = fs.selectMsgBlock(seq); mb == nil {
					// Selecting the message block should return a block that contains this sequence,
					// or a later block if it can't be found.
					// It's an error if we can't find any block within the bounds of first and last seq.
					fs.warn("Error loading msg block with seq %d for recovering message schedules", seq)
					continue
				}
				seq = atomic.LoadUint64(&mb.first.seq)
				mblseq = atomic.LoadUint64(&mb.last.seq)
			}
			if mb.schedules == 0 {
				// None of the messages in the block have message schedules, so don't
				// bother doing anything further with this block, skip to the end.
				seq = atomic.LoadUint64(&mb.last.seq) + 1
			}
			if seq > mblseq {
				// We've reached the end of the loaded block, so let's go back to the
				// beginning and process the next block.
				mb.tryForceExpireCache()
				mb = nil
				if seq <= fs.state.LastSeq {
					goto retry
				}
				// Done.
				break
			}
			msg, _, err := mb.fetchMsgNoCopy(seq, &sm)
			if err != nil {
				fs.warn("Error loading msg seq %d for recovering message schedules: %s", seq, err)
				continue
			}
			if len(msg.hdr) == 0 {
				continue
			}
			if schedule, ok := getMessageSchedule(sm.hdr); ok && !schedule.IsZero() {
				fs.scheduling.init(seq, sm.subj, schedule.UnixNano())
			}
		}
	}
	return nil
}

// Grabs last checksum for the named block file.
// Takes into account encryption etc.
func (mb *msgBlock) lastChecksum() []byte {
	f, err := mb.openBlock()
	if err != nil {
		return nil
	}
	defer f.Close()

	var lchk [8]byte
	if fi, _ := f.Stat(); fi != nil {
		mb.rbytes = uint64(fi.Size())
	}
	if mb.rbytes < checksumSize {
		return lchk[:]
	}
	// Encrypted?
	if err := mb.checkAndLoadEncryption(); err != nil {
		return nil
	}
	if mb.bek != nil {
		if buf, _ := mb.loadBlock(nil); len(buf) >= checksumSize {
			if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
				return nil
			}
			copy(lchk[0:], buf[len(buf)-checksumSize:])
		}
	} else {
		f.ReadAt(lchk[:], int64(mb.rbytes)-checksumSize)
	}
	return lchk[:]
}

// This will make sure we clean up old idx and fss files.
func (fs *fileStore) cleanupOldMeta() {
	fs.mu.RLock()
	mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
	fs.mu.RUnlock()

	<-dios
	f, err := os.Open(mdir)
	dios <- struct{}{}
	if err != nil {
		return
	}

	dirs, _ := f.ReadDir(-1)
	f.Close()

	const (
		minLen    = 4
		idxSuffix = ".idx"
		fssSuffix = ".fss"
	)
	for _, fi := range dirs {
		if name := fi.Name(); strings.HasSuffix(name, idxSuffix) || strings.HasSuffix(name, fssSuffix) {
			os.Remove(filepath.Join(mdir, name))
		}
	}
}

func (fs *fileStore) recoverMsgs() error {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	// Check for any left over purged messages.
	<-dios
	pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir)
	if _, err := os.Stat(pdir); err == nil {
		os.RemoveAll(pdir)
	}
	mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
	f, err := os.Open(mdir)
	if err != nil {
		dios <- struct{}{}
		return errNotReadable
	}
	dirs, err := f.ReadDir(-1)
	f.Close()
	dios <- struct{}{}

	if err != nil {
		return errNotReadable
	}

	indices := make(sort.IntSlice, 0, len(dirs))
	var index int
	for _, fi := range dirs {
		// Ensure it's actually a block file, otherwise fmt.Sscanf also matches %d.blk.tmp
		if !strings.HasSuffix(fi.Name(), blkSuffix) {
			continue
		}
		if n, err := fmt.Sscanf(fi.Name(), blkScan, &index); err == nil && n == 1 {
			indices = append(indices, index)
		}
	}
	indices.Sort()

	// Recover all of the msg blocks.
	// We now guarantee they are coming in order.
	for _, index := range indices {
		if mb, err := fs.recoverMsgBlock(uint32(index)); err == nil && mb != nil {
			// This is a truncate block with possibly no index. If the OS got shutdown
			// out from underneath of us this is possible.
			mb.mu.Lock()
			if atomic.LoadUint64(&mb.first.seq) == 0 {
				mb.dirtyCloseWithRemove(true)
				fs.removeMsgBlockFromList(mb)
				mb.mu.Unlock()
				continue
			}
			// If the stream is empty, reset the first/last sequences so these can
			// properly move up based purely on tombstones spread over multiple blocks.
			if fs.state.Msgs == 0 {
				fs.state.FirstSeq, fs.state.LastSeq = 0, 0
				fs.state.FirstTime, fs.state.LastTime = time.Time{}, time.Time{}
			}
			fseq := atomic.LoadUint64(&mb.first.seq)
			if fs.state.FirstSeq == 0 || (fseq < fs.state.FirstSeq && mb.first.ts != 0) {
				fs.state.FirstSeq = fseq
				if mb.first.ts == 0 {
					fs.state.FirstTime = time.Time{}
				} else {
					fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
				}
			}
			// Preserve last time, could have erased the last message in one block, and then
			// have a tombstone with the proper timestamp afterward in another block
			if lseq := atomic.LoadUint64(&mb.last.seq); lseq >= fs.state.LastSeq {
				fs.state.LastSeq = lseq
				if mb.last.ts == 0 {
					fs.state.LastTime = time.Time{}
				} else {
					fs.state.LastTime = time.Unix(0, mb.last.ts).UTC()
				}
			}
			fs.state.Msgs += mb.msgs
			fs.state.Bytes += mb.bytes
			// If the block is empty, correct the sequences to be aligned with the current filestore state.
			if mb.msgs == 0 {
				atomic.StoreUint64(&mb.first.seq, fs.state.LastSeq+1)
				mb.first.ts = 0
				atomic.StoreUint64(&mb.last.seq, fs.state.LastSeq)
				mb.last.ts = fs.state.LastTime.UnixNano()
			}
			mb.mu.Unlock()
		} else {
			return err
		}
	}

	if len(fs.blks) > 0 {
		fs.lmb = fs.blks[len(fs.blks)-1]
	} else {
		_, err = fs.newMsgBlockForWrite()
	}

	// Check if we encountered any lost data.
	if fs.ld != nil {
		var emptyBlks []*msgBlock
		for _, mb := range fs.blks {
			if mb.msgs == 0 && mb.rbytes == 0 && mb != fs.lmb {
				emptyBlks = append(emptyBlks, mb)
			}
		}
		for _, mb := range emptyBlks {
			// Need the mb lock here.
			mb.mu.Lock()
			fs.removeMsgBlock(mb)
			mb.mu.Unlock()
		}
	}

	if err != nil {
		return err
	}

	// Check for keyfiles orphans.
	if kms, err := filepath.Glob(filepath.Join(mdir, keyScanAll)); err == nil && len(kms) > 0 {
		valid := make(map[uint32]bool)
		for _, mb := range fs.blks {
			valid[mb.index] = true
		}
		for _, fn := range kms {
			var index uint32
			shouldRemove := true
			if n, err := fmt.Sscanf(filepath.Base(fn), keyScan, &index); err == nil && n == 1 && valid[index] {
				shouldRemove = false
			}
			if shouldRemove {
				os.Remove(fn)
			}
		}
	}

	return nil
}

// Will expire msgs that have aged out on restart.
// We will treat this differently in case we have a recovery
// that will expire alot of messages on startup.
// Should only be called on startup.
func (fs *fileStore) expireMsgsOnRecover() error {
	if fs.state.Msgs == 0 {
		return nil
	}

	// If subject delete markers is configured, can't expire on recover.
	// When clustered we need to go through proposals.
	if fs.cfg.SubjectDeleteMarkerTTL > 0 {
		return nil
	}

	var minAge = time.Now().UnixNano() - int64(fs.cfg.MaxAge)
	var purged, bytes uint64
	var deleted int
	var nts int64

	// If we expire all make sure to write out a tombstone. Need to be done by hand here,
	// usually taken care of by fs.removeMsgBlock() but we do not call that here.
	var last msgId

	deleteEmptyBlock := func(mb *msgBlock) error {
		// If we are the last keep state to remember first/last sequence.
		// Do this part by hand since not deleting one by one.
		if mb == fs.lmb {
			last.seq = atomic.LoadUint64(&mb.last.seq)
			last.ts = mb.last.ts
		}
		// Make sure we do subject cleanup as well.
		mb.ensurePerSubjectInfoLoaded()
		mb.fss.IterOrdered(func(bsubj []byte, ss *SimpleState) bool {
			subj := bytesToString(bsubj)
			for i := uint64(0); i < ss.Msgs; i++ {
				fs.removePerSubject(subj)
			}
			return true
		})
		err := mb.dirtyCloseWithRemove(true)
		if isPermissionError(err) {
			return err
		}
		deleted++
		return nil
	}

	for _, mb := range fs.blks {
		mb.mu.Lock()
		if minAge < mb.first.ts {
			nts = mb.first.ts
			mb.mu.Unlock()
			break
		}
		// Can we remove whole block here?
		if mb.last.ts <= minAge {
			purged += mb.msgs
			bytes += mb.bytes
			err := deleteEmptyBlock(mb)
			mb.mu.Unlock()
			if isPermissionError(err) {
				return err
			}
			continue
		}

		// If we are here we have to process the interior messages of this blk.
		// This will load fss as well.
		if err := mb.loadMsgsWithLock(); err != nil {
			mb.mu.Unlock()
			break
		}

		var smv StoreMsg
		var needNextFirst bool

		// Walk messages and remove if expired.
		fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
		for seq := fseq; seq <= lseq; seq++ {
			sm, err := mb.cacheLookupNoCopy(seq, &smv)
			// Process interior deleted msgs.
			if err == errDeletedMsg {
				// Update dmap.
				if mb.dmap.Exists(seq) {
					mb.dmap.Delete(seq)
				}
				// Keep this updated just in case since we are removing dmap entries.
				atomic.StoreUint64(&mb.first.seq, seq)
				needNextFirst = true
				continue
			}
			// Break on other errors.
			if err != nil || sm == nil {
				atomic.StoreUint64(&mb.first.seq, seq)
				needNextFirst = true
				break
			}

			// No error and sm != nil from here onward.

			// Check for done.
			if minAge < sm.ts {
				atomic.StoreUint64(&mb.first.seq, sm.seq)
				mb.first.ts = sm.ts
				needNextFirst = false
				nts = sm.ts
				break
			}

			// Delete the message here.
			if mb.msgs > 0 {
				atomic.StoreUint64(&mb.first.seq, seq)
				needNextFirst = true
				sz := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg)
				if sz > mb.bytes {
					sz = mb.bytes
				}
				mb.bytes -= sz
				bytes += sz
				mb.msgs--
				purged++
			}
			// Update fss
			// Make sure we have fss loaded.
			mb.removeSeqPerSubject(sm.subj, seq)
			fs.removePerSubject(sm.subj)
		}
		// Make sure we have a proper next first sequence.
		if needNextFirst {
			mb.selectNextFirst()
		}
		// Check if empty after processing, could happen if tail of messages are all deleted.
		if mb.msgs == 0 {
			deleteEmptyBlock(mb)
		}
		mb.finishedWithCache()
		mb.mu.Unlock()
		break
	}

	if nts > 0 {
		// Make sure to set age check based on this value.
		fs.resetAgeChk(nts - minAge)
	}

	if deleted > 0 {
		// Update block map.
		if fs.bim != nil {
			for _, mb := range fs.blks[:deleted] {
				delete(fs.bim, mb.index)
			}
		}
		// Update blks slice.
		fs.blks = copyMsgBlocks(fs.blks[deleted:])
		if lb := len(fs.blks); lb == 0 {
			fs.lmb = nil
		} else {
			fs.lmb = fs.blks[lb-1]
		}
	}
	// Update top level accounting.
	if purged < fs.state.Msgs {
		fs.state.Msgs -= purged
	} else {
		fs.state.Msgs = 0
	}
	if bytes < fs.state.Bytes {
		fs.state.Bytes -= bytes
	} else {
		fs.state.Bytes = 0
	}
	// Make sure to we properly set the fs first sequence and timestamp.
	fs.selectNextFirst()

	// Check if we have no messages and blocks left.

	if fs.lmb == nil && last.seq != 0 {
		if lmb, _ := fs.newMsgBlockForWrite(); lmb != nil {
			fs.writeTombstone(last.seq, last.ts)
		}
		// Clear any global subject state.
		fs.psim, fs.tsl = fs.psim.Empty(), 0
	}

	// If we purged anything, make sure we kick flush state loop.
	if purged > 0 {
		fs.dirty++
	}
	return nil
}

func copyMsgBlocks(src []*msgBlock) []*msgBlock {
	if src == nil {
		return nil
	}
	dst := make([]*msgBlock, len(src))
	copy(dst, src)
	return dst
}

// GetSeqFromTime looks for the first sequence number that has
// the message with >= timestamp.
func (fs *fileStore) GetSeqFromTime(t time.Time) uint64 {
	fs.mu.RLock()
	lastSeq := fs.state.LastSeq
	closed := fs.isClosed()
	fs.mu.RUnlock()

	if closed {
		return 0
	}

	mb := fs.selectMsgBlockForStart(t)
	if mb == nil {
		return lastSeq + 1
	}

	fseq := atomic.LoadUint64(&mb.first.seq)
	lseq := atomic.LoadUint64(&mb.last.seq)

	var (
		smv  StoreMsg
		cts  int64
		cseq uint64
		off  uint64
	)
	ts := t.UnixNano()

	// Using a binary search, but need to be aware of interior deletes in the block.
	seq := lseq + 1
loop:
	for fseq <= lseq {
		mid := fseq + (lseq-fseq)/2
		off = 0
		// Potentially skip over gaps. We keep the original middle but keep track of a
		// potential delete range with an offset.
		for {
			sm, _, err := mb.fetchMsgNoCopy(mid+off, &smv)
			if err != nil || sm == nil {
				off++
				if mid+off <= lseq {
					continue
				} else {
					// Continue search to the left. Purposely ignore the skipped deletes here.
					lseq = mid - 1
					continue loop
				}
			}
			cts = sm.ts
			cseq = sm.seq
			break
		}
		if cts >= ts {
			seq = cseq
			if mid == fseq {
				break
			}
			// Continue search to the left.
			lseq = mid - 1
		} else {
			// Continue search to the right (potentially skipping over interior deletes).
			fseq = mid + off + 1
		}
	}
	return seq
}

// Find the first matching message against a sublist.
func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
	mb.mu.Lock()
	var didLoad bool
	var updateLLTS bool
	defer func() {
		if updateLLTS {
			mb.llts = ats.AccessTime()
		}
		mb.finishedWithCache()
		mb.mu.Unlock()
	}()

	if mb.fssNotLoaded() {
		// Make sure we have fss loaded.
		if err := mb.loadMsgsWithLock(); err != nil {
			return nil, false, err
		}
		didLoad = true
	}
	// Mark fss activity.
	mb.lsts = ats.AccessTime()

	// Make sure to start at mb.first.seq if fseq < mb.first.seq
	if seq := atomic.LoadUint64(&mb.first.seq); seq > start {
		start = seq
	}
	lseq := atomic.LoadUint64(&mb.last.seq)

	// If the FSS state has fewer entries than sequences in the linear scan,
	// then use intersection instead as likely going to be cheaper. This will
	// often be the case with high numbers of deletes, as well as a smaller
	// number of subjects in the block.
	if uint64(mb.fss.Size()) < lseq-start {
		// If there are no subject matches then this is effectively no-op.
		hseq := uint64(math.MaxUint64)
		var ierr error
		stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) {
			if ierr != nil {
				return
			}
			if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
				// mb is already loaded into the cache so should be fast-ish.
				mb.recalculateForSubj(bytesToString(subj), ss)
			}
			first := max(start, ss.First)
			if first > ss.Last || first >= hseq {
				// The start cutoff is after the last sequence for this subject,
				// or we think we already know of a subject with an earlier msg
				// than our first seq for this subject.
				return
			}
			// Need messages loaded from here on out.
			if mb.cacheNotLoaded() {
				if ierr = mb.loadMsgsWithLock(); ierr != nil {
					return
				}
				didLoad = true
			}
			if sm == nil {
				sm = new(StoreMsg)
			}
			if first == ss.First {
				// If the start floor is below where this subject starts then we can
				// short-circuit, avoiding needing to scan for the next message.
				if fsm, err := mb.cacheLookup(ss.First, sm); err == nil {
					sm = fsm
					hseq = ss.First
				}
				return
			}
			for seq := first; seq <= ss.Last; seq++ {
				// Otherwise we have a start floor that intersects where this subject
				// has messages in the block, so we need to walk up until we find a
				// message matching the subject.
				if mb.dmap.Exists(seq) {
					// Optimisation to avoid calling cacheLookup which hits time.Now().
					// Instead we will update it only once in a defer.
					updateLLTS = true
					continue
				}
				llseq := mb.llseq
				fsm, err := mb.cacheLookup(seq, sm)
				if err != nil {
					continue
				}
				updateLLTS = false // cacheLookup already updated it.
				if sl.HasInterest(fsm.subj) {
					hseq = seq
					sm = fsm
					break
				}
				// If we are here we did not match, so put the llseq back.
				mb.llseq = llseq
			}
		})
		if ierr != nil {
			return nil, false, ierr
		}
		if hseq < uint64(math.MaxUint64) && sm != nil {
			return sm, didLoad && start == lseq, nil
		}
	} else {
		// Need messages loaded from here on out.
		if mb.cacheNotLoaded() {
			if err := mb.loadMsgsWithLock(); err != nil {
				return nil, false, err
			}
			didLoad = true
		}
		if sm == nil {
			sm = new(StoreMsg)
		}

		for seq := start; seq <= lseq; seq++ {
			if mb.dmap.Exists(seq) {
				// Optimisation to avoid calling cacheLookup which hits time.Now().
				// Instead we will update it only once in a defer.
				updateLLTS = true
				continue
			}
			llseq := mb.llseq
			fsm, err := mb.cacheLookup(seq, sm)
			if err != nil {
				continue
			}
			expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq
			updateLLTS = false // cacheLookup already updated it.
			if sl.HasInterest(fsm.subj) {
				return fsm, expireOk, nil
			}
			// If we are here we did not match, so put the llseq back.
			mb.llseq = llseq
		}
	}

	return nil, didLoad, ErrStoreMsgNotFound
}

// Find the first matching message.
// fs lock should be held.
func (mb *msgBlock) firstMatching(filter string, wc bool, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
	mb.mu.Lock()
	var updateLLTS bool
	defer func() {
		if updateLLTS {
			mb.llts = ats.AccessTime()
		}
		mb.finishedWithCache()
		mb.mu.Unlock()
	}()

	fseq, isAll := start, filter == _EMPTY_ || filter == fwcs

	var didLoad bool
	if mb.fssNotLoaded() {
		// Make sure we have fss loaded.
		if err := mb.loadMsgsWithLock(); err != nil {
			return nil, false, err
		}
		didLoad = true
	}
	// Mark fss activity.
	mb.lsts = ats.AccessTime()

	if filter == _EMPTY_ {
		filter = fwcs
		wc = true
	}

	// If we only have 1 subject currently and it matches our filter we can also set isAll.
	if !isAll && mb.fss.Size() == 1 {
		if !wc {
			_, isAll = mb.fss.Find(stringToBytes(filter))
		} else {
			// Since mb.fss.Find won't work if filter is a wildcard, need to use Match instead.
			mb.fss.Match(stringToBytes(filter), func(subject []byte, _ *SimpleState) {
				isAll = true
			})
		}
		// If the only subject in this block isn't our filter, can simply short-circuit.
		if !isAll {
			return nil, didLoad, ErrStoreMsgNotFound
		}
	}
	// Make sure to start at mb.first.seq if fseq < mb.first.seq
	fseq = max(fseq, atomic.LoadUint64(&mb.first.seq))
	lseq := atomic.LoadUint64(&mb.last.seq)

	// Optionally build the isMatch for wildcard filters.
	var isMatch func(subj string) bool
	// Decide to build.
	if wc {
		_tsa, _fsa := [32]string{}, [32]string{}
		tsa, fsa := _tsa[:0], tokenizeSubjectIntoSlice(_fsa[:0], filter)
		isMatch = func(subj string) bool {
			tsa = tokenizeSubjectIntoSlice(tsa[:0], subj)
			return isSubsetMatchTokenized(tsa, fsa)
		}
	}

	subjs := mb.fs.cfg.Subjects
	// If isAll or our single filter matches the filter arg do linear scan.
	doLinearScan := isAll || (wc && len(subjs) == 1 && subjs[0] == filter)
	// If we do not think we should do a linear scan check how many fss we
	// would need to scan vs the full range of the linear walk. Optimize for
	// 25th quantile of a match in a linear walk. Filter should be a wildcard.
	// We should consult fss if our cache is not loaded and we only have fss loaded.
	if !doLinearScan && wc && mb.cacheAlreadyLoaded() {
		doLinearScan = mb.fss.Size()*4 > int(lseq-fseq)
	}

	if !doLinearScan {
		// If we have a wildcard match against all tracked subjects we know about.
		fseq = lseq + 1
		if bfilter := stringToBytes(filter); wc {
			mb.fss.Match(bfilter, func(bsubj []byte, ss *SimpleState) {
				if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
					mb.recalculateForSubj(bytesToString(bsubj), ss)
				}
				if start <= ss.Last {
					fseq = min(fseq, max(start, ss.First))
				}
			})
		} else if ss, _ := mb.fss.Find(bfilter); ss != nil {
			if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
				mb.recalculateForSubj(filter, ss)
			}
			if start <= ss.Last {
				fseq = min(fseq, max(start, ss.First))
			}
		}
	}

	if fseq > lseq {
		return nil, didLoad, ErrStoreMsgNotFound
	}

	// Need messages loaded from here on out.
	if mb.cacheNotLoaded() {
		if err := mb.loadMsgsWithLock(); err != nil {
			return nil, false, err
		}
		didLoad = true
	}

	if sm == nil {
		sm = new(StoreMsg)
	}

	for seq := fseq; seq <= lseq; seq++ {
		if mb.dmap.Exists(seq) {
			// Optimisation to avoid calling cacheLookup which hits time.Now().
			// Instead we will update it only once in a defer.
			updateLLTS = true
			continue
		}
		llseq := mb.llseq
		fsm, err := mb.cacheLookup(seq, sm)
		if err != nil {
			if err == errPartialCache || err == errNoCache {
				return nil, false, err
			}
			continue
		}
		updateLLTS = false // cacheLookup already updated it.
		expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq
		if isAll {
			return fsm, expireOk, nil
		}
		if wc && isMatch(sm.subj) {
			return fsm, expireOk, nil
		} else if !wc && fsm.subj == filter {
			return fsm, expireOk, nil
		}
		// If we are here we did not match, so put the llseq back.
		mb.llseq = llseq
	}

	return nil, didLoad, ErrStoreMsgNotFound
}

// Find the previous matching message against a sublist, working BACKWARDS from start.
func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
	mb.mu.Lock()
	var didLoad bool
	var updateLLTS bool
	defer func() {
		if updateLLTS {
			mb.llts = ats.AccessTime()
		}
		mb.finishedWithCache()
		mb.mu.Unlock()
	}()

	// Need messages loaded from here on out.
	if mb.cacheNotLoaded() {
		if err := mb.loadMsgsWithLock(); err != nil {
			return nil, false, err
		}
		didLoad = true
	}

	// Make sure to start at mb.last.seq if lseq < mb.last.seq
	if seq := atomic.LoadUint64(&mb.last.seq); start > seq {
		start = seq
	}
	lseq := atomic.LoadUint64(&mb.first.seq)

	if sm == nil {
		sm = new(StoreMsg)
	}

	// If the FSS state has fewer entries than sequences in the linear scan,
	// then use intersection instead as likely going to be cheaper. This will
	// often be the case with high numbers of deletes, as well as a smaller
	// number of subjects in the block.
	if uint64(mb.fss.Size()) < start-lseq {
		// If there are no subject matches then this is effectively no-op.
		hseq := uint64(0)
		stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) {
			if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
				// mb is already loaded into the cache so should be fast-ish.
				mb.recalculateForSubj(bytesToString(subj), ss)
			}
			first := min(start, ss.Last)
			// Skip if cutoff is before this subject's first, or if we already
			// have a higher-or-equal candidate (hseq holds the highest found).
			if first < ss.First || first <= hseq {
				// The start cutoff is before the first sequence for this subject,
				// or we already know of a subject with a later-or-equal msg.
				return
			}
			if first == ss.Last {
				// If the start floor is above where this subject starts then we can
				// short-circuit, avoiding needing to scan for the next message.
				if fsm, err := mb.cacheLookup(ss.Last, sm); err == nil {
					sm = fsm
					hseq = ss.Last
				}
				return
			}
			for seq := first; seq >= ss.First; seq-- {
				// Otherwise we have a start floor that intersects where this subject
				// has messages in the block, so we need to walk up until we find a
				// message matching the subject.
				if mb.dmap.Exists(seq) {
					// Optimisation to avoid calling cacheLookup which hits time.Now().
					// Instead we will update it only once in a defer.
					updateLLTS = true
					continue
				}
				llseq := mb.llseq
				fsm, err := mb.cacheLookup(seq, sm)
				if err != nil {
					continue
				}
				updateLLTS = false // cacheLookup already updated it.
				if sl.HasInterest(fsm.subj) {
					hseq = seq
					sm = fsm
					break
				}
				// If we are here we did not match, so put the llseq back.
				mb.llseq = llseq
			}
		})
		if hseq > 0 && sm != nil {
			return sm, didLoad && start == lseq, nil
		}
	} else {
		for seq := start; seq >= lseq; seq-- {
			if mb.dmap.Exists(seq) {
				// Optimisation to avoid calling cacheLookup which hits time.Now().
				// Instead we will update it only once in a defer.
				updateLLTS = true
				continue
			}
			llseq := mb.llseq
			fsm, err := mb.cacheLookup(seq, sm)
			if err != nil {
				continue
			}
			expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq
			updateLLTS = false // cacheLookup already updated it.
			if sl.HasInterest(fsm.subj) {
				return fsm, expireOk, nil
			}
			// If we are here we did not match, so put the llseq back.
			mb.llseq = llseq
		}
	}

	return nil, didLoad, ErrStoreMsgNotFound
}

// This will traverse a message block and generate the filtered pending.
func (mb *msgBlock) filteredPending(subj string, wc bool, seq uint64) (total, first, last uint64) {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	return mb.filteredPendingLocked(subj, wc, seq)
}

// This will traverse a message block and generate the filtered pending.
// Lock should be held.
func (mb *msgBlock) filteredPendingLocked(filter string, wc bool, sseq uint64) (total, first, last uint64) {
	isAll := filter == _EMPTY_ || filter == fwcs

	// First check if we can optimize this part.
	// This means we want all and the starting sequence was before this block.
	if isAll {
		if fseq := atomic.LoadUint64(&mb.first.seq); sseq <= fseq {
			return mb.msgs, fseq, atomic.LoadUint64(&mb.last.seq)
		}
	}

	if filter == _EMPTY_ {
		filter, wc = fwcs, true
	}

	update := func(ss *SimpleState) {
		total += ss.Msgs
		if first == 0 || ss.First < first {
			first = ss.First
		}
		if ss.Last > last {
			last = ss.Last
		}
	}

	// Make sure we have fss loaded.
	mb.ensurePerSubjectInfoLoaded()

	var havePartial bool

	// If we are not a wildcard just use Find() here. Avoids allocations.
	if !wc {
		if ss, ok := mb.fss.Find(stringToBytes(filter)); ok && ss != nil {
			if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
				mb.recalculateForSubj(filter, ss)
			}
			if sseq <= ss.First {
				update(ss)
			} else if sseq <= ss.Last {
				// We matched but its a partial.
				havePartial = true
			}
		}
	} else {
		mb.fss.Match(stringToBytes(filter), func(bsubj []byte, ss *SimpleState) {
			if havePartial {
				// If we already found a partial then don't do anything else.
				return
			}
			if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
				mb.recalculateForSubj(bytesToString(bsubj), ss)
			}
			if sseq <= ss.First {
				update(ss)
			} else if sseq <= ss.Last {
				// We matched but its a partial.
				havePartial = true
			}
		})
	}

	// If we did not encounter any partials we can return here.
	if !havePartial {
		return total, first, last
	}

	// If we are here we need to scan the msgs.
	// Clear what we had.
	total, first, last = 0, 0, 0

	// If we load the cache for a linear scan we want to expire that cache upon exit.
	var shouldExpire bool
	if mb.cacheNotLoaded() {
		mb.loadMsgsWithLock()
		shouldExpire = true
	}
	defer mb.finishedWithCache()

	_tsa, _fsa := [32]string{}, [32]string{}
	tsa, fsa := _tsa[:0], _fsa[:0]
	var isMatch func(subj string) bool

	if !wc {
		isMatch = func(subj string) bool { return subj == filter }
	} else {
		fsa = tokenizeSubjectIntoSlice(fsa[:0], filter)
		isMatch = func(subj string) bool {
			tsa = tokenizeSubjectIntoSlice(tsa[:0], subj)
			return isSubsetMatchTokenized(tsa, fsa)
		}
	}

	// 1. See if we match any subs from fss.
	// 2. If we match and the sseq is past ss.Last then we can use meta only.
	// 3. If we match and we need to do a partial, break and clear any totals and do a full scan like num pending.

	var smv StoreMsg
	for seq, lseq := sseq, atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
		sm, _ := mb.cacheLookupNoCopy(seq, &smv)
		if sm == nil {
			continue
		}
		if isAll || isMatch(sm.subj) {
			total++
			if first == 0 || seq < first {
				first = seq
			}
			if seq > last {
				last = seq
			}
		}
	}
	// If we loaded this block for this operation go ahead and expire it here.
	if shouldExpire {
		mb.tryForceExpireCacheLocked()
	}

	return total, first, last
}

// FilteredState will return the SimpleState associated with the filtered subject and a proposed starting sequence.
func (fs *fileStore) FilteredState(sseq uint64, subj string) SimpleState {
	fs.mu.RLock()
	defer fs.mu.RUnlock()

	lseq := fs.state.LastSeq
	if sseq < fs.state.FirstSeq {
		sseq = fs.state.FirstSeq
	}

	// Returned state.
	var ss SimpleState

	// If past the end no results.
	if sseq > lseq {
		// Make sure we track sequences
		ss.First = fs.state.FirstSeq
		ss.Last = fs.state.LastSeq
		return ss
	}

	// If we want all msgs that match we can shortcircuit.
	// TODO(dlc) - This can be extended for all cases but would
	// need to be careful on total msgs calculations etc.
	if sseq == fs.state.FirstSeq {
		fs.numFilteredPending(subj, &ss)
	} else {
		wc := subjectHasWildcard(subj)
		// Tracking subject state.
		// TODO(dlc) - Optimize for 2.10 with avl tree and no atomics per block.
		for _, mb := range fs.blks {
			// Skip blocks that are less than our starting sequence.
			if sseq > atomic.LoadUint64(&mb.last.seq) {
				continue
			}
			t, f, l := mb.filteredPending(subj, wc, sseq)
			ss.Msgs += t
			if ss.First == 0 || (f > 0 && f < ss.First) {
				ss.First = f
			}
			if l > ss.Last {
				ss.Last = l
			}
		}
	}

	return ss
}

// This is used to see if we can selectively jump start blocks based on filter subject and a starting block index.
// Will return -1 and ErrStoreEOF if no matches at all or no more from where we are.
func (fs *fileStore) checkSkipFirstBlock(filter string, wc bool, bi int) (int, error) {
	// If we match everything, just move to next blk.
	if filter == _EMPTY_ || filter == fwcs {
		return bi + 1, nil
	}
	// Move through psim to gather start and stop bounds.
	start, stop := uint32(math.MaxUint32), uint32(0)
	if wc {
		fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) {
			if psi.fblk < start {
				start = psi.fblk
			}
			if psi.lblk > stop {
				stop = psi.lblk
			}
		})
	} else if psi, ok := fs.psim.Find(stringToBytes(filter)); ok {
		start, stop = psi.fblk, psi.lblk
	}
	// Nothing was found.
	if start == uint32(math.MaxUint32) {
		return -1, ErrStoreEOF
	}
	return fs.selectSkipFirstBlock(bi, start, stop)
}

// This is used to see if we can selectively jump start blocks based on filter subjects and a starting block index.
// Will return -1 and ErrStoreEOF if no matches at all or no more from where we are.
func (fs *fileStore) checkSkipFirstBlockMulti(sl *gsl.SimpleSublist, bi int) (int, error) {
	// Move through psim to gather start and stop bounds.
	start, stop := uint32(math.MaxUint32), uint32(0)
	stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
		if psi.fblk < start {
			start = psi.fblk
		}
		if psi.lblk > stop {
			stop = psi.lblk
		}
	})
	// Nothing was found.
	if start == uint32(math.MaxUint32) {
		return -1, ErrStoreEOF
	}
	return fs.selectSkipFirstBlock(bi, start, stop)
}

func (fs *fileStore) selectSkipFirstBlock(bi int, start, stop uint32) (int, error) {
	// Can not be nil so ok to inline dereference.
	mbi := fs.blks[bi].getIndex()
	// All matching msgs are behind us.
	// Less than AND equal is important because we were called because we missed searching bi.
	if stop <= mbi {
		return -1, ErrStoreEOF
	}
	// If start is > index return dereference of fs.blks index.
	if start > mbi {
		if mb := fs.bim[start]; mb != nil {
			ni, _ := fs.selectMsgBlockWithIndex(atomic.LoadUint64(&mb.last.seq))
			return ni, nil
		}
	}
	// Otherwise just bump to the next one.
	return bi + 1, nil
}

// Optimized way for getting all num pending matching a filter subject.
// Lock should be held.
func (fs *fileStore) numFilteredPending(filter string, ss *SimpleState) {
	fs.numFilteredPendingWithLast(filter, true, ss)
}

// Optimized way for getting all num pending matching a filter subject and first sequence only.
// Lock should be held.
func (fs *fileStore) numFilteredPendingNoLast(filter string, ss *SimpleState) {
	fs.numFilteredPendingWithLast(filter, false, ss)
}

// Optimized way for getting all num pending matching a filter subject.
// Optionally look up last sequence. Sometimes do not need last and this avoids cost.
// Read lock should be held.
func (fs *fileStore) numFilteredPendingWithLast(filter string, last bool, ss *SimpleState) {
	isAll := filter == _EMPTY_ || filter == fwcs

	// If isAll we do not need to do anything special to calculate the first and last and total.
	if isAll {
		ss.First = fs.state.FirstSeq
		ss.Last = fs.state.LastSeq
		ss.Msgs = fs.state.Msgs
		return
	}
	// Always reset.
	ss.First, ss.Last, ss.Msgs = 0, 0, 0

	// We do need to figure out the first and last sequences.
	wc := subjectHasWildcard(filter)
	start, stop := uint32(math.MaxUint32), uint32(0)

	if wc {
		fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) {
			ss.Msgs += psi.total
			// Keep track of start and stop indexes for this subject.
			if psi.fblk < start {
				start = psi.fblk
			}
			if psi.lblk > stop {
				stop = psi.lblk
			}
		})
	} else if psi, ok := fs.psim.Find(stringToBytes(filter)); ok {
		ss.Msgs += psi.total
		start, stop = psi.fblk, psi.lblk
	}

	// Did not find anything.
	if stop == 0 {
		return
	}

	// Do start
	mb := fs.bim[start]
	if mb != nil {
		_, f, _ := mb.filteredPending(filter, wc, 0)
		ss.First = f
	}

	if ss.First == 0 {
		// This is a miss. This can happen since psi.fblk is lazy.
		// We will make sure to update fblk.

		// Hold this outside loop for psim fblk updates when done.
		i := start + 1
		for ; i <= stop; i++ {
			mb := fs.bim[i]
			if mb == nil {
				continue
			}
			if _, f, _ := mb.filteredPending(filter, wc, 0); f > 0 {
				ss.First = f
				break
			}
		}
		// Update fblk since fblk was outdated.
		// We only require read lock here as that is desirable,
		// so we need to do this in a go routine to acquire write lock.
		go func() {
			fs.mu.Lock()
			defer fs.mu.Unlock()
			if !wc {
				if info, ok := fs.psim.Find(stringToBytes(filter)); ok {
					if i > info.fblk {
						info.fblk = i
					}
				}
			} else {
				fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) {
					if i > psi.fblk {
						psi.fblk = i
					}
				})
			}
		}()
	}
	// Now gather last sequence if asked to do so.
	if last {
		if mb = fs.bim[stop]; mb != nil {
			_, _, l := mb.filteredPending(filter, wc, 0)
			ss.Last = l
		}
	}
}

// SubjectsState returns a map of SimpleState for all matching subjects.
func (fs *fileStore) SubjectsState(subject string) map[string]SimpleState {
	fs.mu.RLock()
	defer fs.mu.RUnlock()

	if fs.state.Msgs == 0 || fs.noTrackSubjects() {
		return nil
	}

	if subject == _EMPTY_ {
		subject = fwcs
	}

	start, stop := fs.blks[0], fs.lmb
	// We can short circuit if not a wildcard using psim for start and stop.
	if !subjectHasWildcard(subject) {
		info, ok := fs.psim.Find(stringToBytes(subject))
		if !ok {
			return nil
		}
		if f := fs.bim[info.fblk]; f != nil {
			start = f
		}
		if l := fs.bim[info.lblk]; l != nil {
			stop = l
		}
	}

	// Aggregate fss.
	fss := make(map[string]SimpleState)
	var startFound bool

	for _, mb := range fs.blks {
		if !startFound {
			if mb != start {
				continue
			}
			startFound = true
		}

		mb.mu.Lock()
		var shouldExpire bool
		if mb.fssNotLoaded() {
			// Make sure we have fss loaded.
			if err := mb.loadMsgsWithLock(); err != nil {
				mb.mu.Unlock()
				return nil
			}
			shouldExpire = true
		}
		// Mark fss activity.
		mb.lsts = ats.AccessTime()
		mb.fss.Match(stringToBytes(subject), func(bsubj []byte, ss *SimpleState) {
			subj := string(bsubj)
			if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
				mb.recalculateForSubj(subj, ss)
			}
			oss := fss[subj]
			if oss.First == 0 { // New
				fss[subj] = *ss
			} else {
				// Merge here.
				oss.Last, oss.Msgs = ss.Last, oss.Msgs+ss.Msgs
				fss[subj] = oss
			}
		})
		if shouldExpire {
			// Expire this cache before moving on.
			mb.tryForceExpireCacheLocked()
		} else {
			mb.finishedWithCache()
		}
		mb.mu.Unlock()

		if mb == stop {
			break
		}
	}

	return fss
}

// AllLastSeqs will return a sorted list of last sequences for all subjects.
func (fs *fileStore) AllLastSeqs() ([]uint64, error) {
	fs.mu.RLock()
	defer fs.mu.RUnlock()
	return fs.allLastSeqsLocked()
}

// allLastSeqsLocked will return a sorted list of last sequences for all
// subjects, but won't take the lock to do it, to avoid the issue of compounding
// read locks causing a deadlock with a write lock.
func (fs *fileStore) allLastSeqsLocked() ([]uint64, error) {
	if fs.state.Msgs == 0 || fs.noTrackSubjects() {
		return nil, nil
	}

	numSubjects := fs.psim.Size()
	seqs := make([]uint64, 0, numSubjects)
	subs := make(map[string]struct{}, numSubjects)

	for i := len(fs.blks) - 1; i >= 0; i-- {
		if len(subs) == numSubjects {
			break
		}
		mb := fs.blks[i]
		mb.mu.Lock()

		var shouldExpire bool
		if mb.fssNotLoaded() {
			// Make sure we have fss loaded.
			if err := mb.loadMsgsWithLock(); err != nil {
				mb.mu.Unlock()
				return nil, err
			}
			shouldExpire = true
		}

		mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool {
			// Check if already been processed and accounted.
			if _, ok := subs[string(bsubj)]; !ok {
				// Check if we need to recalculate. We only care about the last sequence.
				if ss.lastNeedsUpdate {
					// mb is already loaded into the cache so should be fast-ish.
					mb.recalculateForSubj(bytesToString(bsubj), ss)
				}
				seqs = append(seqs, ss.Last)
				subs[string(bsubj)] = struct{}{}
			}
			return true
		})
		if shouldExpire {
			// Expire this cache before moving on.
			mb.tryForceExpireCacheLocked()
		}
		mb.finishedWithCache()
		mb.mu.Unlock()
	}

	slices.Sort(seqs)
	return seqs, nil
}

// Helper to determine if the filter(s) represent all the subjects.
// Most clients send in subjects even if they match the stream's ingest subjects.
// Lock should be held.
func (fs *fileStore) filterIsAll(filters []string) bool {
	if len(filters) != len(fs.cfg.Subjects) {
		return false
	}
	// Sort so we can compare.
	slices.Sort(filters)
	slices.Sort(fs.cfg.Subjects)
	for i, subj := range filters {
		if !subjectIsSubsetMatch(fs.cfg.Subjects[i], subj) {
			return false
		}
	}
	return true
}

// MultiLastSeqs will return a sorted list of sequences that match all subjects presented in filters.
// We will not exceed the maxSeq, which if 0 becomes the store's last sequence.
func (fs *fileStore) MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed int) ([]uint64, error) {
	fs.mu.RLock()
	defer fs.mu.RUnlock()

	if fs.state.Msgs == 0 || fs.noTrackSubjects() {
		return nil, nil
	}

	// See if we can short circuit if we think they are asking for all last sequences and have no maxSeq or maxAllowed set.
	if maxSeq == 0 && maxAllowed <= 0 && fs.filterIsAll(filters) {
		return fs.allLastSeqsLocked()
	}

	lastBlkIndex := len(fs.blks) - 1
	lastMB := fs.blks[lastBlkIndex]

	// Implied last sequence.
	if maxSeq == 0 {
		maxSeq = fs.state.LastSeq
	} else {
		// Udate last mb index if not last seq.
		lastBlkIndex, lastMB = fs.selectMsgBlockWithIndex(maxSeq)
	}
	// Make sure non-nil
	if lastMB == nil {
		return nil, nil
	}

	// Grab our last mb index (not same as blk index).
	lastMB.mu.RLock()
	lastMBIndex := lastMB.index
	lastMB.mu.RUnlock()

	subs := make(map[string]*psi)
	var numLess int
	var maxBlk uint32

	for _, filter := range filters {
		fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) {
			subs[string(subj)] = psi
			if psi.lblk < lastMBIndex {
				numLess++
				if psi.lblk > maxBlk {
					maxBlk = psi.lblk
				}
			}
		})
	}

	// If all subjects have a lower last index, select the largest for our walk backwards.
	if numLess == len(subs) {
		lastMB = fs.bim[maxBlk]
	}

	// Collect all sequences needed.
	seqs := make([]uint64, 0, len(subs))
	for i, lnf := lastBlkIndex, false; i >= 0; i-- {
		if len(subs) == 0 {
			break
		}
		mb := fs.blks[i]
		if !lnf {
			if mb != lastMB {
				continue
			}
			lnf = true
		}
		// We can start properly looking here.
		mb.mu.Lock()
		mb.ensurePerSubjectInfoLoaded()

		// Iterate the fss and check against our subs. We will delete from subs as we add.
		// Once len(subs) == 0 we are done.
		var ierr error
		mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool {
			// Already been processed and accounted for was not matched in the first place.
			if subs[string(bsubj)] == nil {
				return true
			}
			// Check if we need to recalculate. We only care about the last sequence.
			if ss.lastNeedsUpdate {
				// mb is already loaded into the cache so should be fast-ish.
				mb.recalculateForSubj(bytesToString(bsubj), ss)
			}
			// If we are equal or below just add to seqs slice.
			if ss.Last <= maxSeq {
				seqs = append(seqs, ss.Last)
				delete(subs, bytesToString(bsubj))
			} else {
				// Need to search for the real last since recorded last is > maxSeq.
				var didLoad bool
				if mb.cacheNotLoaded() {
					if ierr = mb.loadMsgsWithLock(); ierr != nil {
						return false
					}
					didLoad = true
				}
				var smv StoreMsg
				fseq := atomic.LoadUint64(&mb.first.seq)
				lseq := min(atomic.LoadUint64(&mb.last.seq), maxSeq)
				ssubj := bytesToString(bsubj)
				for seq := lseq; seq >= fseq; seq-- {
					sm, _ := mb.cacheLookupNoCopy(seq, &smv)
					if sm == nil || sm.subj != ssubj {
						continue
					}
					seqs = append(seqs, sm.seq)
					delete(subs, ssubj)
					break
				}
				if didLoad {
					mb.finishedWithCache()
				}
			}
			return true
		})
		mb.mu.Unlock()
		if ierr != nil {
			return nil, ierr
		}

		// If maxAllowed was sepcified check that we will not exceed that.
		if maxAllowed > 0 && len(seqs) > maxAllowed {
			return nil, ErrTooManyResults
		}
	}
	if len(seqs) == 0 {
		return nil, nil
	}
	slices.Sort(seqs)
	return seqs, nil
}

// NumPending will return the number of pending messages matching the filter subject starting at sequence.
// Optimized for stream num pending calculations for consumers.
func (fs *fileStore) NumPending(sseq uint64, filter string, lastPerSubject bool) (total, validThrough uint64, err error) {
	fs.mu.RLock()
	defer fs.mu.RUnlock()

	// This can always be last for these purposes.
	validThrough = fs.state.LastSeq

	if fs.state.Msgs == 0 || sseq > fs.state.LastSeq {
		return 0, validThrough, nil
	}

	// If sseq is less then our first set to first.
	if sseq < fs.state.FirstSeq {
		sseq = fs.state.FirstSeq
	}
	// Track starting for both block for the sseq and staring block that matches any subject.
	var seqStart int
	// See if we need to figure out starting block per sseq.
	if sseq > fs.state.FirstSeq {
		// This should not, but can return -1, so make sure we check to avoid panic below.
		if seqStart, _ = fs.selectMsgBlockWithIndex(sseq); seqStart < 0 {
			seqStart = 0
		}
	}

	isAll := filter == _EMPTY_ || filter == fwcs
	if isAll && filter == _EMPTY_ {
		filter = fwcs
	}
	wc := subjectHasWildcard(filter)

	// See if filter was provided but its the only subject.
	if !isAll && !wc && fs.psim.Size() == 1 {
		_, isAll = fs.psim.Find(stringToBytes(filter))
	}
	// If we are isAll and have no deleted we can do a simpler calculation.
	if !lastPerSubject && isAll && (fs.state.LastSeq-fs.state.FirstSeq+1) == fs.state.Msgs {
		if sseq == 0 {
			return fs.state.Msgs, validThrough, nil
		}
		return fs.state.LastSeq - sseq + 1, validThrough, nil
	}

	_tsa, _fsa := [32]string{}, [32]string{}
	tsa, fsa := _tsa[:0], _fsa[:0]
	if wc {
		fsa = tokenizeSubjectIntoSlice(fsa[:0], filter)
	}

	isMatch := func(subj string) bool {
		if isAll {
			return true
		}
		if !wc {
			return subj == filter
		}
		tsa = tokenizeSubjectIntoSlice(tsa[:0], subj)
		return isSubsetMatchTokenized(tsa, fsa)
	}

	// Handle last by subject a bit differently.
	// We will scan PSIM since we accurately track the last block we have seen the subject in. This
	// allows us to only need to load at most one block now.
	// For the last block, we need to track the subjects that we know are in that block, and track seen
	// while in the block itself, but complexity there worth it.
	if lastPerSubject {
		// If we want all and our start sequence is equal or less than first return number of subjects.
		if isAll && sseq <= fs.state.FirstSeq {
			return uint64(fs.psim.Size()), validThrough, nil
		}
		// If we are here we need to scan. We are going to scan the PSIM looking for lblks that are >= seqStart.
		// This will build up a list of all subjects from the selected block onward.
		lbm := make(map[string]bool)
		mb := fs.blks[seqStart]
		bi := mb.index

		fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) {
			// If the select blk start is greater than entry's last blk skip.
			if bi > psi.lblk {
				return
			}
			total++
			// We will track the subjects that are an exact match to the last block.
			// This is needed for last block processing.
			if psi.lblk == bi {
				lbm[string(subj)] = true
			}
		})

		// Now check if we need to inspect the seqStart block.
		// Grab write lock in case we need to load in msgs.
		mb.mu.Lock()
		var updateLLTS bool
		var shouldExpire bool
		// We need to walk this block to correct accounting from above.
		if sseq > mb.first.seq {
			// Track the ones we add back in case more than one.
			seen := make(map[string]bool)
			// We need to discount the total by subjects seen before sseq, but also add them right back in if they are >= sseq for this blk.
			// This only should be subjects we know have the last blk in this block.
			if mb.cacheNotLoaded() {
				if err = mb.loadMsgsWithLock(); err != nil {
					mb.mu.Unlock()
					return 0, 0, err
				}
				shouldExpire = true
			}
			var smv StoreMsg
			for seq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
				if mb.dmap.Exists(seq) {
					// Optimisation to avoid calling cacheLookup which hits time.Now().
					updateLLTS = true
					continue
				}
				sm, _ := mb.cacheLookupNoCopy(seq, &smv)
				if sm == nil || sm.subj == _EMPTY_ || !lbm[sm.subj] {
					continue
				}
				updateLLTS = false // cacheLookup already updated it.
				if isMatch(sm.subj) {
					// If less than sseq adjust off of total as long as this subject matched the last block.
					if seq < sseq {
						if !seen[sm.subj] {
							total--
							seen[sm.subj] = true
						}
					} else if seen[sm.subj] {
						// This is equal or more than sseq, so add back in.
						total++
						// Make sure to not process anymore.
						delete(seen, sm.subj)
					}
				}
			}
		}
		// If we loaded the block try to force expire.
		if shouldExpire {
			mb.tryForceExpireCacheLocked()
		} else {
			mb.finishedWithCache()
		}
		if updateLLTS {
			mb.llts = ats.AccessTime()
		}
		mb.mu.Unlock()
		return total, validThrough, nil
	}

	// If we would need to scan more from the beginning, revert back to calculating directly here.
	// TODO(dlc) - Redo properly with sublists etc for subject-based filtering.
	if seqStart >= (len(fs.blks) / 2) {
		for i := seqStart; i < len(fs.blks); i++ {
			var shouldExpire bool
			mb := fs.blks[i]
			// Hold write lock in case we need to load cache.
			mb.mu.Lock()
			if isAll && sseq <= atomic.LoadUint64(&mb.first.seq) {
				total += mb.msgs
				mb.mu.Unlock()
				continue
			}
			// If we are here we need to at least scan the subject fss.
			// Make sure we have fss loaded.
			if mb.fssNotLoaded() {
				if err = mb.loadMsgsWithLock(); err != nil {
					mb.mu.Unlock()
					return 0, 0, err
				}
				shouldExpire = true
			}
			// Mark fss activity.
			mb.lsts = ats.AccessTime()

			var t uint64
			var havePartial bool
			mb.fss.Match(stringToBytes(filter), func(bsubj []byte, ss *SimpleState) {
				if havePartial {
					// If we already found a partial then don't do anything else.
					return
				}
				subj := bytesToString(bsubj)
				if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
					mb.recalculateForSubj(subj, ss)
				}
				if sseq <= ss.First {
					t += ss.Msgs
				} else if sseq <= ss.Last {
					// We matched but its a partial.
					havePartial = true
				}
			})

			// See if we need to scan msgs here.
			if havePartial {
				// Make sure we have the cache loaded.
				if mb.cacheNotLoaded() {
					if err = mb.loadMsgsWithLock(); err != nil {
						mb.mu.Unlock()
						return 0, 0, err
					}
					shouldExpire = true
				}
				// Clear on partial.
				t = 0
				start := sseq
				if fseq := atomic.LoadUint64(&mb.first.seq); fseq > start {
					start = fseq
				}
				var smv StoreMsg
				for seq, lseq := start, atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
					if sm, _ := mb.cacheLookupNoCopy(seq, &smv); sm != nil && isMatch(sm.subj) {
						t++
					}
				}
			}
			// If we loaded this block for this operation go ahead and expire it here.
			if shouldExpire {
				mb.tryForceExpireCacheLocked()
			} else {
				mb.finishedWithCache()
			}
			mb.mu.Unlock()
			total += t
		}
		return total, validThrough, nil
	}

	// If we are here it's better to calculate totals from psim and adjust downward by scanning less blocks.
	// TODO(dlc) - Eventually when sublist uses generics, make this sublist driven instead.
	start := uint32(math.MaxUint32)
	fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) {
		total += psi.total
		// Keep track of start index for this subject.
		if psi.fblk < start {
			start = psi.fblk
		}
	})
	// See if we were asked for all, if so we are done.
	if sseq <= fs.state.FirstSeq {
		return total, validThrough, nil
	}

	// If we are here we need to calculate partials for the first blocks.
	firstSubjBlk := fs.bim[start]
	var firstSubjBlkFound bool
	// Adjust in case not found.
	if firstSubjBlk == nil {
		firstSubjBlkFound = true
	}

	// Track how many we need to adjust against the total.
	var adjust uint64
	for i := 0; i <= seqStart; i++ {
		mb := fs.blks[i]
		// We can skip blks if we know they are below the first one that has any subject matches.
		if !firstSubjBlkFound {
			if firstSubjBlkFound = (mb == firstSubjBlk); !firstSubjBlkFound {
				continue
			}
		}
		// We need to scan this block.
		var shouldExpire bool
		var updateLLTS bool
		mb.mu.Lock()
		// Check if we should include all of this block in adjusting. If so work with metadata.
		if sseq > atomic.LoadUint64(&mb.last.seq) {
			if isAll {
				adjust += mb.msgs
			} else {
				// We need to adjust for all matches in this block.
				// Make sure we have fss loaded. This loads whole block now.
				if mb.fssNotLoaded() {
					if err = mb.loadMsgsWithLock(); err != nil {
						mb.mu.Unlock()
						return 0, 0, err
					}
					shouldExpire = true
				}
				// Mark fss activity.
				mb.lsts = ats.AccessTime()

				mb.fss.Match(stringToBytes(filter), func(bsubj []byte, ss *SimpleState) {
					adjust += ss.Msgs
				})
			}
		} else {
			// This is the last block. We need to scan per message here.
			if mb.cacheNotLoaded() {
				if err = mb.loadMsgsWithLock(); err != nil {
					mb.mu.Unlock()
					return 0, 0, err
				}
				shouldExpire = true
			}
			var last = atomic.LoadUint64(&mb.last.seq)
			if sseq < last {
				last = sseq
			}
			// We need to walk all messages in this block
			var smv StoreMsg
			for seq := atomic.LoadUint64(&mb.first.seq); seq < last; seq++ {
				if mb.dmap.Exists(seq) {
					// Optimisation to avoid calling cacheLookup which hits time.Now().
					updateLLTS = true
					continue
				}
				sm, _ := mb.cacheLookupNoCopy(seq, &smv)
				if sm == nil || sm.subj == _EMPTY_ {
					continue
				}
				updateLLTS = false // cacheLookup already updated it.
				// Check if it matches our filter.
				if sm.seq < sseq && isMatch(sm.subj) {
					adjust++
				}
			}
		}
		// If we loaded the block try to force expire.
		if shouldExpire {
			mb.tryForceExpireCacheLocked()
		} else {
			mb.finishedWithCache()
		}
		if updateLLTS {
			mb.llts = ats.AccessTime()
		}
		mb.mu.Unlock()
	}
	// Make final adjustment.
	if adjust > total {
		total = 0
	} else {
		total -= adjust
	}

	return total, validThrough, nil
}

// NumPending will return the number of pending messages matching any subject in the sublist starting at sequence.
// Optimized for stream num pending calculations for consumers with lots of filtered subjects.
// Subjects should not overlap, this property is held when doing multi-filtered consumers.
func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerSubject bool) (total, validThrough uint64, err error) {
	fs.mu.RLock()
	defer fs.mu.RUnlock()

	// This can always be last for these purposes.
	validThrough = fs.state.LastSeq

	if fs.state.Msgs == 0 || sseq > fs.state.LastSeq {
		return 0, validThrough, nil
	}

	// If sseq is less then our first set to first.
	if sseq < fs.state.FirstSeq {
		sseq = fs.state.FirstSeq
	}
	// Track starting for both block for the sseq and staring block that matches any subject.
	var seqStart int
	// See if we need to figure out starting block per sseq.
	if sseq > fs.state.FirstSeq {
		// This should not, but can return -1, so make sure we check to avoid panic below.
		if seqStart, _ = fs.selectMsgBlockWithIndex(sseq); seqStart < 0 {
			seqStart = 0
		}
	}

	isAll := sl == nil

	// See if filter was provided but its the only subject.
	if !isAll && fs.psim.Size() == 1 {
		fs.psim.IterFast(func(subject []byte, _ *psi) bool {
			isAll = sl.HasInterest(bytesToString(subject))
			return true
		})
	}
	// If we are isAll and have no deleted we can do a simpler calculation.
	if !lastPerSubject && isAll && (fs.state.LastSeq-fs.state.FirstSeq+1) == fs.state.Msgs {
		if sseq == 0 {
			return fs.state.Msgs, validThrough, nil
		}
		return fs.state.LastSeq - sseq + 1, validThrough, nil
	}
	// Setup the isMatch function.
	isMatch := func(subj string) bool {
		if isAll {
			return true
		}
		return sl.HasInterest(subj)
	}

	// Handle last by subject a bit differently.
	// We will scan PSIM since we accurately track the last block we have seen the subject in. This
	// allows us to only need to load at most one block now.
	// For the last block, we need to track the subjects that we know are in that block, and track seen
	// while in the block itself, but complexity there worth it.
	if lastPerSubject {
		// If we want all and our start sequence is equal or less than first return number of subjects.
		if isAll && sseq <= fs.state.FirstSeq {
			return uint64(fs.psim.Size()), validThrough, nil
		}
		// If we are here we need to scan. We are going to scan the PSIM looking for lblks that are >= seqStart.
		// This will build up a list of all subjects from the selected block onward.
		lbm := make(map[string]bool)
		mb := fs.blks[seqStart]
		bi := mb.index

		stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
			// If the select blk start is greater than entry's last blk skip.
			if bi > psi.lblk {
				return
			}
			total++
			// We will track the subjects that are an exact match to the last block.
			// This is needed for last block processing.
			if psi.lblk == bi {
				lbm[string(subj)] = true
			}
		})

		// Now check if we need to inspect the seqStart block.
		// Grab write lock in case we need to load in msgs.
		mb.mu.Lock()
		var shouldExpire bool
		var updateLLTS bool
		// We need to walk this block to correct accounting from above.
		if sseq > atomic.LoadUint64(&mb.first.seq) {
			// Track the ones we add back in case more than one.
			seen := make(map[string]bool)
			// We need to discount the total by subjects seen before sseq, but also add them right back in if they are >= sseq for this blk.
			// This only should be subjects we know have the last blk in this block.
			if mb.cacheNotLoaded() {
				if err = mb.loadMsgsWithLock(); err != nil {
					mb.mu.Unlock()
					return 0, 0, err
				}
				shouldExpire = true
			}
			var smv StoreMsg
			for seq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
				if mb.dmap.Exists(seq) {
					// Optimisation to avoid calling cacheLookup which hits time.Now().
					updateLLTS = true
					continue
				}
				sm, _ := mb.cacheLookupNoCopy(seq, &smv)
				if sm == nil || sm.subj == _EMPTY_ || !lbm[sm.subj] {
					continue
				}
				updateLLTS = false // cacheLookup already updated it.
				if isMatch(sm.subj) {
					// If less than sseq adjust off of total as long as this subject matched the last block.
					if seq < sseq {
						if !seen[sm.subj] {
							total--
							seen[sm.subj] = true
						}
					} else if seen[sm.subj] {
						// This is equal or more than sseq, so add back in.
						total++
						// Make sure to not process anymore.
						delete(seen, sm.subj)
					}
				}
			}
		}
		// If we loaded the block try to force expire.
		if shouldExpire {
			mb.tryForceExpireCacheLocked()
		} else {
			mb.finishedWithCache()
		}
		if updateLLTS {
			mb.llts = ats.AccessTime()
		}
		mb.mu.Unlock()
		return total, validThrough, nil
	}

	// If we would need to scan more from the beginning, revert back to calculating directly here.
	if seqStart >= (len(fs.blks) / 2) {
		for i := seqStart; i < len(fs.blks); i++ {
			var shouldExpire bool
			mb := fs.blks[i]
			// Hold write lock in case we need to load cache.
			mb.mu.Lock()
			if isAll && sseq <= atomic.LoadUint64(&mb.first.seq) {
				total += mb.msgs
				mb.mu.Unlock()
				continue
			}
			// If we are here we need to at least scan the subject fss.
			// Make sure we have fss loaded.
			if mb.fssNotLoaded() {
				if err = mb.loadMsgsWithLock(); err != nil {
					mb.mu.Unlock()
					return 0, 0, err
				}
				shouldExpire = true
			}
			// Mark fss activity.
			mb.lsts = ats.AccessTime()

			var t uint64
			var havePartial bool
			var updateLLTS bool
			stree.IntersectGSL[SimpleState](mb.fss, sl, func(bsubj []byte, ss *SimpleState) {
				subj := bytesToString(bsubj)
				if havePartial {
					// If we already found a partial then don't do anything else.
					return
				}
				if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
					mb.recalculateForSubj(subj, ss)
				}
				if sseq <= ss.First {
					t += ss.Msgs
				} else if sseq <= ss.Last {
					// We matched but its a partial.
					havePartial = true
				}
			})

			// See if we need to scan msgs here.
			if havePartial {
				// Make sure we have the cache loaded.
				if mb.cacheNotLoaded() {
					if err = mb.loadMsgsWithLock(); err != nil {
						mb.mu.Unlock()
						return 0, 0, err
					}
					shouldExpire = true
				}
				// Clear on partial.
				t = 0
				start := sseq
				if fseq := atomic.LoadUint64(&mb.first.seq); fseq > start {
					start = fseq
				}
				var smv StoreMsg
				for seq, lseq := start, atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
					if mb.dmap.Exists(seq) {
						// Optimisation to avoid calling cacheLookup which hits time.Now().
						updateLLTS = true
						continue
					}
					if sm, _ := mb.cacheLookupNoCopy(seq, &smv); sm != nil && isMatch(sm.subj) {
						t++
						updateLLTS = false // cacheLookup already updated it.
					}
				}
			}
			// If we loaded this block for this operation go ahead and expire it here.
			if shouldExpire {
				mb.tryForceExpireCacheLocked()
			} else {
				mb.finishedWithCache()
			}
			if updateLLTS {
				mb.llts = ats.AccessTime()
			}
			mb.mu.Unlock()
			total += t
		}
		return total, validThrough, nil
	}

	// If we are here it's better to calculate totals from psim and adjust downward by scanning less blocks.
	start := uint32(math.MaxUint32)
	stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
		total += psi.total
		// Keep track of start index for this subject.
		if psi.fblk < start {
			start = psi.fblk
		}
	})

	// See if we were asked for all, if so we are done.
	if sseq <= fs.state.FirstSeq {
		return total, validThrough, nil
	}

	// If we are here we need to calculate partials for the first blocks.
	firstSubjBlk := fs.bim[start]
	var firstSubjBlkFound bool
	// Adjust in case not found.
	if firstSubjBlk == nil {
		firstSubjBlkFound = true
	}

	// Track how many we need to adjust against the total.
	var adjust uint64
	for i := 0; i <= seqStart; i++ {
		mb := fs.blks[i]
		// We can skip blks if we know they are below the first one that has any subject matches.
		if !firstSubjBlkFound {
			if firstSubjBlkFound = (mb == firstSubjBlk); !firstSubjBlkFound {
				continue
			}
		}
		// We need to scan this block.
		var shouldExpire bool
		var updateLLTS bool
		mb.mu.Lock()
		// Check if we should include all of this block in adjusting. If so work with metadata.
		if sseq > atomic.LoadUint64(&mb.last.seq) {
			if isAll {
				adjust += mb.msgs
			} else {
				// We need to adjust for all matches in this block.
				// Make sure we have fss loaded. This loads whole block now.
				if mb.fssNotLoaded() {
					if err = mb.loadMsgsWithLock(); err != nil {
						mb.mu.Unlock()
						return 0, 0, err
					}
					shouldExpire = true
				}
				// Mark fss activity.
				mb.lsts = ats.AccessTime()
				stree.IntersectGSL(mb.fss, sl, func(bsubj []byte, ss *SimpleState) {
					adjust += ss.Msgs
				})
			}
		} else {
			// This is the last block. We need to scan per message here.
			if mb.cacheNotLoaded() {
				if err = mb.loadMsgsWithLock(); err != nil {
					mb.mu.Unlock()
					return 0, 0, err
				}
				shouldExpire = true
			}
			var last = atomic.LoadUint64(&mb.last.seq)
			if sseq < last {
				last = sseq
			}
			// We need to walk all messages in this block
			var smv StoreMsg
			for seq := atomic.LoadUint64(&mb.first.seq); seq < last; seq++ {
				if mb.dmap.Exists(seq) {
					// Optimisation to avoid calling cacheLookup which hits time.Now().
					updateLLTS = true
					continue
				}
				sm, _ := mb.cacheLookupNoCopy(seq, &smv)
				if sm == nil || sm.subj == _EMPTY_ {
					continue
				}
				updateLLTS = false // cacheLookup already updated it.
				// Check if it matches our filter.
				if sm.seq < sseq && isMatch(sm.subj) {
					adjust++
				}
			}
		}
		// If we loaded the block try to force expire.
		if shouldExpire {
			mb.tryForceExpireCacheLocked()
		} else {
			mb.finishedWithCache()
		}
		if updateLLTS {
			mb.llts = ats.AccessTime()
		}
		mb.mu.Unlock()
	}
	// Make final adjustment.
	if adjust > total {
		total = 0
	} else {
		total -= adjust
	}

	return total, validThrough, nil
}

// SubjectsTotals return message totals per subject.
func (fs *fileStore) SubjectsTotals(filter string) map[string]uint64 {
	fs.mu.RLock()
	defer fs.mu.RUnlock()
	return fs.subjectsTotalsLocked(filter)
}

// Lock should be held.
func (fs *fileStore) subjectsTotalsLocked(filter string) map[string]uint64 {
	if fs.psim.Size() == 0 {
		return nil
	}
	// Match all if no filter given.
	if filter == _EMPTY_ {
		filter = fwcs
	}
	fst := make(map[string]uint64)
	fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) {
		fst[string(subj)] = psi.total
	})
	return fst
}

// RegisterStorageUpdates registers a callback for updates to storage changes.
// It will present number of messages and bytes as a signed integer and an
// optional sequence number of the message if a single.
func (fs *fileStore) RegisterStorageUpdates(cb StorageUpdateHandler) {
	fs.mu.Lock()
	fs.scb = cb
	bsz := fs.state.Bytes
	fs.mu.Unlock()
	if cb != nil && bsz > 0 {
		cb(0, int64(bsz), 0, _EMPTY_)
	}
}

// RegisterStorageRemoveMsg registers a callback to remove messages.
// Replicated streams should propose removals, R1 can remove inline.
func (fs *fileStore) RegisterStorageRemoveMsg(cb StorageRemoveMsgHandler) {
	fs.mu.Lock()
	fs.rmcb = cb
	fs.mu.Unlock()
}

// RegisterProcessJetStreamMsg registers a callback to process new JetStream messages.
func (fs *fileStore) RegisterProcessJetStreamMsg(cb ProcessJetStreamMsgHandler) {
	fs.mu.Lock()
	fs.pmsgcb = cb
	fs.mu.Unlock()
}

// Helper to get hash key for specific message block.
// Lock should be held
func (fs *fileStore) hashKeyForBlock(index uint32) []byte {
	return []byte(fmt.Sprintf("%s-%d", fs.cfg.Name, index))
}

func (mb *msgBlock) setupWriteCache(buf []byte) error {
	// Not safe to use mb.cacheAlreadyLoaded() here as the heuristic
	// is wrong for writing to the beginning of an empty block cache.
	if mb.cache != nil {
		return nil
	}
	if mb.cache = mb.ecache.Value(); mb.cache != nil {
		return nil
	}

	// If there's an existing block on disk then we'll load it in.
	var fi os.FileInfo
	if mb.mfd != nil {
		fi, _ = mb.mfd.Stat()
	} else if mb.mfn != _EMPTY_ {
		fi, _ = os.Stat(mb.mfn)
	}
	if fi != nil {
		return mb.loadMsgsWithLock()
	}

	// Looks like there isn't an existing file on disk, mint a new cache.
	mb.cache = &cache{buf: buf}
	mb.ecache.Set(mb.cache)
	mb.llts = ats.AccessTime()
	mb.startCacheExpireTimer()
	return nil
}

// finishedWithCache removes the strong reference to the cache, instead
// returning it to the elastic pointer. Note that the elastic pointer can
// be either weak or strong, they are strengthened when there are pending
// writes and weak otherwise.
// Lock must be held.
func (mb *msgBlock) finishedWithCache() {
	if mb.cache != nil && mb.pendingWriteSizeLocked() == 0 {
		mb.cache = nil
	}
}

// This rolls to a new append msg block.
// Lock should be held.
func (fs *fileStore) newMsgBlockForWrite() (*msgBlock, error) {
	index := uint32(1)
	var rbuf []byte

	if lmb := fs.lmb; lmb != nil {
		lmb.mu.Lock()
		index = lmb.index + 1

		// Quit our loops.
		if lmb.qch != nil {
			close(lmb.qch)
			lmb.qch = nil
		}
		// Flush any pending messages.
		lmb.flushPendingMsgsLocked()
		// Determine if we can reclaim any resources here.
		lmb.closeFDsLockedNoCheck()
		if lmb.cache != nil {
			// Reset write timestamp and see if we can expire this cache.
			rbuf = lmb.tryExpireWriteCache()
		}
		lmb.mu.Unlock()

		if fs.fcfg.Compression != NoCompression {
			// We've now reached the end of this message block, if we want
			// to compress blocks then now's the time to do it.
			go func() {
				lmb.mu.Lock()
				defer lmb.mu.Unlock()
				lmb.recompressOnDiskIfNeeded()
			}()
		}
	}

	mb := fs.initMsgBlock(index)
	// Lock should be held to quiet race detector.
	mb.mu.Lock()
	if err := mb.setupWriteCache(rbuf); err != nil {
		mb.mu.Unlock()
		return nil, err
	}
	mb.fss = stree.NewSubjectTree[SimpleState]()

	// Set cache time to creation time to start.
	mb.llts, mb.lwts = 0, ats.AccessTime()
	// Remember our last sequence number.
	atomic.StoreUint64(&mb.first.seq, fs.state.LastSeq+1)
	atomic.StoreUint64(&mb.last.seq, fs.state.LastSeq)
	mb.mu.Unlock()

	// Now do local hash.
	key := sha256.Sum256(fs.hashKeyForBlock(index))
	hh, err := highwayhash.NewDigest64(key[:])
	if err != nil {
		return nil, fmt.Errorf("could not create hash: %v", err)
	}
	mb.hh = hh

	<-dios
	mfd, err := os.OpenFile(mb.mfn, os.O_CREATE|os.O_RDWR, defaultFilePerms)
	dios <- struct{}{}

	if err != nil {
		if isPermissionError(err) {
			return nil, err
		}
		mb.dirtyCloseWithRemove(true)
		return nil, fmt.Errorf("Error creating msg block file: %v", err)
	}
	mb.mfd = mfd

	// Check if encryption is enabled.
	if fs.prf != nil {
		if err := fs.genEncryptionKeysForBlock(mb); err != nil {
			return nil, err
		}
	}

	// If we know we will need this so go ahead and spin up.
	if !fs.fip {
		mb.spinUpFlushLoop()
	}

	// Add to our list of blocks and mark as last.
	fs.addMsgBlock(mb)

	return mb, nil
}

// Generate the keys for this message block and write them out.
func (fs *fileStore) genEncryptionKeysForBlock(mb *msgBlock) error {
	if mb == nil {
		return nil
	}
	key, bek, seed, encrypted, err := fs.genEncryptionKeys(fmt.Sprintf("%s:%d", fs.cfg.Name, mb.index))
	if err != nil {
		return err
	}
	mb.aek, mb.bek, mb.seed, mb.nonce = key, bek, seed, encrypted[:key.NonceSize()]
	mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
	keyFile := filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index))
	if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) {
		return err
	}
	err = fs.writeFileWithOptionalSync(keyFile, encrypted, defaultFilePerms)
	if err != nil {
		return err
	}
	mb.kfn = keyFile
	return nil
}

// Stores a raw message with expected sequence number and timestamp.
// Lock should be held.
func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64) (err error) {
	if fs.isClosed() {
		return ErrStoreClosed
	}

	// Per subject max check needed.
	mmp := uint64(fs.cfg.MaxMsgsPer)
	var psmc uint64
	psmax := mmp > 0 && len(subj) > 0
	if psmax {
		if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
			psmc = info.total
		}
	}

	var fseq uint64
	// Check if we are discarding new messages when we reach the limit.
	if fs.cfg.Discard == DiscardNew {
		var asl bool
		if psmax && psmc >= mmp {
			// If we are instructed to discard new per subject, this is an error.
			if fs.cfg.DiscardNewPer {
				return ErrMaxMsgsPerSubject
			}
			if fseq, err = fs.firstSeqForSubj(subj); err != nil {
				return err
			}
			asl = true
		}
		// If we are discard new and limits policy and clustered, we do the enforcement
		// above and should not disqualify the message here since it could cause replicas to drift.
		if fs.cfg.Retention == LimitsPolicy || fs.cfg.Replicas == 1 {
			if fs.cfg.MaxMsgs > 0 && fs.state.Msgs >= uint64(fs.cfg.MaxMsgs) && !asl {
				return ErrMaxMsgs
			}
			if fs.cfg.MaxBytes > 0 && fs.state.Bytes+fileStoreMsgSize(subj, hdr, msg) >= uint64(fs.cfg.MaxBytes) {
				if !asl || fs.sizeForSeq(fseq) <= int(fileStoreMsgSize(subj, hdr, msg)) {
					return ErrMaxBytes
				}
			}
		}
	}

	// Check sequence.
	if seq != fs.state.LastSeq+1 {
		if seq > 0 {
			return ErrSequenceMismatch
		}
		seq = fs.state.LastSeq + 1
	}

	// Write msg record.
	// Add expiry bit to sequence if needed. This is so that if we need to
	// rebuild, we know which messages to look at more quickly.
	n, err := fs.writeMsgRecord(seq, ts, subj, hdr, msg)
	if err != nil {
		return err
	}

	// Adjust top level tracking of per subject msg counts.
	if len(subj) > 0 && fs.psim != nil {
		index := fs.lmb.index
		if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
			info.total++
			if index > info.lblk {
				info.lblk = index
			}
		} else {
			fs.psim.Insert(stringToBytes(subj), psi{total: 1, fblk: index, lblk: index})
			fs.tsl += len(subj)
		}
	}

	// Adjust first if needed.
	now := time.Unix(0, ts).UTC()
	if fs.state.Msgs == 0 {
		fs.state.FirstSeq = seq
		fs.state.FirstTime = now
	}

	fs.state.Msgs++
	fs.state.Bytes += n
	fs.state.LastSeq = seq
	fs.state.LastTime = now

	// Enforce per message limits.
	// We snapshotted psmc before our actual write, so >= comparison needed.
	if psmax && psmc >= mmp {
		// We may have done this above.
		if fseq == 0 {
			fseq, _ = fs.firstSeqForSubj(subj)
		}
		if ok, _ := fs.removeMsgViaLimits(fseq); ok {
			// Make sure we are below the limit.
			if psmc--; psmc >= mmp {
				bsubj := stringToBytes(subj)
				for info, ok := fs.psim.Find(bsubj); ok && info.total > mmp; info, ok = fs.psim.Find(bsubj) {
					if seq, _ := fs.firstSeqForSubj(subj); seq > 0 {
						if ok, _ := fs.removeMsgViaLimits(seq); !ok {
							break
						}
					} else {
						break
					}
				}
			}
		} else if mb := fs.selectMsgBlock(fseq); mb != nil {
			// If we are here we could not remove fseq from above, so rebuild.
			var ld *LostStreamData
			if ld, _, _ = mb.rebuildState(); ld != nil {
				fs.rebuildStateLocked(ld)
			}
		}
	}

	// Limits checks and enforcement.
	// If they do any deletions they will update the
	// byte count on their own, so no need to compensate.
	fs.enforceMsgLimit()
	fs.enforceBytesLimit()

	// Per-message TTL.
	if ttl > 0 {
		if fs.ttls != nil {
			expires := time.Duration(ts) + (time.Second * time.Duration(ttl))
			fs.ttls.Add(seq, int64(expires))
		}
		// Need to count these regardless as we might want to enable TTLs
		// later via UpdateConfig.
		fs.lmb.ttls++
	}

	// Check if we have and need the age expiration timer running.
	switch {
	case fs.ttls != nil && ttl > 0:
		fs.resetAgeChk(0)
	case fs.ageChk == nil && (fs.cfg.MaxAge > 0 || fs.ttls != nil):
		fs.startAgeChk()
	}

	// Message scheduling.
	if fs.scheduling != nil {
		if schedule, ok := getMessageSchedule(hdr); ok && !schedule.IsZero() {
			fs.scheduling.add(seq, subj, schedule.UnixNano())
			fs.lmb.schedules++
		} else {
			fs.scheduling.removeSubject(subj)
		}
	}

	return nil
}

// StoreRawMsg stores a raw message with expected sequence number and timestamp.
func (fs *fileStore) StoreRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64) error {
	fs.mu.Lock()
	err := fs.storeRawMsg(subj, hdr, msg, seq, ts, ttl)
	cb := fs.scb
	// Check if first message timestamp requires expiry
	// sooner than initial replica expiry timer set to MaxAge when initializing.
	if !fs.receivedAny && fs.cfg.MaxAge != 0 && ts > 0 {
		fs.receivedAny = true
		fs.resetAgeChk(0)
	}
	fs.mu.Unlock()

	if err == nil && cb != nil {
		cb(1, int64(fileStoreMsgSize(subj, hdr, msg)), seq, subj)
	}

	return err
}

// Store stores a message. We hold the main filestore lock for any write operation.
func (fs *fileStore) StoreMsg(subj string, hdr, msg []byte, ttl int64) (uint64, int64, error) {
	fs.mu.Lock()
	seq, ts := fs.state.LastSeq+1, time.Now().UnixNano()
	err := fs.storeRawMsg(subj, hdr, msg, seq, ts, ttl)
	cb := fs.scb
	fs.mu.Unlock()

	if err != nil {
		seq, ts = 0, 0
	} else if cb != nil {
		cb(1, int64(fileStoreMsgSize(subj, hdr, msg)), seq, subj)
	}

	return seq, ts, err
}

// skipMsg will update this message block for a skipped message.
// If we do not have any messages, just update the metadata, otherwise
// we will place an empty record marking the sequence as used. The
// sequence will be marked erased.
// fs lock should be held.
func (mb *msgBlock) skipMsg(seq uint64, now int64) {
	if mb == nil {
		return
	}
	var needsRecord bool

	mb.mu.Lock()
	// If we are empty can just do meta.
	if mb.msgs == 0 {
		atomic.StoreUint64(&mb.last.seq, seq)
		mb.last.ts = now
		atomic.StoreUint64(&mb.first.seq, seq+1)
		mb.first.ts = 0
		needsRecord = mb == mb.fs.lmb
	} else {
		needsRecord = true
		mb.dmap.Insert(seq)
	}
	if needsRecord {
		mb.writeMsgRecordLocked(emptyRecordLen, seq|ebit, _EMPTY_, nil, nil, now, true, true)
	}
	mb.mu.Unlock()
	if !needsRecord {
		mb.kickFlusher()
	}
}

// SkipMsg will use the next sequence number but not store anything.
func (fs *fileStore) SkipMsg(seq uint64) (uint64, error) {
	// Grab time.
	now := ats.AccessTime()

	fs.mu.Lock()
	defer fs.mu.Unlock()

	// Check sequence matches our last sequence.
	if seq != fs.state.LastSeq+1 {
		if seq > 0 {
			return 0, ErrSequenceMismatch
		}
		seq = fs.state.LastSeq + 1
	}

	// Grab our current last message block.
	mb, err := fs.checkLastBlock(emptyRecordLen)
	if err != nil {
		return 0, err
	}

	// Write skip msg.
	mb.skipMsg(seq, now)

	// Update fs state.
	fs.state.LastSeq, fs.state.LastTime = seq, time.Unix(0, now).UTC()
	if fs.state.Msgs == 0 {
		fs.state.FirstSeq, fs.state.FirstTime = seq, time.Time{}
	}
	if seq == fs.state.FirstSeq {
		fs.state.FirstSeq, fs.state.FirstTime = seq+1, time.Time{}
	}
	// Mark as dirty for stream state.
	fs.dirty++

	return seq, nil
}

// Skip multiple msgs. We will determine if we can fit into current lmb or we need to create a new block.
func (fs *fileStore) SkipMsgs(seq uint64, num uint64) error {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	// Check sequence matches our last sequence.
	if seq != fs.state.LastSeq+1 {
		if seq > 0 {
			return ErrSequenceMismatch
		}
		seq = fs.state.LastSeq + 1
	}

	// Limit number of dmap entries
	const maxDeletes = 64 * 1024
	mb := fs.lmb

	var msgs uint64
	numDeletes := int(num)
	if mb != nil {
		mb.mu.RLock()
		numDeletes += mb.dmap.Size()
		msgs = mb.msgs
		mb.mu.RUnlock()
	}
	if mb == nil || numDeletes > maxDeletes && msgs > 0 || msgs > 0 && mb.blkSize()+emptyRecordLen > fs.fcfg.BlockSize {
		var err error
		if mb, err = fs.newMsgBlockForWrite(); err != nil {
			return err
		}
	}

	// Insert into dmap all entries and place last as marker.
	now := ats.AccessTime()
	lseq := seq + num - 1

	mb.mu.Lock()
	// If we are empty update meta directly.
	if mb.msgs == 0 {
		atomic.StoreUint64(&mb.last.seq, lseq)
		mb.last.ts = now
		atomic.StoreUint64(&mb.first.seq, lseq+1)
		mb.first.ts = 0
	} else {
		for ; seq <= lseq; seq++ {
			mb.dmap.Insert(seq)
		}
	}
	// Write out our placeholder.
	mb.writeMsgRecordLocked(emptyRecordLen, lseq|ebit, _EMPTY_, nil, nil, now, true, true)
	mb.mu.Unlock()

	// Now update FS accounting.
	// Update fs state.
	fs.state.LastSeq, fs.state.LastTime = lseq, time.Unix(0, now).UTC()
	if fs.state.Msgs == 0 {
		fs.state.FirstSeq, fs.state.FirstTime = lseq+1, time.Time{}
	}

	// Mark as dirty for stream state.
	fs.dirty++

	return nil
}

// FlushAllPending flushes all data that was still pending to be written.
func (fs *fileStore) FlushAllPending() {
	fs.mu.Lock()
	defer fs.mu.Unlock()
	fs.checkAndFlushLastBlock()
}

// Lock should be held.
func (fs *fileStore) rebuildFirst() {
	if len(fs.blks) == 0 {
		return
	}
	fmb := fs.blks[0]
	if fmb == nil {
		return
	}

	ld, _, _ := fmb.rebuildState()
	fmb.mu.RLock()
	isEmpty := fmb.msgs == 0
	fmb.mu.RUnlock()
	if isEmpty {
		fmb.mu.Lock()
		fs.removeMsgBlock(fmb)
		fmb.mu.Unlock()
	}
	fs.selectNextFirst()
	fs.rebuildStateLocked(ld)
}

// Optimized helper function to return first sequence.
// subj will always be publish subject here, meaning non-wildcard.
// We assume a fast check that this subj even exists already happened.
// Write lock should be held.
func (fs *fileStore) firstSeqForSubj(subj string) (uint64, error) {
	if len(fs.blks) == 0 {
		return 0, nil
	}

	// See if we can optimize where we start.
	start, stop := fs.blks[0].index, fs.lmb.index
	if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
		start, stop = info.fblk, info.lblk
	}

	for i := start; i <= stop; i++ {
		mb := fs.bim[i]
		if mb == nil {
			continue
		}
		// If we need to load msgs here and we need to walk multiple blocks this
		// could tie up the upper fs lock, so release while dealing with the block.
		fs.mu.Unlock()

		mb.mu.Lock()
		var shouldExpire bool
		if mb.fssNotLoaded() {
			// Make sure we have fss loaded.
			if err := mb.loadMsgsWithLock(); err != nil {
				mb.mu.Unlock()
				// Re-acquire fs lock
				fs.mu.Lock()
				return 0, err
			}
			shouldExpire = true
		}
		// Mark fss activity.
		mb.lsts = ats.AccessTime()

		bsubj := stringToBytes(subj)
		if ss, ok := mb.fss.Find(bsubj); ok && ss != nil {
			if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
				mb.recalculateForSubj(subj, ss)
			}
			mb.mu.Unlock()
			// Re-acquire fs lock
			fs.mu.Lock()
			// Adjust first if it was not where we thought it should be.
			if i != start {
				if info, ok := fs.psim.Find(bsubj); ok {
					info.fblk = i
				}
			}
			return ss.First, nil
		}
		// If we did not find it and we loaded this msgBlock try to expire as long as not the last.
		if shouldExpire {
			// Expire this cache before moving on.
			mb.tryForceExpireCacheLocked()
		} else {
			mb.finishedWithCache()
		}
		mb.mu.Unlock()
		// Re-acquire fs lock
		fs.mu.Lock()
	}
	return 0, nil
}

// Will check the msg limit and drop firstSeq msg if needed.
// Lock should be held.
func (fs *fileStore) enforceMsgLimit() {
	if fs.cfg.Discard != DiscardOld {
		return
	}
	if fs.cfg.MaxMsgs <= 0 || fs.state.Msgs <= uint64(fs.cfg.MaxMsgs) {
		return
	}
	for nmsgs := fs.state.Msgs; nmsgs > uint64(fs.cfg.MaxMsgs); nmsgs = fs.state.Msgs {
		// If the first block can be removed fully, purge it entirely without needing to walk sequences.
		if len(fs.blks) > 0 {
			fmb := fs.blks[0]
			fmb.mu.RLock()
			msgs := fmb.msgs
			fmb.mu.RUnlock()
			if nmsgs-msgs > uint64(fs.cfg.MaxMsgs) {
				fs.purgeMsgBlock(fmb)
				continue
			}
		}
		if removed, err := fs.deleteFirstMsg(); err != nil || !removed {
			fs.rebuildFirst()
			return
		}
	}
}

// Will check the bytes limit and drop msgs if needed.
// Lock should be held.
func (fs *fileStore) enforceBytesLimit() {
	if fs.cfg.Discard != DiscardOld {
		return
	}
	if fs.cfg.MaxBytes <= 0 || fs.state.Bytes <= uint64(fs.cfg.MaxBytes) {
		return
	}
	for bs := fs.state.Bytes; bs > uint64(fs.cfg.MaxBytes); bs = fs.state.Bytes {
		// If the first block can be removed fully, purge it entirely without needing to walk sequences.
		if len(fs.blks) > 0 {
			fmb := fs.blks[0]
			fmb.mu.RLock()
			bytes := fmb.bytes
			fmb.mu.RUnlock()
			if bs-bytes > uint64(fs.cfg.MaxBytes) {
				fs.purgeMsgBlock(fmb)
				continue
			}
		}
		if removed, err := fs.deleteFirstMsg(); err != nil || !removed {
			fs.rebuildFirst()
			return
		}
	}
}

// Will make sure we have limits honored for max msgs per subject on recovery or config update.
// We will make sure to go through all msg blocks etc. but in practice this
// will most likely only be the last one, so can take a more conservative approach.
// Lock should be held.
func (fs *fileStore) enforceMsgPerSubjectLimit(fireCallback bool) {
	start := time.Now()
	defer func() {
		if took := time.Since(start); took > time.Minute {
			fs.warn("enforceMsgPerSubjectLimit took %v", took.Round(time.Millisecond))
		}
	}()

	maxMsgsPer := uint64(fs.cfg.MaxMsgsPer)

	// We may want to suppress callbacks from remove during this process
	// since these should have already been deleted and accounted for.
	if !fireCallback {
		cb := fs.scb
		fs.scb = nil
		defer func() { fs.scb = cb }()
	}

	var numMsgs uint64

	// collect all that are not correct.
	needAttention := stree.NewSubjectTree[uint64]()
	fblk, lblk := uint32(math.MaxUint32), uint32(0)
	fs.psim.IterFast(func(subj []byte, psi *psi) bool {
		numMsgs += psi.total
		if psi.total > maxMsgsPer {
			needAttention.Insert(subj, psi.total)
			if psi.fblk < fblk {
				fblk = psi.fblk
			}
			if psi.lblk > lblk {
				lblk = psi.lblk
			}
		}
		return true
	})

	// We had an issue with a use case where psim (and hence fss) were correct but idx was not and was not properly being caught.
	// So do a quick sanity check here. If we detect a skew do a rebuild then re-check.
	if numMsgs != fs.state.Msgs {
		fs.warn("Detected skew in subject-based total (%d) vs raw total (%d), rebuilding", numMsgs, fs.state.Msgs)
		// Clear any global subject state.
		fs.psim, fs.tsl = fs.psim.Empty(), 0
		for _, mb := range fs.blks {
			ld, _, err := mb.rebuildState()
			if err != nil && ld != nil {
				fs.addLostData(ld)
			}
			fs.populateGlobalPerSubjectInfo(mb)
		}
		// Rebuild fs state too.
		fs.rebuildStateLocked(nil)
		// Need to redo blocks that need attention.
		needAttention.Empty()
		fblk, lblk = uint32(math.MaxUint32), uint32(0)
		fs.psim.IterFast(func(subj []byte, psi *psi) bool {
			if psi.total > maxMsgsPer {
				needAttention.Insert(subj, psi.total)
				if psi.fblk < fblk {
					fblk = psi.fblk
				}
				if psi.lblk > lblk {
					lblk = psi.lblk
				}
			}
			return true
		})
	}

	// If nothing to do then stop.
	if fblk == math.MaxUint32 {
		return
	}

	// Collect all the msgBlks we alter.
	blks := make(map[*msgBlock]struct{})

	// For re-use below.
	var sm StoreMsg
	var fss *stree.SubjectTree[*SimpleState]
	for i := fblk; i <= lblk; i++ {
		mb := fs.bim[i]
		if mb == nil {
			continue
		}
		mb.mu.Lock()
		mb.ensurePerSubjectInfoLoaded()
		// It isn't safe to intersect mb.fss directly, because removeMsgViaLimits modifies it
		// during the iteration, which can cause us to miss keys. We won't copy the entire
		// SimpleState structs though but rather just take pointers for speed.
		fss = fss.Empty()
		mb.fss.IterFast(func(subject []byte, val *SimpleState) bool {
			fss.Insert(subject, val)
			return true
		})
		mb.mu.Unlock()
		stree.LazyIntersect(needAttention, fss, func(subj []byte, total *uint64, ssptr **SimpleState) {
			if ssptr == nil || total == nil {
				return
			}
			ss := *ssptr
			if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
				mb.mu.Lock()
				mb.recalculateForSubj(bytesToString(subj), ss)
				mb.mu.Unlock()
			}
			for first := ss.First; *total > maxMsgsPer && first <= ss.Last; {
				m, _, err := mb.firstMatching(bytesToString(subj), false, first, &sm)
				if err != nil {
					break
				}
				first = m.seq + 1
				if removed, _ := fs.removeMsgViaLimits(m.seq); removed {
					blks[mb] = struct{}{}
					*total--
				}
			}
		})
	}

	// Expire the cache if we can.
	for mb := range blks {
		mb.mu.Lock()
		if mb.msgs > 0 {
			mb.tryForceExpireCacheLocked()
		}
		mb.mu.Unlock()
	}
}

// Lock should be held.
func (fs *fileStore) deleteFirstMsg() (bool, error) {
	return fs.removeMsgViaLimits(fs.state.FirstSeq)
}

// If we remove via limits that can always be recovered on a restart we
// do not force the system to update the index file.
// Lock should be held.
func (fs *fileStore) removeMsgViaLimits(seq uint64) (bool, error) {
	return fs.removeMsg(seq, false, true, false)
}

// RemoveMsg will remove the message from this store.
// Will return the number of bytes removed.
func (fs *fileStore) RemoveMsg(seq uint64) (bool, error) {
	return fs.removeMsg(seq, false, false, true)
}

func (fs *fileStore) EraseMsg(seq uint64) (bool, error) {
	return fs.removeMsg(seq, true, false, true)
}

// Convenience function to remove per subject tracking at the filestore level.
// Lock should be held.
func (fs *fileStore) removePerSubject(subj string) uint64 {
	if len(subj) == 0 || fs.psim == nil {
		return 0
	}
	// We do not update sense of fblk here but will do so when we resolve during lookup.
	bsubj := stringToBytes(subj)
	if info, ok := fs.psim.Find(bsubj); ok {
		info.total--
		if info.total == 1 {
			info.fblk = info.lblk
		} else if info.total == 0 {
			if _, ok = fs.psim.Delete(bsubj); ok {
				fs.tsl -= len(subj)
				return 0
			}
		}
		return info.total
	}
	return 0
}

// Remove a message, optionally rewriting the mb file.
func (fs *fileStore) removeMsg(seq uint64, secure, viaLimits, needFSLock bool) (bool, error) {
	if seq == 0 {
		return false, ErrStoreMsgNotFound
	}
	fsLock := func() {
		if needFSLock {
			fs.mu.Lock()
		}
	}
	fsUnlock := func() {
		if needFSLock {
			fs.mu.Unlock()
		}
	}

	fsLock()

	if fs.isClosed() {
		fsUnlock()
		return false, ErrStoreClosed
	}
	// If in encrypted mode negate secure rewrite here.
	if secure && fs.prf != nil {
		secure = false
	}

	mb := fs.selectMsgBlock(seq)
	if mb == nil {
		var err = ErrStoreEOF
		if seq <= fs.state.LastSeq {
			err = ErrStoreMsgNotFound
		}
		fsUnlock()
		return false, err
	}

	mb.mu.Lock()

	// See if we are closed or the sequence number is still relevant or if we know its deleted.
	if mb.closed || seq < atomic.LoadUint64(&mb.first.seq) || mb.dmap.Exists(seq) {
		mb.mu.Unlock()
		fsUnlock()
		return false, nil
	}

	fifo := seq == atomic.LoadUint64(&mb.first.seq)
	isLastBlock := mb == fs.lmb
	isEmpty := mb.msgs == 1 // ... about to be zero though.

	// We used to not have to load in the messages except with callbacks or the filtered subject state (which is now always on).
	// Now just load regardless.
	// TODO(dlc) - Figure out a way not to have to load it in, we need subject tracking outside main data block.
	var didLoad bool
	if mb.cacheNotLoaded() {
		if err := mb.loadMsgsWithLock(); err != nil {
			mb.mu.Unlock()
			fsUnlock()
			return false, err
		}
		didLoad = true
	}
	finishedWithCache := func() {
		if didLoad {
			mb.finishedWithCache()
		}
	}

	var (
		smv        StoreMsg
		subj       string
		ts         int64
		lhdr, lmsg int
		ttl        int64
	)
	// We don't use a copy as long as that's possible. When unlocking mb or erasing, we'll copy the subject.
	sm, err := mb.cacheLookupNoCopy(seq, &smv)
	if err != nil {
		finishedWithCache()
		mb.mu.Unlock()
		fsUnlock()
		// Mimic err behavior from above check to dmap. No error returned if already removed.
		if err == errDeletedMsg {
			err = nil
		}
		return false, err
	} else if sm != nil {
		subj = sm.subj
		ts = sm.ts
		lhdr = len(sm.hdr)
		lmsg = len(sm.msg)
		ttl, _ = getMessageTTL(sm.hdr)
	}

	// Check if we need to write a deleted record tombstone.
	// This is for user initiated removes or to hold the first seq
	// when the last block is empty.
	// If not via limits and not empty (empty writes tombstone below if last) write tombstone.
	if !viaLimits && !isEmpty && sm != nil {
		// Need to copy the subject since we unlock and re-acquire, and the cache could change.
		subj = copyString(subj)
		mb.mu.Unlock() // Only safe way to checkLastBlock is to unlock here...
		lmb, err := fs.checkLastBlock(emptyRecordLen)
		if err != nil {
			finishedWithCache()
			fsUnlock()
			return false, err
		}
		if err := lmb.writeTombstone(seq, ts); err != nil {
			finishedWithCache()
			fsUnlock()
			return false, err
		}
		mb.mu.Lock() // We'll need the lock back to carry on safely.
	}

	// Grab size
	msz := fileStoreMsgSizeRaw(len(subj), lhdr, lmsg)

	// Set cache timestamp for last remove.
	mb.lrts = ats.AccessTime()

	// Must always perform the erase, even if the block is empty as it could contain tombstones.
	if secure {
		// Grab record info, but use the pre-computed record length.
		ri, _, _, err := mb.slotInfo(int(seq - mb.cache.fseq))
		if err != nil {
			finishedWithCache()
			mb.mu.Unlock()
			fsUnlock()
			return false, err
		}
		// Need to copy the subject, as eraseMsg will overwrite the cache and we won't
		// be able to access sm.subj anymore later on.
		subj = copyString(subj)
		if err := mb.eraseMsg(seq, int(ri), int(msz), isLastBlock); err != nil {
			finishedWithCache()
			mb.mu.Unlock()
			fsUnlock()
			return false, err
		}
	}

	// Global stats
	if fs.state.Msgs > 0 {
		fs.state.Msgs--
	}
	if msz < fs.state.Bytes {
		fs.state.Bytes -= msz
	} else {
		fs.state.Bytes = 0
	}

	// Now local mb updates.
	if mb.msgs > 0 {
		mb.msgs--
	}
	if msz < mb.bytes {
		mb.bytes -= msz
	} else {
		mb.bytes = 0
	}

	// Allow us to check compaction again.
	mb.noCompact = false

	// Mark as dirty for stream state.
	fs.dirty++

	// If we are tracking subjects here make sure we update that accounting.
	mb.ensurePerSubjectInfoLoaded()

	// If we are tracking multiple subjects here make sure we update that accounting.
	mb.removeSeqPerSubject(subj, seq)
	fs.removePerSubject(subj)
	if fs.ttls != nil && ttl > 0 {
		expires := time.Duration(ts) + (time.Second * time.Duration(ttl))
		fs.ttls.Remove(seq, int64(expires))
	}

	if fifo {
		mb.selectNextFirst()
		if !isEmpty {
			// Can update this one in place.
			if seq == fs.state.FirstSeq {
				fs.state.FirstSeq = atomic.LoadUint64(&mb.first.seq) // new one.
				if mb.first.ts == 0 {
					fs.state.FirstTime = time.Time{}
				} else {
					fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
				}
			}
		}
	} else if !isEmpty {
		// Out of order delete.
		mb.dmap.Insert(seq)
		// Make simple check here similar to Compact(). If we can save 50% and over a certain threshold do inline.
		// All other more thorough cleanup will happen in syncBlocks logic.
		// Note that we do not have to store empty records for the deleted, so don't use to calculate.
		// TODO(dlc) - This should not be inline, should kick the sync routine.
		if !isLastBlock && mb.shouldCompactInline() {
			mb.compact()
		}
	}

	if secure {
		if ld, _ := mb.flushPendingMsgsLocked(); ld != nil {
			// We have the mb lock here, this needs the mb locks so do in its own go routine.
			go fs.rebuildState(ld)
		}
	}

	// If empty remove this block and check if we need to update first sequence.
	// We will write a tombstone at the end.
	var firstSeqNeedsUpdate bool
	if isEmpty {
		// This writes tombstone iff mb == lmb, so no need to do above.
		fs.removeMsgBlock(mb)
		firstSeqNeedsUpdate = seq == fs.state.FirstSeq
	}
	finishedWithCache()
	mb.mu.Unlock()

	// If we emptied the current message block and the seq was state.FirstSeq
	// then we need to jump message blocks. We will also write the index so
	// we don't lose track of the first sequence.
	if firstSeqNeedsUpdate {
		fs.selectNextFirst()
	}

	if cb := fs.scb; cb != nil {
		// If we have a callback registered we need to release lock regardless since cb might need it to lookup msg, etc.
		fs.mu.Unlock()
		// Storage updates.
		delta := int64(msz)
		cb(-1, -delta, seq, subj)

		if !needFSLock {
			fs.mu.Lock()
		}
	} else if needFSLock {
		// We acquired it so release it.
		fs.mu.Unlock()
	}

	return true, nil
}

// Tests whether we should try to compact this block while inline removing msgs.
// We will want rbytes to be over the minimum and have a 2x potential savings.
// If we compacted before but rbytes didn't improve much, guard against constantly compacting.
// Lock should be held.
func (mb *msgBlock) shouldCompactInline() bool {
	return mb.rbytes > compactMinimum && mb.bytes*2 < mb.rbytes && (mb.cbytes == 0 || mb.bytes*2 < mb.cbytes)
}

// Tests whether we should try to compact this block while running periodic sync.
// We will want rbytes to be over the minimum and have a 2x potential savings.
// Ignores 2MB minimum.
// Lock should be held.
func (mb *msgBlock) shouldCompactSync() bool {
	return mb.bytes*2 < mb.rbytes && !mb.noCompact
}

// This will compact and rewrite this block. This version will not process any tombstone cleanup.
// Write lock needs to be held.
func (mb *msgBlock) compact() {
	mb.compactWithFloor(0)
}

// This will compact and rewrite this block. This should only be called when we know we want to rewrite this block.
// This should not be called on the lmb since we will prune tail deleted messages which could cause issues with
// writing new messages. We will silently bail on any issues with the underlying block and let someone else detect.
// if fseq > 0 we will attempt to cleanup stale tombstones.
// Write lock needs to be held.
func (mb *msgBlock) compactWithFloor(floor uint64) error {
	wasLoaded := mb.cache != nil && mb.cacheAlreadyLoaded()
	if !wasLoaded {
		if err := mb.loadMsgsWithLock(); err != nil {
			return err
		}
	}
	defer mb.finishedWithCache()

	buf := mb.cache.buf
	nbuf := getMsgBlockBuf(len(buf))
	// Recycle our nbuf when we are done.
	defer recycleMsgBlockBuf(nbuf)

	var le = binary.LittleEndian
	var firstSet bool
	var last uint64
	var msgs uint64

	fseq := atomic.LoadUint64(&mb.first.seq)
	lseq := atomic.LoadUint64(&mb.last.seq)
	isDeleted := func(seq uint64) bool {
		return seq == 0 || seq&ebit != 0 || mb.dmap.Exists(seq) || seq < fseq
	}

	for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; {
		if index+msgHdrSize > lbuf {
			return fmt.Errorf("message overrun")
		}
		hdr := buf[index : index+msgHdrSize]
		rl, slen := le.Uint32(hdr[0:]), int(le.Uint16(hdr[20:]))
		hasHeaders := rl&hbit != 0
		// Clear any headers bit that could be set.
		rl &^= hbit
		shlen := slen
		if hasHeaders {
			shlen += 4
		}
		dlen := int(rl) - msgHdrSize
		// Do some quick sanity checks here.
		if dlen < 0 || shlen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh {
			return fmt.Errorf("sanity check failed")
		}
		// Only need to process non-deleted messages.
		seq := le.Uint64(hdr[4:])
		ts := int64(le.Uint64(hdr[12:]))

		if !isDeleted(seq) {
			// Check for tombstones.
			if seq&tbit != 0 {
				seq = seq &^ tbit
				// If this entry is for a lower seq than ours then keep around.
				// We also check that it is greater than our floor. Floor is zero on normal
				// calls to compact.
				if seq < fseq && seq >= floor {
					nbuf = append(nbuf, buf[index:index+rl]...)
				}
			} else {
				// Normal message here.
				msgs++
				nbuf = append(nbuf, buf[index:index+rl]...)
				if !firstSet {
					firstSet = true
					atomic.StoreUint64(&mb.first.seq, seq)
				}
				if seq >= last {
					last = seq
					atomic.StoreUint64(&mb.last.seq, last)
					mb.last.ts = ts
				}
			}
		}
		// Advance to next record.
		index += rl
	}

	// Handle compression
	if mb.cmp != NoCompression && len(nbuf) > 0 {
		originalSize := len(nbuf)
		var err error
		if nbuf, err = mb.cmp.Compress(nbuf); err != nil {
			return err
		}
		meta := &CompressionInfo{
			Algorithm:    mb.cmp,
			OriginalSize: uint64(originalSize),
		}
		nbuf = append(meta.MarshalMetadata(), nbuf...)
	}

	// Check for encryption.
	if mb.bek != nil && len(nbuf) > 0 {
		// Recreate to reset counter.
		rbek, err := genBlockEncryptionKey(mb.fs.fcfg.Cipher, mb.seed, mb.nonce)
		if err != nil {
			return err
		}
		rbek.XORKeyStream(nbuf, nbuf)
	}

	// Close FDs first.
	mb.closeFDsLocked()

	// We will write to a new file and mv/rename it in case of failure.
	mfn := filepath.Join(mb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(newScan, mb.index))
	<-dios
	err := os.WriteFile(mfn, nbuf, defaultFilePerms)
	dios <- struct{}{}
	if err != nil {
		os.Remove(mfn)
		return err
	}
	if err := os.Rename(mfn, mb.mfn); err != nil {
		os.Remove(mfn)
		return err
	}

	// Make sure to sync
	mb.needSync = true

	// Capture the updated rbytes.
	if rbytes := uint64(len(nbuf)); rbytes == mb.rbytes {
		// No change, so set our noCompact bool here to avoid attempting to continually compress in syncBlocks.
		mb.noCompact = true
	} else {
		mb.rbytes = rbytes
	}
	mb.cbytes = mb.bytes

	// Remove any seqs from the beginning of the blk.
	for seq, nfseq := fseq, atomic.LoadUint64(&mb.first.seq); seq < nfseq; seq++ {
		mb.dmap.Delete(seq)
	}
	// Remove any seqs from the ending of the blk.
	for seq, nlseq := lseq, atomic.LoadUint64(&mb.last.seq); seq > nlseq; seq-- {
		mb.dmap.Delete(seq)
	}
	// If the block itself has no messages anymore (could still contain tombstones though),
	// then we need to account for that by resetting the last sequence and timestamp.
	if msgs == 0 {
		atomic.StoreUint64(&mb.last.seq, fseq-1)
		mb.last.ts = 0
		mb.dmap.Empty()
	}
	// Make sure we clear the cache since no longer valid.
	mb.clearCacheAndOffset()
	// If we entered with the msgs loaded make sure to reload them.
	if wasLoaded {
		if err := mb.loadMsgsWithLock(); err != nil {
			return err
		}
	}
	return nil
}

// Grab info from a slot.
// Lock should be held.
func (mb *msgBlock) slotInfo(slot int) (uint32, uint32, bool, error) {
	switch {
	case mb.cache == nil: // Shouldn't be possible, but check it anyway.
		return 0, 0, false, errNoCache
	case slot < 0:
		mb.fs.warn("Partial cache: offset slot index %d is less zero", slot)
		return 0, 0, false, errPartialCache
	case slot >= len(mb.cache.idx):
		mb.fs.warn("Partial cache: offset slot index %d is greater than index len %d", slot, len(mb.cache.idx))
		return 0, 0, false, errPartialCache
	}

	bi := mb.cache.idx[slot]
	ri, hashChecked := (bi &^ cbit), (bi&cbit) != 0

	// If this is a deleted slot return here.
	if bi == dbit {
		return 0, 0, false, errDeletedMsg
	}

	// Determine record length
	var rl uint32
	// Need to account for dbit markers in idx.
	// So we will walk until we find valid idx slot to calculate rl.
	for i := 1; slot+i < len(mb.cache.idx); i++ {
		ni := mb.cache.idx[slot+i] &^ cbit
		if ni == dbit {
			continue
		}
		rl = ni - ri
		break
	}
	// check if we had all trailing dbits.
	// If so use len of cache buf minus ri.
	if rl == 0 {
		rl = uint32(len(mb.cache.buf)) - ri
	}
	if rl < msgHdrSize {
		return 0, 0, false, errBadMsg{mb.mfn, fmt.Sprintf("length too short for slot %d", slot)}
	}
	return uint32(ri), rl, hashChecked, nil
}

func (fs *fileStore) isClosed() bool {
	return fs.closed.Load()
}

// Will spin up our flush loop.
func (mb *msgBlock) spinUpFlushLoop() {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	mb.spinUpFlushLoopLocked()
}

// Will spin up our flush loop.
// Lock should be held.
func (mb *msgBlock) spinUpFlushLoopLocked() {
	// Are we already running or closed?
	if mb.flusher || mb.closed {
		return
	}
	mb.flusher = true
	mb.fch = make(chan struct{}, 1)
	mb.qch = make(chan struct{})
	fch, qch := mb.fch, mb.qch

	go mb.flushLoop(fch, qch)
}

// Raw low level kicker for flush loops.
func kickFlusher(fch chan struct{}) {
	if fch != nil {
		select {
		case fch <- struct{}{}:
		default:
		}
	}
}

// Kick flusher for this message block.
func (mb *msgBlock) kickFlusher() {
	mb.mu.RLock()
	defer mb.mu.RUnlock()
	kickFlusher(mb.fch)
}

func (mb *msgBlock) setInFlusher() {
	mb.mu.Lock()
	mb.flusher = true
	mb.mu.Unlock()
}

func (mb *msgBlock) clearInFlusher() {
	mb.mu.Lock()
	mb.flusher = false
	if mb.qch != nil {
		close(mb.qch)
		mb.qch = nil
	}
	if mb.fch != nil {
		close(mb.fch)
		mb.fch = nil
	}
	mb.mu.Unlock()
}

// flushLoop watches for messages, index info, or recently closed msg block updates.
func (mb *msgBlock) flushLoop(fch, qch chan struct{}) {
	mb.setInFlusher()
	defer mb.clearInFlusher()

	for {
		select {
		case <-fch:
			// If we have pending messages process them first.
			if waiting := mb.pendingWriteSize(); waiting != 0 {
				ts := 1 * time.Millisecond
				var waited time.Duration

				for waiting < coalesceMinimum {
					time.Sleep(ts)
					select {
					case <-qch:
						return
					default:
					}
					newWaiting := mb.pendingWriteSize()
					if waited = waited + ts; waited > maxFlushWait || newWaiting <= waiting {
						break
					}
					waiting = newWaiting
					ts *= 2
				}

				mb.flushPendingMsgs()
			}

			// Check if we are no longer the last message block. If we are
			// not we can close FDs and exit.
			mb.fs.mu.RLock()
			notLast := mb != mb.fs.lmb
			mb.fs.mu.RUnlock()

			if notLast {
				if err := mb.closeFDs(); err == nil {
					return
				}
			}
		case <-qch:
			return
		}
	}
}

// Lock should be held.
func (mb *msgBlock) eraseMsg(seq uint64, ri, rl int, isLastBlock bool) error {
	var le = binary.LittleEndian
	var hdr [msgHdrSize]byte

	le.PutUint32(hdr[0:], uint32(rl))
	le.PutUint64(hdr[4:], seq|ebit)
	le.PutUint64(hdr[12:], 0)
	le.PutUint16(hdr[20:], 0)

	// Randomize record
	data := make([]byte, rl-emptyRecordLen)
	if n, err := rand.Read(data); err != nil {
		return err
	} else if n != len(data) {
		return fmt.Errorf("not enough overwrite bytes read (%d != %d)", n, len(data))
	}

	// Now write to underlying buffer.
	var b bytes.Buffer
	b.Write(hdr[:])
	b.Write(data)

	// Calculate hash.
	mb.hh.Reset()
	mb.hh.Write(hdr[4:20])
	mb.hh.Write(data)
	var hb [highwayhash.Size64]byte
	checksum := mb.hh.Sum(hb[:0])
	// Write to msg record.
	b.Write(checksum)

	// Update both cache and disk.
	nbytes := b.Bytes()

	// Cache
	if ri+rl <= len(mb.cache.buf) {
		copy(mb.cache.buf[ri:ri+rl], nbytes)
	}

	// Disk
	if mb.cache.wp > ri {
		if err := mb.atomicOverwriteFile(mb.cache.buf, !isLastBlock); err != nil {
			return err
		}
	}
	return nil
}

// Truncate this message block to the tseq and ts.
// Lock should be held.
func (mb *msgBlock) truncate(tseq uint64, ts int64) (nmsgs, nbytes uint64, err error) {
	// Make sure we are loaded to process messages etc.
	if err := mb.loadMsgsWithLock(); err != nil {
		return 0, 0, err
	}
	defer mb.finishedWithCache()

	// Calculate new eof using slot info from our new last sm.
	ri, rl, _, err := mb.slotInfo(int(tseq - mb.cache.fseq))
	if err != nil {
		return 0, 0, err
	}
	// Calculate new eof.
	eof := int64(ri + rl)

	// FIXME(dlc) - We could be smarter here.
	if buf, _ := mb.bytesPending(); len(buf) > 0 {
		ld, err := mb.flushPendingMsgsLocked()
		if ld != nil && mb.fs != nil {
			// We do not know if fs is locked or not at this point.
			// This should be an exceptional condition so do so in Go routine.
			go mb.fs.rebuildState(ld)
		}
		if err != nil {
			return 0, 0, err
		}
	}

	var purged, bytes uint64

	checkDmap := mb.dmap.Size() > 0
	var smv StoreMsg

	for seq := atomic.LoadUint64(&mb.last.seq); seq > tseq; seq-- {
		if checkDmap {
			if mb.dmap.Exists(seq) {
				// Delete and skip to next.
				mb.dmap.Delete(seq)
				checkDmap = !mb.dmap.IsEmpty()
				continue
			}
		}
		// We should have a valid msg to calculate removal stats.
		if m, err := mb.cacheLookupNoCopy(seq, &smv); err == nil {
			if mb.msgs > 0 {
				rl := fileStoreMsgSize(m.subj, m.hdr, m.msg)
				mb.msgs--
				if rl > mb.bytes {
					rl = mb.bytes
				}
				mb.bytes -= rl
				mb.rbytes -= rl
				// For return accounting.
				purged++
				bytes += uint64(rl)
			}
		}
	}

	// If the block is compressed/encrypted then we have to load it into memory
	// and decompress/decrypt it, truncate it and then write it back out.
	// Otherwise, truncate the file itself and close the descriptor.
	if mb.cmp != NoCompression || mb.bek != nil {
		buf, err := mb.loadBlock(nil)
		if err != nil {
			return 0, 0, fmt.Errorf("failed to load block from disk: %w", err)
		}
		if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
			return 0, 0, err
		}
		if buf, err = mb.decompressIfNeeded(buf); err != nil {
			return 0, 0, fmt.Errorf("failed to decompress block: %w", err)
		}
		buf = buf[:eof]
		copy(mb.lchk[0:], buf[len(buf)-checksumSize:])
		// We did decompress but don't recompress the truncated buffer here since we're the last block
		// and would otherwise have compressed data and allow to write uncompressed data in the same block.
		if err = mb.atomicOverwriteFile(buf, false); err != nil {
			return 0, 0, err
		}
	} else if mb.mfd != nil {
		mb.mfd.Truncate(eof)
		mb.mfd.Sync()
		// Update our checksum.
		var lchk [8]byte
		mb.mfd.ReadAt(lchk[:], eof-8)
		copy(mb.lchk[0:], lchk[:])
	} else {
		return 0, 0, fmt.Errorf("failed to truncate msg block %d, file not open", mb.index)
	}

	// Update our last msg.
	atomic.StoreUint64(&mb.last.seq, tseq)
	mb.last.ts = ts

	// Clear our cache.
	mb.clearCacheAndOffset()

	// Redo per subject info for this block.
	mb.resetPerSubjectInfo()

	// Load msgs again.
	return purged, bytes, mb.loadMsgsWithLock()
}

// Helper to determine if the mb is empty.
func (mb *msgBlock) isEmpty() bool {
	return atomic.LoadUint64(&mb.first.seq) > atomic.LoadUint64(&mb.last.seq)
}

// Lock should be held.
func (mb *msgBlock) selectNextFirst() {
	var seq uint64
	fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
	for seq = fseq + 1; seq <= lseq; seq++ {
		if mb.dmap.Exists(seq) {
			// We will move past this so we can delete the entry.
			mb.dmap.Delete(seq)
		} else {
			break
		}
	}
	// Set new first sequence.
	atomic.StoreUint64(&mb.first.seq, seq)

	// Check if we are empty..
	if seq > lseq {
		mb.first.ts = 0
		return
	}

	// Need to get the timestamp.
	// We will try the cache direct and fallback if needed.
	var smv StoreMsg
	sm, _ := mb.cacheLookupNoCopy(seq, &smv)
	if sm == nil {
		// Slow path, need to unlock.
		mb.mu.Unlock()
		sm, _, _ = mb.fetchMsgNoCopy(seq, &smv)
		mb.mu.Lock()
	}
	if sm != nil {
		mb.first.ts = sm.ts
	} else {
		mb.first.ts = 0
	}
}

// Select the next FirstSeq
// Also cleans up empty blocks at the start only containing tombstones.
// Lock should be held.
func (fs *fileStore) selectNextFirst() {
	if len(fs.blks) > 0 {
		for len(fs.blks) > 1 {
			mb := fs.blks[0]
			mb.mu.Lock()
			empty := mb.msgs == 0
			if !empty {
				mb.mu.Unlock()
				break
			}
			fs.forceRemoveMsgBlock(mb)
			mb.mu.Unlock()
		}
		mb := fs.blks[0]
		mb.mu.RLock()
		fs.state.FirstSeq = atomic.LoadUint64(&mb.first.seq)
		if mb.first.ts == 0 {
			fs.state.FirstTime = time.Time{}
		} else {
			fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
		}
		mb.mu.RUnlock()
	} else {
		// Could not find anything, so treat like purge
		fs.state.FirstSeq = fs.state.LastSeq + 1
		fs.state.FirstTime = time.Time{}
	}
	// Mark first as moved. Plays into tombstone cleanup for syncBlocks.
	fs.firstMoved = true
}

// Lock should be held.
func (mb *msgBlock) resetCacheExpireTimer(td time.Duration) {
	if td == 0 {
		td = mb.cexp + 100*time.Millisecond
	}
	if mb.ctmr == nil {
		mb.ctmr = time.AfterFunc(td, mb.expireCache)
	} else {
		mb.ctmr.Reset(td)
	}
}

// Lock should be held.
func (mb *msgBlock) startCacheExpireTimer() {
	mb.resetCacheExpireTimer(0)
}

// Used when we load in a message block.
// Lock should be held.
func (mb *msgBlock) clearCacheAndOffset() {
	// Reset linear scan tracker.
	mb.llseq = 0
	mb.clearCache()
}

// Lock should be held.
func (mb *msgBlock) clearCache() {
	if mb.ctmr != nil {
		tsla := mb.sinceLastActivity()
		if mb.fss == nil || tsla > mb.fexp {
			// Force
			mb.fss = nil
			mb.ctmr.Stop()
			mb.ctmr = nil
		} else {
			mb.resetCacheExpireTimer(mb.fexp - tsla)
		}
	}

	mbcache := mb.cache
	if mbcache == nil {
		mbcache = mb.ecache.Value()
	}
	if mbcache == nil {
		return
	}

	buf := mbcache.buf
	mb.cache = nil
	mb.ecache.Set(nil)
	recycleMsgBlockBuf(buf)
}

// Called to possibly expire a message block cache.
func (mb *msgBlock) expireCache() {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	mb.expireCacheLocked()
}

func (mb *msgBlock) tryForceExpireCache() {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	mb.tryForceExpireCacheLocked()
}

// We will attempt to force expire this by temporarily clearing the last load time.
func (mb *msgBlock) tryForceExpireCacheLocked() {
	llts, lwts := mb.llts, mb.lwts
	mb.llts, mb.lwts = 0, 0
	mb.expireCacheLocked()
	mb.llts, mb.lwts = llts, lwts
}

// This is for expiration of the write cache, which will be partial with fip.
// So we want to bypass the Pools here.
// Lock should be held.
func (mb *msgBlock) tryExpireWriteCache() []byte {
	if mb.cache == nil {
		return nil
	}
	lwts, buf, llts, nra := mb.lwts, mb.cache.buf, mb.llts, mb.cache.nra
	mb.lwts, mb.cache.nra = 0, true
	mb.expireCacheLocked()
	mb.lwts = lwts
	if mb.cache != nil {
		mb.cache.nra = nra
	}
	// We could check for a certain time since last load, but to be safe just reuse if no loads at all.
	if llts == 0 && (mb.cache == nil || mb.cache.buf == nil) {
		// Clear last write time since we now are about to move on to a new lmb.
		mb.lwts = 0
		return buf[:0]
	}
	return nil
}

// Lock should be held.
func (mb *msgBlock) expireCacheLocked() {
	var strengthened bool
	if mb.cache == nil {
		mb.cache = mb.ecache.Value()
		strengthened = true
	}
	if mb.cache == nil && mb.fss == nil {
		if mb.ctmr != nil {
			mb.ctmr.Stop()
			mb.ctmr = nil
		}
		return
	}

	// Can't expire if we still have pending.
	if mb.cache != nil && len(mb.cache.buf)-int(mb.cache.wp) > 0 {
		mb.resetCacheExpireTimer(mb.cexp)
		if strengthened {
			mb.finishedWithCache()
		}
		return
	}

	// Grab timestamp to compare.
	tns := ats.AccessTime()

	// For the core buffer of messages, we care about reads and writes, but not removes.
	bufts := mb.llts
	if mb.lwts > bufts {
		bufts = mb.lwts
	}

	// Check for activity on the cache that would prevent us from expiring.
	if tns-bufts <= int64(mb.cexp) {
		mb.resetCacheExpireTimer(mb.cexp - time.Duration(tns-bufts))
		if strengthened {
			mb.finishedWithCache()
		}
		return
	}

	// If we are here we will at least expire the core msg buffer.
	// We need to capture offset in case we do a write next before a full load.
	if mb.cache != nil {
		if !mb.cache.nra {
			recycleMsgBlockBuf(mb.cache.buf)
		}
		mb.cache.buf = nil
		mb.cache.idx = mb.cache.idx[:0]
		mb.cache.wp = 0
	}

	// Check if we can clear out our idx unless under force expire.
	// fss we keep longer and expire under sync timer checks.
	mb.clearCache()
}

func (fs *fileStore) startAgeChk() {
	if fs.ageChk != nil {
		return
	}
	if fs.cfg.MaxAge != 0 || fs.ttls != nil {
		fs.ageChk = time.AfterFunc(fs.cfg.MaxAge, fs.expireMsgs)
	}
}

// Lock should be held.
func (fs *fileStore) resetAgeChk(delta int64) {
	// If we're already expiring messages, it will make sure to reset.
	// Don't trigger again, as that could result in many expire goroutines.
	if fs.ageChkRun {
		return
	}

	var next int64 = math.MaxInt64
	if fs.ttls != nil {
		next = fs.ttls.GetNextExpiration(next)
	}

	// If there's no MaxAge and there's nothing waiting to be expired then
	// don't bother continuing. The next storeRawMsg() will wake us up if
	// needs be.
	if fs.cfg.MaxAge <= 0 && next == math.MaxInt64 {
		clearTimer(&fs.ageChk)
		return
	}

	// Check to see if we should be firing sooner than MaxAge for an expiring TTL.
	fireIn := fs.cfg.MaxAge

	// If delta for next-to-expire message is unset, but we still have messages to remove.
	// Assume messages are removed through proposals, and we need to speed up subsequent age check.
	if delta == 0 && fs.state.Msgs > 0 {
		if until := 2 * time.Second; until < fireIn {
			fireIn = until
		}
	}

	if next < math.MaxInt64 {
		// Looks like there's a next expiration, use it either if there's no
		// MaxAge set or if it looks to be sooner than MaxAge is.
		if until := time.Until(time.Unix(0, next)); fireIn == 0 || until < fireIn {
			fireIn = until
		}
	}

	// If not then look at the delta provided (usually gap to next age expiry).
	if delta > 0 {
		if fireIn == 0 || time.Duration(delta) < fireIn {
			fireIn = time.Duration(delta)
		}
	}

	// Make sure we aren't firing too often either way, otherwise we can
	// negatively impact stream ingest performance.
	if fireIn < 250*time.Millisecond {
		fireIn = 250 * time.Millisecond
	}

	// If we want to kick the timer to run later than what was assigned before, don't reset it.
	// Otherwise, we could get in a situation where the timer is continuously reset, and it never runs.
	expires := ats.AccessTime() + fireIn.Nanoseconds()
	if fs.ageChkTime > 0 && expires > fs.ageChkTime {
		return
	}

	fs.ageChkTime = expires
	if fs.ageChk != nil {
		fs.ageChk.Reset(fireIn)
	} else {
		fs.ageChk = time.AfterFunc(fireIn, fs.expireMsgs)
	}
}

// Lock should be held.
func (fs *fileStore) cancelAgeChk() {
	if fs.ageChk != nil {
		fs.ageChk.Stop()
		fs.ageChk = nil
		fs.ageChkTime = 0
	}
}

// Will expire msgs that are too old.
func (fs *fileStore) expireMsgs() {
	// We need to delete one by one here and can not optimize for the time being.
	// Reason is that we need more information to adjust ack pending in consumers.
	var smv StoreMsg
	var sm *StoreMsg

	fs.mu.Lock()
	maxAge := int64(fs.cfg.MaxAge)
	minAge := ats.AccessTime() - maxAge
	rmcb := fs.rmcb
	pmsgcb := fs.pmsgcb
	sdmTTL := int64(fs.cfg.SubjectDeleteMarkerTTL.Seconds())
	sdmEnabled := sdmTTL > 0

	// If SDM is enabled, but handlers aren't set up yet. Try again later.
	if sdmEnabled && (rmcb == nil || pmsgcb == nil) {
		fs.resetAgeChk(0)
		fs.mu.Unlock()
		return
	}
	fs.ageChkRun = true
	fs.mu.Unlock()

	if maxAge > 0 {
		var seq uint64
		for sm, seq, _ = fs.LoadNextMsg(fwcs, true, 0, &smv); sm != nil && sm.ts <= minAge; sm, seq, _ = fs.LoadNextMsg(fwcs, true, seq+1, &smv) {
			if len(sm.hdr) > 0 {
				if ttl, err := getMessageTTL(sm.hdr); err == nil && ttl < 0 {
					// The message has a negative TTL, therefore it must "never expire".
					minAge = ats.AccessTime() - maxAge
					continue
				}
			}
			// Remove the message and then, if LimitsTTL is enabled, try and work out
			// if it was the last message of that particular subject that we just deleted.
			if sdmEnabled {
				if last, ok := fs.shouldProcessSdm(seq, sm.subj); ok {
					sdm := last && !isSubjectDeleteMarker(sm.hdr)
					fs.handleRemovalOrSdm(seq, sm.subj, sdm, sdmTTL)
				}
			} else {
				fs.mu.Lock()
				fs.removeMsgViaLimits(sm.seq)
				fs.mu.Unlock()
			}
			// Recalculate in case we are expiring a bunch.
			minAge = ats.AccessTime() - maxAge
		}
	}
	var ageDelta int64
	if sm != nil {
		ageDelta = sm.ts - minAge
	}

	fs.mu.Lock()
	defer fs.mu.Unlock()

	// TODO: Not great that we're holding the lock here, but the timed hash wheel isn't thread-safe.
	nextTTL := int64(math.MaxInt64)
	var rmSeqs []thw.HashWheelEntry
	if fs.ttls != nil {
		fs.ttls.ExpireTasks(func(seq uint64, ts int64) bool {
			rmSeqs = append(rmSeqs, thw.HashWheelEntry{Seq: seq, Expires: ts})
			// We might need to remove messages out of band, those can fail, and we can be shutdown halfway
			// through so don't remove from THW just yet.
			return false
		})
		nextTTL = fs.ttls.GetNextExpiration(math.MaxInt64)
	}

	// Remove messages collected by THW.
	if !sdmEnabled {
		for _, rm := range rmSeqs {
			fs.removeMsg(rm.Seq, false, false, false)
		}
	} else {
		// THW is unordered, so must sort by sequence and must not be holding the lock.
		fs.mu.Unlock()
		slices.SortFunc(rmSeqs, func(a, b thw.HashWheelEntry) int {
			if a.Seq == b.Seq {
				return 0
			} else if a.Seq < b.Seq {
				return -1
			} else {
				return 1
			}
		})
		for _, rm := range rmSeqs {
			// Need to grab subject for the specified sequence if for SDM, and check
			// if the message hasn't been removed in the meantime.
			// We need to grab the message and check if we should process SDM while holding the lock,
			// otherwise we can race if a deletion of this message is in progress.
			fs.mu.Lock()
			sm, _ = fs.msgForSeqLocked(rm.Seq, &smv, false)
			if sm == nil {
				fs.ttls.Remove(rm.Seq, rm.Expires)
				fs.mu.Unlock()
				continue
			}
			last, ok := fs.shouldProcessSdmLocked(rm.Seq, sm.subj)
			fs.mu.Unlock()
			if ok {
				sdm := last && !isSubjectDeleteMarker(sm.hdr)
				fs.handleRemovalOrSdm(rm.Seq, sm.subj, sdm, sdmTTL)
			}
		}
		fs.mu.Lock()
	}

	// Only cancel if no message left, not on potential lookup error that would result in sm == nil.
	fs.ageChkRun, fs.ageChkTime = false, 0
	if fs.state.Msgs == 0 && nextTTL == math.MaxInt64 {
		fs.cancelAgeChk()
	} else {
		fs.resetAgeChk(ageDelta)
	}
}

func (fs *fileStore) shouldProcessSdm(seq uint64, subj string) (bool, bool) {
	fs.mu.Lock()
	defer fs.mu.Unlock()
	return fs.shouldProcessSdmLocked(seq, subj)
}

// Lock should be held.
func (fs *fileStore) shouldProcessSdmLocked(seq uint64, subj string) (bool, bool) {
	if fs.sdm == nil {
		fs.sdm = newSDMMeta()
	}

	if p, ok := fs.sdm.pending[seq]; ok {
		// Don't allow more proposals for the same sequence if we already did recently.
		if time.Since(time.Unix(0, p.ts)) < 2*time.Second {
			return p.last, false
		}
		// If we're about to use the cached value, and we knew it was last before,
		// quickly check that we don't have more remaining messages for the subject now.
		// Which means we are not the last anymore and must reset to not remove later data.
		if p.last {
			msgs := fs.subjectsTotalsLocked(subj)[subj]
			numPending := fs.sdm.totals[subj]
			if remaining := msgs - numPending; remaining > 0 {
				p.last = false
			}
		}
		fs.sdm.pending[seq] = SDMBySeq{p.last, time.Now().UnixNano()}
		return p.last, true
	}

	msgs := fs.subjectsTotalsLocked(subj)[subj]
	if msgs == 0 {
		return false, true
	}
	numPending := fs.sdm.totals[subj]
	remaining := msgs - numPending
	return fs.sdm.trackPending(seq, subj, remaining == 1), true
}

func (fs *fileStore) handleRemovalOrSdm(seq uint64, subj string, sdm bool, sdmTTL int64) {
	if sdm {
		var _hdr [128]byte
		hdr := fmt.Appendf(
			_hdr[:0],
			"NATS/1.0\r\n%s: %s\r\n%s: %s\r\n%s: %s\r\n\r\n",
			JSMarkerReason, JSMarkerReasonMaxAge,
			JSMessageTTL, time.Duration(sdmTTL)*time.Second,
			JSMsgRollup, JSMsgRollupSubject,
		)
		msg := &inMsg{
			subj: subj,
			hdr:  hdr,
		}
		fs.pmsgcb(msg)
	} else {
		fs.rmcb(seq)
	}
}

// Will run through scheduled messages.
func (fs *fileStore) runMsgScheduling() {
	// TODO: Not great that we're holding the lock here, but the timed hash wheel and message scheduling isn't thread-safe.
	fs.mu.Lock()
	defer fs.mu.Unlock()

	// If scheduling is enabled, but handler isn't set up yet. Try again later.
	if fs.scheduling == nil {
		return
	}
	if fs.pmsgcb == nil {
		fs.scheduling.resetTimer()
		return
	}
	fs.scheduling.running = true

	scheduledMsgs := fs.scheduling.getScheduledMessages(func(seq uint64, smv *StoreMsg) *StoreMsg {
		sm, _ := fs.msgForSeqLocked(seq, smv, false)
		return sm
	})
	if len(scheduledMsgs) > 0 {
		fs.mu.Unlock()
		for _, msg := range scheduledMsgs {
			fs.pmsgcb(msg)
		}
		fs.mu.Lock()
	}

	fs.scheduling.running, fs.scheduling.deadline = false, 0
	fs.scheduling.resetTimer()
}

// Lock should be held.
func (fs *fileStore) checkAndFlushLastBlock() {
	lmb := fs.lmb
	if lmb == nil {
		return
	}
	if lmb.pendingWriteSize() > 0 {
		// Since fs lock is held need to pull this apart in case we need to rebuild state.
		lmb.mu.Lock()
		ld, _ := lmb.flushPendingMsgsLocked()
		lmb.mu.Unlock()
		if ld != nil {
			fs.rebuildStateLocked(ld)
		}
	}
}

// This will check all the checksums on messages and report back any sequence numbers with errors.
func (fs *fileStore) checkMsgs() *LostStreamData {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	fs.checkAndFlushLastBlock()

	// Clear any global subject state.
	fs.psim, fs.tsl = fs.psim.Empty(), 0

	for _, mb := range fs.blks {
		// Make sure encryption loaded if needed for the block.
		fs.loadEncryptionForMsgBlock(mb)
		// FIXME(dlc) - check tombstones here too?
		if ld, _, err := mb.rebuildState(); err != nil && ld != nil {
			// Rebuild fs state too.
			fs.rebuildStateLocked(ld)
		}
		fs.populateGlobalPerSubjectInfo(mb)
	}

	return fs.ld
}

// Lock should be held.
func (mb *msgBlock) enableForWriting(fip bool) error {
	if mb == nil {
		return errNoMsgBlk
	}
	if mb.mfd != nil {
		return nil
	}
	<-dios
	mfd, err := os.OpenFile(mb.mfn, os.O_CREATE|os.O_RDWR, defaultFilePerms)
	dios <- struct{}{}
	if err != nil {
		return fmt.Errorf("error opening msg block file [%q]: %v", mb.mfn, err)
	}
	mb.mfd = mfd

	// Spin up our flusher loop if needed.
	if !fip {
		mb.spinUpFlushLoopLocked()
	}

	return nil
}

// Helper function to place a delete tombstone.
func (mb *msgBlock) writeTombstone(seq uint64, ts int64) error {
	return mb.writeMsgRecord(emptyRecordLen, seq|tbit, _EMPTY_, nil, nil, ts, true)
}

// Helper function to place a delete tombstone without flush.
// Lock should not be held.
func (mb *msgBlock) writeTombstoneNoFlush(seq uint64, ts int64) error {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	return mb.writeMsgRecordLocked(emptyRecordLen, seq|tbit, _EMPTY_, nil, nil, ts, false, false)
}

// Will write the message record to the underlying message block.
// filestore lock will be held.
func (mb *msgBlock) writeMsgRecord(rl, seq uint64, subj string, mhdr, msg []byte, ts int64, flush bool) error {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	return mb.writeMsgRecordLocked(rl, seq, subj, mhdr, msg, ts, flush, true)
}

// Will write the message record to the underlying message block.
// filestore lock will be held.
// mb lock should be held.
func (mb *msgBlock) writeMsgRecordLocked(rl, seq uint64, subj string, mhdr, msg []byte, ts int64, flush, kick bool) error {
	// Enable for writing if our mfd is not open.
	if mb.mfd == nil {
		if err := mb.enableForWriting(flush && kick); err != nil {
			return err
		}
	}

	// Check if we are tracking per subject for our simple state.
	// Do this before changing the cache that would trigger a flush pending msgs call
	// if we needed to regenerate the per subject info.
	// Note that tombstones have no subject so will not trigger here.
	if len(subj) > 0 && !mb.noTrack {
		if err := mb.ensurePerSubjectInfoLoaded(); err != nil {
			return err
		}
		// Mark fss activity.
		mb.lsts = ats.AccessTime()
		if ss, ok := mb.fss.Find(stringToBytes(subj)); ok && ss != nil {
			ss.Msgs++
			ss.Last = seq
			ss.lastNeedsUpdate = false
		} else {
			mb.fss.Insert(stringToBytes(subj), SimpleState{Msgs: 1, First: seq, Last: seq})
		}
	}

	// Make sure we have a cache setup. Do so after ensurePerSubjectInfoLoaded as it may
	// have already brought in a cache for us.
	if err := mb.setupWriteCache(nil); err != nil {
		return err
	}

	// Make sure that the GC can't take away our writes by strengthening the elastic
	// reference. It will now stay strong until the flusher decides it is time to weaken.
	mb.ecache.Strengthen()

	// Make sure we have enough space to write into. If we don't then we can pull a buffer
	// from the next pool size up to save us from reallocating in append() below.
	if nsz := len(mb.cache.buf) + int(rl); cap(mb.cache.buf) < nsz {
		prev := mb.cache.buf
		mb.cache.buf = getMsgBlockBuf(nsz)
		if prev != nil {
			mb.cache.buf = mb.cache.buf[:copy(mb.cache.buf[:nsz], prev)]
			recycleMsgBlockBuf(prev)
		}
	}

	// Indexing
	index := len(mb.cache.buf)

	// Formats
	// Format with no header
	// total_len(4) sequence(8) timestamp(8) subj_len(2) subj msg hash(8)
	// With headers, high bit on total length will be set.
	// total_len(4) sequence(8) timestamp(8) subj_len(2) subj hdr_len(4) hdr msg hash(8)

	var le = binary.LittleEndian

	l := uint32(rl)
	hasHeaders := len(mhdr) > 0
	if hasHeaders {
		l |= hbit
	}

	// Reserve space for the header on the underlying buffer.
	mb.cache.buf = append(mb.cache.buf, make([]byte, msgHdrSize)...)
	hdr := mb.cache.buf[len(mb.cache.buf)-msgHdrSize : len(mb.cache.buf)]
	le.PutUint32(hdr[0:], l)
	le.PutUint64(hdr[4:], seq)
	le.PutUint64(hdr[12:], uint64(ts))
	le.PutUint16(hdr[20:], uint16(len(subj)))

	// Now write to underlying buffer.
	mb.cache.buf = append(mb.cache.buf, subj...)

	if hasHeaders {
		var hlen [4]byte
		le.PutUint32(hlen[0:], uint32(len(mhdr)))
		mb.cache.buf = append(mb.cache.buf, hlen[:]...)
		mb.cache.buf = append(mb.cache.buf, mhdr...)
	}
	mb.cache.buf = append(mb.cache.buf, msg...)

	// Calculate hash.
	mb.hh.Reset()
	mb.hh.Write(hdr[4:20])
	mb.hh.Write(stringToBytes(subj))
	if hasHeaders {
		mb.hh.Write(mhdr)
	}
	mb.hh.Write(msg)
	checksum := mb.hh.Sum(mb.lchk[:0:highwayhash.Size64])
	copy(mb.lchk[0:], checksum)

	// Update write through cache.
	// Write to msg record.
	mb.cache.buf = append(mb.cache.buf, checksum...)

	// Set cache timestamp for last store.
	mb.lwts = ts

	// Only update index and do accounting if not a delete tombstone.
	if seq&tbit == 0 {
		last := atomic.LoadUint64(&mb.last.seq)
		// Accounting, do this before stripping ebit, it is ebit aware.
		mb.updateAccounting(seq, ts, rl)
		// Strip ebit if set.
		seq = seq &^ ebit
		// If we have a hole due to skipping many messages, fill it.
		if len(mb.cache.idx) > 0 && last+1 < seq {
			for dseq := last + 1; dseq < seq; dseq++ {
				mb.cache.idx = append(mb.cache.idx, dbit)
			}
		}
		// Write index
		if mb.cache.idx = append(mb.cache.idx, uint32(index)|cbit); len(mb.cache.idx) == 1 {
			mb.cache.fseq = seq
		}
	} else {
		// If the block is empty, still adjust the accounting accordingly.
		tseq := seq &^ tbit
		if mb.msgs == 0 && tseq > atomic.LoadUint64(&mb.last.seq) {
			atomic.StoreUint64(&mb.last.seq, tseq)
			mb.last.ts = ts
			atomic.StoreUint64(&mb.first.seq, tseq+1)
			mb.first.ts = 0
		}
		// Make sure to account for tombstones in rbytes.
		mb.rbytes += rl
	}

	fch, werr := mb.fch, mb.werr

	// If we should be flushing, or had a write error, do so here.
	if (flush && mb.fs.fip) || werr != nil {
		ld, err := mb.flushPendingMsgsLocked()
		if ld != nil {
			// We have the mb lock here, this needs the mb locks so do in its own go routine.
			go mb.fs.rebuildState(ld)
		}
		if err != nil {
			return err
		}
	} else if kick {
		// Kick the flusher here.
		kickFlusher(fch)
	}
	return nil
}

// How many bytes pending to be written for this message block.
func (mb *msgBlock) pendingWriteSize() int {
	if mb == nil {
		return 0
	}
	mb.mu.RLock()
	defer mb.mu.RUnlock()
	return mb.pendingWriteSizeLocked()
}

// How many bytes pending to be written for this message block.
func (mb *msgBlock) pendingWriteSizeLocked() int {
	if mb == nil {
		return 0
	}
	var pending int
	if !mb.closed && mb.mfd != nil && mb.cache != nil {
		pending = len(mb.cache.buf) - int(mb.cache.wp)
	}
	return pending
}

// Try to close our FDs if we can.
func (mb *msgBlock) closeFDs() error {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	return mb.closeFDsLocked()
}

func (mb *msgBlock) closeFDsLocked() error {
	if buf, _ := mb.bytesPending(); len(buf) > 0 {
		return errPendingData
	}
	mb.closeFDsLockedNoCheck()
	return nil
}

func (mb *msgBlock) closeFDsLockedNoCheck() {
	if mb.mfd != nil {
		mb.mfd.Close()
		mb.mfd = nil
	}
}

// bytesPending returns the buffer to be used for writing to the underlying file.
// This marks we are in flush and will return nil if asked again until cleared.
// Lock should be held.
func (mb *msgBlock) bytesPending() ([]byte, error) {
	if mb == nil || mb.mfd == nil {
		return nil, errNoPending
	}
	if mb.cache == nil {
		return nil, errNoCache
	}
	if len(mb.cache.buf) <= mb.cache.wp {
		return nil, errNoPending
	}
	buf := mb.cache.buf[mb.cache.wp:]
	if len(buf) == 0 {
		return nil, errNoPending
	}
	return buf, nil
}

// Returns the current blkSize including deleted msgs etc.
func (mb *msgBlock) blkSize() uint64 {
	if mb == nil {
		return 0
	}
	mb.mu.RLock()
	nb := mb.rbytes
	mb.mu.RUnlock()
	return nb
}

// Update accounting on a write msg.
// Lock should be held.
func (mb *msgBlock) updateAccounting(seq uint64, ts int64, rl uint64) {
	isDeleted := seq&ebit != 0
	if isDeleted {
		seq = seq &^ ebit
	}

	fseq := atomic.LoadUint64(&mb.first.seq)
	if (fseq == 0 || mb.first.ts == 0) && seq >= fseq {
		atomic.StoreUint64(&mb.first.seq, seq)
		mb.first.ts = ts
	}
	// Need atomics here for selectMsgBlock speed.
	atomic.StoreUint64(&mb.last.seq, seq)
	mb.last.ts = ts
	mb.rbytes += rl
	if !isDeleted {
		mb.bytes += rl
		mb.msgs++
	}
}

// Helper to check last msg block and create new one if too big.
// Lock should be held.
func (fs *fileStore) checkLastBlock(rl uint64) (lmb *msgBlock, err error) {
	// Grab our current last message block.
	lmb = fs.lmb
	rbytes := lmb.blkSize()
	if lmb == nil || (rbytes > 0 && rbytes+rl > fs.fcfg.BlockSize) {
		if lmb, err = fs.newMsgBlockForWrite(); err != nil {
			return nil, err
		}
	}
	return lmb, nil
}

// Lock should be held.
func (fs *fileStore) writeMsgRecord(seq uint64, ts int64, subj string, hdr, msg []byte) (uint64, error) {
	// Get size for this message.
	rl := fileStoreMsgSize(subj, hdr, msg)
	if rl&hbit != 0 || rl > rlBadThresh {
		return 0, ErrMsgTooLarge
	}
	// Grab our current last message block.
	mb, err := fs.checkLastBlock(rl)
	if err != nil {
		return 0, err
	}

	// Mark as dirty for stream state.
	fs.dirty++

	// Ask msg block to store in write through cache.
	err = mb.writeMsgRecord(rl, seq, subj, hdr, msg, ts, fs.fip)

	return rl, err
}

// For writing tombstones to our lmb. This version will enforce maximum block sizes.
// Lock should be held.
func (fs *fileStore) writeTombstone(seq uint64, ts int64) error {
	// Grab our current last message block.
	lmb, err := fs.checkLastBlock(emptyRecordLen)
	if err != nil {
		return err
	}
	return lmb.writeTombstone(seq, ts)
}

// For writing tombstones to our lmb. This version will enforce maximum block sizes.
// This version does not flush contents.
// Lock should be held.
func (fs *fileStore) writeTombstoneNoFlush(seq uint64, ts int64) error {
	lmb, err := fs.checkLastBlock(emptyRecordLen)
	if err != nil {
		return err
	}
	// Write tombstone without flush or kick.
	return lmb.writeTombstoneNoFlush(seq, ts)
}

// Lock should be held.
func (mb *msgBlock) recompressOnDiskIfNeeded() error {
	alg := mb.fs.fcfg.Compression

	// Open up the file block and read in the entire contents into memory.
	// One of two things will happen:
	// 1. The block will be compressed already and have a valid metadata
	//    header, in which case we do nothing.
	// 2. The block will be uncompressed, in which case we will compress it
	//    and then write it back out to disk, re-encrypting if necessary.
	<-dios
	origBuf, err := os.ReadFile(mb.mfn)
	dios <- struct{}{}

	if err != nil {
		return fmt.Errorf("failed to read original block from disk: %w", err)
	}

	// If the block is encrypted then we will need to decrypt it before
	// doing anything. We always encrypt after compressing because then the
	// compression can be as efficient as possible on the raw data, whereas
	// the encrypted ciphertext will not compress anywhere near as well.
	// The block encryption also covers the optional compression metadata.
	if err = mb.encryptOrDecryptIfNeeded(origBuf); err != nil {
		return err
	}

	meta := &CompressionInfo{}
	if _, err := meta.UnmarshalMetadata(origBuf); err != nil {
		// An error is only returned here if there's a problem with parsing
		// the metadata. If the file has no metadata at all, no error is
		// returned and the algorithm defaults to no compression.
		return fmt.Errorf("failed to read existing metadata header: %w", err)
	}
	if meta.Algorithm == alg {
		// The block is already compressed with the chosen algorithm so there
		// is nothing else to do. This is not a common case, it is here only
		// to ensure we don't do unnecessary work in case something asked us
		// to recompress an already compressed block with the same algorithm.
		return nil
	} else if alg != NoCompression {
		// The block is already compressed using some algorithm, so we need
		// to decompress the block using the existing algorithm before we can
		// recompress it with the new one.
		if origBuf, err = meta.Algorithm.Decompress(origBuf); err != nil {
			return fmt.Errorf("failed to decompress original block: %w", err)
		}
	}

	return mb.atomicOverwriteFile(origBuf, true)
}

// Lock should be held.
func (mb *msgBlock) atomicOverwriteFile(buf []byte, allowCompress bool) error {
	if mb.mfd != nil {
		mb.closeFDsLockedNoCheck()
		defer mb.enableForWriting(mb.fs.fip)
	}

	origFN := mb.mfn               // The original message block on disk.
	tmpFN := mb.mfn + blkTmpSuffix // The new block will be written here.

	// Rather than modifying the existing block on disk (which is a dangerous
	// operation if something goes wrong), create a new temporary file. We will
	// write out the new block here and then swap the files around afterwards
	// once everything else has succeeded correctly.
	<-dios
	tmpFD, err := os.OpenFile(tmpFN, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultFilePerms)
	dios <- struct{}{}

	if err != nil {
		return fmt.Errorf("failed to create temporary file: %w", err)
	}

	errorCleanup := func(err error) error {
		tmpFD.Close()
		os.Remove(tmpFN)
		return err
	}

	alg := NoCompression
	if calg := mb.fs.fcfg.Compression; calg != NoCompression && allowCompress {
		alg = calg
		// The original buffer at this point is uncompressed, so we will now compress
		// it if needed. Note that if the selected algorithm is NoCompression, the
		// Compress function will just return the input buffer unmodified.
		originalSize := len(buf)
		if buf, err = alg.Compress(buf); err != nil {
			return errorCleanup(fmt.Errorf("failed to compress block: %w", err))
		}

		// We only need to write out the metadata header if compression is enabled.
		// If we're trying to uncompress the file on disk at this point, don't bother
		// writing metadata.
		meta := &CompressionInfo{
			Algorithm:    alg,
			OriginalSize: uint64(originalSize),
		}
		buf = append(meta.MarshalMetadata(), buf...)
	}

	// Re-encrypt the block if necessary.
	if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
		return errorCleanup(err)
	}

	// Write the new block data (which might be compressed or encrypted) to the
	// temporary file.
	if n, err := tmpFD.Write(buf); err != nil {
		return errorCleanup(fmt.Errorf("failed to write to temporary file: %w", err))
	} else if n != len(buf) {
		return errorCleanup(fmt.Errorf("short write to temporary file (%d != %d)", n, len(buf)))
	}
	if err := tmpFD.Sync(); err != nil {
		return errorCleanup(fmt.Errorf("failed to sync temporary file: %w", err))
	}
	if err := tmpFD.Close(); err != nil {
		return errorCleanup(fmt.Errorf("failed to close temporary file: %w", err))
	}

	// Now replace the original file with the newly updated temp file.
	if err := os.Rename(tmpFN, origFN); err != nil {
		return fmt.Errorf("failed to move temporary file into place: %w", err)
	}

	// Since the message block might be retained in memory, make sure the
	// compression algorithm is up-to-date, since this will be needed when
	// compacting or truncating.
	mb.cmp = alg

	// Also update rbytes
	mb.rbytes = uint64(len(buf))

	return nil
}

// Lock should be held.
func (mb *msgBlock) decompressIfNeeded(buf []byte) ([]byte, error) {
	var meta CompressionInfo
	if n, err := meta.UnmarshalMetadata(buf); err != nil {
		// There was a problem parsing the metadata header of the block.
		// If there's no metadata header, an error isn't returned here,
		// we will instead just use default values of no compression.
		return nil, err
	} else if n == 0 {
		// There were no metadata bytes, so we assume the block is not
		// compressed and return it as-is.
		return buf, nil
	} else {
		// Metadata was present so it's quite likely the block contents
		// are compressed. If by any chance the metadata claims that the
		// block is uncompressed, then the input slice is just returned
		// unmodified.
		return meta.Algorithm.Decompress(buf[n:])
	}
}

// Lock should be held.
func (mb *msgBlock) encryptOrDecryptIfNeeded(buf []byte) error {
	if mb.bek != nil && len(buf) > 0 {
		bek, err := genBlockEncryptionKey(mb.fs.fcfg.Cipher, mb.seed, mb.nonce)
		if err != nil {
			return err
		}
		mb.bek = bek
		mb.bek.XORKeyStream(buf, buf)
	}
	return nil
}

// Lock should be held.
func (mb *msgBlock) ensureRawBytesLoaded() error {
	if mb.rbytes > 0 {
		return nil
	}
	f, err := mb.openBlock()
	if err != nil {
		return err
	}
	defer f.Close()
	if fi, err := f.Stat(); fi != nil && err == nil {
		mb.rbytes = uint64(fi.Size())
	} else {
		return err
	}
	return nil
}

// Sync msg and index files as needed. This is called from a timer.
func (fs *fileStore) syncBlocks() {
	if fs.isClosed() {
		return
	}
	fs.mu.Lock()
	blks := append([]*msgBlock(nil), fs.blks...)
	lmb, firstMoved, firstSeq := fs.lmb, fs.firstMoved, fs.state.FirstSeq
	// Clear first moved.
	fs.firstMoved = false
	fs.mu.Unlock()

	var markDirty bool
	for _, mb := range blks {
		// Do actual sync. Hold lock for consistency.
		mb.mu.Lock()
		if mb.closed {
			mb.mu.Unlock()
			continue
		}
		// See if we can close FDs due to being idle.
		if mb.mfd != nil && mb.sinceLastWriteActivity() > closeFDsIdle && mb.pendingWriteSizeLocked() == 0 {
			mb.dirtyCloseWithRemove(false)
		}
		// If our first has moved and we are set to noCompact (which is from tombstones),
		// clear so that we might cleanup tombstones.
		if firstMoved && mb.noCompact {
			mb.noCompact = false
		}
		// Check if we should compact here as well.
		// Do not compact last mb.
		var needsCompact bool
		if mb != lmb && mb.ensureRawBytesLoaded() == nil && mb.shouldCompactSync() {
			needsCompact = true
			markDirty = true
		}

		// Flush anything that may be pending.
		mb.flushPendingMsgsLocked()
		// Check if we need to sync. We will not hold lock during actual sync.
		needSync := mb.needSync
		mb.mu.Unlock()

		// Check if we should compact here.
		// Need to hold fs lock in case we reference psim when loading in the mb and we may remove this block if truly empty.
		if needsCompact {
			fs.mu.RLock()
			mb.mu.Lock()
			mb.compactWithFloor(firstSeq)
			// If this compact removed all raw bytes due to tombstone cleanup, schedule to remove.
			shouldRemove := mb.rbytes == 0
			mb.mu.Unlock()
			fs.mu.RUnlock()

			// Check if we should remove. This will not be common, so we will re-take fs write lock here vs changing
			//  it above which we would prefer to be a readlock such that other lookups can occur while compacting this block.
			if shouldRemove {
				fs.mu.Lock()
				mb.mu.Lock()
				fs.removeMsgBlock(mb)
				mb.mu.Unlock()
				fs.mu.Unlock()
				needSync = false
			}
		}

		// Check if we need to sync this block.
		if needSync {
			mb.mu.Lock()
			var fd *os.File
			var didOpen bool
			if mb.mfd != nil {
				fd = mb.mfd
			} else {
				<-dios
				fd, _ = os.OpenFile(mb.mfn, os.O_RDWR, defaultFilePerms)
				dios <- struct{}{}
				didOpen = true
			}
			// If we have an fd.
			if fd != nil {
				canClear := fd.Sync() == nil
				// If we opened the file close the fd.
				if didOpen {
					fd.Close()
				}
				// Only clear sync flag on success.
				if canClear {
					mb.needSync = false
				}
			}
			mb.mu.Unlock()
		}
	}

	if fs.isClosed() {
		return
	}
	fs.mu.Lock()
	fs.setSyncTimer()
	if markDirty {
		fs.dirty++
	}

	// Sync state file if we are not running with sync always.
	if !fs.fcfg.SyncAlways {
		fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)
		<-dios
		fd, _ := os.OpenFile(fn, os.O_RDWR, defaultFilePerms)
		dios <- struct{}{}
		if fd != nil {
			fd.Sync()
			fd.Close()
		}
	}
	fs.mu.Unlock()
}

// Select the message block where this message should be found.
// Return nil if not in the set.
// Read lock should be held.
func (fs *fileStore) selectMsgBlock(seq uint64) *msgBlock {
	_, mb := fs.selectMsgBlockWithIndex(seq)
	return mb
}

// Lock should be held.
func (fs *fileStore) selectMsgBlockWithIndex(seq uint64) (int, *msgBlock) {
	// Check for out of range.
	if seq < fs.state.FirstSeq || seq > fs.state.LastSeq || fs.state.Msgs == 0 {
		return -1, nil
	}

	const linearThresh = 32
	nb := len(fs.blks) - 1

	if nb < linearThresh {
		for i, mb := range fs.blks {
			if seq <= atomic.LoadUint64(&mb.last.seq) {
				return i, mb
			}
		}
		return -1, nil
	}

	// Do traditional binary search here since we know the blocks are sorted by sequence first and last.
	for low, high, mid := 0, nb, nb/2; low <= high; mid = (low + high) / 2 {
		mb := fs.blks[mid]
		// Right now these atomic loads do not factor in, so fine to leave. Was considering
		// uplifting these to fs scope to avoid atomic load but not needed.
		first, last := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
		if seq > last {
			low = mid + 1
		} else if seq < first {
			// A message block's first sequence can change here meaning we could find a gap.
			// We want to behave like above, which if inclusive (we check at start) should
			// always return an index and a valid mb.
			// If we have a gap then our seq would be > fs.blks[mid-1].last.seq
			if mid == 0 || seq > atomic.LoadUint64(&fs.blks[mid-1].last.seq) {
				return mid, mb
			}
			high = mid - 1
		} else {
			return mid, mb
		}
	}

	return -1, nil
}

// Select the message block where this message should be found.
// Return nil if not in the set.
func (fs *fileStore) selectMsgBlockForStart(minTime time.Time) *msgBlock {
	fs.mu.RLock()
	defer fs.mu.RUnlock()

	// Binary search for first block where last.ts >= t.
	i, _ := slices.BinarySearchFunc(fs.blks, minTime.UnixNano(), func(mb *msgBlock, target int64) int {
		mb.mu.RLock()
		last := mb.last.ts
		mb.mu.RUnlock()
		switch {
		case last < target:
			return -1
		case last > target:
			return 1
		default:
			return 0
		}
	})

	// BinarySearchFunc returns an insertion point if not found.
	// Either way, i is the index of the first mb where mb.last.ts >= t.
	if i < len(fs.blks) {
		return fs.blks[i]
	}
	return nil
}

// Index a raw msg buffer.
// Lock should be held.
func (mb *msgBlock) indexCacheBuf(buf []byte) error {
	var le = binary.LittleEndian

	var alloc bool
	var idx []uint32
	var index uint32

	mbFirstSeq := atomic.LoadUint64(&mb.first.seq)
	mbLastSeq := atomic.LoadUint64(&mb.last.seq)

	// Sanity check here since we calculate size to allocate based on this.
	if mbFirstSeq > (mbLastSeq + 1) { // Purged state first == last + 1
		mb.fs.warn("indexCacheBuf corrupt state in %s: mb.first %d mb.last %d", mb.mfn, mbFirstSeq, mbLastSeq)
		// This would cause idxSz to wrap.
		return errCorruptState
	}

	idxSz := mbLastSeq - mbFirstSeq + 1

	if mb.cache == nil {
		mb.cache = mb.ecache.Value()
	}
	if mb.cache == nil {
		mb.cache = &cache{}
		alloc = true
	} else {
		// The buf arg already came from the pool probably, so there's
		// no point in reusing mb.cache.buf's underlying capacity here.
		// Just recycle it for the next block load.
		recycleMsgBlockBuf(mb.cache.buf)
	}
	if idx = mb.cache.idx; uint64(cap(idx)) >= idxSz {
		idx = idx[:0]
	} else {
		idx = make([]uint32, 0, idxSz)
	}
	if !alloc {
		// We didn't allocate a new *cache so instead wipe the current
		// one, we already have reused idx if possible.
		*mb.cache = cache{}
	}

	// Create FSS if we should track.
	var popFss bool
	if mb.fssNotLoaded() {
		mb.fss = stree.NewSubjectTree[SimpleState]()
		popFss = true
	}
	// Mark fss activity.
	mb.lsts = ats.AccessTime()
	mb.ttls = 0
	mb.schedules = 0

	lbuf := uint32(len(buf))
	var seq, ttls, schedules uint64
	var sm StoreMsg // Used for finding headers

	// To ensure the sequence keeps moving up. As well as confirming our index
	// is aligned with the mb's first and last sequence.
	var first uint64
	var last uint64

	for index < lbuf {
		if index+msgHdrSize > lbuf {
			return errCorruptState
		}
		hdr := buf[index : index+msgHdrSize]
		rl, slen := le.Uint32(hdr[0:]), int(le.Uint16(hdr[20:]))
		seq = le.Uint64(hdr[4:])

		hasHeaders := rl&hbit != 0
		// Clear any headers bit that could be set.
		rl &^= hbit
		shlen := slen
		if hasHeaders {
			shlen += 4
		}
		dlen := int(rl) - msgHdrSize
		// Do some quick sanity checks here.
		if dlen < 0 || shlen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh {
			mb.fs.warn("indexCacheBuf corrupt record state in %s: dlen %d slen %d index %d rl %d lbuf %d", mb.mfn, dlen, slen, index, rl, lbuf)
			// This means something is off.
			// TODO(dlc) - Add into bad list?
			return errCorruptState
		}

		// Check for tombstones which we can skip in terms of indexing.
		if seq&tbit != 0 {
			index += rl
			continue
		}

		// Clear any erase bits.
		erased := seq&ebit != 0
		seq = seq &^ ebit

		// The sequence needs to only ever move up.
		if seq <= last {
			// Advance to next record.
			// We've already accounted for this sequence and marked it as deleted.
			index += rl
			continue
		}
		// We defer checksum checks to individual msg cache lookups to amortorize costs and
		// not introduce latency for first message from a newly loaded block.
		if seq >= mbFirstSeq {
			last = seq

			// If the first sequence doesn't align with what we had in-memory, we need to rebuild.
			if first == 0 {
				first = seq
				if mbFirstSeq != first {
					return errCorruptState
				}
			}

			// Track that we do not have holes.
			if slot := int(seq - mbFirstSeq); slot != len(idx) {
				// If we have a hole fill it.
				for dseq := mbFirstSeq + uint64(len(idx)); dseq < seq; dseq++ {
					idx = append(idx, dbit)
					if dseq != 0 {
						mb.dmap.Insert(dseq)
					}
				}
			}
			// Add to our index.
			idx = append(idx, index)

			// Make sure our dmap has this entry if it was erased.
			// If not, that means this erased message was not accounted for in our in-memory state.
			if erased && seq != 0 && !mb.dmap.Exists(seq) {
				return errCorruptState
			}

			// Handle FSS inline here.
			if popFss && slen > 0 && !mb.noTrack && !erased && !mb.dmap.Exists(seq) {
				bsubj := buf[index+msgHdrSize : index+msgHdrSize+uint32(slen)]
				if ss, ok := mb.fss.Find(bsubj); ok && ss != nil {
					ss.Msgs++
					ss.Last = seq
					ss.lastNeedsUpdate = false
				} else {
					mb.fss.Insert(bsubj, SimpleState{
						Msgs:  1,
						First: seq,
						Last:  seq,
					})
				}
			}

			// Count how many TTLs/schedules we think are in this message block.
			// TODO(nat): Not terribly optimal...
			if hasHeaders {
				if fsm, err := mb.msgFromBufNoCopy(buf[index:], &sm, nil); err == nil && fsm != nil {
					if ttl := sliceHeader(JSMessageTTL, fsm.hdr); len(ttl) > 0 {
						ttls++
					}
					if sched := sliceHeader(JSSchedulePattern, fsm.hdr); len(sched) > 0 {
						schedules++
					}
				}
			}
		}
		index += rl
	}

	// If we ended up with a smaller or larger index, or the first/last sequence
	// doesn't align with what we had in-memory, we need to rebuild.
	if len(idx) != int(idxSz) || (first > 0 && mbFirstSeq != first) || (last > 0 && mbLastSeq != last) {
		return errCorruptState
	}

	mb.cache.buf = buf
	mb.cache.idx = idx
	mb.cache.fseq = mbFirstSeq
	mb.cache.wp = int(lbuf)
	mb.ttls = ttls
	mb.schedules = schedules

	return nil
}

// flushPendingMsgs writes out any messages for this message block.
func (mb *msgBlock) flushPendingMsgs() error {
	mb.mu.Lock()
	fsLostData, err := mb.flushPendingMsgsLocked()
	fs := mb.fs
	mb.mu.Unlock()

	// Signals us that we need to rebuild filestore state.
	if fsLostData != nil && fs != nil {
		// Rebuild fs state too.
		fs.rebuildState(fsLostData)
	}
	return err
}

// Write function for actual data.
// mb.mfd should not be nil.
// Lock should held.
func (mb *msgBlock) writeAt(buf []byte, woff int64) (int, error) {
	// Used to mock write failures.
	if mb.mockWriteErr {
		// Reset on trip.
		mb.mockWriteErr = false
		return 0, errors.New("mock write error")
	}
	<-dios
	n, err := mb.mfd.WriteAt(buf, woff)
	dios <- struct{}{}
	return n, err
}

// flushPendingMsgsLocked writes out any messages for this message block.
// Lock should be held.
func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) {
	// Signals us that we need to rebuild filestore state.
	var fsLostData *LostStreamData

	var weakenCache bool
	if mb.cache == nil {
		mb.cache = mb.ecache.Value()
		weakenCache = mb.cache != nil
	}
	if mb.cache == nil || mb.mfd == nil {
		return nil, errNoCache
	}

	buf, err := mb.bytesPending()
	// If we got an error back return here.
	if err != nil {
		// No pending data to be written is not an error.
		if err == errNoPending || err == errNoCache {
			err = nil
		}
		return nil, err
	}

	wp := int64(mb.cache.wp)
	lob := len(buf)

	// TODO(dlc) - Normally we would not hold the lock across I/O so we can improve performance.
	// We will hold to stabilize the code base, as we have had a few anomalies with partial cache errors
	// under heavy load.

	// Check if we need to encrypt.
	if err := mb.checkAndLoadEncryption(); err != nil {
		return nil, err
	}
	if mb.bek != nil && lob > 0 {
		// Need to leave original alone.
		var dst []byte
		if lob <= defaultLargeBlockSize {
			dst = getMsgBlockBuf(lob)[:lob]
			defer recycleMsgBlockBuf(dst)
		} else {
			dst = make([]byte, lob)
		}
		mb.bek.XORKeyStream(dst, buf)
		buf = dst
	}

	// Append new data to the message block file.
	for lbb := lob; lbb > 0; lbb = len(buf) {
		n, err := mb.writeAt(buf, wp)
		if err != nil {
			mb.dirtyCloseWithRemove(false)
			ld, _, _ := mb.rebuildStateLocked()
			mb.werr = err
			return ld, err
		}
		// Update our write offset.
		wp += int64(n)
		buf = buf[n:]
	}

	// Clear any error.
	mb.werr = nil

	// Cache may be gone.
	if mb.cache == nil || mb.mfd == nil {
		return fsLostData, mb.werr
	}

	// Update write pointer.
	mb.cache.wp = int(wp)

	// Check if we are in sync always mode.
	if mb.syncAlways {
		mb.mfd.Sync()
	} else {
		mb.needSync = true
	}

	// Check for additional writes while we were writing to the disk.
	// TODO(nat): This should not be possible at present since, as above, we are
	// not releasing the lock during I/O operation. Therefore this will always
	// return zero.
	if mb.pendingWriteSizeLocked() > 0 {
		return fsLostData, mb.werr
	}

	// Check last access time. If we think the block still has read interest
	// then we will weaken the pointer but otherwise try to hold onto it.
	if ts := ats.AccessTime(); ts < mb.llts || (ts-mb.llts) <= int64(mb.cexp) {
		if weakenCache {
			mb.cache = nil
			mb.ecache.Weaken()
		}
		mb.resetCacheExpireTimer(0)
		return fsLostData, mb.werr
	}

	// If not, we'll just drop the cache altogether & recycle the buffer.
	mb.cache.nra = false
	mb.expireCacheLocked()
	return fsLostData, mb.werr
}

// Lock should be held.
func (mb *msgBlock) clearLoading() {
	mb.loading = false
}

// Will load msgs from disk.
func (mb *msgBlock) loadMsgs() error {
	// We hold the lock here the whole time by design.
	mb.mu.Lock()
	defer mb.mu.Unlock()
	return mb.loadMsgsWithLock()
}

// Lock should be held.
func (mb *msgBlock) cacheAlreadyLoaded() bool {
	if mb.cache == nil {
		mb.cache = mb.ecache.Value()
	}
	if mb.cache == nil || mb.cache.fseq == 0 || len(mb.cache.buf) == 0 {
		return false
	}
	numEntries := mb.msgs + uint64(mb.dmap.Size()) + (atomic.LoadUint64(&mb.first.seq) - mb.cache.fseq)
	return numEntries == uint64(len(mb.cache.idx))
}

// Lock should be held.
func (mb *msgBlock) cacheNotLoaded() bool {
	return !mb.cacheAlreadyLoaded()
}

// Report if our fss is not loaded.
// Lock should be held.
func (mb *msgBlock) fssNotLoaded() bool {
	return mb.fss == nil && !mb.noTrack
}

// Wrap openBlock for the gated semaphore processing.
// Lock should be held
func (mb *msgBlock) openBlock() (*os.File, error) {
	// Gate with concurrent IO semaphore.
	<-dios
	f, err := os.Open(mb.mfn)
	dios <- struct{}{}
	return f, err
}

// Used to load in the block contents.
// Lock should be held and all conditionals satisfied prior.
func (mb *msgBlock) loadBlock(buf []byte) ([]byte, error) {
	var f *os.File
	// Re-use if we have mfd open.
	if mb.mfd != nil {
		f = mb.mfd
		if n, err := f.Seek(0, 0); n != 0 || err != nil {
			f = nil
			mb.closeFDsLockedNoCheck()
		}
	}
	if f == nil {
		var err error
		f, err = mb.openBlock()
		if err != nil {
			if os.IsNotExist(err) {
				err = errNoBlkData
			}
			return nil, err
		}
		defer f.Close()
	}

	var sz int
	if info, err := f.Stat(); err == nil {
		sz64 := info.Size()
		if int64(int(sz64)) == sz64 {
			sz = int(sz64)
		} else {
			return nil, errMsgBlkTooBig
		}
	}

	if buf == nil || cap(buf) < sz {
		// getMsgBlockBuf will try to return a slice that fits from the
		// buffer pool, but if by chance `sz` is greater than the maximum
		// large block size, it'll return a new slice to fit regardless.
		buf = getMsgBlockBuf(sz)
	}

	<-dios
	n, err := io.ReadFull(f, buf[:sz])
	dios <- struct{}{}
	// On success capture raw bytes size.
	if err == nil {
		mb.rbytes = uint64(n)
	}
	return buf[:n], err
}

// Lock should be held.
func (mb *msgBlock) loadMsgsWithLock() error {
	if err := mb.checkAndLoadEncryption(); err != nil {
		return err
	}

	// Check to see if we are loading already.
	if mb.loading {
		return nil
	}

	// Set loading status.
	mb.loading = true
	defer mb.clearLoading()

	var nchecks int

checkCache:
	nchecks++
	if nchecks > 8 {
		return errNoCache
	}

	// Check to see if we have a full cache.
	if mb.cacheAlreadyLoaded() {
		return nil
	}

	mb.llts = ats.AccessTime()

	// FIXME(dlc) - We could be smarter here.
	if buf, _ := mb.bytesPending(); len(buf) > 0 {
		ld, err := mb.flushPendingMsgsLocked()
		if ld != nil && mb.fs != nil {
			// We do not know if fs is locked or not at this point.
			// This should be an exceptional condition so do so in Go routine.
			go mb.fs.rebuildState(ld)
		}
		if err != nil {
			return err
		}
		goto checkCache
	}

	// Load in the whole block.
	// We want to hold the mb lock here to avoid any changes to state.
	buf, err := mb.loadBlock(nil)
	if err != nil {
		mb.fs.warn("loadBlock error: %v", err)
		if err == errNoBlkData {
			if ld, _, err := mb.rebuildStateLocked(); err != nil && ld != nil {
				// Rebuild fs state too.
				go mb.fs.rebuildState(ld)
			}
		}
		return err
	}

	// Check if we need to decrypt.
	if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
		return err
	}
	// Check for compression.
	if buf, err = mb.decompressIfNeeded(buf); err != nil {
		return err
	}

	if err := mb.indexCacheBuf(buf); err != nil {
		if err == errCorruptState {
			var ld *LostStreamData
			ld, _, err = mb.rebuildStateLocked()
			// We do not know if fs is locked or not at this point.
			// This should be an exceptional condition so do so in Go routine.
			// Always rebuild the filestore's state if indexing fails, even if no data was lost,
			// our in-memory state was stale in that case.
			go mb.fs.rebuildState(ld)
		}
		if err != nil {
			return err
		}
		goto checkCache
	}

	if len(buf) > 0 {
		mb.cloads++
		mb.startCacheExpireTimer()
	}

	// Update the cache pointer since we cleared & rebuilt.
	mb.ecache.Set(mb.cache)

	return nil
}

// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
// Lock should not be held.
func (mb *msgBlock) fetchMsg(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
	return mb.fetchMsgEx(seq, sm, true)
}

// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
// We will not copy the msg data.
// Lock should not be held.
func (mb *msgBlock) fetchMsgNoCopy(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
	return mb.fetchMsgEx(seq, sm, false)
}

// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
// We will copy the msg data based on doCopy boolean.
// Lock should not be held.
func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg, bool, error) {
	mb.mu.Lock()
	defer mb.mu.Unlock()

	fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
	if seq < fseq || seq > lseq {
		return nil, false, ErrStoreMsgNotFound
	}

	// See if we can short circuit if we already know msg deleted.
	if mb.dmap.Exists(seq) {
		// Update for scanning like cacheLookup would have.
		llseq := mb.llseq
		if mb.llseq == 0 || seq < mb.llseq || seq == mb.llseq+1 || seq == mb.llseq-1 {
			mb.llseq = seq
		}
		expireOk := (seq == lseq && llseq == seq-1) || (seq == fseq && llseq == seq+1)
		return nil, expireOk, errDeletedMsg
	}

	if mb.cacheNotLoaded() {
		if err := mb.loadMsgsWithLock(); err != nil {
			return nil, false, err
		}
	}
	defer mb.finishedWithCache()
	llseq := mb.llseq

	fsm, err := mb.cacheLookupEx(seq, sm, doCopy)
	if err != nil {
		return nil, false, err
	}
	expireOk := (seq == lseq && llseq == seq-1) || (seq == fseq && llseq == seq+1)
	return fsm, expireOk, err
}

var (
	errNoCache       = errors.New("no message cache")
	errDeletedMsg    = errors.New("deleted message")
	errPartialCache  = errors.New("partial cache")
	errNoPending     = errors.New("message block does not have pending data")
	errNotReadable   = errors.New("storage directory not readable")
	errCorruptState  = errors.New("corrupt state file")
	errPriorState    = errors.New("prior state file")
	errPendingData   = errors.New("pending data still present")
	errNoEncryption  = errors.New("encryption not enabled")
	errBadKeySize    = errors.New("encryption bad key size")
	errNoMsgBlk      = errors.New("no message block")
	errMsgBlkTooBig  = errors.New("message block size exceeded int capacity")
	errUnknownCipher = errors.New("unknown cipher")
	errNoMainKey     = errors.New("encrypted store encountered with no main key")
	errNoBlkData     = errors.New("message block data missing")
	errStateTooBig   = errors.New("store state too big for optional write")
)

type (
	errBadMsg struct{ fn, detail string }
)

func (e errBadMsg) Error() string {
	if e.detail != _EMPTY_ {
		return fmt.Sprintf("malformed or corrupt message in %s: %s", filepath.Base(e.fn), e.detail)
	}
	return fmt.Sprintf("malformed or corrupt message in %s", filepath.Base(e.fn))
}

const (
	// "Checksum bit" is used in "mb.cache.idx" for marking messages that have had their checksums checked.
	cbit = 1 << 31
	// "Delete bit" is used in "mb.cache.idx" to mark an index as deleted and non-existent.
	dbit = 1 << 30
	// "Header bit" is used in "rl" to signal a message record with headers.
	hbit = 1 << 31
	// "Erase bit" is used in "seq" for marking erased messages sequences.
	ebit = 1 << 63
	// "Tombstone bit" is used in "seq" for marking tombstone sequences.
	tbit = 1 << 62
)

// Will do a lookup from cache.
// This will copy the msg from the cache.
// Lock should be held.
func (mb *msgBlock) cacheLookup(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
	return mb.cacheLookupEx(seq, sm, true)
}

// Will do a lookup from cache.
// This will NOT copy the msg from the cache.
// Lock should be held.
func (mb *msgBlock) cacheLookupNoCopy(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
	return mb.cacheLookupEx(seq, sm, false)
}

// Will do a lookup from cache.
// Lock should be held.
func (mb *msgBlock) cacheLookupEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg, error) {
	fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
	switch {
	case lseq == fseq-1:
		// The block is empty, no messages have been written yet. This works because
		// newMsgBlockForWrite sets fseq=fs.State.LastSeq+1 and lseq=fs.State.LastSeq.
		return nil, ErrStoreMsgNotFound
	case seq < fseq || seq > lseq:
		// Sequence is out of range for this block.
		return nil, ErrStoreMsgNotFound
	}

	// The llseq signals us when we can expire a cache at the end of a linear scan.
	// We want to only update when we know the last reads (multiple consumers) are sequential.
	// We want to account for forwards and backwards linear scans.
	if mb.llseq == 0 || seq < mb.llseq || seq == mb.llseq+1 || seq == mb.llseq-1 {
		mb.llseq = seq
	}

	// If we have a delete map check it.
	if mb.dmap.Exists(seq) {
		mb.llts = ats.AccessTime()
		return nil, errDeletedMsg
	}

	// Detect no cache loaded.
	if mb.cache == nil {
		mb.cache = mb.ecache.Value()
	}
	if mb.cache == nil || mb.cache.fseq == 0 || len(mb.cache.idx) == 0 || len(mb.cache.buf) == 0 {
		var reason string
		if mb.cache == nil {
			reason = "no cache"
		} else if mb.cache.fseq == 0 {
			reason = "fseq is 0"
		} else if len(mb.cache.idx) == 0 {
			reason = "no idx present"
		} else {
			reason = "cache buf empty"
		}
		mb.fs.warn("Cache lookup for sequence %d in block %d detected no cache: %s", seq, mb.index, reason)
		if mb.cache != nil {
			mb.tryForceExpireCacheLocked()
		}
		return nil, errNoCache
	}
	// Check partial cache status.
	if seq < mb.cache.fseq {
		mb.fs.warn("Partial cache: seq %d is less than cache fseq %d", seq, mb.cache.fseq)
		return nil, errPartialCache
	}

	bi, _, hashChecked, err := mb.slotInfo(int(seq - mb.cache.fseq))
	if err != nil {
		return nil, err
	}

	// Update cache activity.
	mb.llts = ats.AccessTime()

	li := int(bi)
	if li >= len(mb.cache.buf) {
		mb.fs.warn("Partial cache: slot index %d is less than cache buffer len %d", li, len(mb.cache.buf))
		return nil, errPartialCache
	}
	buf := mb.cache.buf[li:]

	// We use the high bit to denote we have already checked the checksum.
	var hh *highwayhash.Digest64
	if !hashChecked {
		hh = mb.hh // This will force the hash check in msgFromBuf.
	}

	// Parse from the raw buffer.
	fsm, err := mb.msgFromBufEx(buf, sm, hh, doCopy)
	if err != nil || fsm == nil {
		return nil, err
	}

	// Deleted messages that are decoded return a 0 for sequence.
	if fsm.seq == 0 {
		return nil, errDeletedMsg
	}

	if seq != fsm.seq { // See TestFileStoreInvalidIndexesRebuilt.
		mb.tryForceExpireCacheLocked()
		return nil, fmt.Errorf("sequence numbers for cache load did not match, %d vs %d", seq, fsm.seq)
	}

	// Clear the check bit here after we know all is good.
	if !hashChecked {
		mb.cache.idx[seq-mb.cache.fseq] = (bi | cbit)
	}

	return fsm, nil
}

// Used when we are checking if discarding a message due to max msgs per subject will give us
// enough room for a max bytes condition.
// Lock should be already held.
func (fs *fileStore) sizeForSeq(seq uint64) int {
	if seq == 0 {
		return 0
	}
	var smv StoreMsg
	if mb := fs.selectMsgBlock(seq); mb != nil {
		if sm, _, _ := mb.fetchMsgNoCopy(seq, &smv); sm != nil {
			return int(fileStoreMsgSize(sm.subj, sm.hdr, sm.msg))
		}
	}
	return 0
}

// Will return message for the given sequence number.
// This will be returned to external callers.
func (fs *fileStore) msgForSeq(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
	return fs.msgForSeqLocked(seq, sm, true)
}

// Will return message for the given sequence number.
func (fs *fileStore) msgForSeqLocked(seq uint64, sm *StoreMsg, needFSLock bool) (*StoreMsg, error) {
	if fs.isClosed() {
		return nil, ErrStoreClosed
	}
	// TODO(dlc) - Since Store, Remove, Skip all hold the write lock on fs this will
	// be stalled. Need another lock if want to happen in parallel.
	if needFSLock {
		fs.mu.RLock()
	}
	// Indicates we want first msg.
	if seq == 0 {
		seq = fs.state.FirstSeq
	}
	// Make sure to snapshot here.
	mb, lseq := fs.selectMsgBlock(seq), fs.state.LastSeq
	if needFSLock {
		fs.mu.RUnlock()
	}

	if mb == nil {
		var err = ErrStoreEOF
		if seq <= lseq {
			err = ErrStoreMsgNotFound
		}
		return nil, err
	}

	fsm, expireOk, err := mb.fetchMsg(seq, sm)
	if err != nil {
		return nil, err
	}

	// We detected a linear scan and access to the last message.
	// If we are not the last message block we can try to expire the cache.
	if expireOk {
		mb.tryForceExpireCache()
	}

	return fsm, nil
}

// Internal function to return msg parts from a raw buffer.
// Raw buffer will be copied into sm.
// Lock should be held.
func (mb *msgBlock) msgFromBuf(buf []byte, sm *StoreMsg, hh *highwayhash.Digest64) (*StoreMsg, error) {
	return mb.msgFromBufEx(buf, sm, hh, true)
}

// Internal function to return msg parts from a raw buffer.
// Raw buffer will NOT be copied into sm.
// Only use for internal use, any message that is passed to upper layers should use mb.msgFromBuf.
// Lock should be held.
func (mb *msgBlock) msgFromBufNoCopy(buf []byte, sm *StoreMsg, hh *highwayhash.Digest64) (*StoreMsg, error) {
	return mb.msgFromBufEx(buf, sm, hh, false)
}

// Internal function to return msg parts from a raw buffer.
// copy boolean will determine if we make a copy or not.
// Lock should be held.
func (mb *msgBlock) msgFromBufEx(buf []byte, sm *StoreMsg, hh *highwayhash.Digest64, doCopy bool) (*StoreMsg, error) {
	if len(buf) < emptyRecordLen {
		return nil, errBadMsg{mb.mfn, "record too short"}
	}
	var le = binary.LittleEndian

	hdr := buf[:msgHdrSize]
	rl := le.Uint32(hdr[0:])
	hasHeaders := rl&hbit != 0
	rl &^= hbit // clear header bit
	dlen := int(rl) - msgHdrSize
	slen := int(le.Uint16(hdr[20:]))

	shlen := slen
	if hasHeaders {
		shlen += 4
	}
	// Simple sanity check.
	if dlen < 0 || shlen > (dlen-recordHashSize) || dlen > int(rl) || int(rl) > len(buf) || rl > rlBadThresh {
		return nil, errBadMsg{mb.mfn, fmt.Sprintf("sanity check failed (dlen %d slen %d rl %d buf %d)", dlen, slen, rl, buf)}
	}
	data := buf[msgHdrSize : msgHdrSize+dlen]
	// Do checksum tests here if requested.
	if hh != nil {
		hh.Reset()
		hh.Write(hdr[4:20])
		hh.Write(data[:slen])
		if hasHeaders {
			hh.Write(data[slen+4 : dlen-recordHashSize])
		} else {
			hh.Write(data[slen : dlen-recordHashSize])
		}
		var hb [highwayhash.Size64]byte
		if !bytes.Equal(hh.Sum(hb[:0]), data[len(data)-8:]) {
			return nil, errBadMsg{mb.mfn, "invalid checksum"}
		}
	}
	seq := le.Uint64(hdr[4:])
	if seq&ebit != 0 {
		seq = 0
	}
	ts := int64(le.Uint64(hdr[12:]))

	// Create a StoreMsg if needed.
	if sm == nil {
		sm = new(StoreMsg)
	} else {
		sm.clear()
	}
	// To recycle the large blocks we can never pass back a reference, so need to copy for the upper
	// layers and for us to be safe to expire, and recycle, the large msgBlocks.
	end := dlen - 8
	if len(data) < end {
		return nil, errBadMsg{mb.mfn, "invalid data length"}
	}

	if hasHeaders {
		if slen+4 > len(data) {
			return nil, errBadMsg{mb.mfn, "invalid subject length greataer than data length"}
		}
		hl := le.Uint32(data[slen:])
		bi := slen + 4
		li := bi + int(hl)
		if bi > end {
			return nil, errBadMsg{mb.mfn, "invalid buffer index"}
		}
		if doCopy {
			sm.buf = append(sm.buf, data[bi:end]...)
		} else {
			sm.buf = data[bi:end]
		}
		li, end = li-bi, end-bi
		if li > len(sm.buf) || end > len(sm.buf) {
			return nil, errBadMsg{mb.mfn, "invalid message length or end greater than buffer length"}
		}
		sm.hdr = sm.buf[0:li:li]
		sm.msg = sm.buf[li:end]
	} else {
		if slen > end {
			return nil, errBadMsg{mb.mfn, "invalid subject length greater than end"}
		}
		if doCopy {
			sm.buf = append(sm.buf, data[slen:end]...)
		} else {
			sm.buf = data[slen:end]
		}
		mlen := end - slen
		if mlen > len(sm.buf) {
			return nil, errBadMsg{mb.mfn, "invalid message length greater than buffer length"}
		}
		sm.msg = sm.buf[0:mlen]
	}
	sm.seq, sm.ts = seq, ts
	if slen > 0 {
		if slen > len(data) {
			return nil, errBadMsg{mb.mfn, "invalid subject length greater than data length"}
		}
		if doCopy {
			// Make a copy since sm.subj lifetime may last longer.
			sm.subj = string(data[:slen])
		} else {
			sm.subj = bytesToString(data[:slen])
		}
	}

	return sm, nil
}

// SubjectForSeq will return what the subject is for this sequence if found.
func (fs *fileStore) SubjectForSeq(seq uint64) (string, error) {
	fs.mu.RLock()
	if seq < fs.state.FirstSeq {
		fs.mu.RUnlock()
		return _EMPTY_, ErrStoreMsgNotFound
	}
	var smv StoreMsg
	mb := fs.selectMsgBlock(seq)
	fs.mu.RUnlock()
	if mb != nil {
		if sm, _, _ := mb.fetchMsgNoCopy(seq, &smv); sm != nil {
			// Copy the subject, as it's used elsewhere, and the backing cache could be reused in the meantime.
			return copyString(sm.subj), nil
		}
	}
	return _EMPTY_, ErrStoreMsgNotFound
}

// LoadMsg will lookup the message by sequence number and return it if found.
func (fs *fileStore) LoadMsg(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
	return fs.msgForSeq(seq, sm)
}

// loadLast will load the last message for a subject. Subject should be non empty and not ">".
func (fs *fileStore) loadLast(subj string, sm *StoreMsg) (lsm *StoreMsg, err error) {
	if fs.isClosed() {
		return nil, ErrStoreClosed
	}

	fs.mu.RLock()
	defer fs.mu.RUnlock()

	if fs.lmb == nil {
		return nil, ErrStoreClosed
	}

	if len(fs.blks) == 0 {
		return nil, ErrStoreMsgNotFound
	}

	wc := subjectHasWildcard(subj)
	var start, stop uint32

	// If literal subject check for presence.
	if wc {
		start = fs.lmb.index
		fs.psim.Match(stringToBytes(subj), func(_ []byte, psi *psi) {
			// Keep track of start and stop indexes for this subject.
			if psi.fblk < start {
				start = psi.fblk
			}
			if psi.lblk > stop {
				stop = psi.lblk
			}
		})
		// None matched.
		if stop == 0 {
			return nil, ErrStoreMsgNotFound
		}
		// These need to be swapped.
		start, stop = stop, start
	} else if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
		start, stop = info.lblk, info.fblk
	} else {
		return nil, ErrStoreMsgNotFound
	}

	// Walk blocks backwards.
	for i := start; i >= stop; i-- {
		mb := fs.bim[i]
		if mb == nil {
			continue
		}
		mb.mu.Lock()
		if err := mb.ensurePerSubjectInfoLoaded(); err != nil {
			mb.mu.Unlock()
			return nil, err
		}
		// Mark fss activity.
		mb.lsts = ats.AccessTime()

		var l uint64
		// Optimize if subject is not a wildcard.
		if !wc {
			if ss, ok := mb.fss.Find(stringToBytes(subj)); ok && ss != nil {
				// Check if we need to recalculate. We only care about the last sequence.
				if ss.lastNeedsUpdate {
					// mb is already loaded into the cache so should be fast-ish.
					mb.recalculateForSubj(subj, ss)
				}
				l = ss.Last
			}
		}
		if l == 0 {
			_, _, l = mb.filteredPendingLocked(subj, wc, atomic.LoadUint64(&mb.first.seq))
		}
		var didLoad bool
		if l > 0 {
			if mb.cacheNotLoaded() {
				if err := mb.loadMsgsWithLock(); err != nil {
					mb.mu.Unlock()
					return nil, err
				}
				didLoad = true
			}
			lsm, err = mb.cacheLookup(l, sm)
		}
		if didLoad {
			mb.finishedWithCache()
		}
		mb.mu.Unlock()
		if l > 0 {
			break
		}
	}
	return lsm, err
}

// LoadLastMsg will return the last message we have that matches a given subject.
// The subject can be a wildcard.
func (fs *fileStore) LoadLastMsg(subject string, smv *StoreMsg) (sm *StoreMsg, err error) {
	if subject == _EMPTY_ || subject == fwcs {
		sm, err = fs.msgForSeq(fs.lastSeq(), smv)
	} else {
		sm, err = fs.loadLast(subject, smv)
	}
	if sm == nil || (err != nil && err != ErrStoreClosed) {
		err = ErrStoreMsgNotFound
	}
	return sm, err
}

// LoadNextMsgMulti will find the next message matching any entry in the sublist.
func (fs *fileStore) LoadNextMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) {
	if fs.isClosed() {
		return nil, 0, ErrStoreClosed
	}
	if sl == nil {
		return fs.LoadNextMsg(_EMPTY_, false, start, smp)
	}
	fs.mu.RLock()
	defer fs.mu.RUnlock()

	if fs.state.Msgs == 0 || start > fs.state.LastSeq {
		return nil, fs.state.LastSeq, ErrStoreEOF
	}
	if start < fs.state.FirstSeq {
		start = fs.state.FirstSeq
	}

	// If start is less than or equal to beginning of our stream, meaning our first call,
	// let's check the psim to see if we can skip ahead.
	if start <= fs.state.FirstSeq {
		var total uint64
		blkStart := uint32(math.MaxUint32)
		stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
			total += psi.total
			// Keep track of start index for this subject.
			if psi.fblk < blkStart {
				blkStart = psi.fblk
			}
		})
		// Nothing available.
		if total == 0 {
			return nil, fs.state.LastSeq, ErrStoreEOF
		}
		// We can skip ahead.
		if mb := fs.bim[blkStart]; mb != nil {
			fseq := atomic.LoadUint64(&mb.first.seq)
			if fseq > start {
				start = fseq
			}
		}
	}

	if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 {
		for i := bi; i < len(fs.blks); i++ {
			mb := fs.blks[i]
			if sm, expireOk, err := mb.firstMatchingMulti(sl, start, smp); err == nil {
				if expireOk {
					mb.tryForceExpireCache()
				}
				return sm, sm.seq, nil
			} else if err != ErrStoreMsgNotFound {
				return nil, 0, err
			} else {
				// Nothing found in this block. We missed, if first block (bi) check psim.
				// Similar to above if start <= first seq.
				// TODO(dlc) - For v2 track these by filter subject since they will represent filtered consumers.
				// We should not do this at all if we are already on the last block.
				if i == bi && i < len(fs.blks)-1 {
					nbi, err := fs.checkSkipFirstBlockMulti(sl, bi)
					// Nothing available.
					if err == ErrStoreEOF {
						return nil, fs.state.LastSeq, ErrStoreEOF
					}
					// See if we can jump ahead here.
					// Right now we can only spin on first, so if we have interior sparseness need to favor checking per block fss if loaded.
					// For v2 will track all blocks that have matches for psim.
					if nbi > i {
						i = nbi - 1 // For the iterator condition i++
					}
				}
				// Check if we can expire.
				if expireOk {
					mb.tryForceExpireCache()
				}
			}
		}
	}

	return nil, fs.state.LastSeq, ErrStoreEOF

}

func (fs *fileStore) LoadNextMsg(filter string, wc bool, start uint64, sm *StoreMsg) (*StoreMsg, uint64, error) {
	if fs.isClosed() {
		return nil, 0, ErrStoreClosed
	}

	fs.mu.RLock()
	defer fs.mu.RUnlock()

	if fs.state.Msgs == 0 || start > fs.state.LastSeq {
		return nil, fs.state.LastSeq, ErrStoreEOF
	}
	if start < fs.state.FirstSeq {
		start = fs.state.FirstSeq
	}

	// If start is less than or equal to beginning of our stream, meaning our first call,
	// let's check the psim to see if we can skip ahead.
	if start <= fs.state.FirstSeq {
		var ss SimpleState
		fs.numFilteredPendingNoLast(filter, &ss)
		// Nothing available.
		if ss.Msgs == 0 {
			return nil, fs.state.LastSeq, ErrStoreEOF
		}
		// We can skip ahead.
		if ss.First > start {
			start = ss.First
		}
	}

	if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 {
		for i := bi; i < len(fs.blks); i++ {
			mb := fs.blks[i]
			if sm, expireOk, err := mb.firstMatching(filter, wc, start, sm); err == nil {
				if expireOk {
					mb.tryForceExpireCache()
				}
				return sm, sm.seq, nil
			} else if err != ErrStoreMsgNotFound {
				return nil, 0, err
			} else {
				// Nothing found in this block. We missed, if first block (bi) check psim.
				// Similar to above if start <= first seq.
				// TODO(dlc) - For v2 track these by filter subject since they will represent filtered consumers.
				// We should not do this at all if we are already on the last block.
				// Also if we are a wildcard do not check if large subject space.
				const wcMaxSizeToCheck = 64 * 1024
				if i == bi && i < len(fs.blks)-1 && (!wc || fs.psim.Size() < wcMaxSizeToCheck) {
					nbi, err := fs.checkSkipFirstBlock(filter, wc, bi)
					// Nothing available.
					if err == ErrStoreEOF {
						return nil, fs.state.LastSeq, ErrStoreEOF
					}
					// See if we can jump ahead here.
					// Right now we can only spin on first, so if we have interior sparseness need to favor checking per block fss if loaded.
					// For v2 will track all blocks that have matches for psim.
					if nbi > i {
						i = nbi - 1 // For the iterator condition i++
					}
				}
				// Check if we can expire.
				if expireOk {
					mb.tryForceExpireCache()
				}
			}
		}
	}

	return nil, fs.state.LastSeq, ErrStoreEOF
}

// Will load the next non-deleted msg starting at the start sequence and walking backwards.
func (fs *fileStore) LoadPrevMsg(start uint64, smp *StoreMsg) (sm *StoreMsg, err error) {
	if fs.isClosed() {
		return nil, ErrStoreClosed
	}

	fs.mu.RLock()
	defer fs.mu.RUnlock()

	if fs.state.Msgs == 0 || start < fs.state.FirstSeq {
		return nil, ErrStoreEOF
	}

	if start > fs.state.LastSeq {
		start = fs.state.LastSeq
	}
	if smp == nil {
		smp = new(StoreMsg)
	}

	if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 {
		for i := bi; i >= 0; i-- {
			mb := fs.blks[i]
			mb.mu.Lock()
			// Need messages loaded from here on out.
			if mb.cacheNotLoaded() {
				if err := mb.loadMsgsWithLock(); err != nil {
					mb.mu.Unlock()
					return nil, err
				}
			}

			lseq, fseq := atomic.LoadUint64(&mb.last.seq), atomic.LoadUint64(&mb.first.seq)
			if start > lseq {
				start = lseq
			}
			for seq := start; seq >= fseq; seq-- {
				if mb.dmap.Exists(seq) {
					continue
				}
				if sm, err := mb.cacheLookup(seq, smp); err == nil {
					mb.finishedWithCache()
					mb.mu.Unlock()
					return sm, nil
				}
			}
			mb.finishedWithCache()
			mb.mu.Unlock()
		}
	}

	return nil, ErrStoreEOF
}

// LoadPrevMsgMulti will find the previous message matching any entry in the sublist.
func (fs *fileStore) LoadPrevMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) {
	if fs.isClosed() {
		return nil, 0, ErrStoreClosed
	}

	if sl == nil {
		sm, err = fs.LoadPrevMsg(start, smp)
		return
	}
	fs.mu.RLock()
	defer fs.mu.RUnlock()

	if fs.state.Msgs == 0 || start < fs.state.FirstSeq {
		return nil, fs.state.FirstSeq, ErrStoreEOF
	}
	if start > fs.state.LastSeq {
		start = fs.state.LastSeq
	}

	if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 {
		for i := bi; i >= 0; i-- {
			mb := fs.blks[i]
			if sm, expireOk, err := mb.prevMatchingMulti(sl, start, smp); err == nil {
				if expireOk {
					mb.tryForceExpireCache()
				}
				return sm, sm.seq, nil
			} else if err != ErrStoreMsgNotFound {
				return nil, 0, err
			} else if expireOk {
				mb.tryForceExpireCache()
			}
		}
	}

	return nil, fs.state.FirstSeq, ErrStoreEOF
}

// Type returns the type of the underlying store.
func (fs *fileStore) Type() StorageType {
	return FileStorage
}

// Returns number of subjects in this store.
// Lock should be held.
func (fs *fileStore) numSubjects() int {
	return fs.psim.Size()
}

// numConsumers uses new lock.
func (fs *fileStore) numConsumers() int {
	fs.cmu.RLock()
	defer fs.cmu.RUnlock()
	return len(fs.cfs)
}

// FastState will fill in state with only the following.
// Msgs, Bytes, First and Last Sequence and Time and NumDeleted.
func (fs *fileStore) FastState(state *StreamState) {
	fs.mu.RLock()
	state.Msgs = fs.state.Msgs
	state.Bytes = fs.state.Bytes
	state.FirstSeq = fs.state.FirstSeq
	state.FirstTime = fs.state.FirstTime
	state.LastSeq = fs.state.LastSeq
	state.LastTime = fs.state.LastTime
	// Make sure to reset if being re-used.
	state.Deleted, state.NumDeleted = nil, 0
	if state.LastSeq > state.FirstSeq {
		state.NumDeleted = int((state.LastSeq - state.FirstSeq + 1) - state.Msgs)
		if state.NumDeleted < 0 {
			state.NumDeleted = 0
		}
	}
	state.Consumers = fs.numConsumers()
	state.NumSubjects = fs.numSubjects()
	fs.mu.RUnlock()
}

// State returns the current state of the stream.
func (fs *fileStore) State() StreamState {
	fs.mu.RLock()
	state := fs.state
	state.Consumers = fs.numConsumers()
	state.NumSubjects = fs.numSubjects()
	state.Deleted = nil // make sure.

	if numDeleted := int((state.LastSeq - state.FirstSeq + 1) - state.Msgs); numDeleted > 0 {
		state.Deleted = make([]uint64, 0, numDeleted)
		cur := fs.state.FirstSeq

		for _, mb := range fs.blks {
			mb.mu.Lock()
			fseq := atomic.LoadUint64(&mb.first.seq)
			// Account for messages missing from the head.
			if fseq > cur {
				for seq := cur; seq < fseq; seq++ {
					state.Deleted = append(state.Deleted, seq)
				}
			}
			// Only advance cur if we are increasing. We could have marker blocks with just tombstones.
			if last := atomic.LoadUint64(&mb.last.seq); last >= cur {
				cur = last + 1 // Expected next first.
			}
			// Add in deleted.
			mb.dmap.Range(func(seq uint64) bool {
				state.Deleted = append(state.Deleted, seq)
				return true
			})
			mb.mu.Unlock()
		}
	}
	fs.mu.RUnlock()

	state.Lost = fs.lostData()

	// Can not be guaranteed to be sorted.
	if len(state.Deleted) > 0 {
		slices.Sort(state.Deleted)
		state.NumDeleted = len(state.Deleted)
	}
	return state
}

func (fs *fileStore) Utilization() (total, reported uint64, err error) {
	fs.mu.RLock()
	defer fs.mu.RUnlock()
	for _, mb := range fs.blks {
		mb.mu.RLock()
		reported += mb.bytes
		total += mb.rbytes
		mb.mu.RUnlock()
	}
	return total, reported, nil
}

func fileStoreMsgSizeRaw(slen, hlen, mlen int) uint64 {
	if hlen == 0 {
		// length of the message record (4bytes) + seq(8) + ts(8) + subj_len(2) + subj + msg + hash(8)
		return uint64(22 + slen + mlen + 8)
	}
	// length of the message record (4bytes) + seq(8) + ts(8) + subj_len(2) + subj + hdr_len(4) + hdr + msg + hash(8)
	return uint64(22 + slen + 4 + hlen + mlen + 8)
}

func fileStoreMsgSize(subj string, hdr, msg []byte) uint64 {
	return fileStoreMsgSizeRaw(len(subj), len(hdr), len(msg))
}

func fileStoreMsgSizeEstimate(slen, maxPayload int) uint64 {
	return uint64(emptyRecordLen + slen + 4 + maxPayload)
}

// ResetState resets any state that's temporary. For example when changing leaders.
func (fs *fileStore) ResetState() {
	fs.mu.Lock()
	defer fs.mu.Unlock()
	if fs.scheduling != nil {
		fs.scheduling.clearInflight()
	}
}

// Determine time since any last activity, read/load, write or remove.
func (mb *msgBlock) sinceLastActivity() time.Duration {
	if mb.closed {
		return 0
	}
	last := mb.lwts
	if mb.lrts > last {
		last = mb.lrts
	}
	if mb.llts > last {
		last = mb.llts
	}
	if mb.lsts > last {
		last = mb.lsts
	}
	return time.Since(time.Unix(0, last).UTC())
}

// Determine time since last write or remove of a message.
// Read lock should be held.
func (mb *msgBlock) sinceLastWriteActivity() time.Duration {
	if mb.closed {
		return 0
	}
	last := mb.lwts
	if mb.lrts > last {
		last = mb.lrts
	}
	return time.Since(time.Unix(0, last).UTC())
}

func checkNewHeader(hdr []byte) error {
	if len(hdr) < 2 || hdr[0] != magic ||
		(hdr[1] != version && hdr[1] != newVersion) {
		return errCorruptState
	}
	return nil
}

// readIndexInfo will read in the index information for the message block.
func (mb *msgBlock) readIndexInfo() error {
	ifn := filepath.Join(mb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(indexScan, mb.index))
	buf, err := os.ReadFile(ifn)
	if err != nil {
		return err
	}

	// Set if first time.
	if mb.liwsz == 0 {
		mb.liwsz = int64(len(buf))
	}

	// Decrypt if needed.
	if mb.aek != nil {
		buf, err = mb.aek.Open(buf[:0], mb.nonce, buf, nil)
		if err != nil {
			return err
		}
	}

	if err := checkNewHeader(buf); err != nil {
		defer os.Remove(ifn)
		return fmt.Errorf("bad index file")
	}

	bi := hdrLen

	// Helpers, will set i to -1 on error.
	readSeq := func() uint64 {
		if bi < 0 {
			return 0
		}
		seq, n := binary.Uvarint(buf[bi:])
		if n <= 0 {
			bi = -1
			return 0
		}
		bi += n
		return seq &^ ebit
	}
	readCount := readSeq
	readTimeStamp := func() int64 {
		if bi < 0 {
			return 0
		}
		ts, n := binary.Varint(buf[bi:])
		if n <= 0 {
			bi = -1
			return -1
		}
		bi += n
		return ts
	}
	mb.msgs = readCount()
	mb.bytes = readCount()
	atomic.StoreUint64(&mb.first.seq, readSeq())
	mb.first.ts = readTimeStamp()
	atomic.StoreUint64(&mb.last.seq, readSeq())
	mb.last.ts = readTimeStamp()
	dmapLen := readCount()

	// Check if this is a short write index file.
	if bi < 0 || bi+checksumSize > len(buf) {
		os.Remove(ifn)
		return fmt.Errorf("short index file")
	}

	// Check for consistency if accounting. If something is off bail and we will rebuild.
	if mb.msgs != (atomic.LoadUint64(&mb.last.seq)-atomic.LoadUint64(&mb.first.seq)+1)-dmapLen {
		os.Remove(ifn)
		return fmt.Errorf("accounting inconsistent")
	}

	// Checksum
	copy(mb.lchk[0:], buf[bi:bi+checksumSize])
	bi += checksumSize

	// Now check for presence of a delete map
	if dmapLen > 0 {
		// New version is encoded avl seqset.
		if buf[1] == newVersion {
			dmap, _, err := avl.Decode(buf[bi:])
			if err != nil {
				return fmt.Errorf("could not decode avl dmap: %v", err)
			}
			mb.dmap = *dmap
		} else {
			// This is the old version.
			for i, fseq := 0, atomic.LoadUint64(&mb.first.seq); i < int(dmapLen); i++ {
				seq := readSeq()
				if seq == 0 {
					break
				}
				mb.dmap.Insert(seq + fseq)
			}
		}
	}

	return nil
}

// Will return total number of cache loads.
func (fs *fileStore) cacheLoads() uint64 {
	var tl uint64
	fs.mu.RLock()
	for _, mb := range fs.blks {
		mb.mu.RLock()
		tl += mb.cloads
		mb.mu.RUnlock()
	}
	fs.mu.RUnlock()
	return tl
}

// Will return total number of cached bytes.
func (fs *fileStore) cacheSize() uint64 {
	var sz uint64
	fs.mu.RLock()
	for _, mb := range fs.blks {
		mb.mu.Lock()
		var needsCleanup bool
		if mb.cache == nil {
			mb.cache = mb.ecache.Value()
			needsCleanup = mb.cache != nil
		}
		if mb.cache != nil {
			sz += uint64(len(mb.cache.buf))
		}
		if needsCleanup {
			mb.finishedWithCache()
		}
		mb.mu.Unlock()
	}
	fs.mu.RUnlock()
	return sz
}

// Will return total number of dmapEntries for all msg blocks.
func (fs *fileStore) dmapEntries() int {
	var total int
	fs.mu.RLock()
	for _, mb := range fs.blks {
		total += mb.dmap.Size()
	}
	fs.mu.RUnlock()
	return total
}

// Fixed helper for iterating.
func subjectsEqual(a, b string) bool {
	return a == b
}

func subjectsAll(a, b string) bool {
	return true
}

func compareFn(subject string) func(string, string) bool {
	if subject == _EMPTY_ || subject == fwcs {
		return subjectsAll
	}
	if subjectHasWildcard(subject) {
		return subjectIsSubsetMatch
	}
	return subjectsEqual
}

// PurgeEx will remove messages based on subject filters, sequence and number of messages to keep.
// Will return the number of purged messages.
func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) {
	if subject == _EMPTY_ || subject == fwcs {
		if keep == 0 && sequence == 0 {
			return fs.purge(0)
		}
		if sequence > 1 {
			return fs.compact(sequence)
		}
	}

	// Make sure to not leave subject if empty and we reach this spot.
	if subject == _EMPTY_ {
		subject = fwcs
	}

	eq, wc := compareFn(subject), subjectHasWildcard(subject)
	var firstSeqNeedsUpdate bool
	var bytes uint64

	// If we have a "keep" designation need to get full filtered state so we know how many to purge.
	var maxp uint64
	if keep > 0 {
		ss := fs.FilteredState(1, subject)
		if keep >= ss.Msgs {
			return 0, nil
		}
		maxp = ss.Msgs - keep
	}

	var smv StoreMsg
	var tombs []msgId
	var lowSeq uint64

	fs.mu.Lock()
	// We may remove blocks as we purge, so don't range directly on fs.blks
	// otherwise we may jump over some (see https://github.com/nats-io/nats-server/issues/3528)
	for i := 0; i < len(fs.blks); i++ {
		mb := fs.blks[i]
		mb.mu.Lock()

		// If we do not have our fss, try to expire the cache if we have no items in this block.
		shouldExpire := mb.fssNotLoaded()

		t, f, l := mb.filteredPendingLocked(subject, wc, atomic.LoadUint64(&mb.first.seq))
		if t == 0 {
			// Expire if we were responsible for loading.
			if shouldExpire {
				// Expire this cache before moving on.
				mb.tryForceExpireCacheLocked()
			}
			mb.mu.Unlock()
			continue
		}

		if sequence > 1 && sequence <= l {
			l = sequence - 1
		}

		if mb.cacheNotLoaded() {
			if err := mb.loadMsgsWithLock(); err != nil {
				mb.mu.Unlock()
				fs.mu.Unlock()
				return 0, err
			}
			shouldExpire = true
		}

		var nrg uint64 // Number of remaining messages globally after removal from psim.

		for seq, te := f, len(tombs); seq <= l; seq++ {
			if sm, _ := mb.cacheLookupNoCopy(seq, &smv); sm != nil && eq(sm.subj, subject) {
				rl := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg)
				// Do fast in place remove.
				// Stats
				if mb.msgs > 0 {
					// Msgs
					fs.state.Msgs--
					mb.msgs--
					// Bytes, make sure to not go negative.
					if rl > fs.state.Bytes {
						rl = fs.state.Bytes
					}
					if rl > mb.bytes {
						rl = mb.bytes
					}
					fs.state.Bytes -= rl
					mb.bytes -= rl
					// Totals
					purged++
					bytes += rl
				}
				// PSIM and FSS updates.
				nr := mb.removeSeqPerSubject(sm.subj, seq)
				nrg = fs.removePerSubject(sm.subj)

				// Track tombstones we need to write.
				tombs = append(tombs, msgId{sm.seq, sm.ts})
				if sm.seq < lowSeq || lowSeq == 0 {
					lowSeq = sm.seq
				}

				// Check for first message.
				if seq == atomic.LoadUint64(&mb.first.seq) {
					mb.selectNextFirst()
					if mb.isEmpty() {
						// Since we are removing this block don't need to write tombstones.
						tombs = tombs[:te]
						fs.removeMsgBlock(mb)
						i--
						// keep flag set, if set previously
						firstSeqNeedsUpdate = firstSeqNeedsUpdate || seq == fs.state.FirstSeq
					} else if seq == fs.state.FirstSeq {
						fs.state.FirstSeq = atomic.LoadUint64(&mb.first.seq) // new one.
						if mb.first.ts == 0 {
							fs.state.FirstTime = time.Time{}
						} else {
							fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
						}
					}
				} else {
					// Out of order delete.
					mb.dmap.Insert(seq)
				}
				// Break if we have emptied this block or if we set a maximum purge count.
				if mb.isEmpty() || (maxp > 0 && purged >= maxp) {
					break
				}
				// Also break if we know we have no more messages matching here.
				// This is only applicable for non-wildcarded filters.
				if !wc && nr == 0 {
					break
				}
			}
		}
		// Expire if we were responsible for loading and we do not seem to be doing successive purgeEx calls.
		// On successive calls - most likely from KV purge deletes, we want to keep the data loaded.
		if shouldExpire && time.Since(fs.lpex) > time.Second {
			// Expire this cache before moving on.
			mb.tryForceExpireCacheLocked()
		} else {
			mb.finishedWithCache()
		}
		mb.mu.Unlock()

		// Check if we should break out of top level too.
		if maxp > 0 && purged >= maxp {
			break
		}
		// Also check if not wildcarded and we have no remaining matches.
		if !wc && nrg == 0 {
			break
		}
	}
	if firstSeqNeedsUpdate {
		fs.selectNextFirst()
	}

	// Update the last purgeEx call time.
	defer func() { fs.lpex = time.Now() }()

	// Write any tombstones as needed.
	// When writing multiple tombstones we will flush at the end.
	if len(tombs) > 0 {
		for _, tomb := range tombs {
			if err := fs.writeTombstoneNoFlush(tomb.seq, tomb.ts); err != nil {
				fs.mu.Unlock()
				return purged, err
			}
		}
		// Flush any pending. If we change blocks the newMsgBlockForWrite() will flush any pending for us.
		if lmb := fs.lmb; lmb != nil {
			lmb.flushPendingMsgs()
		}
	}

	fs.dirty++
	cb := fs.scb
	fs.mu.Unlock()

	if cb != nil {
		if purged == 1 {
			cb(-int64(purged), -int64(bytes), lowSeq, subject)
		} else {
			// FIXME(dlc) - Since we track lowSeq we could send to upper layer if they dealt with the condition properly.
			cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
		}
	}

	return purged, nil
}

// Purge will remove all messages from this store.
// Will return the number of purged messages.
func (fs *fileStore) Purge() (uint64, error) {
	return fs.purge(0)
}

func (fs *fileStore) purge(fseq uint64) (uint64, error) {
	if fs.isClosed() {
		return 0, ErrStoreClosed
	}

	fs.mu.Lock()

	purged := fs.state.Msgs
	rbytes := int64(fs.state.Bytes)

	fs.state.FirstSeq = fs.state.LastSeq + 1
	fs.state.FirstTime = time.Time{}

	fs.state.Bytes = 0
	fs.state.Msgs = 0

	for _, mb := range fs.blks {
		mb.dirtyClose()
	}

	fs.blks = nil
	fs.lmb = nil
	fs.bim = make(map[uint32]*msgBlock)
	// Clear any per subject tracking.
	fs.psim, fs.tsl = fs.psim.Empty(), 0
	fs.sdm.empty()
	// Mark dirty.
	fs.dirty++

	// Move the msgs directory out of the way, will delete out of band.
	// FIXME(dlc) - These can error and we need to change api above to propagate?
	mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
	pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir)
	// If purge directory still exists then we need to wait
	// in place and remove since rename would fail.
	if _, err := os.Stat(pdir); err == nil {
		<-dios
		os.RemoveAll(pdir)
		dios <- struct{}{}
	}

	<-dios
	os.Rename(mdir, pdir)
	dios <- struct{}{}

	go func() {
		<-dios
		os.RemoveAll(pdir)
		dios <- struct{}{}
	}()

	// Create new one.
	<-dios
	os.MkdirAll(mdir, defaultDirPerms)
	dios <- struct{}{}

	// Make sure we have a lmb to write to.
	if _, err := fs.newMsgBlockForWrite(); err != nil {
		fs.mu.Unlock()
		return purged, err
	}

	// Check if we need to set the first seq to a new number.
	if fseq > fs.state.FirstSeq {
		fs.state.FirstSeq = fseq
		fs.state.LastSeq = fseq - 1
	}

	lmb := fs.lmb
	atomic.StoreUint64(&lmb.first.seq, fs.state.FirstSeq)
	atomic.StoreUint64(&lmb.last.seq, fs.state.LastSeq)
	lmb.last.ts = fs.state.LastTime.UnixNano()

	if lseq := atomic.LoadUint64(&lmb.last.seq); lseq > 0 {
		// Leave a tombstone so we can remember our starting sequence in case
		// full state becomes corrupted.
		fs.writeTombstone(lseq, lmb.last.ts)
	}

	cb := fs.scb
	fs.mu.Unlock()

	// Force a new index.db to be written.
	if purged > 0 {
		fs.forceWriteFullState()
	}

	if cb != nil {
		cb(-int64(purged), -rbytes, 0, _EMPTY_)
	}

	return purged, nil
}

// Compact will remove all messages from this store up to
// but not including the seq parameter.
// Will return the number of purged messages.
func (fs *fileStore) Compact(seq uint64) (uint64, error) {
	return fs.compact(seq)
}

func (fs *fileStore) compact(seq uint64) (uint64, error) {
	if seq == 0 {
		return fs.purge(seq)
	}

	var purged, bytes uint64

	fs.mu.Lock()
	// Same as purge all.
	if lseq := fs.state.LastSeq; seq > lseq {
		fs.mu.Unlock()
		return fs.purge(seq)
	}
	// Short-circuit if the store was already compacted past this point.
	if fs.state.FirstSeq > seq {
		fs.mu.Unlock()
		return purged, nil
	}
	// We have to delete interior messages.
	smb := fs.selectMsgBlock(seq)
	if smb == nil {
		fs.mu.Unlock()
		return 0, nil
	}

	// All msgblocks up to this one can be thrown away.
	var deleted int
	for _, mb := range fs.blks {
		if mb == smb {
			break
		}
		mb.mu.Lock()
		purged += mb.msgs
		bytes += mb.bytes
		// Make sure we do subject cleanup as well.
		mb.ensurePerSubjectInfoLoaded()
		mb.fss.IterOrdered(func(bsubj []byte, ss *SimpleState) bool {
			subj := bytesToString(bsubj)
			for i := uint64(0); i < ss.Msgs; i++ {
				fs.removePerSubject(subj)
			}
			return true
		})
		// Now close.
		mb.dirtyCloseWithRemove(true)
		mb.mu.Unlock()
		deleted++
	}

	var smv StoreMsg
	var err error
	var tombs []msgId

	smb.mu.Lock()
	if atomic.LoadUint64(&smb.first.seq) == seq {
		fs.state.FirstSeq = atomic.LoadUint64(&smb.first.seq)
		fs.state.FirstTime = time.Unix(0, smb.first.ts).UTC()
		goto SKIP
	}

	// Make sure we have the messages loaded.
	if smb.cacheNotLoaded() {
		if err = smb.loadMsgsWithLock(); err != nil {
			goto SKIP
		}
		defer func() {
			// The lock is released once we get here, so need to re-acquire.
			smb.mu.Lock()
			smb.finishedWithCache()
			smb.mu.Unlock()
		}()
	}
	for mseq := atomic.LoadUint64(&smb.first.seq); mseq < seq; mseq++ {
		sm, err := smb.cacheLookupNoCopy(mseq, &smv)
		if err == errDeletedMsg {
			// Update dmap.
			if !smb.dmap.IsEmpty() {
				smb.dmap.Delete(mseq)
			}
		} else if sm != nil {
			sz := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg)
			if smb.msgs > 0 {
				smb.msgs--
				if sz > smb.bytes {
					sz = smb.bytes
				}
				smb.bytes -= sz
				bytes += sz
				purged++
			}
			// Update fss
			smb.removeSeqPerSubject(sm.subj, mseq)
			fs.removePerSubject(sm.subj)
			tombs = append(tombs, msgId{sm.seq, sm.ts})
		}
	}

	// Check if empty after processing, could happen if tail of messages are all deleted.
	if isEmpty := smb.msgs == 0; isEmpty {
		// Only remove if not the last block.
		if smb != fs.lmb {
			smb.dirtyCloseWithRemove(true)
			deleted++
		} else {
			// Make sure to sync changes.
			smb.needSync = true
		}
		// Update fs first here as well.
		fs.state.FirstSeq = atomic.LoadUint64(&smb.last.seq) + 1
		fs.state.FirstTime = time.Time{}

	} else {
		// Make sure to sync changes.
		smb.needSync = true
		// Just for start condition for selectNextFirst.
		if smb.first.seq < seq {
			atomic.StoreUint64(&smb.first.seq, seq-1)
		} else {
			// selectNextFirst always adds 1, so need to subtract 1 here.
			atomic.StoreUint64(&smb.first.seq, smb.first.seq-1)
		}
		smb.selectNextFirst()

		fs.state.FirstSeq = atomic.LoadUint64(&smb.first.seq)
		fs.state.FirstTime = time.Unix(0, smb.first.ts).UTC()

		// Check if we should reclaim the head space from this block.
		// This will be optimistic only, so don't continue if we encounter any errors here.
		if smb.rbytes > compactMinimum && smb.bytes*2 < smb.rbytes {
			var moff uint32
			moff, _, _, err = smb.slotInfo(int(atomic.LoadUint64(&smb.first.seq) - smb.cache.fseq))
			if err != nil || moff >= uint32(len(smb.cache.buf)) {
				goto SKIP
			}
			buf := smb.cache.buf[moff:]
			// Don't reuse, copy to new recycled buf.
			nbuf := getMsgBlockBuf(len(buf))
			nbuf = append(nbuf, buf...)
			smb.closeFDsLockedNoCheck()
			// Check for encryption.
			if smb.bek != nil && len(nbuf) > 0 {
				// Recreate to reset counter.
				bek, err := genBlockEncryptionKey(smb.fs.fcfg.Cipher, smb.seed, smb.nonce)
				if err != nil {
					goto SKIP
				}
				// For future writes make sure to set smb.bek to keep counter correct.
				smb.bek = bek
				smb.bek.XORKeyStream(nbuf, nbuf)
			}
			// Recompress if necessary (smb.cmp contains the algorithm used when
			// the block was loaded from disk, or defaults to NoCompression if not)
			if nbuf, err = smb.cmp.Compress(nbuf); err != nil {
				goto SKIP
			}

			// We will write to a new file and mv/rename it in case of failure.
			mfn := filepath.Join(smb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(newScan, smb.index))
			<-dios
			err := os.WriteFile(mfn, nbuf, defaultFilePerms)
			dios <- struct{}{}
			if err != nil {
				os.Remove(mfn)
				goto SKIP
			}
			if err := os.Rename(mfn, smb.mfn); err != nil {
				os.Remove(mfn)
				goto SKIP
			}

			// Make sure to remove fss state.
			smb.fss = nil
			smb.clearCacheAndOffset()
			smb.rbytes = uint64(len(nbuf))
			// Make sure we don't write any additional tombstones.
			tombs = nil
		}
	}

SKIP:
	smb.mu.Unlock()

	// Write any tombstones as needed.
	// When writing multiple tombstones we will flush at the end.
	if len(tombs) > 0 {
		for _, tomb := range tombs {
			if err := fs.writeTombstoneNoFlush(tomb.seq, tomb.ts); err != nil {
				fs.mu.Unlock()
				return purged, err
			}
		}
		// Flush any pending. If we change blocks the newMsgBlockForWrite() will flush any pending for us.
		if lmb := fs.lmb; lmb != nil {
			lmb.flushPendingMsgs()
		}
	}

	if deleted > 0 {
		// Update block map.
		if fs.bim != nil {
			for _, mb := range fs.blks[:deleted] {
				delete(fs.bim, mb.index)
			}
		}
		// Update blks slice.
		fs.blks = copyMsgBlocks(fs.blks[deleted:])
		if lb := len(fs.blks); lb == 0 {
			fs.lmb = nil
		} else {
			fs.lmb = fs.blks[lb-1]
		}
	}

	// Update top level accounting.
	if purged > fs.state.Msgs {
		purged = fs.state.Msgs
	}
	fs.state.Msgs -= purged
	if fs.state.Msgs == 0 {
		fs.state.FirstSeq = fs.state.LastSeq + 1
		fs.state.FirstTime = time.Time{}
	}

	if bytes > fs.state.Bytes {
		bytes = fs.state.Bytes
	}
	fs.state.Bytes -= bytes

	// Any existing state file no longer applicable. We will force write a new one
	// after we release the lock.
	os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
	fs.dirty++
	cb := fs.scb
	fs.mu.Unlock()

	// Force a new index.db to be written.
	if purged > 0 {
		fs.forceWriteFullState()
	}

	if cb != nil && purged > 0 {
		cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
	}

	return purged, err
}

// Will completely reset our store.
func (fs *fileStore) reset() error {
	if fs.isClosed() {
		return ErrStoreClosed
	}

	fs.mu.Lock()

	var purged, bytes uint64
	cb := fs.scb

	for _, mb := range fs.blks {
		mb.mu.Lock()
		purged += mb.msgs
		bytes += mb.bytes
		mb.dirtyCloseWithRemove(true)
		mb.mu.Unlock()
	}

	// Reset
	fs.state.FirstSeq = 0
	fs.state.FirstTime = time.Time{}
	fs.state.LastSeq = 0
	fs.state.LastTime = time.Now().UTC()
	// Update msgs and bytes.
	fs.state.Msgs = 0
	fs.state.Bytes = 0

	// Reset blocks.
	fs.blks, fs.lmb = nil, nil

	// Reset subject mappings.
	fs.psim, fs.tsl = fs.psim.Empty(), 0
	fs.sdm.empty()
	fs.bim = make(map[uint32]*msgBlock)

	// If we purged anything, make sure we kick flush state loop.
	if purged > 0 {
		fs.dirty++
	}

	fs.mu.Unlock()

	if cb != nil {
		cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
	}

	return nil
}

// Return all active tombstones in this msgBlock.
func (mb *msgBlock) tombs() []msgId {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	return mb.tombsLocked()
}

// Return all active tombstones in this msgBlock.
// Write lock should be held.
func (mb *msgBlock) tombsLocked() []msgId {
	if mb.cacheNotLoaded() {
		if err := mb.loadMsgsWithLock(); err != nil {
			return nil
		}
	}
	defer mb.finishedWithCache()

	var tombs []msgId
	var le = binary.LittleEndian
	buf := mb.cache.buf

	for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; {
		if index+msgHdrSize > lbuf {
			return tombs
		}
		hdr := buf[index : index+msgHdrSize]
		rl, seq := le.Uint32(hdr[0:]), le.Uint64(hdr[4:])
		// Clear any headers bit that could be set.
		rl &^= hbit
		// Check for tombstones.
		if seq&tbit != 0 {
			ts := int64(le.Uint64(hdr[12:]))
			tombs = append(tombs, msgId{seq &^ tbit, ts})
		}
		// Advance to next record.
		index += rl
	}

	return tombs
}

// Truncate will truncate a stream store up to seq. Sequence needs to be valid.
func (fs *fileStore) Truncate(seq uint64) error {
	if fs.isClosed() {
		return ErrStoreClosed
	}

	// Check for request to reset.
	if seq == 0 {
		return fs.reset()
	}

	fs.mu.Lock()

	// Any existing state file will no longer be applicable. We will force write a new one
	// at the end, after we release the lock.
	os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))

	var lsm *StoreMsg
	smb := fs.selectMsgBlock(seq)
	if smb != nil {
		lsm, _, _ = smb.fetchMsgNoCopy(seq, nil)
	}

	// Reset last so new block doesn't contain truncated sequences/timestamps.
	var lastTime int64
	if lsm != nil {
		lastTime = lsm.ts
	} else if smb != nil {
		lastTime = smb.last.ts
	} else {
		lastTime = fs.state.LastTime.UnixNano()
	}
	fs.state.LastSeq = seq
	fs.state.LastTime = time.Unix(0, lastTime).UTC()

	// Always create a new write block for any tombstones.
	// We'll truncate the selected message block as the last step, so can't write tombstones to it.
	// If we end up not needing to write tombstones, this block will be cleaned up at the end.
	tmb, err := fs.newMsgBlockForWrite()
	if err != nil {
		fs.mu.Unlock()
		return err
	}

	// The selected message block needs to be removed if it needs to be fully truncated.
	var removeSmb bool
	if smb != nil {
		removeSmb = atomic.LoadUint64(&smb.first.seq) > seq
	}

	// If the selected block is not found or the message was deleted, we'll need to write a tombstone
	// at the truncated sequence so we don't roll backward on our last sequence and timestamp.
	if lsm == nil || removeSmb {
		fs.writeTombstone(seq, lastTime)
	}

	var purged, bytes uint64

	// Remove any left over msg blocks.
	getLastMsgBlock := func() *msgBlock {
		// Start at one before last, tmb will be the last most of the time
		// unless a new block gets added for tombstones.
		for i := len(fs.blks) - 2; i >= 0; i-- {
			if mb := fs.blks[i]; mb.index < tmb.index {
				return mb
			}
		}
		return nil
	}
	for mb := getLastMsgBlock(); mb != nil && mb != smb; mb = getLastMsgBlock() {
		mb.mu.Lock()
		purged += mb.msgs
		bytes += mb.bytes

		// We could have tombstones for messages before the truncated sequence.
		// Need to store those for blocks we're about to remove.
		if tombs := mb.tombsLocked(); len(tombs) > 0 {
			// Temporarily unlock while we write tombstones.
			mb.mu.Unlock()
			for _, tomb := range tombs {
				if tomb.seq < seq {
					fs.writeTombstone(tomb.seq, tomb.ts)
				}
			}
			mb.mu.Lock()
		}
		fs.forceRemoveMsgBlock(mb)
		mb.mu.Unlock()
	}

	hasWrittenTombstones := len(tmb.tombs()) > 0
	if smb != nil {
		smb.mu.Lock()
		if removeSmb {
			purged += smb.msgs
			bytes += smb.bytes

			// We could have tombstones for messages before the truncated sequence.
			if tombs := smb.tombsLocked(); len(tombs) > 0 {
				// Temporarily unlock while we write tombstones.
				smb.mu.Unlock()
				for _, tomb := range tombs {
					if tomb.seq < seq {
						fs.writeTombstone(tomb.seq, tomb.ts)
					}
				}
				smb.mu.Lock()
			}
			fs.forceRemoveMsgBlock(smb)
			smb.mu.Unlock()
			goto SKIP
		}

		// Make sure writeable.
		if err := smb.enableForWriting(fs.fip); err != nil {
			smb.mu.Unlock()
			fs.mu.Unlock()
			return err
		}

		// Truncate our selected message block.
		nmsgs, nbytes, err := smb.truncate(seq, lastTime)
		if err != nil {
			smb.mu.Unlock()
			fs.mu.Unlock()
			return fmt.Errorf("smb.truncate: %w", err)
		}
		// Account for the truncated msgs and bytes.
		purged += nmsgs
		bytes += nbytes

		// The selected message block is not the last anymore, need to close down resources.
		if hasWrittenTombstones {
			// Quit our loops.
			if smb.qch != nil {
				close(smb.qch)
				smb.qch = nil
			}
			smb.closeFDsLockedNoCheck()
			smb.recompressOnDiskIfNeeded()
		}
		smb.mu.Unlock()
	}

SKIP:
	// If no tombstones were written, we can remove the block and
	// purely rely on the selected block as the last block.
	if !hasWrittenTombstones {
		fs.lmb = smb
		tmb.mu.Lock()
		fs.forceRemoveMsgBlock(tmb)
		tmb.mu.Unlock()
	}

	// Reset last.
	fs.state.LastSeq = seq
	fs.state.LastTime = time.Unix(0, lastTime).UTC()
	// Update msgs and bytes.
	if purged > fs.state.Msgs {
		purged = fs.state.Msgs
	}
	fs.state.Msgs -= purged
	if bytes > fs.state.Bytes {
		bytes = fs.state.Bytes
	}
	fs.state.Bytes -= bytes

	// Reset our subject lookup info.
	fs.resetGlobalPerSubjectInfo()

	fs.dirty++

	cb := fs.scb
	fs.mu.Unlock()

	// Force a new index.db to be written.
	if purged > 0 {
		fs.forceWriteFullState()
	}

	if cb != nil {
		cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
	}

	return nil
}

func (fs *fileStore) lastSeq() uint64 {
	fs.mu.RLock()
	seq := fs.state.LastSeq
	fs.mu.RUnlock()
	return seq
}

// Returns number of msg blks.
func (fs *fileStore) numMsgBlocks() int {
	fs.mu.RLock()
	defer fs.mu.RUnlock()
	return len(fs.blks)
}

// Will add a new msgBlock.
// Lock should be held.
func (fs *fileStore) addMsgBlock(mb *msgBlock) {
	fs.blks = append(fs.blks, mb)
	fs.lmb = mb
	fs.bim[mb.index] = mb
}

// Remove from our list of blks.
// Both locks should be held.
func (fs *fileStore) removeMsgBlockFromList(mb *msgBlock) {
	// Remove from list.
	for i, omb := range fs.blks {
		if mb == omb {
			fs.dirty++
			blks := append(fs.blks[:i], fs.blks[i+1:]...)
			fs.blks = copyMsgBlocks(blks)
			if fs.bim != nil {
				delete(fs.bim, mb.index)
			}
			break
		}
	}
}

// Removes the msgBlock
// Both locks should be held.
func (fs *fileStore) removeMsgBlock(mb *msgBlock) {
	// Check for us being last message block
	lseq, lts := atomic.LoadUint64(&mb.last.seq), mb.last.ts
	if mb == fs.lmb {
		// Creating a new message write block requires that the lmb lock is not held.
		mb.mu.Unlock()
		// Write the tombstone to remember since this was last block.
		if lmb, _ := fs.newMsgBlockForWrite(); lmb != nil {
			fs.writeTombstone(lseq, lts)
		}
		mb.mu.Lock()
	} else if lseq == fs.state.LastSeq {
		// Need to write a tombstone for the last sequence if we're removing the block containing it.
		fs.writeTombstone(lseq, lts)
	}
	// Only delete message block after (potentially) writing a tombstone.
	fs.forceRemoveMsgBlock(mb)
}

// Removes the msgBlock, without writing tombstones to ensure the last sequence is preserved.
// Both locks should be held.
func (fs *fileStore) forceRemoveMsgBlock(mb *msgBlock) {
	mb.dirtyCloseWithRemove(true)
	fs.removeMsgBlockFromList(mb)
}

// Purges and removes the msgBlock from the store.
// Lock should be held.
func (fs *fileStore) purgeMsgBlock(mb *msgBlock) {
	mb.mu.Lock()
	// Adjust per-subject tracking if present.
	if err := mb.ensurePerSubjectInfoLoaded(); err == nil && mb.fss != nil {
		mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool {
			subj := bytesToString(bsubj)
			for range ss.Msgs {
				fs.removePerSubject(subj)
			}
			return true
		})
	}
	// Clean up scheduled message metadata if we know this block contained any.
	if fs.scheduling != nil && mb.schedules > 0 {
		cacheLoaded := !mb.cacheNotLoaded()
		if !cacheLoaded {
			cacheLoaded = mb.loadMsgsWithLock() == nil
		}
		if cacheLoaded {
			var smv StoreMsg
			fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
			for seq := fseq; seq <= lseq; seq++ {
				sm, err := mb.cacheLookupNoCopy(seq, &smv)
				if err != nil || sm == nil {
					continue
				}
				if schedule, ok := getMessageSchedule(sm.hdr); ok && !schedule.IsZero() {
					fs.scheduling.remove(seq)
				}
			}
		}
	}
	// Update top level accounting.
	msgs, bytes := mb.msgs, mb.bytes
	if msgs > fs.state.Msgs {
		msgs = fs.state.Msgs
	}
	if bytes > fs.state.Bytes {
		bytes = fs.state.Bytes
	}
	fs.state.Msgs -= msgs
	fs.state.Bytes -= bytes
	fs.removeMsgBlock(mb)
	mb.tryForceExpireCacheLocked()
	mb.finishedWithCache()
	mb.mu.Unlock()
	fs.selectNextFirst()

	if cb := fs.scb; cb != nil {
		// If we have a callback registered, we need to release lock regardless since consumers will recalculate pending.
		fs.mu.Unlock()
		// Storage updates.
		cb(-int64(msgs), -int64(bytes), 0, _EMPTY_)
		fs.mu.Lock()
	}
}

// Called by purge to simply get rid of the cache and close our fds.
// Lock should not be held.
func (mb *msgBlock) dirtyClose() {
	mb.mu.Lock()
	defer mb.mu.Unlock()
	mb.dirtyCloseWithRemove(false)
}

// Should be called with lock held.
func (mb *msgBlock) dirtyCloseWithRemove(remove bool) error {
	if mb == nil {
		return nil
	}
	// Stop cache expiration timer.
	if mb.ctmr != nil {
		mb.ctmr.Stop()
		mb.ctmr = nil
	}
	// Close cache
	mb.clearCacheAndOffset()
	// Quit our loops.
	if mb.qch != nil {
		close(mb.qch)
		mb.qch = nil
	}
	if mb.mfd != nil {
		mb.mfd.Close()
		mb.mfd = nil
	}
	if remove {
		// Clear any tracking by subject if we are removing.
		mb.fss = nil
		if mb.mfn != _EMPTY_ {
			err := os.Remove(mb.mfn)
			if isPermissionError(err) {
				return err
			}
			mb.mfn = _EMPTY_
		}
		if mb.kfn != _EMPTY_ {
			err := os.Remove(mb.kfn)
			if isPermissionError(err) {
				return err
			}
		}
	}
	return nil
}

// Remove a seq from the fss and select new first.
// Lock should be held.
func (mb *msgBlock) removeSeqPerSubject(subj string, seq uint64) uint64 {
	mb.ensurePerSubjectInfoLoaded()
	if mb.fss == nil {
		return 0
	}
	bsubj := stringToBytes(subj)
	ss, ok := mb.fss.Find(bsubj)
	if !ok || ss == nil {
		return 0
	}

	mb.fs.sdm.removeSeqAndSubject(seq, subj)
	if ss.Msgs == 1 {
		mb.fss.Delete(bsubj)
		return 0
	}

	ss.Msgs--

	// Only one left.
	if ss.Msgs == 1 {
		if !ss.lastNeedsUpdate && seq != ss.Last {
			ss.First = ss.Last
			ss.firstNeedsUpdate = false
			return 1
		}
		if !ss.firstNeedsUpdate && seq != ss.First {
			ss.Last = ss.First
			ss.lastNeedsUpdate = false
			return 1
		}
	}

	// We can lazily calculate the first/last sequence when needed.
	ss.firstNeedsUpdate = seq == ss.First || ss.firstNeedsUpdate
	ss.lastNeedsUpdate = seq == ss.Last || ss.lastNeedsUpdate

	return ss.Msgs
}

// Will recalculate the first and/or last sequence for this subject in this block.
// Will avoid slower path message lookups and scan the cache directly instead.
func (mb *msgBlock) recalculateForSubj(subj string, ss *SimpleState) {
	// Need to make sure messages are loaded.
	if mb.cacheNotLoaded() {
		if err := mb.loadMsgsWithLock(); err != nil {
			return
		}
		defer mb.finishedWithCache()
	}

	startSlot := int(ss.First - mb.cache.fseq)
	if startSlot < 0 {
		startSlot = 0
	}
	if startSlot >= len(mb.cache.idx) {
		ss.First = ss.Last
		ss.firstNeedsUpdate = false
		ss.lastNeedsUpdate = false
		return
	}

	endSlot := int(ss.Last - mb.cache.fseq)
	if endSlot < 0 {
		endSlot = 0
	}
	if endSlot >= len(mb.cache.idx) || startSlot > endSlot {
		return
	}

	var le = binary.LittleEndian
	if ss.firstNeedsUpdate {
		// Mark first as updated.
		ss.firstNeedsUpdate = false

		fseq := ss.First + 1
		if mbFseq := atomic.LoadUint64(&mb.first.seq); fseq < mbFseq {
			fseq = mbFseq
		}
		for slot := startSlot; slot < len(mb.cache.idx); slot++ {
			bi := mb.cache.idx[slot] &^ cbit
			if bi == dbit {
				// delete marker so skip.
				continue
			}
			li := int(bi)
			if li >= len(mb.cache.buf) {
				ss.First = ss.Last
				// Only need to reset ss.lastNeedsUpdate, ss.firstNeedsUpdate is already reset above.
				ss.lastNeedsUpdate = false
				return
			}
			buf := mb.cache.buf[li:]
			hdr := buf[:msgHdrSize]
			slen := int(le.Uint16(hdr[20:]))
			if subj == bytesToString(buf[msgHdrSize:msgHdrSize+slen]) {
				seq := le.Uint64(hdr[4:])
				if seq < fseq || seq&ebit != 0 || mb.dmap.Exists(seq) {
					continue
				}
				ss.First = seq
				if ss.Msgs == 1 {
					ss.Last = seq
					ss.lastNeedsUpdate = false
					return
				}
				// Skip the start slot ahead, if we need to recalculate last we can stop early.
				startSlot = slot
				break
			}
		}
	}
	if ss.lastNeedsUpdate {
		// Mark last as updated.
		ss.lastNeedsUpdate = false

		lseq := ss.Last - 1
		if mbLseq := atomic.LoadUint64(&mb.last.seq); lseq > mbLseq {
			lseq = mbLseq
		}
		for slot := endSlot; slot >= startSlot; slot-- {
			bi := mb.cache.idx[slot] &^ cbit
			if bi == dbit {
				// delete marker so skip.
				continue
			}
			li := int(bi)
			if li >= len(mb.cache.buf) {
				// Can't overwrite ss.Last, just skip.
				return
			}
			buf := mb.cache.buf[li:]
			hdr := buf[:msgHdrSize]
			slen := int(le.Uint16(hdr[20:]))
			if subj == bytesToString(buf[msgHdrSize:msgHdrSize+slen]) {
				seq := le.Uint64(hdr[4:])
				if seq > lseq || seq&ebit != 0 || mb.dmap.Exists(seq) {
					continue
				}
				// Sequence should never be lower, but guard against it nonetheless.
				if seq < ss.First {
					seq = ss.First
				}
				ss.Last = seq
				if ss.Msgs == 1 {
					ss.First = seq
					ss.firstNeedsUpdate = false
				}
				return
			}
		}
	}
}

// Lock should be held.
func (fs *fileStore) resetGlobalPerSubjectInfo() {
	// Clear any global subject state.
	fs.psim, fs.tsl = fs.psim.Empty(), 0
	if fs.noTrackSubjects() {
		return
	}
	for _, mb := range fs.blks {
		fs.populateGlobalPerSubjectInfo(mb)
	}
}

// Lock should be held.
func (mb *msgBlock) resetPerSubjectInfo() error {
	mb.fss = nil
	return mb.generatePerSubjectInfo()
}

// generatePerSubjectInfo will generate the per subject info via the raw msg block.
// Lock should be held.
func (mb *msgBlock) generatePerSubjectInfo() error {
	// Check if this mb is empty. This can happen when its the last one and we are holding onto it for seq and timestamp info.
	if mb.msgs == 0 {
		return nil
	}

	if mb.cacheNotLoaded() {
		if err := mb.loadMsgsWithLock(); err != nil {
			return err
		}
		// indexCacheBuf can produce fss now, so if non-nil we are good.
		if mb.fss != nil {
			return nil
		}
		defer mb.finishedWithCache()
	}

	// Create new one regardless.
	mb.fss = mb.fss.Empty()

	var smv StoreMsg
	fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
	for seq := fseq; seq <= lseq; seq++ {
		if mb.dmap.Exists(seq) {
			// Optimisation to avoid calling cacheLookup which hits time.Now().
			// It gets set later on if the fss is non-empty anyway.
			continue
		}
		sm, err := mb.cacheLookupNoCopy(seq, &smv)
		if err != nil {
			// Since we are walking by sequence we can ignore some errors that are benign to rebuilding our state.
			if err == ErrStoreMsgNotFound || err == errDeletedMsg {
				continue
			}
			if err == errNoCache {
				return nil
			}
			return err
		}
		if sm != nil && len(sm.subj) > 0 {
			if ss, ok := mb.fss.Find(stringToBytes(sm.subj)); ok && ss != nil {
				ss.Msgs++
				ss.Last = seq
				ss.lastNeedsUpdate = false
			} else {
				mb.fss.Insert(stringToBytes(sm.subj), SimpleState{Msgs: 1, First: seq, Last: seq})
			}
		}
	}

	if mb.fss.Size() > 0 {
		// Make sure we run the cache expire timer.
		mb.llts = ats.AccessTime()
		// Mark fss activity same as load time.
		mb.lsts = mb.llts
		mb.startCacheExpireTimer()
	}
	return nil
}

// Helper to make sure fss loaded if we are tracking.
// Lock should be held
func (mb *msgBlock) ensurePerSubjectInfoLoaded() error {
	if mb.fss != nil || mb.noTrack {
		if mb.fss != nil {
			// Mark fss activity.
			mb.lsts = ats.AccessTime()
		}
		return nil
	}
	if mb.msgs == 0 {
		mb.fss = stree.NewSubjectTree[SimpleState]()
		return nil
	}
	return mb.generatePerSubjectInfo()
}

// Called on recovery to populate the global psim state.
// Lock should be held.
func (fs *fileStore) populateGlobalPerSubjectInfo(mb *msgBlock) {
	mb.mu.Lock()
	defer mb.mu.Unlock()

	if err := mb.ensurePerSubjectInfoLoaded(); err != nil {
		return
	}

	// Now populate psim.
	mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool {
		if len(bsubj) > 0 {
			if info, ok := fs.psim.Find(bsubj); ok {
				info.total += ss.Msgs
				if mb.index > info.lblk {
					info.lblk = mb.index
				}
			} else {
				fs.psim.Insert(bsubj, psi{total: ss.Msgs, fblk: mb.index, lblk: mb.index})
				fs.tsl += len(bsubj)
			}
		}
		return true
	})
}

// Calls os.RemoveAll on the given `dir` directory, but if an error occurs,
// retries up to one second. If that still fails, returns the last error
// that os.RemoveAll returned.
func removeAllWithRetry(dir string) error {
	<-dios
	err := os.RemoveAll(dir)
	dios <- struct{}{}
	if err == nil {
		return nil
	}
	ttl := time.Now().Add(time.Second)
	for time.Now().Before(ttl) {
		time.Sleep(10 * time.Millisecond)
		<-dios
		err = os.RemoveAll(dir)
		dios <- struct{}{}
		if err == nil {
			return nil
		}
	}
	return err
}

// Close the message block.
func (mb *msgBlock) close(sync bool) {
	if mb == nil {
		return
	}
	mb.mu.Lock()
	defer mb.mu.Unlock()

	if mb.closed {
		return
	}

	// Stop cache expiration timer.
	if mb.ctmr != nil {
		mb.ctmr.Stop()
		mb.ctmr = nil
	}

	// Clear fss.
	mb.fss = nil

	// Flush pending writes.
	mb.flushPendingMsgsLocked()
	// Close cache
	mb.clearCacheAndOffset()
	// Quit our loops.
	if mb.qch != nil {
		close(mb.qch)
		mb.qch = nil
	}
	if mb.mfd != nil {
		if sync {
			mb.mfd.Sync()
		}
		mb.mfd.Close()
	}
	mb.mfd = nil
	// Mark as closed.
	mb.closed = true
}

func (fs *fileStore) closeAllMsgBlocks(sync bool) {
	for _, mb := range fs.blks {
		mb.close(sync)
	}
}

func (fs *fileStore) Delete(inline bool) error {
	if fs.isClosed() {
		// Always attempt to remove since we could have been closed beforehand.
		os.RemoveAll(fs.fcfg.StoreDir)
		// Since we did remove, if we did have anything remaining make sure to
		// call into any storage updates that had been registered.
		fs.mu.Lock()
		cb, msgs, bytes := fs.scb, int64(fs.state.Msgs), int64(fs.state.Bytes)
		// Guard against double accounting if called twice.
		fs.state.Msgs, fs.state.Bytes = 0, 0
		fs.mu.Unlock()
		if msgs > 0 && cb != nil {
			cb(-msgs, -bytes, 0, _EMPTY_)
		}
		return ErrStoreClosed
	}

	pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir)
	// If purge directory still exists then we need to wait
	// in place and remove since rename would fail.
	if _, err := os.Stat(pdir); err == nil {
		os.RemoveAll(pdir)
	}

	// Quickly close all blocks and simulate a purge w/o overhead an new write block.
	fs.mu.Lock()
	for _, mb := range fs.blks {
		mb.dirtyClose()
	}
	dmsgs := fs.state.Msgs
	dbytes := int64(fs.state.Bytes)
	fs.state.Msgs, fs.state.Bytes = 0, 0
	fs.blks = nil
	cb := fs.scb
	fs.mu.Unlock()

	if cb != nil {
		cb(-int64(dmsgs), -dbytes, 0, _EMPTY_)
	}

	if err := fs.stop(true, false); err != nil {
		return err
	}

	// Make sure we will not try to recover if killed before removal below completes.
	if err := os.Remove(filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFile)); err != nil {
		return err
	}
	// Now move into different directory with "." prefix.
	ndir := filepath.Join(filepath.Dir(fs.fcfg.StoreDir), tsep+filepath.Base(fs.fcfg.StoreDir))
	if err := os.Rename(fs.fcfg.StoreDir, ndir); err != nil {
		return err
	}
	// Do this in separate Go routine in case lots of blocks.
	// Purge above protects us as does the removal of meta artifacts above.
	if inline {
		if err := removeAllWithRetry(ndir); err != nil {
			return err
		}
	} else {
		go removeAllWithRetry(ndir)
	}
	return nil
}

// Lock should be held.
func (fs *fileStore) setSyncTimer() {
	if fs.syncTmr != nil {
		fs.syncTmr.Reset(fs.fcfg.SyncInterval)
	} else {
		// First time this fires will be between SyncInterval/2 and SyncInterval,
		// so that different stores are spread out, rather than having many of
		// them trying to all sync at once, causing blips and contending dios.
		start := (fs.fcfg.SyncInterval / 2) + (time.Duration(mrand.Int63n(int64(fs.fcfg.SyncInterval / 2))))
		fs.syncTmr = time.AfterFunc(start, fs.syncBlocks)
	}
}

// Lock should be held.
func (fs *fileStore) cancelSyncTimer() {
	if fs.syncTmr != nil {
		fs.syncTmr.Stop()
		fs.syncTmr = nil
	}
}

// The full state file is versioned.
// - 0x1: original binary index.db format
// - 0x2: adds support for TTL count field after num deleted
const (
	fullStateMagic      = uint8(11)
	fullStateMinVersion = uint8(1) // What is the minimum version we know how to parse?
	fullStateVersion    = uint8(3) // What is the current version written out to index.db?
)

// This go routine periodically writes out our full stream state index.
func (fs *fileStore) flushStreamStateLoop(qch, done chan struct{}) {
	// Signal we are done on exit.
	defer close(done)

	// Make sure we do not try to write these out too fast.
	// Spread these out for large numbers on a server restart.
	const writeThreshold = 2 * time.Minute
	writeJitter := time.Duration(mrand.Int63n(int64(30 * time.Second)))
	t := time.NewTicker(writeThreshold + writeJitter)
	defer t.Stop()

	for {
		select {
		case <-t.C:
			err := fs.writeFullState()
			if isPermissionError(err) && fs.srv != nil {
				fs.warn("File system permission denied when flushing stream state, disabling JetStream: %v", err)
				// messages in block cache could be lost in the worst case.
				// In the clustered mode it is very highly unlikely as a result of replication.
				fs.srv.DisableJetStream()
				return
			}

		case <-qch:
			return
		}
	}
}

// Helper since unixnano of zero time undefined.
func timestampNormalized(t time.Time) int64 {
	if t.IsZero() {
		return 0
	}
	return t.UnixNano()
}

// writeFullState will proceed to write the full meta state iff not complex and time consuming.
// Since this is for quick recovery it is optional and should not block/stall normal operations.
func (fs *fileStore) writeFullState() error {
	return fs._writeFullState(false, true)
}

// forceWriteFullState will proceed to write the full meta state.
func (fs *fileStore) forceWriteFullState() error {
	return fs._writeFullState(true, true)
}

// forceWriteFullStateLocked will proceed to write the full meta state. This should only be called by stop()
func (fs *fileStore) forceWriteFullStateLocked() error {
	return fs._writeFullState(true, false)
}

// This will write the full binary state for the stream.
// This plus everything new since last hash will be the total recovered state.
// This state dump will have the following.
// 1. Stream summary - Msgs, Bytes, First and Last (Sequence and Timestamp)
// 2. PSIM - Per Subject Index Map - Tracks first and last blocks with subjects present.
// 3. MBs - Index, Bytes, First and Last Sequence and Timestamps, and the deleted map (avl.seqset).
// 4. Last block index and hash of record inclusive to this stream state.
func (fs *fileStore) _writeFullState(force, needLock bool) error {
	if fs.isClosed() {
		return nil
	}

	fsLock := func() {
		if needLock {
			fs.mu.Lock()
		}
	}
	fsUnlock := func() {
		if needLock {
			fs.mu.Unlock()
		}
	}

	start := time.Now()
	fsLock()
	if fs.dirty == 0 {
		fsUnlock()
		return nil
	}

	// For calculating size and checking time costs for non forced calls.
	numSubjects := fs.numSubjects()

	// If we are not being forced to write out our state, check the complexity for time costs as to not
	// block or stall normal operations.
	// We will base off of number of subjects and interior deletes. A very large number of msg blocks could also
	// be used, but for next server version will redo all meta handling to be disk based. So this is temporary.
	if !force {
		const numThreshold = 1_000_000
		// Calculate interior deletes.
		var numDeleted int
		if fs.state.LastSeq > fs.state.FirstSeq {
			numDeleted = int((fs.state.LastSeq - fs.state.FirstSeq + 1) - fs.state.Msgs)
		}
		if numSubjects > numThreshold || numDeleted > numThreshold {
			fsUnlock()
			return errStateTooBig
		}
	}

	// We track this through subsequent runs to get an avg per blk used for subsequent runs.
	avgDmapLen := fs.adml
	// If first time through could be 0
	if avgDmapLen == 0 && ((fs.state.LastSeq-fs.state.FirstSeq+1)-fs.state.Msgs) > 0 {
		avgDmapLen = 1024
	}

	// Calculate and estimate of the uper bound on the  size to avoid multiple allocations.
	sz := hdrLen + // Magic and Version
		(binary.MaxVarintLen64 * 6) + // FS data
		binary.MaxVarintLen64 + fs.tsl + // NumSubjects + total subject length
		numSubjects*(binary.MaxVarintLen64*4) + // psi record
		binary.MaxVarintLen64 + // Num blocks.
		len(fs.blks)*((binary.MaxVarintLen64*8)+avgDmapLen) + // msg blocks, avgDmapLen is est for dmaps
		binary.MaxVarintLen64 + 8 + 8 // last index + record checksum + full state checksum

	// Do 4k on stack if possible.
	const ssz = 4 * 1024
	var buf []byte

	if sz <= ssz {
		var _buf [ssz]byte
		buf, sz = _buf[0:hdrLen:ssz], ssz
	} else {
		buf = make([]byte, hdrLen, sz)
	}

	buf[0], buf[1] = fullStateMagic, fullStateVersion
	buf = binary.AppendUvarint(buf, fs.state.Msgs)
	buf = binary.AppendUvarint(buf, fs.state.Bytes)
	buf = binary.AppendUvarint(buf, fs.state.FirstSeq)
	buf = binary.AppendVarint(buf, timestampNormalized(fs.state.FirstTime))
	buf = binary.AppendUvarint(buf, fs.state.LastSeq)
	buf = binary.AppendVarint(buf, timestampNormalized(fs.state.LastTime))

	// Do per subject information map if applicable.
	buf = binary.AppendUvarint(buf, uint64(numSubjects))
	if numSubjects > 0 {
		fs.psim.Match([]byte(fwcs), func(subj []byte, psi *psi) {
			buf = binary.AppendUvarint(buf, uint64(len(subj)))
			buf = append(buf, subj...)
			buf = binary.AppendUvarint(buf, psi.total)
			buf = binary.AppendUvarint(buf, uint64(psi.fblk))
			if psi.total > 1 {
				buf = binary.AppendUvarint(buf, uint64(psi.lblk))
			}
		})
	}

	// Now walk all blocks and write out first and last and optional dmap encoding.
	var lbi uint32
	var lchk [8]byte

	nb := len(fs.blks)
	buf = binary.AppendUvarint(buf, uint64(nb))

	// Use basetime to save some space.
	baseTime := timestampNormalized(fs.state.FirstTime)
	var scratch [8 * 1024]byte

	// Track the state as represented by the mbs.
	var mstate StreamState

	var dmapTotalLen int
	for _, mb := range fs.blks {
		mb.mu.RLock()
		buf = binary.AppendUvarint(buf, uint64(mb.index))
		buf = binary.AppendUvarint(buf, mb.bytes)
		buf = binary.AppendUvarint(buf, atomic.LoadUint64(&mb.first.seq))
		buf = binary.AppendVarint(buf, mb.first.ts-baseTime)
		buf = binary.AppendUvarint(buf, atomic.LoadUint64(&mb.last.seq))
		buf = binary.AppendVarint(buf, mb.last.ts-baseTime)

		numDeleted := mb.dmap.Size()
		buf = binary.AppendUvarint(buf, uint64(numDeleted))
		buf = binary.AppendUvarint(buf, mb.ttls)      // Field is new in version 2
		buf = binary.AppendUvarint(buf, mb.schedules) // Field is new in version 3
		if numDeleted > 0 {
			dmap, _ := mb.dmap.Encode(scratch[:0])
			dmapTotalLen += len(dmap)
			buf = append(buf, dmap...)
		}
		// If this is the last one grab the last checksum and the block index, e.g. 22.blk, 22 is the block index.
		// We use this to quickly open this file on recovery.
		if mb == fs.lmb {
			lbi = mb.index
			mb.ensureLastChecksumLoaded()
			copy(lchk[0:], mb.lchk[:])
		}
		updateTrackingState(&mstate, mb)
		mb.mu.RUnlock()
	}
	if dmapTotalLen > 0 {
		fs.adml = dmapTotalLen / len(fs.blks)
	}

	// Place block index and hash onto the end.
	buf = binary.AppendUvarint(buf, uint64(lbi))
	buf = append(buf, lchk[:]...)

	// Encrypt if needed.
	if fs.prf != nil {
		if err := fs.setupAEK(); err != nil {
			fsUnlock()
			return err
		}
		nonce := make([]byte, fs.aek.NonceSize(), fs.aek.NonceSize()+len(buf)+fs.aek.Overhead())
		if n, err := rand.Read(nonce); err != nil {
			fsUnlock()
			return err
		} else if n != len(nonce) {
			fsUnlock()
			return fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
		}
		buf = fs.aek.Seal(nonce, nonce, buf, nil)
	}

	fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)

	fs.hh.Reset()
	fs.hh.Write(buf)
	buf = fs.hh.Sum(buf)

	// Snapshot prior dirty count.
	priorDirty := fs.dirty

	statesEqual := trackingStatesEqual(&fs.state, &mstate)
	// Release lock.
	fsUnlock()

	// Check consistency here.
	if !statesEqual {
		fs.warn("Stream state encountered internal inconsistency on write")
		// Rebuild our fs state from the mb state.
		if needLock {
			fs.rebuildState(nil)
		} else {
			fs.rebuildStateLocked(nil)
		}
		return errCorruptState
	}

	if cap(buf) > sz {
		fs.debug("WriteFullState reallocated from %d to %d", sz, cap(buf))
	}

	// Only warn about construction time since file write not holding any locks.
	if took := time.Since(start); took > time.Minute {
		fs.warn("WriteFullState took %v (%d bytes)", took.Round(time.Millisecond), len(buf))
	}

	// Write our update index.db
	// Protect with dios.
	<-dios
	err := os.WriteFile(fn, buf, defaultFilePerms)
	// if file system is not writable isPermissionError is set to true
	dios <- struct{}{}
	if isPermissionError(err) {
		return err
	}

	// Update dirty if successful.
	if err == nil {
		fsLock()
		fs.dirty -= priorDirty
		fsUnlock()
	}

	// Attempt to write both files, an error in one should not prevent the other from being written.
	ttlErr := fs.writeTTLState(needLock)
	schedErr := fs.writeMsgSchedulingState(needLock)
	if ttlErr != nil {
		return ttlErr
	} else if schedErr != nil {
		return schedErr
	}
	return nil
}

func (fs *fileStore) writeTTLState(needLock bool) error {
	if needLock {
		fs.mu.RLock()
	}
	if fs.ttls == nil {
		if needLock {
			fs.mu.RUnlock()
		}
		return nil
	}
	fn := filepath.Join(fs.fcfg.StoreDir, msgDir, ttlStreamStateFile)
	// Must be lseq+1 to identify up to which sequence the TTLs are valid.
	buf := fs.ttls.Encode(fs.state.LastSeq + 1)
	if needLock {
		fs.mu.RUnlock()
	}

	return fs.writeFileWithOptionalSync(fn, buf, defaultFilePerms)
}

func (fs *fileStore) writeMsgSchedulingState(needLock bool) error {
	if needLock {
		fs.mu.RLock()
	}
	if fs.scheduling == nil {
		if needLock {
			fs.mu.RUnlock()
		}
		return nil
	}
	fn := filepath.Join(fs.fcfg.StoreDir, msgDir, msgSchedulingStreamStateFile)
	// Must be lseq+1 to identify up to which sequence the schedules are valid.
	buf := fs.scheduling.encode(fs.state.LastSeq + 1)
	if needLock {
		fs.mu.RUnlock()
	}

	return fs.writeFileWithOptionalSync(fn, buf, defaultFilePerms)
}

// Stop the current filestore.
func (fs *fileStore) Stop() error {
	return fs.stop(false, true)
}

// Stop the current filestore.
func (fs *fileStore) stop(delete, writeState bool) error {
	if fs.isClosed() {
		return ErrStoreClosed
	}

	fs.mu.Lock()
	if fs.closing {
		fs.mu.Unlock()
		return ErrStoreClosed
	}

	// Mark as closing. Do before releasing the lock to wait on the state flush loop
	// so we don't end up with this function running more than once.
	fs.closing = true

	// Release the state flusher loop.
	if fs.qch != nil {
		close(fs.qch)
		fs.qch = nil
	}

	if writeState {
		// Wait for the state flush loop to exit.
		fsld := fs.fsld
		fs.mu.Unlock()
		<-fsld
		fs.mu.Lock()

		fs.checkAndFlushLastBlock()
	}
	fs.closeAllMsgBlocks(false)

	fs.cancelSyncTimer()
	fs.cancelAgeChk()

	if writeState {
		// Write full state if needed. If not dirty this is a no-op.
		fs.forceWriteFullStateLocked()
	}

	// Mark as closed. Last message block needs to be cleared after
	// writeFullState has completed.
	fs.closed.Store(true)
	fs.lmb = nil

	// We should update the upper usage layer on a stop.
	cb, bytes := fs.scb, int64(fs.state.Bytes)
	fs.mu.Unlock()

	fs.cmu.Lock()
	var _cfs [256]ConsumerStore
	cfs := append(_cfs[:0], fs.cfs...)
	fs.cfs = nil
	fs.cmu.Unlock()

	for _, o := range cfs {
		if delete {
			o.StreamDelete()
		} else {
			o.Stop()
		}
	}

	if bytes > 0 && cb != nil {
		cb(0, -bytes, 0, _EMPTY_)
	}

	// Unregister from the access time service.
	ats.Unregister()

	return nil
}

const errFile = "errors.txt"

// Stream our snapshot through S2 compression and tar.
func (fs *fileStore) streamSnapshot(w io.WriteCloser, includeConsumers bool, errCh chan string) {
	defer close(errCh)
	defer w.Close()

	enc := s2.NewWriter(w)
	defer enc.Close()

	tw := tar.NewWriter(enc)
	defer tw.Close()

	defer func() {
		fs.mu.Lock()
		fs.sips--
		fs.mu.Unlock()
	}()

	modTime := time.Now().UTC()

	writeFile := func(name string, buf []byte) error {
		hdr := &tar.Header{
			Name:    name,
			Mode:    0600,
			ModTime: modTime,
			Uname:   "nats",
			Gname:   "nats",
			Size:    int64(len(buf)),
			Format:  tar.FormatPAX,
		}
		if err := tw.WriteHeader(hdr); err != nil {
			return err
		}
		if _, err := tw.Write(buf); err != nil {
			return err
		}
		return nil
	}

	writeErr := func(err string) {
		writeFile(errFile, []byte(err))
		errCh <- err
	}

	fs.mu.Lock()
	blks := fs.blks
	// Grab our general meta data.
	// We do this now instead of pulling from files since they could be encrypted.
	meta, err := json.Marshal(fs.cfg)
	if err != nil {
		fs.mu.Unlock()
		writeErr(fmt.Sprintf("Could not gather stream meta file: %v", err))
		return
	}
	hh := fs.hh
	hh.Reset()
	hh.Write(meta)
	var hb [highwayhash.Size64]byte
	sum := []byte(hex.EncodeToString(fs.hh.Sum(hb[:0])))
	fs.mu.Unlock()

	// Meta first.
	if writeFile(JetStreamMetaFile, meta) != nil {
		return
	}
	if writeFile(JetStreamMetaFileSum, sum) != nil {
		return
	}

	// Can't use join path here, tar only recognizes relative paths with forward slashes.
	msgPre := msgDir + "/"
	var bbuf []byte

	// Now do messages themselves.
	for _, mb := range blks {
		if mb.pendingWriteSize() > 0 {
			mb.flushPendingMsgs()
		}
		mb.mu.Lock()
		// We could stream but don't want to hold the lock and prevent changes, so just read in and
		// release the lock for now.
		bbuf, err = mb.loadBlock(bbuf)
		if err != nil {
			mb.mu.Unlock()
			writeErr(fmt.Sprintf("Could not read message block [%d]: %v", mb.index, err))
			return
		}
		// Check for encryption.
		if mb.bek != nil && len(bbuf) > 0 {
			rbek, err := genBlockEncryptionKey(fs.fcfg.Cipher, mb.seed, mb.nonce)
			if err != nil {
				mb.mu.Unlock()
				writeErr(fmt.Sprintf("Could not create encryption key for message block [%d]: %v", mb.index, err))
				return
			}
			rbek.XORKeyStream(bbuf, bbuf)
		}
		// Check for compression.
		if bbuf, err = mb.decompressIfNeeded(bbuf); err != nil {
			mb.mu.Unlock()
			writeErr(fmt.Sprintf("Could not decompress message block [%d]: %v", mb.index, err))
			return
		}
		mb.mu.Unlock()

		// Do this one unlocked.
		if writeFile(msgPre+fmt.Sprintf(blkScan, mb.index), bbuf) != nil {
			return
		}
	}

	// Do index.db last. We will force a write as well.
	// Write out full state as well before proceeding.
	if err := fs.forceWriteFullState(); err == nil {
		const minLen = 32
		sfn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)
		if buf, err := os.ReadFile(sfn); err == nil && len(buf) >= minLen {
			fs.mu.Lock()
			if fs.aek != nil {
				ns := fs.aek.NonceSize()
				buf, err = fs.aek.Open(nil, buf[:ns], buf[ns:len(buf)-highwayhash.Size64], nil)
				if err == nil {
					// Redo hash checksum at end on plaintext.
					hh.Reset()
					hh.Write(buf)
					buf = fs.hh.Sum(buf)
				}
			}
			fs.mu.Unlock()
			if err == nil && writeFile(msgPre+streamStreamStateFile, buf) != nil {
				return
			}
		}
	}

	// Bail if no consumers requested.
	if !includeConsumers {
		return
	}

	// Do consumers' state last.
	fs.cmu.RLock()
	cfs := fs.cfs
	fs.cmu.RUnlock()

	for _, cs := range cfs {
		o, ok := cs.(*consumerFileStore)
		if !ok {
			continue
		}
		o.mu.Lock()
		// Grab our general meta data.
		// We do this now instead of pulling from files since they could be encrypted.
		meta, err := json.Marshal(o.cfg)
		if err != nil {
			o.mu.Unlock()
			writeErr(fmt.Sprintf("Could not gather consumer meta file for %q: %v", o.name, err))
			return
		}
		o.hh.Reset()
		o.hh.Write(meta)
		var hb [highwayhash.Size64]byte
		sum := []byte(hex.EncodeToString(o.hh.Sum(hb[:0])))

		// We can have the running state directly encoded now.
		state, err := o.encodeState()
		if err != nil {
			o.mu.Unlock()
			writeErr(fmt.Sprintf("Could not encode consumer state for %q: %v", o.name, err))
			return
		}
		odirPre := filepath.Join(consumerDir, o.name)
		o.mu.Unlock()

		// Write all the consumer files.
		if writeFile(filepath.Join(odirPre, JetStreamMetaFile), meta) != nil {
			return
		}
		if writeFile(filepath.Join(odirPre, JetStreamMetaFileSum), sum) != nil {
			return
		}
		writeFile(filepath.Join(odirPre, consumerState), state)
	}
}

// Create a snapshot of this stream and its consumer's state along with messages.
func (fs *fileStore) Snapshot(deadline time.Duration, checkMsgs, includeConsumers bool) (*SnapshotResult, error) {
	if fs.isClosed() {
		return nil, ErrStoreClosed
	}

	fs.mu.Lock()

	// Only allow one at a time.
	if fs.sips > 0 {
		fs.mu.Unlock()
		return nil, ErrStoreSnapshotInProgress
	}
	// Mark us as snapshotting
	fs.sips += 1
	fs.mu.Unlock()

	if checkMsgs {
		ld := fs.checkMsgs()
		if ld != nil && len(ld.Msgs) > 0 {
			return nil, fmt.Errorf("snapshot check detected %d bad messages", len(ld.Msgs))
		}
	}

	pr, pw := net.Pipe()

	// Set a write deadline here to protect ourselves.
	if deadline > 0 {
		pw.SetWriteDeadline(time.Now().Add(deadline))
	}

	// We can add to our stream while snapshotting but not "user" delete anything.
	var state StreamState
	fs.FastState(&state)

	// Stream in separate Go routine.
	errCh := make(chan string, 1)
	go fs.streamSnapshot(pw, includeConsumers, errCh)

	return &SnapshotResult{pr, state, errCh}, nil
}

// Helper to return the config.
func (fs *fileStore) fileStoreConfig() FileStoreConfig {
	fs.mu.RLock()
	defer fs.mu.RUnlock()
	return fs.fcfg
}

// Read lock all existing message blocks.
// Lock held on entry.
func (fs *fileStore) readLockAllMsgBlocks() {
	for _, mb := range fs.blks {
		mb.mu.RLock()
	}
}

// Read unlock all existing message blocks.
// Lock held on entry.
func (fs *fileStore) readUnlockAllMsgBlocks() {
	for _, mb := range fs.blks {
		mb.mu.RUnlock()
	}
}

// Binary encoded state snapshot, >= v2.10 server.
func (fs *fileStore) EncodedStreamState(failed uint64) ([]byte, error) {
	fs.mu.RLock()
	defer fs.mu.RUnlock()

	// Calculate deleted.
	var numDeleted int64
	if fs.state.LastSeq > fs.state.FirstSeq {
		numDeleted = int64(fs.state.LastSeq-fs.state.FirstSeq+1) - int64(fs.state.Msgs)
		if numDeleted < 0 {
			numDeleted = 0
		}
	}

	// Encoded is Msgs, Bytes, FirstSeq, LastSeq, Failed, NumDeleted and optional DeletedBlocks
	var buf [1024]byte
	buf[0], buf[1] = streamStateMagic, streamStateVersion
	n := hdrLen
	n += binary.PutUvarint(buf[n:], fs.state.Msgs)
	n += binary.PutUvarint(buf[n:], fs.state.Bytes)
	n += binary.PutUvarint(buf[n:], fs.state.FirstSeq)
	n += binary.PutUvarint(buf[n:], fs.state.LastSeq)
	n += binary.PutUvarint(buf[n:], failed)
	n += binary.PutUvarint(buf[n:], uint64(numDeleted))

	b := buf[0:n]

	if numDeleted > 0 {
		var scratch [4 * 1024]byte

		fs.readLockAllMsgBlocks()
		defer fs.readUnlockAllMsgBlocks()

		for _, db := range fs.deleteBlocks() {
			switch db := db.(type) {
			case *DeleteRange:
				first, _, num := db.State()
				scratch[0] = runLengthMagic
				i := 1
				i += binary.PutUvarint(scratch[i:], first)
				i += binary.PutUvarint(scratch[i:], num)
				b = append(b, scratch[0:i]...)
			case *avl.SequenceSet:
				buf, err := db.Encode(scratch[:0])
				if err != nil {
					return nil, err
				}
				b = append(b, buf...)
			default:
				return nil, errors.New("no impl")
			}
		}
	}

	return b, nil
}

// We used to be more sophisticated to save memory, but speed is more important.
// All blocks should be at least read locked.
func (fs *fileStore) deleteBlocks() DeleteBlocks {
	var dbs DeleteBlocks
	var prevLast uint64

	for _, mb := range fs.blks {
		// Detect if we have a gap between these blocks.
		fseq := atomic.LoadUint64(&mb.first.seq)
		if prevLast > 0 && prevLast+1 != fseq {
			dbs = append(dbs, &DeleteRange{First: prevLast + 1, Num: fseq - prevLast - 1})
		}
		if mb.dmap.Size() > 0 {
			dbs = append(dbs, &mb.dmap)
		}
		prevLast = atomic.LoadUint64(&mb.last.seq)
	}
	return dbs
}

// SyncDeleted will make sure this stream has same deleted state as dbs.
// This will only process deleted state within our current state.
func (fs *fileStore) SyncDeleted(dbs DeleteBlocks) {
	if len(dbs) == 0 {
		return
	}

	fs.mu.Lock()
	defer fs.mu.Unlock()

	lseq := fs.state.LastSeq
	var needsCheck DeleteBlocks

	fs.readLockAllMsgBlocks()
	mdbs := fs.deleteBlocks()
	for i, db := range dbs {
		first, last, num := db.State()
		// If the block is same as what we have we can skip.
		if i < len(mdbs) {
			eFirst, eLast, eNum := mdbs[i].State()
			if first == eFirst && last == eLast && num == eNum {
				continue
			}
		} else if first > lseq {
			// Skip blocks not applicable to our current state.
			continue
		}
		// Need to insert these.
		needsCheck = append(needsCheck, db)
	}
	fs.readUnlockAllMsgBlocks()

	for _, db := range needsCheck {
		db.Range(func(dseq uint64) bool {
			fs.removeMsg(dseq, false, true, false)
			return true
		})
	}
}

////////////////////////////////////////////////////////////////////////////////
// Consumers
////////////////////////////////////////////////////////////////////////////////

type consumerFileStore struct {
	mu      sync.Mutex
	fs      *fileStore
	cfg     *FileConsumerInfo
	prf     keyGen
	aek     cipher.AEAD
	name    string
	odir    string
	ifn     string
	hh      *highwayhash.Digest64
	state   ConsumerState
	fch     chan struct{}
	qch     chan struct{}
	flusher bool
	writing bool
	dirty   bool
	closed  bool
}

func (fs *fileStore) ConsumerStore(name string, created time.Time, cfg *ConsumerConfig) (ConsumerStore, error) {
	if fs == nil {
		return nil, fmt.Errorf("filestore is nil")
	}
	if fs.isClosed() {
		return nil, ErrStoreClosed
	}
	if cfg == nil || name == _EMPTY_ {
		return nil, fmt.Errorf("bad consumer config")
	}

	// We now allow overrides from a stream being a filestore type and forcing a consumer to be memory store.
	if cfg.MemoryStorage {
		// Create directly here.
		o := &consumerMemStore{ms: fs, cfg: *cfg}
		fs.AddConsumer(o)
		return o, nil
	}

	odir := filepath.Join(fs.fcfg.StoreDir, consumerDir, name)
	if err := os.MkdirAll(odir, defaultDirPerms); err != nil {
		return nil, fmt.Errorf("could not create consumer directory - %v", err)
	}
	csi := &FileConsumerInfo{Name: name, Created: created, ConsumerConfig: *cfg}
	o := &consumerFileStore{
		fs:   fs,
		cfg:  csi,
		prf:  fs.prf,
		name: name,
		odir: odir,
		ifn:  filepath.Join(odir, consumerState),
	}
	key := sha256.Sum256([]byte(fs.cfg.Name + "/" + name))
	hh, err := highwayhash.NewDigest64(key[:])
	if err != nil {
		return nil, fmt.Errorf("could not create hash: %v", err)
	}
	o.hh = hh

	// Check for encryption.
	if o.prf != nil {
		if ekey, err := os.ReadFile(filepath.Join(odir, JetStreamMetaFileKey)); err == nil {
			if len(ekey) < minBlkKeySize {
				return nil, errBadKeySize
			}
			// Recover key encryption key.
			rb, err := fs.prf([]byte(fs.cfg.Name + tsep + o.name))
			if err != nil {
				return nil, err
			}

			sc := fs.fcfg.Cipher
			kek, err := genEncryptionKey(sc, rb)
			if err != nil {
				return nil, err
			}
			ns := kek.NonceSize()
			nonce := ekey[:ns]
			seed, err := kek.Open(nil, nonce, ekey[ns:], nil)
			if err != nil {
				// We may be here on a cipher conversion, so attempt to convert.
				if err = o.convertCipher(); err != nil {
					return nil, err
				}
			} else {
				o.aek, err = genEncryptionKey(sc, seed)
			}
			if err != nil {
				return nil, err
			}
		}
	}

	// Track if we are creating the directory so that we can clean up if we encounter an error.
	var didCreate bool

	// Write our meta data iff does not exist.
	meta := filepath.Join(odir, JetStreamMetaFile)
	if _, err := os.Stat(meta); err != nil && os.IsNotExist(err) {
		didCreate = true
		if err := o.writeConsumerMeta(); err != nil {
			os.RemoveAll(odir)
			return nil, err
		}
	}

	// If we expect to be encrypted check that what we are restoring is not plaintext.
	// This can happen on snapshot restores or conversions.
	if o.prf != nil {
		keyFile := filepath.Join(odir, JetStreamMetaFileKey)
		if _, err := os.Stat(keyFile); err != nil && os.IsNotExist(err) {
			if err := o.writeConsumerMeta(); err != nil {
				if didCreate {
					os.RemoveAll(odir)
				}
				return nil, err
			}
			// Redo the state file as well here if we have one and we can tell it was plaintext.
			if buf, err := os.ReadFile(o.ifn); err == nil {
				if _, err := decodeConsumerState(buf); err == nil {
					state, err := o.encryptState(buf)
					if err != nil {
						return nil, err
					}
					err = fs.writeFileWithOptionalSync(o.ifn, state, defaultFilePerms)
					if err != nil {
						if didCreate {
							os.RemoveAll(odir)
						}
						return nil, err
					}
				}
			}
		}
	}

	// Create channels to control our flush go routine.
	o.fch = make(chan struct{}, 1)
	o.qch = make(chan struct{})
	go o.flushLoop(o.fch, o.qch)

	// Make sure to load in our state from disk if needed.
	if err = o.loadState(); err != nil {
		return nil, err
	}

	// Assign to filestore.
	fs.AddConsumer(o)

	return o, nil
}

func (o *consumerFileStore) convertCipher() error {
	fs := o.fs
	odir := filepath.Join(fs.fcfg.StoreDir, consumerDir, o.name)

	ekey, err := os.ReadFile(filepath.Join(odir, JetStreamMetaFileKey))
	if err != nil {
		return err
	}
	if len(ekey) < minBlkKeySize {
		return errBadKeySize
	}
	// Recover key encryption key.
	rb, err := fs.prf([]byte(fs.cfg.Name + tsep + o.name))
	if err != nil {
		return err
	}

	// Do these in reverse since converting.
	sc := fs.fcfg.Cipher
	osc := AES
	if sc == AES {
		osc = ChaCha
	}
	kek, err := genEncryptionKey(osc, rb)
	if err != nil {
		return err
	}
	ns := kek.NonceSize()
	nonce := ekey[:ns]
	seed, err := kek.Open(nil, nonce, ekey[ns:], nil)
	if err != nil {
		return err
	}
	aek, err := genEncryptionKey(osc, seed)
	if err != nil {
		return err
	}
	// Now read in and decode our state using the old cipher.
	buf, err := os.ReadFile(o.ifn)
	if err != nil {
		return err
	}
	buf, err = aek.Open(nil, buf[:ns], buf[ns:], nil)
	if err != nil {
		return err
	}

	// Since we are here we recovered our old state.
	// Now write our meta, which will generate the new keys with the new cipher.
	if err := o.writeConsumerMeta(); err != nil {
		return err
	}

	// Now write out or state with the new cipher.
	return o.writeState(buf)
}

// Kick flusher for this consumer.
// Lock should be held.
func (o *consumerFileStore) kickFlusher() {
	if o.fch != nil {
		select {
		case o.fch <- struct{}{}:
		default:
		}
	}
	o.dirty = true
}

// Set in flusher status
func (o *consumerFileStore) setInFlusher() {
	o.mu.Lock()
	o.flusher = true
	o.mu.Unlock()
}

// Clear in flusher status
func (o *consumerFileStore) clearInFlusher() {
	o.mu.Lock()
	o.flusher = false
	o.mu.Unlock()
}

// Report in flusher status
func (o *consumerFileStore) inFlusher() bool {
	o.mu.Lock()
	defer o.mu.Unlock()
	return o.flusher
}

// flushLoop watches for consumer updates and the quit channel.
func (o *consumerFileStore) flushLoop(fch, qch chan struct{}) {

	o.setInFlusher()
	defer o.clearInFlusher()

	// Maintain approximately 10 updates per second per consumer under load.
	const minTime = 100 * time.Millisecond
	var lastWrite time.Time
	var dt *time.Timer

	setDelayTimer := func(addWait time.Duration) {
		if dt == nil {
			dt = time.NewTimer(addWait)
			return
		}
		if !dt.Stop() {
			select {
			case <-dt.C:
			default:
			}
		}
		dt.Reset(addWait)
	}

	for {
		select {
		case <-fch:
			if ts := time.Since(lastWrite); ts < minTime {
				setDelayTimer(minTime - ts)
				select {
				case <-dt.C:
				case <-qch:
					return
				}
			}
			o.mu.Lock()
			if o.closed {
				o.mu.Unlock()
				return
			}
			buf, err := o.encodeState()
			o.mu.Unlock()
			if err != nil {
				return
			}
			// TODO(dlc) - if we error should start failing upwards.
			if err := o.writeState(buf); err == nil {
				lastWrite = time.Now()
			}
		case <-qch:
			return
		}
	}
}

// SetStarting sets our starting stream sequence.
func (o *consumerFileStore) SetStarting(sseq uint64) error {
	o.mu.Lock()
	o.state.Delivered.Stream = sseq
	buf, err := o.encodeState()
	o.mu.Unlock()
	if err != nil {
		return err
	}
	return o.writeState(buf)
}

// UpdateStarting updates our starting stream sequence.
func (o *consumerFileStore) UpdateStarting(sseq uint64) {
	o.mu.Lock()
	defer o.mu.Unlock()

	if sseq > o.state.Delivered.Stream {
		o.state.Delivered.Stream = sseq
		// For AckNone just update delivered and ackfloor at the same time.
		if o.cfg.AckPolicy == AckNone {
			o.state.AckFloor.Stream = sseq
		}
	}
	// Make sure we flush to disk.
	o.kickFlusher()
}

// HasState returns if this store has a recorded state.
func (o *consumerFileStore) HasState() bool {
	o.mu.Lock()
	// We have a running state, or stored on disk but not yet initialized.
	if o.state.Delivered.Consumer != 0 || o.state.Delivered.Stream != 0 {
		o.mu.Unlock()
		return true
	}
	_, err := os.Stat(o.ifn)
	o.mu.Unlock()
	return err == nil
}

// UpdateDelivered is called whenever a new message has been delivered.
func (o *consumerFileStore) UpdateDelivered(dseq, sseq, dc uint64, ts int64) error {
	o.mu.Lock()
	defer o.mu.Unlock()

	if dc != 1 && o.cfg.AckPolicy == AckNone {
		return ErrNoAckPolicy
	}

	// On restarts the old leader may get a replay from the raft logs that are old.
	if dseq <= o.state.AckFloor.Consumer {
		return nil
	}

	// See if we expect an ack for this.
	if o.cfg.AckPolicy != AckNone {
		// Need to create pending records here.
		if o.state.Pending == nil {
			o.state.Pending = make(map[uint64]*Pending)
		}
		var p *Pending
		// Check for an update to a message already delivered.
		if sseq <= o.state.Delivered.Stream {
			if p = o.state.Pending[sseq]; p != nil {
				// Do not update p.Sequence, that should be the original delivery sequence.
				p.Timestamp = ts
			}
		} else {
			// Add to pending.
			o.state.Pending[sseq] = &Pending{dseq, ts}
		}
		// Update delivered as needed.
		if dseq > o.state.Delivered.Consumer {
			o.state.Delivered.Consumer = dseq
		}
		if sseq > o.state.Delivered.Stream {
			o.state.Delivered.Stream = sseq
		}

		if dc > 1 {
			if maxdc := uint64(o.cfg.MaxDeliver); maxdc > 0 && dc > maxdc {
				// Make sure to remove from pending.
				delete(o.state.Pending, sseq)
			}
			if o.state.Redelivered == nil {
				o.state.Redelivered = make(map[uint64]uint64)
			}
			// Only update if greater than what we already have.
			if o.state.Redelivered[sseq] < dc-1 {
				o.state.Redelivered[sseq] = dc - 1
			}
		}
	} else {
		// For AckNone just update delivered and ackfloor at the same time.
		if dseq > o.state.Delivered.Consumer {
			o.state.Delivered.Consumer = dseq
			o.state.AckFloor.Consumer = dseq
		}
		if sseq > o.state.Delivered.Stream {
			o.state.Delivered.Stream = sseq
			o.state.AckFloor.Stream = sseq
		}
	}
	// Make sure we flush to disk.
	o.kickFlusher()

	return nil
}

// UpdateAcks is called whenever a consumer with explicit ack or ack all acks a message.
func (o *consumerFileStore) UpdateAcks(dseq, sseq uint64) error {
	o.mu.Lock()
	defer o.mu.Unlock()

	if o.cfg.AckPolicy == AckNone {
		return ErrNoAckPolicy
	}

	// On restarts the old leader may get a replay from the raft logs that are old.
	if dseq <= o.state.AckFloor.Consumer {
		return nil
	}

	if len(o.state.Pending) == 0 || o.state.Pending[sseq] == nil {
		delete(o.state.Redelivered, sseq)
		return ErrStoreMsgNotFound
	}

	// Check for AckAll here.
	if o.cfg.AckPolicy == AckAll {
		sgap := sseq - o.state.AckFloor.Stream
		o.state.AckFloor.Consumer = dseq
		o.state.AckFloor.Stream = sseq
		if sgap > uint64(len(o.state.Pending)) {
			for seq := range o.state.Pending {
				if seq <= sseq {
					delete(o.state.Pending, seq)
					delete(o.state.Redelivered, seq)
				}
			}
		} else {
			for seq := sseq; seq > sseq-sgap && len(o.state.Pending) > 0; seq-- {
				delete(o.state.Pending, seq)
				delete(o.state.Redelivered, seq)
			}
		}
		o.kickFlusher()
		return nil
	}

	// AckExplicit

	// First delete from our pending state.
	if p, ok := o.state.Pending[sseq]; ok {
		delete(o.state.Pending, sseq)
		if dseq > p.Sequence && p.Sequence > 0 {
			dseq = p.Sequence // Use the original.
		}
	}
	if len(o.state.Pending) == 0 {
		o.state.AckFloor.Consumer = o.state.Delivered.Consumer
		o.state.AckFloor.Stream = o.state.Delivered.Stream
	} else if dseq == o.state.AckFloor.Consumer+1 {
		o.state.AckFloor.Consumer = dseq
		o.state.AckFloor.Stream = sseq

		if o.state.Delivered.Consumer > dseq {
			for ss := sseq + 1; ss <= o.state.Delivered.Stream; ss++ {
				if p, ok := o.state.Pending[ss]; ok {
					if p.Sequence > 0 {
						o.state.AckFloor.Consumer = p.Sequence - 1
						o.state.AckFloor.Stream = ss - 1
					}
					break
				}
			}
		}
	}
	// We do these regardless.
	delete(o.state.Redelivered, sseq)

	o.kickFlusher()
	return nil
}

const seqsHdrSize = 6*binary.MaxVarintLen64 + hdrLen

// Encode our consumer state, version 2.
// Lock should be held.

func (o *consumerFileStore) EncodedState() ([]byte, error) {
	o.mu.Lock()
	defer o.mu.Unlock()
	return o.encodeState()
}

func (o *consumerFileStore) encodeState() ([]byte, error) {
	// Grab reference to state, but make sure we load in if needed, so do not reference o.state directly.
	state, err := o.stateWithCopyLocked(false)
	if err != nil {
		return nil, err
	}
	return encodeConsumerState(state), nil
}

func (o *consumerFileStore) UpdateConfig(cfg *ConsumerConfig) error {
	o.mu.Lock()
	defer o.mu.Unlock()

	// This is mostly unchecked here. We are assuming the upper layers have done sanity checking.
	csi := o.cfg
	csi.ConsumerConfig = *cfg

	return o.writeConsumerMeta()
}

func (o *consumerFileStore) Update(state *ConsumerState) error {
	// Sanity checks.
	if state.AckFloor.Consumer > state.Delivered.Consumer {
		return fmt.Errorf("bad ack floor for consumer")
	}
	if state.AckFloor.Stream > state.Delivered.Stream {
		return fmt.Errorf("bad ack floor for stream")
	}

	// Copy to our state.
	var pending map[uint64]*Pending
	var redelivered map[uint64]uint64
	if len(state.Pending) > 0 {
		pending = make(map[uint64]*Pending, len(state.Pending))
		for seq, p := range state.Pending {
			pending[seq] = &Pending{p.Sequence, p.Timestamp}
			if seq <= state.AckFloor.Stream || seq > state.Delivered.Stream {
				return fmt.Errorf("bad pending entry, sequence [%d] out of range", seq)
			}
		}
	}
	if len(state.Redelivered) > 0 {
		redelivered = make(map[uint64]uint64, len(state.Redelivered))
		for seq, dc := range state.Redelivered {
			redelivered[seq] = dc
		}
	}

	// Replace our state.
	o.mu.Lock()
	defer o.mu.Unlock()

	// Check to see if this is an outdated update.
	if state.Delivered.Consumer < o.state.Delivered.Consumer || state.AckFloor.Stream < o.state.AckFloor.Stream {
		return ErrStoreOldUpdate
	}

	o.state.Delivered = state.Delivered
	o.state.AckFloor = state.AckFloor
	o.state.Pending = pending
	o.state.Redelivered = redelivered

	o.kickFlusher()

	return nil
}

// Will encrypt the state with our asset key. Will be a no-op if encryption not enabled.
// Lock should be held.
func (o *consumerFileStore) encryptState(buf []byte) ([]byte, error) {
	if o.aek == nil {
		return buf, nil
	}
	// TODO(dlc) - Optimize on space usage a bit?
	nonce := make([]byte, o.aek.NonceSize(), o.aek.NonceSize()+len(buf)+o.aek.Overhead())
	if n, err := rand.Read(nonce); err != nil {
		return nil, err
	} else if n != len(nonce) {
		return nil, fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
	}
	return o.aek.Seal(nonce, nonce, buf, nil), nil
}

// Used to limit number of disk IO calls in flight since they could all be blocking an OS thread.
// https://github.com/nats-io/nats-server/issues/2742
var dios chan struct{}

// Used to setup our simplistic counting semaphore using buffered channels.
// golang.org's semaphore seemed a bit heavy.
func init() {
	// Limit ourselves to a sensible number of blocking I/O calls. Range between
	// 4-16 concurrent disk I/Os based on CPU cores, or 50% of cores if greater
	// than 32 cores.
	mp := runtime.GOMAXPROCS(-1)
	nIO := min(16, max(4, mp))
	if mp > 32 {
		// If the system has more than 32 cores then limit dios to 50% of cores.
		nIO = max(16, min(mp, mp/2))
	}
	dios = make(chan struct{}, nIO)
	// Fill it up to start.
	for i := 0; i < nIO; i++ {
		dios <- struct{}{}
	}
}

func (o *consumerFileStore) writeState(buf []byte) error {
	// Check if we have the index file open.
	o.mu.Lock()
	if o.writing || len(buf) == 0 {
		o.mu.Unlock()
		return nil
	}

	// Check on encryption.
	if o.aek != nil {
		var err error
		if buf, err = o.encryptState(buf); err != nil {
			o.mu.Unlock()
			return err
		}
	}

	o.writing = true
	o.dirty = false
	ifn := o.ifn
	o.mu.Unlock()

	// Lock not held here but we do limit number of outstanding calls that could block OS threads.
	err := o.fs.writeFileWithOptionalSync(ifn, buf, defaultFilePerms)

	o.mu.Lock()
	if err != nil {
		o.dirty = true
	}
	o.writing = false
	o.mu.Unlock()

	return err
}

// Will upodate the config. Only used when recovering ephemerals.
func (o *consumerFileStore) updateConfig(cfg ConsumerConfig) error {
	o.mu.Lock()
	defer o.mu.Unlock()
	o.cfg = &FileConsumerInfo{ConsumerConfig: cfg}
	return o.writeConsumerMeta()
}

// Write out the consumer meta data, i.e. state.
// Lock should be held.
func (cfs *consumerFileStore) writeConsumerMeta() error {
	meta := filepath.Join(cfs.odir, JetStreamMetaFile)
	if _, err := os.Stat(meta); err != nil && !os.IsNotExist(err) {
		return err
	}

	if cfs.prf != nil && cfs.aek == nil {
		fs := cfs.fs
		key, _, _, encrypted, err := fs.genEncryptionKeys(fs.cfg.Name + tsep + cfs.name)
		if err != nil {
			return err
		}
		cfs.aek = key
		keyFile := filepath.Join(cfs.odir, JetStreamMetaFileKey)
		if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) {
			return err
		}
		err = cfs.fs.writeFileWithOptionalSync(keyFile, encrypted, defaultFilePerms)
		if err != nil {
			return err
		}
	}

	b, err := json.Marshal(cfs.cfg)
	if err != nil {
		return err
	}
	// Encrypt if needed.
	if cfs.aek != nil {
		nonce := make([]byte, cfs.aek.NonceSize(), cfs.aek.NonceSize()+len(b)+cfs.aek.Overhead())
		if n, err := rand.Read(nonce); err != nil {
			return err
		} else if n != len(nonce) {
			return fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
		}
		b = cfs.aek.Seal(nonce, nonce, b, nil)
	}

	err = cfs.fs.writeFileWithOptionalSync(meta, b, defaultFilePerms)
	if err != nil {
		return err
	}
	cfs.hh.Reset()
	cfs.hh.Write(b)
	var hb [highwayhash.Size64]byte
	checksum := hex.EncodeToString(cfs.hh.Sum(hb[:0]))
	sum := filepath.Join(cfs.odir, JetStreamMetaFileSum)

	err = cfs.fs.writeFileWithOptionalSync(sum, []byte(checksum), defaultFilePerms)
	if err != nil {
		return err
	}
	return nil
}

// Consumer version.
func checkConsumerHeader(hdr []byte) (uint8, error) {
	if len(hdr) < 2 || hdr[0] != magic {
		return 0, errCorruptState
	}
	version := hdr[1]
	switch version {
	case 1, 2:
		return version, nil
	}
	return 0, fmt.Errorf("unsupported version: %d", version)
}

func (o *consumerFileStore) copyPending() map[uint64]*Pending {
	pending := make(map[uint64]*Pending, len(o.state.Pending))
	for seq, p := range o.state.Pending {
		pending[seq] = &Pending{p.Sequence, p.Timestamp}
	}
	return pending
}

func (o *consumerFileStore) copyRedelivered() map[uint64]uint64 {
	redelivered := make(map[uint64]uint64, len(o.state.Redelivered))
	for seq, dc := range o.state.Redelivered {
		redelivered[seq] = dc
	}
	return redelivered
}

// Type returns the type of the underlying store.
func (o *consumerFileStore) Type() StorageType { return FileStorage }

// State retrieves the state from the state file.
// This is not expected to be called in high performance code, only on startup.
func (o *consumerFileStore) State() (*ConsumerState, error) {
	return o.stateWithCopy(true)
}

// This will not copy pending or redelivered, so should only be done under the
// consumer owner's lock.
func (o *consumerFileStore) BorrowState() (*ConsumerState, error) {
	return o.stateWithCopy(false)
}

func (o *consumerFileStore) stateWithCopy(doCopy bool) (*ConsumerState, error) {
	o.mu.Lock()
	defer o.mu.Unlock()
	return o.stateWithCopyLocked(doCopy)
}

// Lock should be held.
func (o *consumerFileStore) stateWithCopyLocked(doCopy bool) (*ConsumerState, error) {
	if o.closed {
		return nil, ErrStoreClosed
	}

	state := &ConsumerState{}

	// See if we have a running state or if we need to read in from disk.
	if o.state.Delivered.Consumer != 0 || o.state.Delivered.Stream != 0 {
		state.Delivered = o.state.Delivered
		state.AckFloor = o.state.AckFloor
		if len(o.state.Pending) > 0 {
			if doCopy {
				state.Pending = o.copyPending()
			} else {
				state.Pending = o.state.Pending
			}
		}
		if len(o.state.Redelivered) > 0 {
			if doCopy {
				state.Redelivered = o.copyRedelivered()
			} else {
				state.Redelivered = o.state.Redelivered
			}
		}
		return state, nil
	}

	// Read the state in here from disk..
	<-dios
	buf, err := os.ReadFile(o.ifn)
	dios <- struct{}{}

	if err != nil && !os.IsNotExist(err) {
		return nil, err
	}

	if len(buf) == 0 {
		return state, nil
	}

	// Check on encryption.
	if o.aek != nil {
		ns := o.aek.NonceSize()
		buf, err = o.aek.Open(nil, buf[:ns], buf[ns:], nil)
		if err != nil {
			return nil, err
		}
	}

	state, err = decodeConsumerState(buf)
	if err != nil {
		return nil, err
	}

	// Copy this state into our own.
	o.state.Delivered = state.Delivered
	o.state.AckFloor = state.AckFloor
	if len(state.Pending) > 0 {
		if doCopy {
			o.state.Pending = make(map[uint64]*Pending, len(state.Pending))
			for seq, p := range state.Pending {
				o.state.Pending[seq] = &Pending{p.Sequence, p.Timestamp}
			}
		} else {
			o.state.Pending = state.Pending
		}
	}
	if len(state.Redelivered) > 0 {
		if doCopy {
			o.state.Redelivered = make(map[uint64]uint64, len(state.Redelivered))
			for seq, dc := range state.Redelivered {
				o.state.Redelivered[seq] = dc
			}
		} else {
			o.state.Redelivered = state.Redelivered
		}
	}

	return state, nil
}

// Lock should be held. Called at startup.
func (o *consumerFileStore) loadState() error {
	if _, err := os.Stat(o.ifn); err == nil {
		// This will load our state in from disk.
		_, err = o.stateWithCopyLocked(false)
		return err
	} else if os.IsNotExist(err) {
		return nil
	} else {
		return err
	}
}

// Decode consumer state.
func decodeConsumerState(buf []byte) (*ConsumerState, error) {
	version, err := checkConsumerHeader(buf)
	if err != nil {
		return nil, err
	}

	bi := hdrLen
	// Helpers, will set i to -1 on error.
	readSeq := func() uint64 {
		if bi < 0 {
			return 0
		}
		seq, n := binary.Uvarint(buf[bi:])
		if n <= 0 {
			bi = -1
			return 0
		}
		bi += n
		return seq
	}
	readTimeStamp := func() int64 {
		if bi < 0 {
			return 0
		}
		ts, n := binary.Varint(buf[bi:])
		if n <= 0 {
			bi = -1
			return -1
		}
		bi += n
		return ts
	}
	// Just for clarity below.
	readLen := readSeq
	readCount := readSeq

	state := &ConsumerState{}
	state.AckFloor.Consumer = readSeq()
	state.AckFloor.Stream = readSeq()
	state.Delivered.Consumer = readSeq()
	state.Delivered.Stream = readSeq()

	if bi == -1 {
		return nil, errCorruptState
	}
	if version == 1 {
		// Adjust back. Version 1 also stored delivered as next to be delivered,
		// so adjust that back down here.
		if state.AckFloor.Consumer > 1 {
			state.Delivered.Consumer += state.AckFloor.Consumer - 1
		}
		if state.AckFloor.Stream > 1 {
			state.Delivered.Stream += state.AckFloor.Stream - 1
		}
	}

	// Protect ourselves against rolling backwards.
	const hbit = 1 << 63
	if state.AckFloor.Stream&hbit != 0 || state.Delivered.Stream&hbit != 0 {
		return nil, errCorruptState
	}

	// We have additional stuff.
	if numPending := readLen(); numPending > 0 {
		mints := readTimeStamp()
		state.Pending = make(map[uint64]*Pending, numPending)
		for i := 0; i < int(numPending); i++ {
			sseq := readSeq()
			var dseq uint64
			if version == 2 {
				dseq = readSeq()
			}
			ts := readTimeStamp()
			// Check the state machine for corruption, not the value which could be -1.
			if bi == -1 {
				return nil, errCorruptState
			}
			// Adjust seq back.
			sseq += state.AckFloor.Stream
			if sseq == 0 {
				return nil, errCorruptState
			}
			if version == 2 {
				dseq += state.AckFloor.Consumer
			}
			// Adjust the timestamp back.
			if version == 1 {
				ts = (ts + mints) * int64(time.Second)
			} else {
				ts = (mints - ts) * int64(time.Second)
			}
			// Store in pending.
			state.Pending[sseq] = &Pending{dseq, ts}
		}
	}

	// We have redelivered entries here.
	if numRedelivered := readLen(); numRedelivered > 0 {
		state.Redelivered = make(map[uint64]uint64, numRedelivered)
		for i := 0; i < int(numRedelivered); i++ {
			if seq, n := readSeq(), readCount(); seq > 0 && n > 0 {
				// Adjust seq back.
				seq += state.AckFloor.Stream
				state.Redelivered[seq] = n
			}
		}
	}

	return state, nil
}

// Stop the processing of the consumers's state.
func (o *consumerFileStore) Stop() error {
	o.mu.Lock()
	if o.closed {
		o.mu.Unlock()
		return nil
	}
	if o.qch != nil {
		close(o.qch)
		o.qch = nil
	}

	var err error
	var buf []byte

	if o.dirty {
		// Make sure to write this out..
		if buf, err = o.encodeState(); err == nil && len(buf) > 0 {
			if o.aek != nil {
				if buf, err = o.encryptState(buf); err != nil {
					o.mu.Unlock()
					return err
				}
			}
		}
	}

	o.odir = _EMPTY_
	o.closed = true
	ifn, fs := o.ifn, o.fs
	o.mu.Unlock()

	fs.RemoveConsumer(o)

	if len(buf) > 0 {
		o.waitOnFlusher()
		err = o.fs.writeFileWithOptionalSync(ifn, buf, defaultFilePerms)
	}
	return err
}

func (o *consumerFileStore) waitOnFlusher() {
	if !o.inFlusher() {
		return
	}

	timeout := time.Now().Add(100 * time.Millisecond)
	for time.Now().Before(timeout) {
		if !o.inFlusher() {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
}

// Delete the consumer.
func (o *consumerFileStore) Delete() error {
	return o.delete(false)
}

func (o *consumerFileStore) StreamDelete() error {
	return o.delete(true)
}

func (o *consumerFileStore) delete(streamDeleted bool) error {
	o.mu.Lock()
	if o.closed {
		o.mu.Unlock()
		return nil
	}
	if o.qch != nil {
		close(o.qch)
		o.qch = nil
	}

	odir := o.odir
	o.odir = _EMPTY_
	o.closed = true
	fs := o.fs
	o.mu.Unlock()

	// If our stream was not deleted this will remove the directories.
	if odir != _EMPTY_ && !streamDeleted {
		if err := removeAllWithRetry(odir); err != nil {
			return err
		}
	}

	if !streamDeleted {
		if err := fs.RemoveConsumer(o); err != nil {
			return err
		}
	}

	return nil
}

func (fs *fileStore) AddConsumer(o ConsumerStore) error {
	fs.cmu.Lock()
	defer fs.cmu.Unlock()
	fs.cfs = append(fs.cfs, o)
	return nil
}

func (fs *fileStore) RemoveConsumer(o ConsumerStore) error {
	fs.cmu.Lock()
	defer fs.cmu.Unlock()
	for i, cfs := range fs.cfs {
		if o == cfs {
			fs.cfs = append(fs.cfs[:i], fs.cfs[i+1:]...)
			break
		}
	}
	return nil
}

////////////////////////////////////////////////////////////////////////////////
// Templates
////////////////////////////////////////////////////////////////////////////////

// Deprecated: stream templates are deprecated and will be removed in a future version.
type templateFileStore struct {
	dir string
	hh  *highwayhash.Digest64
}

// Deprecated: stream templates are deprecated and will be removed in a future version.
func newTemplateFileStore(storeDir string) *templateFileStore {
	tdir := filepath.Join(storeDir, tmplsDir)
	key := sha256.Sum256([]byte("templates"))
	hh, err := highwayhash.NewDigest64(key[:])
	if err != nil {
		return nil
	}
	return &templateFileStore{dir: tdir, hh: hh}
}

// Deprecated: stream templates are deprecated and will be removed in a future version.
func (ts *templateFileStore) Store(t *streamTemplate) error {
	dir := filepath.Join(ts.dir, t.Name)
	if err := os.MkdirAll(dir, defaultDirPerms); err != nil {
		return fmt.Errorf("could not create templates storage directory for %q- %v", t.Name, err)
	}
	meta := filepath.Join(dir, JetStreamMetaFile)
	if _, err := os.Stat(meta); (err != nil && !os.IsNotExist(err)) || err == nil {
		return err
	}
	t.mu.Lock()
	b, err := json.Marshal(t)
	t.mu.Unlock()
	if err != nil {
		return err
	}
	if err := os.WriteFile(meta, b, defaultFilePerms); err != nil {
		return err
	}
	// FIXME(dlc) - Do checksum
	ts.hh.Reset()
	ts.hh.Write(b)
	var hb [highwayhash.Size64]byte
	checksum := hex.EncodeToString(ts.hh.Sum(hb[:0]))
	sum := filepath.Join(dir, JetStreamMetaFileSum)
	if err := os.WriteFile(sum, []byte(checksum), defaultFilePerms); err != nil {
		return err
	}
	return nil
}

// Deprecated: stream templates are deprecated and will be removed in a future version.
func (ts *templateFileStore) Delete(t *streamTemplate) error {
	return os.RemoveAll(filepath.Join(ts.dir, t.Name))
}

////////////////////////////////////////////////////////////////////////////////
// Compression
////////////////////////////////////////////////////////////////////////////////

type CompressionInfo struct {
	Algorithm    StoreCompression
	OriginalSize uint64
}

func (c *CompressionInfo) MarshalMetadata() []byte {
	b := make([]byte, 14) // 4 + potentially up to 10 for uint64
	b[0], b[1], b[2] = 'c', 'm', 'p'
	b[3] = byte(c.Algorithm)
	n := binary.PutUvarint(b[4:], c.OriginalSize)
	return b[:4+n]
}

func (c *CompressionInfo) UnmarshalMetadata(b []byte) (int, error) {
	c.Algorithm = NoCompression
	c.OriginalSize = 0
	if len(b) < 5 { // 4 + min 1 for uvarint uint64
		return 0, nil
	}
	if b[0] != 'c' || b[1] != 'm' || b[2] != 'p' {
		return 0, nil
	}
	var n int
	c.Algorithm = StoreCompression(b[3])
	c.OriginalSize, n = binary.Uvarint(b[4:])
	if n <= 0 {
		return 0, fmt.Errorf("metadata incomplete")
	}
	return 4 + n, nil
}

func (alg StoreCompression) Compress(buf []byte) ([]byte, error) {
	if len(buf) < checksumSize {
		return nil, fmt.Errorf("uncompressed buffer is too short")
	}
	bodyLen := int64(len(buf) - checksumSize)
	var output bytes.Buffer
	var writer io.WriteCloser
	switch alg {
	case NoCompression:
		return buf, nil
	case S2Compression:
		writer = s2.NewWriter(&output)
	default:
		return nil, fmt.Errorf("compression algorithm not known")
	}

	input := bytes.NewReader(buf[:bodyLen])
	checksum := buf[bodyLen:]

	// Compress the block content, but don't compress the checksum.
	// We will preserve it at the end of the block as-is.
	if n, err := io.CopyN(writer, input, bodyLen); err != nil {
		return nil, fmt.Errorf("error writing to compression writer: %w", err)
	} else if n != bodyLen {
		return nil, fmt.Errorf("short write on body (%d != %d)", n, bodyLen)
	}
	if err := writer.Close(); err != nil {
		return nil, fmt.Errorf("error closing compression writer: %w", err)
	}

	// Now add the checksum back onto the end of the block.
	if n, err := output.Write(checksum); err != nil {
		return nil, fmt.Errorf("error writing checksum: %w", err)
	} else if n != checksumSize {
		return nil, fmt.Errorf("short write on checksum (%d != %d)", n, checksumSize)
	}

	return output.Bytes(), nil
}

func (alg StoreCompression) Decompress(buf []byte) ([]byte, error) {
	if len(buf) < checksumSize {
		return nil, fmt.Errorf("compressed buffer is too short")
	}
	bodyLen := int64(len(buf) - checksumSize)
	input := bytes.NewReader(buf[:bodyLen])

	var reader io.ReadCloser
	switch alg {
	case NoCompression:
		return buf, nil
	case S2Compression:
		reader = io.NopCloser(s2.NewReader(input))
	default:
		return nil, fmt.Errorf("compression algorithm not known")
	}

	// Decompress the block content. The checksum isn't compressed so
	// we can preserve it from the end of the block as-is.
	checksum := buf[bodyLen:]
	output, err := io.ReadAll(reader)
	if err != nil {
		return nil, fmt.Errorf("error reading compression reader: %w", err)
	}
	output = append(output, checksum...)

	return output, reader.Close()
}

// writeFileWithOptionalSync is equivalent to os.WriteFile() but optionally
// sets O_SYNC on the open file if SyncAlways is set. The dios semaphore is
// handled automatically by this function, so don't wrap calls to it in dios.
func (fs *fileStore) writeFileWithOptionalSync(name string, data []byte, perm fs.FileMode) error {
	return writeAtomically(name, data, perm, fs.fcfg.SyncAlways)
}

func writeFileWithSync(name string, data []byte, perm fs.FileMode) error {
	return writeAtomically(name, data, perm, true)
}

func writeAtomically(name string, data []byte, perm fs.FileMode, sync bool) error {
	tmp := name + ".tmp"
	flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
	if sync {
		flags = flags | os.O_SYNC
	}
	<-dios
	defer func() {
		dios <- struct{}{}
	}()
	f, err := os.OpenFile(tmp, flags, perm)
	if err != nil {
		return err
	}
	if _, err := f.Write(data); err != nil {
		_ = f.Close()
		_ = os.Remove(tmp)
		return err
	}
	if err := f.Close(); err != nil {
		_ = os.Remove(tmp)
		return err
	}
	if err := os.Rename(tmp, name); err != nil {
		_ = os.Remove(tmp)
		return err
	}
	if sync {
		// To ensure that the file rename was persisted on all filesystems,
		// also try to flush the directory metadata.
		if d, err := os.Open(filepath.Dir(name)); err == nil {
			_ = d.Sync()
			_ = d.Close()
		}
	}
	return nil
}
