package pager

import (
	"encoding/binary"
	"fmt"
	"math"
)

// ───────────────────────────────────────────────────────────────────────────
// Binary row codec
// ───────────────────────────────────────────────────────────────────────────
//
// Replaces JSON encoding for row data in B+Tree values.  The format is
// compact, allocation-free on the write path, and ~5–10× faster than
// encoding/json for []any rows with numeric and string values.
//
// Wire format per row:
//   [0:2]  ColumnCount (uint16 LE)
//   For each column:
//     [0]    TypeTag (uint8)
//     [1..]  Payload (variable)
//
// Type tags:
//   0x00 — nil
//   0x01 — bool (1 byte: 0=false, 1=true)
//   0x02 — int64 (8 bytes LE)
//   0x03 — float64 (8 bytes LE)
//   0x04 — string (uint16 LE length prefix + UTF-8)
//   0x05 — []byte (uint16 LE length prefix + raw)
//   0x06 — vector []float64 (uint32 LE element count + count*8 bytes LE)
//   0x07 — long string (uint32 LE length prefix + UTF-8), for len > 65535
//   0x08 — long []byte (uint32 LE length prefix + raw), for len > 65535
//
// Total overhead per row: 2 + N*(1 + payload).  For a typical 3-column row
// (int, string, float64) this is ~30 bytes vs ~60+ for JSON.
//
// Vectors get a dedicated tag so embedding columns round-trip losslessly as
// raw IEEE-754 binary (8 bytes/dim + 5 bytes overhead) instead of degrading
// to a printed string representation.

const (
	tagNil        byte = 0x00
	tagBool       byte = 0x01
	tagInt64      byte = 0x02
	tagFloat64    byte = 0x03
	tagString     byte = 0x04
	tagBytes      byte = 0x05
	tagVecF64     byte = 0x06
	tagLongString byte = 0x07
	tagLongBytes  byte = 0x08
)

// MaxValueBytes is the defensive decoder bound for one variable-length row
// field. SQL writes are capped earlier by engine.MaxBlobBytes; this second
// boundary keeps damaged on-disk lengths from allocating excessive memory.
const MaxValueBytes = 64 << 20

// MarshalRow encodes a row into the compact binary format.
// It reuses the provided buf if large enough.
func MarshalRow(row []any, buf []byte) []byte {
	// Estimate size: header + per column (1 tag + 8 payload avg).
	est := 2 + len(row)*9
	if cap(buf) >= est {
		buf = buf[:0]
	} else {
		buf = make([]byte, 0, est)
	}

	// Column count.
	var hdr [2]byte
	binary.LittleEndian.PutUint16(hdr[:], uint16(len(row)))
	buf = append(buf, hdr[:]...)

	for _, v := range row {
		switch val := v.(type) {
		case nil:
			buf = append(buf, tagNil)
		case bool:
			buf = append(buf, tagBool)
			if val {
				buf = append(buf, 1)
			} else {
				buf = append(buf, 0)
			}
		case int:
			buf = append(buf, tagInt64)
			var b [8]byte
			binary.LittleEndian.PutUint64(b[:], uint64(int64(val)))
			buf = append(buf, b[:]...)
		case int64:
			buf = append(buf, tagInt64)
			var b [8]byte
			binary.LittleEndian.PutUint64(b[:], uint64(val))
			buf = append(buf, b[:]...)
		case float64:
			buf = append(buf, tagFloat64)
			var b [8]byte
			binary.LittleEndian.PutUint64(b[:], math.Float64bits(val))
			buf = append(buf, b[:]...)
		case string:
			buf = appendTaggedString(buf, val)
		case []byte:
			if len(val) > 0xFFFF {
				buf = append(buf, tagLongBytes)
				var b [4]byte
				binary.LittleEndian.PutUint32(b[:], uint32(len(val)))
				buf = append(buf, b[:]...)
			} else {
				buf = append(buf, tagBytes)
				var b [2]byte
				binary.LittleEndian.PutUint16(b[:], uint16(len(val)))
				buf = append(buf, b[:]...)
			}
			buf = append(buf, val...)
		case []float64:
			buf = append(buf, tagVecF64)
			var b [4]byte
			binary.LittleEndian.PutUint32(b[:], uint32(len(val)))
			buf = append(buf, b[:]...)
			for _, f := range val {
				var e [8]byte
				binary.LittleEndian.PutUint64(e[:], math.Float64bits(f))
				buf = append(buf, e[:]...)
			}
		default:
			// Fallback: store as string representation.
			buf = appendTaggedString(buf, fmt.Sprint(val))
		}
	}
	return buf
}

// appendTaggedString appends a string column, switching to the uint32-length
// tag when the value exceeds the uint16 range (which would otherwise be
// silently truncated).
func appendTaggedString(buf []byte, s string) []byte {
	if len(s) > 0xFFFF {
		buf = append(buf, tagLongString)
		var b [4]byte
		binary.LittleEndian.PutUint32(b[:], uint32(len(s)))
		buf = append(buf, b[:]...)
	} else {
		buf = append(buf, tagString)
		var b [2]byte
		binary.LittleEndian.PutUint16(b[:], uint16(len(s)))
		buf = append(buf, b[:]...)
	}
	return append(buf, s...)
}

// UnmarshalRow decodes a row from the compact binary format.
func UnmarshalRow(data []byte) ([]any, error) {
	return unmarshalRow(data, true)
}

// unmarshalRow decodes a complete row. When copyBytes is false, BLOB columns
// alias query-owned data; callers must never use that mode for an inline view
// into a pinned pager page.
func unmarshalRow(data []byte, copyBytes bool) ([]any, error) {
	if len(data) < 2 {
		return nil, fmt.Errorf("row data too short")
	}
	colCount := int(binary.LittleEndian.Uint16(data[:2]))
	off := 2
	row := make([]any, colCount)

	for i := 0; i < colCount; i++ {
		if off >= len(data) {
			return nil, fmt.Errorf("unexpected end of row at column %d", i)
		}
		tag := data[off]
		off++

		switch tag {
		case tagNil:
			row[i] = nil
		case tagBool:
			if off >= len(data) {
				return nil, fmt.Errorf("truncated bool at column %d", i)
			}
			row[i] = data[off] != 0
			off++
		case tagInt64:
			if off+8 > len(data) {
				return nil, fmt.Errorf("truncated int64 at column %d", i)
			}
			row[i] = float64(int64(binary.LittleEndian.Uint64(data[off : off+8])))
			off += 8
		case tagFloat64:
			if off+8 > len(data) {
				return nil, fmt.Errorf("truncated float64 at column %d", i)
			}
			row[i] = math.Float64frombits(binary.LittleEndian.Uint64(data[off : off+8]))
			off += 8
		case tagString:
			if off+2 > len(data) {
				return nil, fmt.Errorf("truncated string len at column %d", i)
			}
			slen := int(binary.LittleEndian.Uint16(data[off : off+2]))
			off += 2
			if off+slen > len(data) {
				return nil, fmt.Errorf("truncated string data at column %d", i)
			}
			row[i] = string(data[off : off+slen])
			off += slen
		case tagBytes:
			if off+2 > len(data) {
				return nil, fmt.Errorf("truncated bytes len at column %d", i)
			}
			blen := int(binary.LittleEndian.Uint16(data[off : off+2]))
			off += 2
			if off+blen > len(data) {
				return nil, fmt.Errorf("truncated bytes data at column %d", i)
			}
			if copyBytes {
				dst := make([]byte, blen)
				copy(dst, data[off:off+blen])
				row[i] = dst
			} else {
				row[i] = data[off : off+blen]
			}
			off += blen
		case tagVecF64:
			if off+4 > len(data) {
				return nil, fmt.Errorf("truncated vector len at column %d", i)
			}
			n := int(binary.LittleEndian.Uint32(data[off : off+4]))
			off += 4
			if n < 0 || off+n*8 > len(data) {
				return nil, fmt.Errorf("truncated vector data at column %d", i)
			}
			vec := make([]float64, n)
			for j := 0; j < n; j++ {
				vec[j] = math.Float64frombits(binary.LittleEndian.Uint64(data[off : off+8]))
				off += 8
			}
			row[i] = vec
		case tagLongString:
			if off+4 > len(data) {
				return nil, fmt.Errorf("truncated long string len at column %d", i)
			}
			slen := int(binary.LittleEndian.Uint32(data[off : off+4]))
			off += 4
			if slen < 0 || slen > MaxValueBytes || off+slen > len(data) {
				return nil, fmt.Errorf("truncated long string data at column %d", i)
			}
			row[i] = string(data[off : off+slen])
			off += slen
		case tagLongBytes:
			if off+4 > len(data) {
				return nil, fmt.Errorf("truncated long bytes len at column %d", i)
			}
			blen := int(binary.LittleEndian.Uint32(data[off : off+4]))
			off += 4
			if blen < 0 || blen > MaxValueBytes || off+blen > len(data) {
				return nil, fmt.Errorf("truncated long bytes data at column %d", i)
			}
			if copyBytes {
				dst := make([]byte, blen)
				copy(dst, data[off:off+blen])
				row[i] = dst
			} else {
				row[i] = data[off : off+blen]
			}
			off += blen
		default:
			return nil, fmt.Errorf("unknown tag 0x%02x at column %d", tag, i)
		}
	}
	if off != len(data) {
		return nil, fmt.Errorf("trailing data after row: %d bytes", len(data)-off)
	}
	return row, nil
}

// UnmarshalRowColumn decodes only one requested column. Columns before it are
// bounds-checked and skipped without constructing interface values. This is
// useful for typed index readers that need one BLOB or identifier from a wide
// row and have already validated the complete immutable artifact at open.
func UnmarshalRowColumn(data []byte, column int) (any, error) {
	return unmarshalRowColumn(data, column, true)
}

// unmarshalOwnedRowColumn decodes a column from query-owned row memory. BLOBs
// may alias data because the caller transfers ownership of the complete row to
// the returned value; this removes a second payload-sized copy on large-value
// serving paths. It must never be used with a view into a pinned pager page.
func unmarshalOwnedRowColumn(data []byte, column int) (any, error) {
	return unmarshalRowColumn(data, column, false)
}

func unmarshalRowColumn(data []byte, column int, copyBytes bool) (any, error) {
	if len(data) < 2 {
		return nil, fmt.Errorf("row data too short")
	}
	colCount := int(binary.LittleEndian.Uint16(data[:2]))
	if column < 0 || column >= colCount {
		return nil, fmt.Errorf("row column %d outside [0,%d)", column, colCount)
	}
	off := 2
	for i := 0; i <= column; i++ {
		if off >= len(data) {
			return nil, fmt.Errorf("unexpected end of row at column %d", i)
		}
		tag := data[off]
		off++
		want := i == column
		switch tag {
		case tagNil:
			if want {
				return nil, nil
			}
		case tagBool:
			if off >= len(data) {
				return nil, fmt.Errorf("truncated bool at column %d", i)
			}
			value := data[off] != 0
			off++
			if want {
				return value, nil
			}
		case tagInt64:
			if off+8 > len(data) {
				return nil, fmt.Errorf("truncated int64 at column %d", i)
			}
			value := float64(int64(binary.LittleEndian.Uint64(data[off : off+8])))
			off += 8
			if want {
				return value, nil
			}
		case tagFloat64:
			if off+8 > len(data) {
				return nil, fmt.Errorf("truncated float64 at column %d", i)
			}
			value := math.Float64frombits(binary.LittleEndian.Uint64(data[off : off+8]))
			off += 8
			if want {
				return value, nil
			}
		case tagString, tagBytes:
			if off+2 > len(data) {
				return nil, fmt.Errorf("truncated value len at column %d", i)
			}
			length := int(binary.LittleEndian.Uint16(data[off : off+2]))
			off += 2
			if off+length > len(data) {
				return nil, fmt.Errorf("truncated value data at column %d", i)
			}
			payload := data[off : off+length]
			off += length
			if want {
				if tag == tagString {
					return string(payload), nil
				}
				if copyBytes {
					return append([]byte(nil), payload...), nil
				}
				return payload, nil
			}
		case tagVecF64:
			if off+4 > len(data) {
				return nil, fmt.Errorf("truncated vector len at column %d", i)
			}
			n := int(binary.LittleEndian.Uint32(data[off : off+4]))
			off += 4
			if n < 0 || off+n*8 > len(data) {
				return nil, fmt.Errorf("truncated vector data at column %d", i)
			}
			if want {
				vector := make([]float64, n)
				for j := range vector {
					vector[j] = math.Float64frombits(binary.LittleEndian.Uint64(data[off+j*8 : off+(j+1)*8]))
				}
				return vector, nil
			}
			off += n * 8
		case tagLongString, tagLongBytes:
			if off+4 > len(data) {
				return nil, fmt.Errorf("truncated long value len at column %d", i)
			}
			length := int(binary.LittleEndian.Uint32(data[off : off+4]))
			off += 4
			if length < 0 || length > MaxValueBytes || off+length > len(data) {
				return nil, fmt.Errorf("truncated long value data at column %d", i)
			}
			payload := data[off : off+length]
			off += length
			if want {
				if tag == tagLongString {
					return string(payload), nil
				}
				if copyBytes {
					return append([]byte(nil), payload...), nil
				}
				return payload, nil
			}
		default:
			return nil, fmt.Errorf("unknown tag 0x%02x at column %d", tag, i)
		}
	}
	return nil, fmt.Errorf("row column %d was not decoded", column)
}
