package wal

import (
	"errors"
	"fmt"
	"io"
)

// BlockWriter manages a single 32 KB block buffer for writing physical records.
// It handles block boundary padding and flushing complete blocks to an io.Writer.
type BlockWriter struct {
	buf    [WalBlockSize]byte
	offset uint32 // current write position within the block
}

// NewBlockWriter creates a BlockWriter ready to write into a fresh block.
func NewBlockWriter() *BlockWriter {
	return &BlockWriter{}
}

// BlockOffset returns the current write offset within the block (0..WalBlockSize).
func (bw *BlockWriter) BlockOffset() uint32 {
	return bw.offset
}

// WriteRecord writes a single physical record into the block buffer.
// If the record (header + payload) does not fit in the remaining space,
// the current block is padded with zeros and flushed to w, then the record
// is written at the start of a fresh block.
//
// Precondition: payload length must be ≤ WalBlockSize - PhysicalRecordHeaderSize
// (the caller is responsible for splitting large batches into appropriately-sized chunks).
func (bw *BlockWriter) WriteRecord(recType uint8, payload []byte, w io.Writer) error {
	recordSize := PhysicalRecordHeaderSize + len(payload)

	if recordSize > WalBlockSize {
		return fmt.Errorf("wal: record size %d exceeds block size %d",
			recordSize, WalBlockSize)
	}

	// Check if padding is needed before writing this record.
	pad := bw.paddingNeeded()
	if pad > 0 {
		// Pad remaining bytes with zeros and flush.
		if err := bw.flushPadded(w, pad); err != nil {
			return fmt.Errorf("wal: flushing padded block: %w", err)
		}
	}

	// Check if the record fits in the current block.
	remaining := WalBlockSize - bw.offset
	if uint32(recordSize) > remaining {
		// Not enough room — pad the rest and flush, then start a new block.
		pad = int(remaining)
		if err := bw.flushPadded(w, pad); err != nil {
			return fmt.Errorf("wal: flushing partial block: %w", err)
		}
	}

	// Encode physical record directly into the block buffer.
	encoded := EncodePhysicalRecord(recType, payload)
	copy(bw.buf[bw.offset:], encoded)
	bw.offset += uint32(len(encoded))

	// If the block is exactly full, flush it immediately.
	if bw.offset == WalBlockSize {
		if _, err := w.Write(bw.buf[:]); err != nil {
			return fmt.Errorf("wal: writing full block: %w", err)
		}
		bw.offset = 0
	}

	return nil
}

// Flush writes the current block buffer to w, padding unused bytes with zeros.
// If the block is empty (offset == 0), this is a no-op.
func (bw *BlockWriter) Flush(w io.Writer) error {
	if bw.offset == 0 {
		return nil
	}
	return bw.flushPadded(w, int(WalBlockSize-bw.offset))
}

// Reset clears the block buffer, returning it to an empty state.
func (bw *BlockWriter) Reset() {
	bw.offset = 0
	// Zero the buffer so partial blocks are padded with zeros.
	for i := range bw.buf {
		bw.buf[i] = 0
	}
}

// paddingNeeded returns the number of zero-padding bytes required at the current
// block offset. When the remaining space in the block is ≤ PhysicalRecordHeaderSize (7),
// that space cannot hold even a minimal physical record and must be zero-padded.
func (bw *BlockWriter) paddingNeeded() int {
	remaining := WalBlockSize - bw.offset
	if remaining <= PhysicalRecordHeaderSize {
		return int(remaining)
	}
	return 0
}

// flushPadded pads the remaining bytes with zeros and writes the full block to w.
// pad is the number of trailing bytes to zero-fill (WalBlockSize - offset - pad already zero
// from initial state or previous Reset).
func (bw *BlockWriter) flushPadded(w io.Writer, pad int) error {
	if pad <= 0 {
		return nil
	}

	// Zero-fill padding region. The buffer was zeroed at init/reset,
	// but we write explicitly for safety after partial record writes.
	for i := uint32(0); i < uint32(pad); i++ {
		bw.buf[bw.offset+i] = 0
	}

	if _, err := w.Write(bw.buf[:]); err != nil {
		return err
	}

	bw.offset = 0
	for i := range bw.buf {
		bw.buf[i] = 0
	}

	return nil
}

// Bytes returns a copy of the current block contents up to the current offset.
// Useful for testing.
func (bw *BlockWriter) Bytes() []byte {
	out := make([]byte, bw.offset)
	copy(out, bw.buf[:bw.offset])
	return out
}

// FullBlockBytes returns the full block buffer. Only valid when offset == WalBlockSize.
func (bw *BlockWriter) FullBlockBytes() []byte {
	out := make([]byte, WalBlockSize)
	copy(out, bw.buf[:])
	return out
}

// errBlockWriterNil is returned when a nil writer is passed to write operations.
var errBlockWriterNil = errors.New("wal: writer must not be nil")
