// Copyright The Prometheus 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 wlog

import (
	"bufio"
	"encoding/binary"
	"errors"
	"fmt"
	"hash/crc32"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"sync"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/common/promslog"

	"github.com/prometheus/prometheus/tsdb/fileutil"
	"github.com/prometheus/prometheus/util/compression"
)

const (
	DefaultSegmentSize = 128 * 1024 * 1024 // DefaultSegmentSize is 128 MB.
	pageSize           = 32 * 1024         // pageSize is 32KB.
	recordHeaderSize   = 7
	WblDirName         = "wbl"
)

// The table gets initialized with sync.Once but may still cause a race
// with any other use of the crc32 package anywhere. Thus we initialize it
// before.
var castagnoliTable = crc32.MakeTable(crc32.Castagnoli)

// page is an in memory buffer used to batch disk writes.
// Records bigger than the page size are split and flushed separately.
// A flush is triggered when a single records doesn't fit the page size or
// when the next record can't fit in the remaining free page space.
type page struct {
	alloc   int
	flushed int
	buf     [pageSize]byte
}

func (p *page) remaining() int {
	return pageSize - p.alloc
}

func (p *page) full() bool {
	return pageSize-p.alloc < recordHeaderSize
}

func (p *page) reset() {
	for i := range p.buf {
		p.buf[i] = 0
	}
	p.alloc = 0
	p.flushed = 0
}

// SegmentFile represents the underlying file used to store a segment.
type SegmentFile interface {
	Stat() (os.FileInfo, error)
	Sync() error
	io.Writer
	io.Reader
	io.Closer
}

// Segment represents a segment file.
type Segment struct {
	SegmentFile
	dir string
	i   int
}

// Index returns the index of the segment.
func (s *Segment) Index() int {
	return s.i
}

// Dir returns the directory of the segment.
func (s *Segment) Dir() string {
	return s.dir
}

// CorruptionErr is an error that's returned when corruption is encountered.
type CorruptionErr struct {
	Dir     string
	Segment int
	Offset  int64
	Err     error
}

func (e *CorruptionErr) Error() string {
	if e.Segment < 0 {
		return fmt.Sprintf("corruption after %d bytes: %s", e.Offset, e.Err)
	}
	return fmt.Sprintf("corruption in segment %s at %d: %s", SegmentName(e.Dir, e.Segment), e.Offset, e.Err)
}

func (e *CorruptionErr) Unwrap() error {
	return e.Err
}

// OpenWriteSegment opens segment k in dir. The returned segment is ready for new appends.
func OpenWriteSegment(logger *slog.Logger, dir string, k int) (*Segment, error) {
	segName := SegmentName(dir, k)
	f, err := os.OpenFile(segName, os.O_WRONLY|os.O_APPEND, 0o666)
	if err != nil {
		return nil, err
	}
	stat, err := f.Stat()
	if err != nil {
		f.Close()
		return nil, err
	}
	// If the last page is torn, fill it with zeros.
	// In case it was torn after all records were written successfully, this
	// will just pad the page and everything will be fine.
	// If it was torn mid-record, a full read (which the caller should do anyway
	// to ensure integrity) will detect it as a corruption by the end.
	if d := stat.Size() % pageSize; d != 0 {
		logger.Warn("Last page of the wlog is torn, filling it with zeros", "segment", segName)
		if _, err := f.Write(make([]byte, pageSize-d)); err != nil {
			f.Close()
			return nil, fmt.Errorf("zero-pad torn page: %w", err)
		}
	}
	return &Segment{SegmentFile: f, i: k, dir: dir}, nil
}

// CreateSegment creates a new segment k in dir.
func CreateSegment(dir string, k int) (*Segment, error) {
	f, err := os.OpenFile(SegmentName(dir, k), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o666)
	if err != nil {
		return nil, err
	}
	return &Segment{SegmentFile: f, i: k, dir: dir}, nil
}

// OpenReadSegment opens the segment with the given filename.
func OpenReadSegment(fn string) (*Segment, error) {
	k, err := strconv.Atoi(filepath.Base(fn))
	if err != nil {
		return nil, errors.New("not a valid filename")
	}
	f, err := os.Open(fn)
	if err != nil {
		return nil, err
	}
	return &Segment{SegmentFile: f, i: k, dir: filepath.Dir(fn)}, nil
}

// WL is a write log that stores records in segment files.
// It must be read from start to end once before logging new data.
// If an error occurs during read, the repair procedure must be called
// before it's safe to do further writes.
//
// Segments are written to in pages of 32KB, with records possibly split
// across page boundaries.
// Records are never split across segments to allow full segments to be
// safely truncated. It also ensures that torn writes never corrupt records
// beyond the most recent segment.
type WL struct {
	dir         string
	logger      *slog.Logger
	segmentSize int
	mtx         sync.RWMutex
	segment     *Segment // Active segment.
	donePages   int      // Pages written to the segment.
	page        *page    // Active page.
	stopc       chan chan struct{}
	actorc      chan func()
	closed      bool // To allow calling Close() more than once without blocking.
	compress    compression.Type
	cEnc        compression.EncodeBuffer

	WriteNotified WriteNotified

	metrics *wlMetrics
}

type wlMetrics struct {
	fsyncDuration    prometheus.Summary
	pageFlushes      prometheus.Counter
	pageCompletions  prometheus.Counter
	truncateFail     prometheus.Counter
	truncateTotal    prometheus.Counter
	currentSegment   prometheus.Gauge
	writesFailed     prometheus.Counter
	walFileSize      prometheus.GaugeFunc
	recordPartWrites prometheus.Counter
	recordPartBytes  prometheus.Counter
	recordBytesSaved *prometheus.CounterVec

	r prometheus.Registerer
}

func (w *wlMetrics) Unregister() {
	if w.r == nil {
		return
	}
	w.r.Unregister(w.fsyncDuration)
	w.r.Unregister(w.pageFlushes)
	w.r.Unregister(w.pageCompletions)
	w.r.Unregister(w.truncateFail)
	w.r.Unregister(w.truncateTotal)
	w.r.Unregister(w.currentSegment)
	w.r.Unregister(w.writesFailed)
	w.r.Unregister(w.walFileSize)
	w.r.Unregister(w.recordPartWrites)
	w.r.Unregister(w.recordPartBytes)
	w.r.Unregister(w.recordBytesSaved)
}

func newWLMetrics(w *WL, r prometheus.Registerer) *wlMetrics {
	return &wlMetrics{
		r: r,
		fsyncDuration: promauto.With(r).NewSummary(prometheus.SummaryOpts{
			Name:       "fsync_duration_seconds",
			Help:       "Duration of write log fsync.",
			Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
		}),
		pageFlushes: promauto.With(r).NewCounter(prometheus.CounterOpts{
			Name: "page_flushes_total",
			Help: "Total number of page flushes.",
		}),
		pageCompletions: promauto.With(r).NewCounter(prometheus.CounterOpts{
			Name: "completed_pages_total",
			Help: "Total number of completed pages.",
		}),
		truncateFail: promauto.With(r).NewCounter(prometheus.CounterOpts{
			Name: "truncations_failed_total",
			Help: "Total number of write log truncations that failed.",
		}),
		truncateTotal: promauto.With(r).NewCounter(prometheus.CounterOpts{
			Name: "truncations_total",
			Help: "Total number of write log truncations attempted.",
		}),
		currentSegment: promauto.With(r).NewGauge(prometheus.GaugeOpts{
			Name: "segment_current",
			Help: "Write log segment index that TSDB is currently writing to.",
		}),
		writesFailed: promauto.With(r).NewCounter(prometheus.CounterOpts{
			Name: "writes_failed_total",
			Help: "Total number of write log writes that failed.",
		}),
		walFileSize: promauto.With(r).NewGaugeFunc(prometheus.GaugeOpts{
			Name: "storage_size_bytes",
			Help: "Size of the write log directory.",
		}, func() float64 {
			val, err := w.Size()
			if err != nil {
				w.logger.Error("Failed to calculate size of \"wal\" dir", "err", err.Error())
			}
			return float64(val)
		}),
		recordPartWrites: promauto.With(r).NewCounter(prometheus.CounterOpts{
			Name: "record_part_writes_total",
			Help: "Total number of record parts written before flushing.",
		}),
		recordPartBytes: promauto.With(r).NewCounter(prometheus.CounterOpts{
			Name: "record_parts_bytes_written_total",
			Help: "Total number of record part bytes written before flushing, including" +
				" CRC and compression headers.",
		}),
		recordBytesSaved: promauto.With(r).NewCounterVec(prometheus.CounterOpts{
			Name: "record_bytes_saved_total",
			Help: "Total number of bytes saved by the optional record compression." +
				" Use this metric to learn about the effectiveness compression.",
		}, []string{"compression"}),
	}
}

// New returns a new WAL over the given directory.
func New(logger *slog.Logger, reg prometheus.Registerer, dir string, compress compression.Type) (*WL, error) {
	return NewSize(logger, reg, dir, DefaultSegmentSize, compress)
}

// NewSize returns a new write log over the given directory.
// New segments are created with the specified size.
func NewSize(logger *slog.Logger, reg prometheus.Registerer, dir string, segmentSize int, compress compression.Type) (*WL, error) {
	if segmentSize%pageSize != 0 {
		return nil, errors.New("invalid segment size")
	}
	if err := os.MkdirAll(dir, 0o777); err != nil {
		return nil, fmt.Errorf("create dir: %w", err)
	}
	if logger == nil {
		logger = promslog.NewNopLogger()
	}

	w := &WL{
		dir:         dir,
		logger:      logger,
		segmentSize: segmentSize,
		page:        &page{},
		actorc:      make(chan func(), 100),
		stopc:       make(chan chan struct{}),
		compress:    compress,
		cEnc:        compression.NewSyncEncodeBuffer(),
	}
	prefix := "prometheus_tsdb_wal_"
	if filepath.Base(dir) == WblDirName {
		prefix = "prometheus_tsdb_out_of_order_wbl_"
	}
	w.metrics = newWLMetrics(w, prometheus.WrapRegistererWithPrefix(prefix, reg))

	_, last, err := Segments(w.Dir())
	if err != nil {
		return nil, fmt.Errorf("get segment range: %w", err)
	}

	// Index of the Segment we want to open and write to.
	writeSegmentIndex := 0
	// If some segments already exist create one with a higher index than the last segment.
	if last != -1 {
		writeSegmentIndex = last + 1
	}

	segment, err := CreateSegment(w.Dir(), writeSegmentIndex)
	if err != nil {
		return nil, err
	}

	if err := w.setSegment(segment); err != nil {
		return nil, err
	}

	go w.run()

	return w, nil
}

// Open an existing WAL.
func Open(logger *slog.Logger, dir string) (*WL, error) {
	if logger == nil {
		logger = promslog.NewNopLogger()
	}

	w := &WL{
		dir:    dir,
		logger: logger,
	}
	return w, nil
}

// CompressionType returns if compression is enabled on this WAL.
func (w *WL) CompressionType() compression.Type {
	return w.compress
}

// Dir returns the directory of the WAL.
func (w *WL) Dir() string {
	return w.dir
}

func (w *WL) SetWriteNotified(wn WriteNotified) {
	w.WriteNotified = wn
}

func (w *WL) run() {
Loop:
	for {
		select {
		case f := <-w.actorc:
			f()
		case donec := <-w.stopc:
			close(w.actorc)
			defer close(donec)
			break Loop
		}
	}
	// Drain and process any remaining functions.
	for f := range w.actorc {
		f()
	}
}

// Repair attempts to repair the WAL based on the error.
// It discards all data after the corruption.
func (w *WL) Repair(origErr error) error {
	// We could probably have a mode that only discards torn records right around
	// the corruption to preserve as data much as possible.
	// But that's not generally applicable if the records have any kind of causality.
	// Maybe as an extra mode in the future if mid-WAL corruptions become
	// a frequent concern.
	var cerr *CorruptionErr
	if !errors.As(origErr, &cerr) {
		return fmt.Errorf("cannot handle error: %w", origErr)
	}
	if cerr.Segment < 0 {
		return errors.New("corruption error does not specify position")
	}
	w.logger.Warn("Starting corruption repair",
		"segment", cerr.Segment, "offset", cerr.Offset)

	// All segments behind the corruption can no longer be used.
	segs, err := listSegments(w.Dir())
	if err != nil {
		return fmt.Errorf("list segments: %w", err)
	}
	w.logger.Warn("Deleting all segments newer than corrupted segment", "segment", cerr.Segment)

	for _, s := range segs {
		if w.segment.i == s.index {
			// The active segment needs to be removed,
			// close it first (Windows!). Can be closed safely
			// as we set the current segment to repaired file
			// below.
			if err := w.segment.Close(); err != nil {
				return fmt.Errorf("close active segment: %w", err)
			}
		}
		if s.index <= cerr.Segment {
			continue
		}
		if err := os.Remove(filepath.Join(w.Dir(), s.name)); err != nil {
			return fmt.Errorf("delete segment:%v: %w", s.index, err)
		}
	}
	// Regardless of the corruption offset, no record reaches into the previous segment.
	// So we can safely repair the WAL by removing the segment and re-inserting all
	// its records up to the corruption.
	w.logger.Warn("Rewrite corrupted segment", "segment", cerr.Segment)

	fn := SegmentName(w.Dir(), cerr.Segment)
	tmpfn := fn + ".repair"

	if err := fileutil.Rename(fn, tmpfn); err != nil {
		return err
	}
	// Create a clean segment and make it the active one.
	s, err := CreateSegment(w.Dir(), cerr.Segment)
	if err != nil {
		return err
	}
	if err := w.setSegment(s); err != nil {
		return err
	}

	f, err := os.Open(tmpfn)
	if err != nil {
		return fmt.Errorf("open segment: %w", err)
	}
	defer f.Close()

	r := NewReader(bufio.NewReader(f))

	for r.Next() {
		// Add records only up to the where the error was.
		if r.Offset() >= cerr.Offset {
			break
		}
		if err := w.Log(r.Record()); err != nil {
			return fmt.Errorf("insert record: %w", err)
		}
	}
	// We expect an error here from r.Err(), so nothing to handle.

	// We need to pad to the end of the last page in the repaired segment
	if err := w.flushPage(true); err != nil {
		return fmt.Errorf("flush page in repair: %w", err)
	}

	// We explicitly close even when there is a defer for Windows to be
	// able to delete it. The defer is in place to close it in-case there
	// are errors above.
	if err := f.Close(); err != nil {
		return fmt.Errorf("close corrupted file: %w", err)
	}
	if err := os.Remove(tmpfn); err != nil {
		return fmt.Errorf("delete corrupted segment: %w", err)
	}

	// Explicitly close the segment we just repaired to avoid issues with Windows.
	s.Close()

	// We always want to start writing to a new Segment rather than an existing
	// Segment, which is handled by NewSize, but earlier in Repair we're deleting
	// all segments that come after the corrupted Segment. Recreate a new Segment here.
	s, err = CreateSegment(w.Dir(), cerr.Segment+1)
	if err != nil {
		return err
	}
	return w.setSegment(s)
}

// SegmentName builds a segment name for the directory.
func SegmentName(dir string, i int) string {
	return filepath.Join(dir, fmt.Sprintf("%08d", i))
}

// NextSegment creates the next segment and closes the previous one asynchronously.
// It returns the file number of the new file.
func (w *WL) NextSegment() (int, error) {
	w.mtx.Lock()
	defer w.mtx.Unlock()
	return w.nextSegment(true)
}

// NextSegmentSync creates the next segment and closes the previous one in sync.
// It returns the file number of the new file.
func (w *WL) NextSegmentSync() (int, error) {
	w.mtx.Lock()
	defer w.mtx.Unlock()
	return w.nextSegment(false)
}

// nextSegment creates the next segment and closes the previous one.
// It returns the file number of the new file.
func (w *WL) nextSegment(async bool) (int, error) {
	if w.closed {
		return 0, errors.New("wlog is closed")
	}

	// Only flush the current page if it actually holds data.
	if w.page.alloc > 0 {
		if err := w.flushPage(true); err != nil {
			return 0, err
		}
	}
	next, err := CreateSegment(w.Dir(), w.segment.Index()+1)
	if err != nil {
		return 0, fmt.Errorf("create new segment file: %w", err)
	}
	prev := w.segment
	if err := w.setSegment(next); err != nil {
		return 0, err
	}

	// Don't block further writes by fsyncing the last segment.
	f := func() {
		if err := w.fsync(prev); err != nil {
			w.logger.Error("sync previous segment", "err", err)
		}
		if err := prev.Close(); err != nil {
			w.logger.Error("close previous segment", "err", err)
		}
	}
	if async {
		w.actorc <- f
	} else {
		f()
	}
	return next.Index(), nil
}

func (w *WL) setSegment(segment *Segment) error {
	w.segment = segment

	// Correctly initialize donePages.
	stat, err := segment.Stat()
	if err != nil {
		return err
	}
	w.donePages = int(stat.Size() / pageSize)
	w.metrics.currentSegment.Set(float64(segment.Index()))
	return nil
}

// flushPage writes the new contents of the page to disk. If no more records will fit into
// the page, the remaining bytes will be set to zero and a new page will be started.
// If forceClear is true, this is enforced regardless of how many bytes are left in the page.
func (w *WL) flushPage(forceClear bool) error {
	w.metrics.pageFlushes.Inc()

	p := w.page
	shouldClear := forceClear || p.full()

	// No more data will fit into the page or an implicit clear.
	// Enqueue and clear it.
	if shouldClear {
		p.alloc = pageSize // Write till end of page.
	}

	n, err := w.segment.Write(p.buf[p.flushed:p.alloc])
	if err != nil {
		p.flushed += n
		return err
	}
	p.flushed += n

	// We flushed an entire page, prepare a new one.
	if shouldClear {
		p.reset()
		w.donePages++
		w.metrics.pageCompletions.Inc()
	}
	return nil
}

// First Byte of header format:
//
//	[3 bits unallocated] [1 bit zstd compression flag] [1 bit snappy compression flag] [3 bit record type ]
const (
	snappyMask  = 1 << 3
	zstdMask    = 1 << 4
	recTypeMask = snappyMask - 1
)

type recType uint8

const (
	recPageTerm recType = 0 // Rest of page is empty.
	recFull     recType = 1 // Full record.
	recFirst    recType = 2 // First fragment of a record.
	recMiddle   recType = 3 // Middle fragments of a record.
	recLast     recType = 4 // Final fragment of a record.
)

func recTypeFromHeader(header byte) recType {
	return recType(header & recTypeMask)
}

func (t recType) String() string {
	switch t {
	case recPageTerm:
		return "zero"
	case recFull:
		return "full"
	case recFirst:
		return "first"
	case recMiddle:
		return "middle"
	case recLast:
		return "last"
	default:
		return "<invalid>"
	}
}

func (w *WL) pagesPerSegment() int {
	return w.segmentSize / pageSize
}

// Log writes the records into the log.
// Multiple records can be passed at once to reduce writes and increase throughput.
func (w *WL) Log(recs ...[]byte) error {
	w.mtx.Lock()
	defer w.mtx.Unlock()
	// Callers could just implement their own list record format but adding
	// a bit of extra logic here frees them from that overhead.
	for i, r := range recs {
		if err := w.log(r, i == len(recs)-1); err != nil {
			w.metrics.writesFailed.Inc()
			return err
		}
	}
	return nil
}

// log writes rec to the log and forces a flush of the current page if:
// - the final record of a batch
// - the record is bigger than the page size
// - the current page is full.
func (w *WL) log(rec []byte, final bool) error {
	// When the last page flush failed the page will remain full.
	// When the page is full, need to flush it before trying to add more records to it.
	if w.page.full() {
		if err := w.flushPage(true); err != nil {
			return err
		}
	}

	// Compress the record before calculating if a new segment is needed.
	finalCompression := w.compress
	enc, err := compression.Encode(w.compress, rec, w.cEnc)
	if err != nil {
		return err
	}
	if w.compress != compression.None {
		savedBytes := len(rec) - len(enc)

		// Even if the compression was applied, skip it, if there's no benefit
		// in the WAL record size (we have a choice). For small records e.g. snappy
		// compression can yield larger records than the uncompressed.
		if savedBytes <= 0 {
			enc = rec
			finalCompression = compression.None
			savedBytes = 0
		}
		w.metrics.recordBytesSaved.WithLabelValues(w.compress).Add(float64(savedBytes))
	}

	// If the record is too big to fit within the active page in the current
	// segment, terminate the active segment and advance to the next one.
	// This ensures that records do not cross segment boundaries.
	left := w.page.remaining() - recordHeaderSize                                   // Free space in the active page.
	left += (pageSize - recordHeaderSize) * (w.pagesPerSegment() - w.donePages - 1) // Free pages in the active segment.

	if len(enc) > left {
		if _, err := w.nextSegment(true); err != nil {
			return err
		}
	}

	// Populate as many pages as necessary to fit the record.
	// Be careful to always do one pass to ensure we write zero-length records.
	for i := 0; i == 0 || len(enc) > 0; i++ {
		p := w.page

		// Find how much of the record we can fit into the page.
		var (
			l    = min(len(enc), (pageSize-p.alloc)-recordHeaderSize)
			part = enc[:l]
			buf  = p.buf[p.alloc:]
			typ  recType
		)

		switch {
		case i == 0 && len(part) == len(enc):
			typ = recFull
		case len(part) == len(enc):
			typ = recLast
		case i == 0:
			typ = recFirst
		default:
			typ = recMiddle
		}

		if finalCompression != compression.None {
			switch finalCompression {
			case compression.Snappy:
				typ |= snappyMask
			case compression.Zstd:
				typ |= zstdMask
			default:
				return fmt.Errorf("unsupported compression type: %v", finalCompression)
			}
		}

		buf[0] = byte(typ)
		crc := crc32.Checksum(part, castagnoliTable)
		binary.BigEndian.PutUint16(buf[1:], uint16(len(part)))
		binary.BigEndian.PutUint32(buf[3:], crc)

		copy(buf[recordHeaderSize:], part)
		p.alloc += len(part) + recordHeaderSize

		w.metrics.recordPartWrites.Inc()
		w.metrics.recordPartBytes.Add(float64(len(part) + recordHeaderSize))

		if w.page.full() {
			if err := w.flushPage(true); err != nil {
				// TODO When the flushing fails at this point and the record has not been
				// fully written to the buffer, we end up with a corrupted WAL because some part of the
				// record have been written to the buffer, while the rest of the record will be discarded.
				return err
			}
		}
		enc = enc[l:]
	}

	// If it's the final record of the batch and the page is not empty, flush it.
	if final && w.page.alloc > 0 {
		if err := w.flushPage(false); err != nil {
			return err
		}
	}

	return nil
}

// LastSegmentAndOffset returns the last segment number of the WAL
// and the offset in that file upto which the segment has been filled.
func (w *WL) LastSegmentAndOffset() (seg, offset int, err error) {
	w.mtx.Lock()
	defer w.mtx.Unlock()

	_, seg, err = Segments(w.Dir())
	if err != nil {
		return seg, offset, err
	}

	offset = (w.donePages * pageSize) + w.page.alloc

	return seg, offset, err
}

// Truncate drops all segments before i.
func (w *WL) Truncate(i int) (err error) {
	w.metrics.truncateTotal.Inc()
	defer func() {
		if err != nil {
			w.metrics.truncateFail.Inc()
		}
	}()
	refs, err := listSegments(w.Dir())
	if err != nil {
		return err
	}
	for _, r := range refs {
		if r.index >= i {
			break
		}
		if err = os.Remove(filepath.Join(w.Dir(), r.name)); err != nil {
			return err
		}
	}
	return nil
}

func (w *WL) fsync(f *Segment) error {
	start := time.Now()
	err := f.Sync()
	w.metrics.fsyncDuration.Observe(time.Since(start).Seconds())
	return err
}

// Sync forces a file sync on the current write log segment. This function is meant
// to be used only on tests due to different behaviour on Operating Systems
// like windows and linux.
func (w *WL) Sync() error {
	return w.fsync(w.segment)
}

// Close flushes all writes and closes active segment.
func (w *WL) Close() (err error) {
	w.mtx.Lock()
	defer w.mtx.Unlock()

	if w.closed {
		return errors.New("wlog already closed")
	}

	if w.segment == nil {
		w.closed = true
		return nil
	}

	// Flush the last page and zero out all its remaining size.
	// We must not flush an empty page as it would falsely signal
	// the segment is done if we start writing to it again after opening.
	if w.page.alloc > 0 {
		if err := w.flushPage(true); err != nil {
			return err
		}
	}

	donec := make(chan struct{})
	w.stopc <- donec
	<-donec

	if err = w.fsync(w.segment); err != nil {
		w.logger.Error("sync previous segment", "err", err)
	}
	if err := w.segment.Close(); err != nil {
		w.logger.Error("close previous segment", "err", err)
	}

	w.metrics.Unregister()
	w.closed = true
	return nil
}

// Segments returns the range [first, n] of currently existing segments.
// If no segments are found, first and n are -1.
func Segments(wlDir string) (first, last int, err error) {
	refs, err := listSegments(wlDir)
	if err != nil {
		return 0, 0, err
	}
	if len(refs) == 0 {
		return -1, -1, nil
	}
	return refs[0].index, refs[len(refs)-1].index, nil
}

type segmentRef struct {
	name  string
	index int
}

func listSegments(dir string) (refs []segmentRef, err error) {
	files, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	for _, f := range files {
		fn := f.Name()
		k, err := strconv.Atoi(fn)
		if err != nil {
			continue
		}
		refs = append(refs, segmentRef{name: fn, index: k})
	}
	slices.SortFunc(refs, func(a, b segmentRef) int {
		return a.index - b.index
	})
	for i := 0; i < len(refs)-1; i++ {
		if refs[i].index+1 != refs[i+1].index {
			return nil, errors.New("segments are not sequential")
		}
	}
	return refs, nil
}

// SegmentRange groups segments by the directory and the first and last index it includes.
type SegmentRange struct {
	Dir         string
	First, Last int
}

// NewSegmentsReader returns a new reader over all segments in the directory.
func NewSegmentsReader(dir string) (io.ReadCloser, error) {
	return NewSegmentsRangeReader(SegmentRange{dir, -1, -1})
}

// NewSegmentsRangeReader returns a new reader over the given WAL segment ranges.
// If first or last are -1, the range is open on the respective end.
func NewSegmentsRangeReader(sr ...SegmentRange) (io.ReadCloser, error) {
	var segs []*Segment

	for _, sgmRange := range sr {
		refs, err := listSegments(sgmRange.Dir)
		if err != nil {
			return nil, fmt.Errorf("list segment in dir:%v: %w", sgmRange.Dir, err)
		}

		for _, r := range refs {
			if sgmRange.First >= 0 && r.index < sgmRange.First {
				continue
			}
			if sgmRange.Last >= 0 && r.index > sgmRange.Last {
				break
			}
			s, err := OpenReadSegment(filepath.Join(sgmRange.Dir, r.name))
			if err != nil {
				return nil, fmt.Errorf("open segment:%v in dir:%v: %w", r.name, sgmRange.Dir, err)
			}
			segs = append(segs, s)
		}
	}
	return NewSegmentBufReader(segs...), nil
}

// segmentBufReader is a buffered reader that reads in multiples of pages.
// The main purpose is that we are able to track segment and offset for
// corruption reporting.  We have to be careful not to increment curr too
// early, as it is used by Reader.Err() to tell Repair which segment is corrupt.
// As such we pad the end of non-page align segments with zeros.
type segmentBufReader struct {
	buf  *bufio.Reader
	segs []*Segment
	cur  int // Index into segs.
	off  int // Offset of read data into current segment.
}

func NewSegmentBufReader(segs ...*Segment) io.ReadCloser {
	if len(segs) == 0 {
		return &segmentBufReader{}
	}

	return &segmentBufReader{
		buf:  bufio.NewReaderSize(segs[0], 16*pageSize),
		segs: segs,
	}
}

func NewSegmentBufReaderWithOffset(offset int, segs ...*Segment) (io.ReadCloser, error) {
	if offset == 0 || len(segs) == 0 {
		return NewSegmentBufReader(segs...), nil
	}

	sbr := &segmentBufReader{
		buf:  bufio.NewReaderSize(segs[0], 16*pageSize),
		segs: segs,
	}
	var err error
	if offset > 0 {
		_, err = sbr.buf.Discard(offset)
	}
	return sbr, err
}

func (r *segmentBufReader) Close() (err error) {
	for _, s := range r.segs {
		if e := s.Close(); e != nil {
			err = e
		}
	}
	return err
}

// Read implements io.Reader.
func (r *segmentBufReader) Read(b []byte) (n int, err error) {
	if len(r.segs) == 0 {
		return 0, io.EOF
	}

	n, err = r.buf.Read(b)
	r.off += n

	// If we succeeded, or hit a non-EOF, we can stop.
	if err == nil || !errors.Is(err, io.EOF) {
		return n, err
	}

	// We hit EOF; fake out zero padding at the end of short segments, so we
	// don't increment curr too early and report the wrong segment as corrupt.
	if r.off%pageSize != 0 {
		i := 0
		for ; n+i < len(b) && (r.off+i)%pageSize != 0; i++ {
			b[n+i] = 0
		}

		// Return early, even if we didn't fill b.
		r.off += i
		return n + i, nil
	}

	// There is no more deta left in the curr segment and there are no more
	// segments left.  Return EOF.
	if r.cur+1 >= len(r.segs) {
		return n, io.EOF
	}

	// Move to next segment.
	r.cur++
	r.off = 0
	r.buf.Reset(r.segs[r.cur])
	return n, nil
}

// Size computes the size of the write log.
// We do this by adding the sizes of all the files under the WAL dir.
func (w *WL) Size() (int64, error) {
	return fileutil.DirSize(w.Dir())
}
