package wal

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
)

// WalBatch represents a single WAL batch — the physical persistence unit.
// A batch consists of an 18-byte header followed by a contiguous Entries region.
type WalBatch struct {
	Flags        uint16
	BaseSequence uint64
	EntryCount   uint32
	EntriesSize  uint32
	Entries      []byte
}

// EncodeWalBatch encodes a WAL batch from the given base sequence and entries.
// The returned byte slice is: BatchHeader(18) + encoded entries.
func EncodeWalBatch(baseSequence uint64, entries []*WalEntry) ([]byte, error) {
	if len(entries) == 0 {
		return nil, errors.New("wal: batch requires at least one entry")
	}
	if uint32(len(entries)) > MaxWalBatchEntryCount {
		return nil, fmt.Errorf("wal: entry count %d exceeds maximum %d", len(entries), MaxWalBatchEntryCount)
	}

	// Encode all entries.
	var entriesBuf bytes.Buffer
	for i, e := range entries {
		encoded, err := EncodeEntry(e)
		if err != nil {
			return nil, fmt.Errorf("wal: encoding entry %d: %w", i, err)
		}
		entriesBuf.Write(encoded)
	}

	entriesSize := uint32(entriesBuf.Len())
	if entriesSize > MaxWalBatchEntriesSize {
		return nil, fmt.Errorf("wal: entries size %d exceeds maximum %d", entriesSize, MaxWalBatchEntriesSize)
	}

	// Build batch: header(18) + entries.
	buf := make([]byte, WalBatchHeaderSize+entriesSize)
	binary.LittleEndian.PutUint16(buf[0:2], 0)          // flags
	binary.LittleEndian.PutUint64(buf[2:10], baseSequence)
	binary.LittleEndian.PutUint32(buf[10:14], uint32(len(entries)))
	binary.LittleEndian.PutUint32(buf[14:18], entriesSize)
	copy(buf[WalBatchHeaderSize:], entriesBuf.Bytes())

	return buf, nil
}

// DecodeWalBatch decodes a WAL batch from raw bytes.
// The data must contain the full batch (header + entries).
func DecodeWalBatch(data []byte) (*WalBatch, error) {
	if len(data) < WalBatchHeaderSize {
		return nil, fmt.Errorf("wal: batch data too short: %d < %d", len(data), WalBatchHeaderSize)
	}

	flags := binary.LittleEndian.Uint16(data[0:2])
	baseSeq := binary.LittleEndian.Uint64(data[2:10])
	entryCount := binary.LittleEndian.Uint32(data[10:14])
	entriesSize := binary.LittleEndian.Uint32(data[14:18])

	if entryCount == 0 {
		return nil, errors.New("wal: batch entry count is zero")
	}
	if entryCount > MaxWalBatchEntryCount {
		return nil, fmt.Errorf("wal: entry count %d exceeds maximum %d", entryCount, MaxWalBatchEntryCount)
	}
	if entriesSize > MaxWalBatchEntriesSize {
		return nil, fmt.Errorf("wal: entries size %d exceeds maximum %d", entriesSize, MaxWalBatchEntriesSize)
	}

	expectedLen := WalBatchHeaderSize + entriesSize
	if uint32(len(data)) < expectedLen {
		return nil, fmt.Errorf("wal: entries size mismatch: header says %d bytes, have %d bytes after header",
			entriesSize, len(data)-WalBatchHeaderSize)
	}

	entries := make([]byte, entriesSize)
	copy(entries, data[WalBatchHeaderSize:WalBatchHeaderSize+entriesSize])

	return &WalBatch{
		Flags:        flags,
		BaseSequence: baseSeq,
		EntryCount:   entryCount,
		EntriesSize:  entriesSize,
		Entries:      entries,
	}, nil
}

// FragmentState represents the state of the fragment collector state machine.
type FragmentState uint8

const (
	// FragmentIdle means no fragments are being collected.
	FragmentIdle FragmentState = 0
	// FragmentCollecting means fragments are being accumulated.
	FragmentCollecting FragmentState = 1
)

// maxFragmentBufferSize is the maximum total bytes the fragment buffer can hold:
// Batch Header size + max entries size.
var maxFragmentBufferSize = uint32(WalBatchHeaderSize) + MaxWalBatchEntriesSize

// FragmentCollector reassembles WAL batches from physical record fragments.
// The state machine transitions between Idle and Collecting based on the
// record type (Full, First, Middle, Last).
type FragmentCollector struct {
	state FragmentState
	buf   bytes.Buffer
}

// NewFragmentCollector creates a new FragmentCollector in Idle state.
func NewFragmentCollector() *FragmentCollector {
	return &FragmentCollector{
		state: FragmentIdle,
	}
}

// Reset clears all collected data and returns the collector to Idle state.
func (fc *FragmentCollector) Reset() {
	fc.state = FragmentIdle
	fc.buf.Reset()
}

// Append feeds a physical record fragment to the collector.
// The state machine enforces valid transitions:
//   - Idle + Full   → collect payload, stay Idle (complete)
//   - Idle + First  → collect payload, → Collecting
//   - Idle + Middle → error
//   - Idle + Last   → error
//   - Collecting + Middle → collect payload
//   - Collecting + Last   → collect payload, → Idle (complete)
//   - Collecting + Full   → error
//   - Collecting + First  → error
func (fc *FragmentCollector) Append(recType uint8, payload []byte) error {
	switch fc.state {
	case FragmentIdle:
		switch recType {
		case RecFull:
			// Complete batch in one record.
			if err := fc.checkBufferCapacity(len(payload)); err != nil {
				return err
			}
			fc.buf.Write(payload)
			// State stays Idle — batch is complete.
			return nil
		case RecFirst:
			// Start collecting fragments.
			if err := fc.checkBufferCapacity(len(payload)); err != nil {
				return err
			}
			fc.buf.Write(payload)
			fc.state = FragmentCollecting
			return nil
		case RecMiddle, RecLast:
			return fmt.Errorf("wal: unexpected fragment type %d in Idle state", recType)
		default:
			return fmt.Errorf("wal: invalid fragment type %d", recType)
		}

	case FragmentCollecting:
		switch recType {
		case RecMiddle:
			if err := fc.checkBufferCapacity(len(payload)); err != nil {
				return err
			}
			fc.buf.Write(payload)
			return nil
		case RecLast:
			if err := fc.checkBufferCapacity(len(payload)); err != nil {
				return err
			}
			fc.buf.Write(payload)
			fc.state = FragmentIdle
			return nil
		case RecFull, RecFirst:
			return fmt.Errorf("wal: unexpected fragment type %d in Collecting state", recType)
		default:
			return fmt.Errorf("wal: invalid fragment type %d", recType)
		}

	default:
		return fmt.Errorf("wal: invalid fragment collector state %d", fc.state)
	}
}

// IsComplete reports whether a complete batch has been collected.
// A batch is complete when the state returns to Idle after collecting data.
func (fc *FragmentCollector) IsComplete() bool {
	return fc.state == FragmentIdle && fc.buf.Len() > 0
}

// BatchData returns the collected batch bytes. Only valid when IsComplete() is true.
func (fc *FragmentCollector) BatchData() []byte {
	return fc.buf.Bytes()
}

// State returns the current fragment collector state.
func (fc *FragmentCollector) State() FragmentState {
	return fc.state
}

// checkBufferCapacity ensures the total collected bytes do not exceed the limit.
func (fc *FragmentCollector) checkBufferCapacity(additional int) error {
	newSize := uint32(fc.buf.Len() + additional)
	if newSize > maxFragmentBufferSize {
		return fmt.Errorf("wal: fragment buffer size %d exceeds maximum %d", newSize, maxFragmentBufferSize)
	}
	return nil
}
