package winpwn

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

// This file is the LFH (Low Fragmentation Heap) layer on top of heap.go's
// plain NT Heap decoder -- the layer ROADMAP.md's Phase 9 plan called out
// as the real acceptance test: replace examples/heap_lfh's empirically-
// discovered "free the most recently allocated object" heuristic with a
// structural answer to "which block in the active subsegment is actually
// free right now", read directly off the live process.
//
// Offsets confirmed via `dt ntdll!_LFH_HEAP` and friends on this machine's
// build (10.0.26100) -- same methodology as heap.go, see its top comment.
// Live-validated end to end against examples/heap_lfh.exe itself: spray a
// batch of Notes, free one, calibrate against a known address, and confirm
// ReadLFHSubsegment's BusyBitmap reports exactly that block's slot as free
// and every other allocated slot as busy -- see USAGE.md's heap walkthrough
// for the exact run.

const (
	lfhOffBuckets           = 0x2a4 // [129]_HEAP_BUCKET, 4 bytes each, inside _LFH_HEAP
	lfhOffSegmentInfoArrays = 0x4a8 // [129]Ptr64 _HEAP_LOCAL_SEGMENT_INFO
	lfhBucketCount          = 129
	lfhBucketStride         = 4

	hlsiOffActiveSubsegment = 0x008 // _HEAP_LOCAL_SEGMENT_INFO.ActiveSubsegment

	subsegOffUserBlocks = 0x008 // _HEAP_SUBSEGMENT.UserBlocks
	subsegOffBlockSize  = 0x024 // uint16, granularity units -- same scheme as HeapBucket.BlockUnits, NOT raw bytes (see below)
	subsegOffBlockCount = 0x028 // uint16, blocks in this subsegment

	userDataOffBitmapSize   = 0x020 // uint64, _RTL_BITMAP_EX.SizeOfBitMap (bits)
	userDataOffBitmapBuffer = 0x028 // ptr64, _RTL_BITMAP_EX.Buffer
)

// HeapBucket is one entry of _LFH_HEAP.Buckets -- which fixed block size
// this size class hands out.
type HeapBucket struct {
	Index      int
	BlockUnits uint16 // granularity units; BlockSize() = BlockUnits*HeapEntrySize
	SizeIndex  uint8
	RawFlags   uint8
}

func (b HeapBucket) BlockSize() uint64 { return uint64(b.BlockUnits) * HeapEntrySize }

// ReadLFHBuckets reads every entry of lfhHeapAddr's (a Heap.FrontEndHeap
// pointer) Buckets array.
func ReadLFHBuckets(r io.ReaderAt, lfhHeapAddr uint64) ([lfhBucketCount]HeapBucket, error) {
	var buckets [lfhBucketCount]HeapBucket
	var buf [lfhBucketCount * lfhBucketStride]byte
	if _, err := r.ReadAt(buf[:], int64(lfhHeapAddr)+lfhOffBuckets); err != nil {
		return buckets, fmt.Errorf("reading Buckets array: %w", err)
	}
	for i := 0; i < lfhBucketCount; i++ {
		off := i * lfhBucketStride
		buckets[i] = HeapBucket{
			Index:      i,
			BlockUnits: binary.LittleEndian.Uint16(buf[off : off+2]),
			SizeIndex:  buf[off+2],
			RawFlags:   buf[off+3],
		}
	}
	return buckets, nil
}

// FindLFHBucket returns the smallest bucket that can actually serve a
// wantSize-byte HeapAlloc request once LFH owns this size class. Buckets
// with BlockUnits==0 are unused size classes (LFH only activates a bucket
// index after enough same-size requests) and are skipped.
//
// Confirmed live against examples/heap_lfh.exe, and worth recording
// because the "obvious" version (BlockSize() >= wantSize, no header
// accounted for) is wrong: an LFH block's BlockSize already includes its
// own 16-byte _HEAP_ENTRY-shaped header the same way a plain NT Heap
// entry's does, so a 32-byte Note allocation is actually routed to the
// 48-byte bucket (BlockSize 48, 48-16=32 usable), not the 32-byte one
// (which only has 16 bytes usable after its own header) -- verified by
// reading SegmentInfoArrays directly and seeing which bucket index
// actually had a live subsegment for a heap doing nothing but 32-byte
// allocations.
func FindLFHBucket(buckets [lfhBucketCount]HeapBucket, wantSize uint64) (HeapBucket, error) {
	for _, b := range buckets {
		if b.BlockUnits == 0 {
			continue
		}
		if b.BlockSize() >= wantSize+HeapEntrySize {
			return b, nil
		}
	}
	return HeapBucket{}, fmt.Errorf("no active LFH bucket covers %d bytes (has LFH actually taken over this size class yet?)", wantSize)
}

// ActiveSubsegment returns the address of bucketIndex's currently-active
// _HEAP_SUBSEGMENT -- the subsegment LFH is issuing new blocks from right
// now for that size class. This is the address every grooming attempt
// (examples/heap_lfh's spray loop, structurally) is actually targeting.
func ActiveSubsegment(r io.ReaderAt, lfhHeapAddr uint64, bucketIndex int) (uint64, error) {
	if bucketIndex < 0 || bucketIndex >= lfhBucketCount {
		return 0, fmt.Errorf("bucket index %d out of range [0,%d)", bucketIndex, lfhBucketCount)
	}
	arrAddr := lfhHeapAddr + lfhOffSegmentInfoArrays + uint64(bucketIndex)*8
	segInfoAddr, err := readUint64At(r, int64(arrAddr))
	if err != nil {
		return 0, fmt.Errorf("reading SegmentInfoArrays[%d]: %w", bucketIndex, err)
	}
	if segInfoAddr == 0 {
		return 0, fmt.Errorf("bucket %d has no segment info yet (LFH hasn't allocated from this size class)", bucketIndex)
	}
	subsegAddr, err := readUint64At(r, int64(segInfoAddr)+hlsiOffActiveSubsegment)
	if err != nil {
		return 0, fmt.Errorf("reading ActiveSubsegment: %w", err)
	}
	if subsegAddr == 0 {
		return 0, fmt.Errorf("bucket %d's segment info has no active subsegment", bucketIndex)
	}
	return subsegAddr, nil
}

// LFHSubsegment is a decoded _HEAP_SUBSEGMENT: a fixed-size-block arena LFH
// is handing blocks out of for one bucket. BlockSize/BlockCount/Busy are
// read directly off the subsegment's own bookkeeping with no decoding
// ambiguity; turning slot index into an address needs one calibration step
// first -- see BlockAddress.
type LFHSubsegment struct {
	Addr           uint64
	UserBlocksAddr uint64
	BlockSize      uint64 // bytes; subsegOffBlockSize*HeapEntrySize, see its comment
	BlockCount     int
	Busy           []bool // Busy[i] is slot i's state, read straight from BusyBitmap
}

// ReadLFHSubsegment decodes subsegAddr's _HEAP_SUBSEGMENT, with Busy/Free
// for every slot read directly from the subsegment's BusyBitmap -- the
// structural replacement for examples/heap_lfh's "spray and see which
// leaked address repeats" technique: this answers "which slot is free" by
// reading the allocator's own bookkeeping instead of inferring it from
// outside.
//
// What it deliberately does NOT do: compute slot addresses. The natural
// place for that, _HEAP_USERDATA_HEADER.EncodedOffsets, decoded as plain
// FirstAllocationOffset(u16)|BlockStride(u16), produced a 29025-byte stride
// for a heap doing nothing but 48-byte allocations -- it's genuinely
// encoded (XORed against something derived from
// RtlpLowFragHeapRandomData/RtlpInitializeLfhRandomDataArray per this
// repo's heap/Deterministic_LFH-master reference material), and the key
// wasn't recovered in this pass. Use BlockAddress + CalibrateLFHFirstBlockOffset
// instead: calibrate against any one address you already know (which is
// also just how this kind of exploitation actually works in practice --
// correlating against a leak, not deriving addresses from nothing).
func ReadLFHSubsegment(r io.ReaderAt, subsegAddr uint64) (*LFHSubsegment, error) {
	userBlocksAddr, err := readUint64At(r, int64(subsegAddr)+subsegOffUserBlocks)
	if err != nil {
		return nil, fmt.Errorf("reading UserBlocks: %w", err)
	}
	if userBlocksAddr == 0 {
		return nil, fmt.Errorf("subsegment at 0x%x has no UserBlocks (not yet committed?)", subsegAddr)
	}

	var blockSizeUnits, blockCountBuf [2]byte
	if _, err := r.ReadAt(blockSizeUnits[:], int64(subsegAddr)+subsegOffBlockSize); err != nil {
		return nil, fmt.Errorf("reading BlockSize: %w", err)
	}
	if _, err := r.ReadAt(blockCountBuf[:], int64(subsegAddr)+subsegOffBlockCount); err != nil {
		return nil, fmt.Errorf("reading BlockCount: %w", err)
	}
	blockSize := uint64(binary.LittleEndian.Uint16(blockSizeUnits[:])) * HeapEntrySize
	blockCount := binary.LittleEndian.Uint16(blockCountBuf[:])
	if blockSize == 0 {
		return nil, fmt.Errorf("decoded BlockSize is 0 at 0x%x -- wrong offset or unsupported build (see heap.go's top comment)", subsegAddr)
	}

	bitmapSizeBits, err := readUint64At(r, int64(userBlocksAddr)+userDataOffBitmapSize)
	if err != nil {
		return nil, fmt.Errorf("reading BusyBitmap.SizeOfBitMap: %w", err)
	}
	bitmapBufferAddr, err := readUint64At(r, int64(userBlocksAddr)+userDataOffBitmapBuffer)
	if err != nil {
		return nil, fmt.Errorf("reading BusyBitmap.Buffer: %w", err)
	}
	if bitmapSizeBits < uint64(blockCount) {
		return nil, fmt.Errorf("BusyBitmap covers %d bits but BlockCount is %d", bitmapSizeBits, blockCount)
	}

	busy := make([]bool, blockCount)
	for i := uint16(0); i < blockCount; i++ {
		b, err := readBitmapBit(r, bitmapBufferAddr, uint64(i))
		if err != nil {
			return nil, fmt.Errorf("reading BusyBitmap bit %d: %w", i, err)
		}
		busy[i] = b
	}

	return &LFHSubsegment{
		Addr:           subsegAddr,
		UserBlocksAddr: userBlocksAddr,
		BlockSize:      blockSize,
		BlockCount:     int(blockCount),
		Busy:           busy,
	}, nil
}

// BlockAddress returns the address of slot index i, given firstBlockOffset
// -- the byte offset of slot 0 relative to UserBlocksAddr. Get
// firstBlockOffset from CalibrateLFHFirstBlockOffset once per subsegment;
// it's constant across every slot of the same subsegment.
func (s *LFHSubsegment) BlockAddress(firstBlockOffset uint64, index int) uint64 {
	return s.UserBlocksAddr + firstBlockOffset + uint64(index)*s.BlockSize
}

// SlotOf returns the slot index of knownAddr within s, given an already-
// calibrated firstBlockOffset (see CalibrateLFHFirstBlockOffset), or false
// if knownAddr doesn't land in this subsegment's range at all.
func (s *LFHSubsegment) SlotOf(firstBlockOffset, knownAddr uint64) (index int, ok bool) {
	base := s.UserBlocksAddr + firstBlockOffset
	if knownAddr < base {
		return 0, false
	}
	off := knownAddr - base
	if off%s.BlockSize != 0 {
		return 0, false
	}
	idx := off / s.BlockSize
	if idx >= uint64(s.BlockCount) {
		return 0, false
	}
	return int(idx), true
}

// CalibrateLFHFirstBlockOffset computes BlockAddress's firstBlockOffset
// from one address you already know lies inside this subsegment -- e.g.
// one of your own freshly leaked allocations. _HEAP_USERDATA_HEADER does
// store this value (as part of the encoded EncodedOffsets field) but it's
// genuinely obfuscated and wasn't decoded in this pass; calibrating
// against a known address sidesteps that entirely, and is also simply how
// you'd correlate against a real target in practice.
//
// The +BlockSize matters and was the second real bug found empirically: the
// naive "(addr-UserBlocksAddr) % BlockSize" residue is the right modular
// class but the wrong absolute offset -- it points at a reserved region
// belonging to _HEAP_USERDATA_HEADER itself (one block-size's worth of
// space with no corresponding BusyBitmap bit at all), one full block before
// where Busy[0]'s real address actually is. Confirmed by freeing a known
// address and watching which bit actually flipped: it was the bit for
// address_index-1 under the naive offset, i.e. exactly one block short.
func CalibrateLFHFirstBlockOffset(s *LFHSubsegment, knownBlockAddr uint64) uint64 {
	return (knownBlockAddr-s.UserBlocksAddr)%s.BlockSize + s.BlockSize
}

// AllSubsegments returns the addresses of every non-null _HEAP_SUBSEGMENT
// associated with bucketIndex in lfhHeapAddr's _LFH_HEAP, not just the
// currently active one. The active subsegment is always first (index 0)
// when present; any additional cached/full ones follow in the order they
// appear in the SegmentInfoArrays chain.
//
// LFH doesn't maintain a traditional linked list of subsegments per
// bucket -- it uses _HEAP_LOCAL_SEGMENT_INFO which has a single
// ActiveSubsegment pointer and an optional cached slot. This walk follows
// ActiveSubsegment (via ActiveSubsegment()) and leaves deeper enumeration
// (e.g. walking the CachedItems or InfoArrays of retired subsegments) for
// a future pass where that extra complexity is actually needed by a CTF task.
func AllSubsegments(r io.ReaderAt, lfhHeapAddr uint64, bucketIndex int) ([]uint64, error) {
	if bucketIndex < 0 || bucketIndex >= lfhBucketCount {
		return nil, fmt.Errorf("bucket index %d out of range [0,%d)", bucketIndex, lfhBucketCount)
	}
	arrAddr := lfhHeapAddr + lfhOffSegmentInfoArrays + uint64(bucketIndex)*8
	segInfoAddr, err := readUint64At(r, int64(arrAddr))
	if err != nil {
		return nil, fmt.Errorf("reading SegmentInfoArrays[%d]: %w", bucketIndex, err)
	}
	if segInfoAddr == 0 {
		return nil, nil // bucket not yet activated, not an error
	}

	var subsegAddrs []uint64
	active, err := readUint64At(r, int64(segInfoAddr)+hlsiOffActiveSubsegment)
	if err != nil {
		return nil, fmt.Errorf("reading ActiveSubsegment: %w", err)
	}
	if active != 0 {
		subsegAddrs = append(subsegAddrs, active)
	}

	// _HEAP_LOCAL_SEGMENT_INFO.CachedItems (an array of 2 ptr slots
	// immediately after ActiveSubsegment for this build -- empirically
	// observed at hlsiOffActiveSubsegment+8 and +16; if it's wrong they'll
	// simply be 0 and get skipped).
	for i := 1; i <= 2; i++ {
		cached, err := readUint64At(r, int64(segInfoAddr)+hlsiOffActiveSubsegment+int64(i)*8)
		if err != nil || cached == 0 || cached == active {
			continue
		}
		subsegAddrs = append(subsegAddrs, cached)
	}

	return subsegAddrs, nil
}

// readBitmapBit reads bit index bitIndex of an RTL_BITMAP_EX-style bitmap
// (an array of 64-bit words starting at bufferAddr).
func readBitmapBit(r io.ReaderAt, bufferAddr uint64, bitIndex uint64) (bool, error) {
	word, err := readUint64At(r, int64(bufferAddr)+int64(bitIndex/64)*8)
	if err != nil {
		return false, err
	}
	return (word>>(bitIndex%64))&1 != 0, nil
}
