//go:build windows

package winpwn

import (
	"fmt"
	"runtime"
	"sync"
	"unsafe"

	"golang.org/x/sys/windows"
)

// This file is winpwn's debugger backend -- the Windows analogue of
// pwntools' gdb module. There is no GDB/ptrace equivalent on Windows, but
// the Windows Debug API (DebugActiveProcess/WaitForDebugEvent/
// ContinueDebugEvent/Get-SetThreadContext) gives the same capability
// natively, and golang.org/x/sys/windows doesn't wrap any of it -- every
// proc below is resolved by hand via LazyDLL, the same escape hatch
// pipe_windows.go would have needed if CreateNamedPipe weren't already
// exposed there.
//
// x64dbg/WinDbg attach was considered and rejected for the actual event
// loop: scripting a GUI debugger from Go would mean driving it through its
// command pipe/plugin API (x64dbg) or shelling out to cdb scripts (WinDbg),
// neither of which gives a typed Go channel of events or direct register
// access -- it would be strictly less capable than calling the same Win32
// API Microsoft's own debuggers are built on. If you want the GUI, attach
// x64dbg to the PID this package reports separately; this backend is for
// scripted/automated control, the same role pwntools' gdb.attach() plays
// when used non-interactively.

const (
	debugExceptionEvent     = 1
	debugCreateThreadEvent  = 2
	debugCreateProcessEvent = 3
	debugExitThreadEvent    = 4
	debugExitProcessEvent   = 5
	debugLoadDllEvent       = 6
	debugUnloadDllEvent     = 7
	debugOutputStringEvent  = 8
	debugRipEvent           = 9
)

const (
	// DBG_CONTINUE / DBG_EXCEPTION_NOT_HANDLED, the two dwContinueStatus
	// values ContinueDebugEvent actually distinguishes -- the rest of
	// NTSTATUS-space is accepted but treated as one or the other by the OS.
	dbgContinue            = 0x00010002
	dbgExceptionNotHandled = 0x80010001

	exceptionBreakpoint      = 0x80000003
	exceptionSingleStep      = 0x80000004
	exceptionAccessViolation = 0xC0000005

	threadAccessForDebug = windows.THREAD_GET_CONTEXT | windows.THREAD_SET_CONTEXT | windows.THREAD_SUSPEND_RESUME | 0x40 /* THREAD_QUERY_INFORMATION */

	eflagsTrapFlag = 0x100

	contextAMD64          = 0x00100000
	contextControl        = contextAMD64 | 0x1
	contextInteger        = contextAMD64 | 0x2
	contextSegments       = contextAMD64 | 0x4
	contextFloatingPoint  = contextAMD64 | 0x8
	contextDebugRegisters = contextAMD64 | 0x10
	contextFull           = contextControl | contextInteger | contextFloatingPoint
)

var (
	modKernel32                   = windows.NewLazySystemDLL("kernel32.dll")
	procWaitForDebugEvent         = modKernel32.NewProc("WaitForDebugEvent")
	procContinueDebugEvent        = modKernel32.NewProc("ContinueDebugEvent")
	procDebugActiveProcess        = modKernel32.NewProc("DebugActiveProcess")
	procDebugActiveProcessStop    = modKernel32.NewProc("DebugActiveProcessStop")
	procDebugSetProcessKillOnExit = modKernel32.NewProc("DebugSetProcessKillOnExit")
	procGetThreadContext          = modKernel32.NewProc("GetThreadContext")
	procSetThreadContext          = modKernel32.NewProc("SetThreadContext")
	procFlushInstructionCache     = modKernel32.NewProc("FlushInstructionCache")
)

// contextX64 mirrors WinNT.h's x64 CONTEXT struct field-for-field. Verified
// by reading back a real thread's context and cross-checking Rip/Rsp
// against a suspended process's known loader-thunk start address (see
// debugger_windows_test.go) -- the same "don't trust a hand-derived struct
// layout, prove it against a real target" rule minidump.go's notes already
// called out, just for a struct the debugger actually *writes*, not only
// decodes, where getting it wrong would corrupt the debuggee's registers
// instead of just misreading a file.
//
// MSDN's remarks for CONTEXT mention 16-byte alignment in the context of
// DECLSPEC_ALIGN(16); tested directly against a real suspended process
// (scratch probe, kept out of the repo) with both a manually-aligned buffer
// and a plain `&contextX64{}` -- both returned identical, correct Rip/Rsp
// from a real GetThreadContext call, so the plain allocation is what's used
// here. If a future Windows build ever proves that wrong, this is the first
// place to look.
type contextX64 struct {
	P1Home, P2Home, P3Home, P4Home, P5Home, P6Home uint64

	ContextFlags uint32
	MxCsr        uint32

	SegCs, SegDs, SegEs, SegFs, SegGs, SegSs uint16
	EFlags                                   uint32

	Dr0, Dr1, Dr2, Dr3, Dr6, Dr7 uint64

	Rax, Rcx, Rdx, Rbx, Rsp, Rbp, Rsi, Rdi uint64
	R8, R9, R10, R11, R12, R13, R14, R15   uint64
	Rip                                    uint64

	FltSave        [512]byte // union of XMM_SAVE_AREA32 with the legacy/XMM register view; opaque here, we only need correct byte width
	VectorRegister [416]byte // M128A VectorRegister[26]
	VectorControl  uint64

	DebugControl, LastBranchToRip, LastBranchFromRip, LastExceptionToRip, LastExceptionFromRip uint64
}

func getThreadContext(th windows.Handle, ctx *contextX64) error {
	ctx.ContextFlags = contextFull | contextDebugRegisters | contextSegments
	r, _, err := procGetThreadContext.Call(uintptr(th), uintptr(unsafe.Pointer(ctx)))
	if r == 0 {
		return fmt.Errorf("GetThreadContext: %w", err)
	}
	return nil
}

func setThreadContext(th windows.Handle, ctx *contextX64) error {
	r, _, err := procSetThreadContext.Call(uintptr(th), uintptr(unsafe.Pointer(ctx)))
	if r == 0 {
		return fmt.Errorf("SetThreadContext: %w", err)
	}
	return nil
}

// Registers is the clean, public register view -- contextX64's FltSave/
// VectorRegister padding is real but nobody scripting an exploit wants to
// see it, the same reasoning RawStream exists in minidump.go for parts of a
// format not worth decoding into a friendly type.
type Registers struct {
	Rax, Rcx, Rdx, Rbx, Rsp, Rbp, Rsi, Rdi uint64
	R8, R9, R10, R11, R12, R13, R14, R15   uint64
	Rip, EFlags                            uint64
}

func registersFromContext(ctx *contextX64) Registers {
	return Registers{
		Rax: ctx.Rax, Rcx: ctx.Rcx, Rdx: ctx.Rdx, Rbx: ctx.Rbx,
		Rsp: ctx.Rsp, Rbp: ctx.Rbp, Rsi: ctx.Rsi, Rdi: ctx.Rdi,
		R8: ctx.R8, R9: ctx.R9, R10: ctx.R10, R11: ctx.R11,
		R12: ctx.R12, R13: ctx.R13, R14: ctx.R14, R15: ctx.R15,
		Rip: ctx.Rip, EFlags: uint64(ctx.EFlags),
	}
}

func applyRegistersToContext(r Registers, ctx *contextX64) {
	ctx.Rax, ctx.Rcx, ctx.Rdx, ctx.Rbx = r.Rax, r.Rcx, r.Rdx, r.Rbx
	ctx.Rsp, ctx.Rbp, ctx.Rsi, ctx.Rdi = r.Rsp, r.Rbp, r.Rsi, r.Rdi
	ctx.R8, ctx.R9, ctx.R10, ctx.R11 = r.R8, r.R9, r.R10, r.R11
	ctx.R12, ctx.R13, ctx.R14, ctx.R15 = r.R12, r.R13, r.R14, r.R15
	ctx.Rip = r.Rip
	ctx.EFlags = uint32(r.EFlags)
}

// exceptionRecord mirrors EXCEPTION_RECORD (the pointer-width-dependent
// version winbase.h's DEBUG_EVENT actually embeds, not EXCEPTION_RECORD64).
type exceptionRecord struct {
	Code        uint32
	Flags       uint32
	Record      uint64
	Address     uint64
	NumParams   uint32
	_           uint32
	Information [15]uint64
}

type exceptionDebugInfo struct {
	Record      exceptionRecord
	FirstChance uint32
}

type createProcessDebugInfo struct {
	HFile               windows.Handle
	HProcess            windows.Handle
	HThread             windows.Handle
	LpBaseOfImage       uint64
	DebugInfoFileOffset uint32
	DebugInfoSize       uint32
	ThreadLocalBase     uint64
	StartAddress        uint64
	ImageName           uint64
	Unicode             uint16
}

type createThreadDebugInfo struct {
	HThread         windows.Handle
	ThreadLocalBase uint64
	StartAddress    uint64
}

type exitDebugInfo struct {
	ExitCode uint32
}

type loadDllDebugInfo struct {
	HFile               windows.Handle
	LpBaseOfDll         uint64
	DebugInfoFileOffset uint32
	DebugInfoSize       uint32
	ImageName           uint64
	Unicode             uint16
}

type outputDebugStringInfo struct {
	LpDebugStringData uint64
	Unicode           uint16
	Length            uint16
}

// rawDebugEvent is DEBUG_EVENT: a 12-byte header (code/pid/tid) followed by
// a union of per-event-kind payloads. Rather than reproduce the union as a
// Go union-of-structs (Go has none), U is sized generously above every real
// member (the largest, EXCEPTION_DEBUG_INFO, is 160 bytes) and reinterpreted
// through unsafe.Pointer into the specific struct decodeEvent expects for
// that Code -- exactly the same "raw bytes, typed view on demand" approach
// minidump.go uses for stream payloads it doesn't always want to fully decode.
type rawDebugEvent struct {
	Code      uint32
	ProcessID uint32
	ThreadID  uint32
	_         uint32
	U         [216]byte
}

// DebugEventKind classifies a DebugEvent for a switch in caller code, the
// winpwn analogue of pwntools' gdb continuing past whatever GDB/MI reports.
type DebugEventKind int

const (
	EventBreakpoint DebugEventKind = iota
	EventSingleStep
	EventException
	EventCreateProcess
	EventCreateThread
	EventExitThread
	EventExitProcess
	EventLoadDll
	EventUnloadDll
	EventOutputDebugString
	EventUnknown
)

func (k DebugEventKind) String() string {
	switch k {
	case EventBreakpoint:
		return "breakpoint"
	case EventSingleStep:
		return "single-step"
	case EventException:
		return "exception"
	case EventCreateProcess:
		return "create-process"
	case EventCreateThread:
		return "create-thread"
	case EventExitThread:
		return "exit-thread"
	case EventExitProcess:
		return "exit-process"
	case EventLoadDll:
		return "load-dll"
	case EventUnloadDll:
		return "unload-dll"
	case EventOutputDebugString:
		return "output-debug-string"
	default:
		return "unknown"
	}
}

// DebugEvent is one decoded WaitForDebugEvent result, delivered over
// (*Debugger).Events(). Exactly one of Addr/ExitCode/Message is meaningful,
// depending on Kind -- see the Kind-specific field comments.
type DebugEvent struct {
	Kind     DebugEventKind
	ThreadID uint32

	Code        uint32  // exception code, for EventException/EventBreakpoint/EventSingleStep
	Addr        uintptr // exception/breakpoint address, or DLL base for Load/UnloadDll
	FirstChance bool

	ExitCode uint32 // for EventExitThread/EventExitProcess

	Message string // DLL path for EventLoadDll, or the string itself for EventOutputDebugString

	// status is the dwContinueStatus Continue should use for this event,
	// decided at decode time: DBG_CONTINUE for everything except a genuine
	// (non-breakpoint, non-our-own-single-step) exception, where it's
	// DBG_EXCEPTION_NOT_HANDLED so a real crash actually terminates/reports
	// instead of being fed back to the debuggee forever.
	status uint32
}

type continueRequest struct {
	threadID uint32
	status   uint32
}

// Debugger wraps a debuggee under control of the Windows Debug API --
// DebugActiveProcess/WaitForDebugEvent/ContinueDebugEvent underneath,
// software breakpoints (INT3 patching) and register/memory access on top.
// The winpwn analogue of a pwntools gdb.Gdb handle, except there's no GDB
// process in the loop: this talks to the same kernel debug object Microsoft's
// own debuggers use.
//
// Get one via Attach(pid) for an already-running (or CREATE_SUSPENDED, not
// yet resumed) process -- compose with SpawnSuspended/ResumeMainThread from
// procmem_windows.go to debug a target from its very first instruction:
//
//	tube, pid, _ := winpwn.SpawnSuspended(target)
//	dbg, _ := winpwn.Attach(pid)
//	winpwn.ResumeMainThread(pid)
//	for ev := range dbg.Events() { ... dbg.Continue(ev) }
//
// That reuses SpawnSuspended/ResumeMainThread instead of this file
// reimplementing CreateProcess+pipe plumbing a second time -- Attach is the
// only entry point on purpose.
type Debugger struct {
	PID     uint32
	process windows.Handle

	breakpoints map[uintptr]byte
	bpMu        sync.Mutex

	events chan DebugEvent
	resume chan continueRequest

	closed    chan struct{}
	closeOnce sync.Once
	closeErr  error
}

// Attach starts debugging an already-existing process (DebugActiveProcess),
// the entry point for this whole file. The OS ties a debug session to the
// specific thread that called DebugActiveProcess -- not just the process --
// so this spawns a dedicated goroutine, pins it to one OS thread for the
// rest of the session via runtime.LockOSThread (never unlocked: the thread
// is retired along with the goroutine when the session ends), and runs the
// entire WaitForDebugEvent/ContinueDebugEvent loop on that one thread.
// Confirmed empirically while building this: calling WaitForDebugEvent from
// any other thread after DebugActiveProcess silently never sees events for
// this process, exactly as the "only the attaching thread" documentation
// says -- there is no error returned, just a hang, which is why
// runtime.LockOSThread isn't optional here.
func Attach(pid uint32) (*Debugger, error) {
	type attachResult struct {
		d   *Debugger
		err error
	}
	resultCh := make(chan attachResult, 1)

	go func() {
		runtime.LockOSThread()

		r, _, err := procDebugActiveProcess.Call(uintptr(pid))
		if r == 0 {
			resultCh <- attachResult{err: fmt.Errorf("DebugActiveProcess(%d): %w", pid, err)}
			runtime.UnlockOSThread()
			return
		}
		// Don't take the debuggee down with us if this process exits/crashes
		// without a clean Detach -- the default on modern Windows is to kill
		// it, which is surprising for "attach to something already running".
		procDebugSetProcessKillOnExit.Call(0)

		proc, oerr := windows.OpenProcess(
			windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ|windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION,
			false, pid)
		if oerr != nil {
			procDebugActiveProcessStop.Call(uintptr(pid))
			resultCh <- attachResult{err: fmt.Errorf("OpenProcess(%d): %w", pid, oerr)}
			runtime.UnlockOSThread()
			return
		}

		d := &Debugger{
			PID:         pid,
			process:     proc,
			breakpoints: make(map[uintptr]byte),
			events:      make(chan DebugEvent),
			resume:      make(chan continueRequest),
			closed:      make(chan struct{}),
		}
		resultCh <- attachResult{d: d}

		d.eventLoop()
		runtime.UnlockOSThread()
	}()

	r := <-resultCh
	return r.d, r.err
}

// eventLoop runs for the lifetime of the debug session, on the single OS
// thread Attach locked for it. It decodes each raw event, hands it to
// Events(), blocks until the caller's Continue(ev) arrives on d.resume, then
// (for a software breakpoint the caller set) transparently steps past the
// patched INT3 before actually resuming -- see stepPastBreakpoint.
func (d *Debugger) eventLoop() {
	defer close(d.events)
	defer windows.CloseHandle(d.process)

	for {
		var raw rawDebugEvent
		r, _, _ := procWaitForDebugEvent.Call(uintptr(unsafe.Pointer(&raw)), uintptr(windows.INFINITE))
		if r == 0 {
			return
		}

		ev := d.decodeEvent(&raw)

		select {
		case d.events <- ev:
		case <-d.closed:
			return
		}

		var req continueRequest
		select {
		case req = <-d.resume:
		case <-d.closed:
			return
		}

		if ev.Kind == EventBreakpoint {
			d.bpMu.Lock()
			orig, known := d.breakpoints[ev.Addr]
			d.bpMu.Unlock()
			if known {
				if err := d.stepPastBreakpoint(ev.ThreadID, ev.Addr, orig); err != nil {
					Warn("debugger: stepping past breakpoint at 0x%x: %v", ev.Addr, err)
				}
			}
		}

		procContinueDebugEvent.Call(uintptr(d.PID), uintptr(ev.ThreadID), uintptr(req.status))

		if ev.Kind == EventExitProcess {
			return
		}
	}
}

// stepPastBreakpoint restores the original byte, rewinds Rip back over the
// INT3 (the CPU already advanced it past the 1-byte instruction by the time
// the exception is delivered -- this is the standard, easy-to-forget
// software-breakpoint bookkeeping step), single-steps exactly one
// instruction via the trap flag, then re-arms the 0xCC so the breakpoint
// persists for the next hit. The single-step it generates is consumed here
// directly (a second WaitForDebugEvent/ContinueDebugEvent round trip on this
// same locked thread) and never reaches the public Events() channel -- the
// caller asked to Continue, not to Step, so this is an implementation detail
// of "resume past a breakpoint", not an event of its own.
func (d *Debugger) stepPastBreakpoint(tid uint32, addr uintptr, orig byte) error {
	if err := d.WriteMemory(addr, []byte{orig}); err != nil {
		return fmt.Errorf("restore original byte: %w", err)
	}

	th, err := windows.OpenThread(threadAccessForDebug, false, tid)
	if err != nil {
		return fmt.Errorf("OpenThread(%d): %w", tid, err)
	}
	defer windows.CloseHandle(th)

	ctx := &contextX64{}
	if err := getThreadContext(th, ctx); err != nil {
		return err
	}
	ctx.EFlags |= eflagsTrapFlag
	if err := setThreadContext(th, ctx); err != nil {
		return err
	}

	procContinueDebugEvent.Call(uintptr(d.PID), uintptr(tid), uintptr(dbgContinue))

	for {
		var raw rawDebugEvent
		r, _, _ := procWaitForDebugEvent.Call(uintptr(unsafe.Pointer(&raw)), uintptr(windows.INFINITE))
		if r == 0 {
			return fmt.Errorf("WaitForDebugEvent failed while stepping past breakpoint at 0x%x", addr)
		}
		if raw.Code == debugExceptionEvent && raw.ThreadID == tid {
			info := (*exceptionDebugInfo)(unsafe.Pointer(&raw.U[0]))
			if info.Record.Code == exceptionSingleStep {
				break
			}
		}
		// Something else fired on another thread mid-step (a second thread
		// hitting its own breakpoint, say) -- let it run, we only care about
		// regaining control of tid.
		procContinueDebugEvent.Call(uintptr(d.PID), uintptr(raw.ThreadID), uintptr(dbgExceptionNotHandled))
	}

	return d.WriteMemory(addr, []byte{0xCC})
}

func (d *Debugger) decodeEvent(raw *rawDebugEvent) DebugEvent {
	ev := DebugEvent{ThreadID: raw.ThreadID, status: dbgContinue}

	switch raw.Code {
	case debugExceptionEvent:
		info := (*exceptionDebugInfo)(unsafe.Pointer(&raw.U[0]))
		ev.Code = info.Record.Code
		ev.Addr = uintptr(info.Record.Address)
		ev.FirstChance = info.FirstChance != 0

		d.bpMu.Lock()
		_, isOurs := d.breakpoints[ev.Addr]
		d.bpMu.Unlock()

		switch {
		case info.Record.Code == exceptionBreakpoint && isOurs:
			ev.Kind = EventBreakpoint
			// Make the paused thread's own Rip already read as the
			// breakpoint address (not address+1, where the CPU left it
			// after executing the INT3) so a caller's GetContext during
			// this event sees what a human would expect at a breakpoint --
			// the same fixup every real debugger applies before showing you
			// anything.
			if th, err := windows.OpenThread(threadAccessForDebug, false, raw.ThreadID); err == nil {
				ctx := &contextX64{}
				if getThreadContext(th, ctx) == nil {
					ctx.Rip--
					setThreadContext(th, ctx)
				}
				windows.CloseHandle(th)
			}
		case info.Record.Code == exceptionBreakpoint:
			// Not one of ours -- almost always the loader breakpoint ntdll
			// raises once initialization finishes (the same stop every
			// WinDbg/x64dbg session opens on), occasionally a genuine int3
			// already in the target. Reported as a plain exception since
			// there's no INT3 *we* patched in to account for.
			ev.Kind = EventException
		case info.Record.Code == exceptionSingleStep:
			ev.Kind = EventSingleStep
		default:
			ev.Kind = EventException
			// Most callers want to inspect and decide for themselves, but
			// the safe default if they just Continue() without handling it
			// is to let the OS's normal second-chance/crash path run
			// instead of looping the same fault back into the debuggee
			// forever -- true whether it's first-chance or not.
			ev.status = dbgExceptionNotHandled
		}

	case debugCreateProcessEvent:
		info := (*createProcessDebugInfo)(unsafe.Pointer(&raw.U[0]))
		ev.Kind = EventCreateProcess
		ev.Addr = uintptr(info.LpBaseOfImage)

	case debugCreateThreadEvent:
		ev.Kind = EventCreateThread

	case debugExitThreadEvent:
		info := (*exitDebugInfo)(unsafe.Pointer(&raw.U[0]))
		ev.Kind = EventExitThread
		ev.ExitCode = info.ExitCode

	case debugExitProcessEvent:
		info := (*exitDebugInfo)(unsafe.Pointer(&raw.U[0]))
		ev.Kind = EventExitProcess
		ev.ExitCode = info.ExitCode

	case debugLoadDllEvent:
		info := (*loadDllDebugInfo)(unsafe.Pointer(&raw.U[0]))
		ev.Kind = EventLoadDll
		ev.Addr = uintptr(info.LpBaseOfDll)
		ev.Message = d.readDllPath(info.ImageName, info.Unicode != 0)

	case debugUnloadDllEvent:
		info := (*struct{ LpBaseOfDll uint64 })(unsafe.Pointer(&raw.U[0]))
		ev.Kind = EventUnloadDll
		ev.Addr = uintptr(info.LpBaseOfDll)

	case debugOutputStringEvent:
		info := (*outputDebugStringInfo)(unsafe.Pointer(&raw.U[0]))
		ev.Kind = EventOutputDebugString
		ev.Message = d.readDebugString(info)

	default:
		ev.Kind = EventUnknown
	}

	return ev
}

// readDllPath best-effort reads the LOAD_DLL_DEBUG_INFO.lpImageName pointer.
// It's deliberately tolerant of failure: lpImageName is documented as
// sometimes null or pointing at a pointer-to-a-pointer depending on the
// loader's mood, so a miss here just means an empty Message, not an error
// that would derail the whole event.
func (d *Debugger) readDllPath(addr uint64, unicode bool) string {
	if addr == 0 {
		return ""
	}
	var ptrBuf [8]byte
	if _, err := d.ReadMemory(uintptr(addr), ptrBuf[:]); err != nil {
		return ""
	}
	strAddr := *(*uint64)(unsafe.Pointer(&ptrBuf[0]))
	if strAddr == 0 {
		return ""
	}
	buf := make([]byte, 512)
	n, _ := d.ReadMemory(uintptr(strAddr), buf)
	buf = buf[:n]
	if unicode {
		u16 := make([]uint16, len(buf)/2)
		for i := range u16 {
			u16[i] = uint16(buf[2*i]) | uint16(buf[2*i+1])<<8
		}
		return windows.UTF16ToString(u16)
	}
	if idx := indexByte(buf, 0); idx >= 0 {
		buf = buf[:idx]
	}
	return string(buf)
}

func (d *Debugger) readDebugString(info *outputDebugStringInfo) string {
	if info.LpDebugStringData == 0 || info.Length == 0 {
		return ""
	}
	buf := make([]byte, info.Length)
	n, err := d.ReadMemory(uintptr(info.LpDebugStringData), buf)
	if err != nil {
		return ""
	}
	buf = buf[:n]
	if info.Unicode != 0 {
		u16 := make([]uint16, len(buf)/2)
		for i := range u16 {
			u16[i] = uint16(buf[2*i]) | uint16(buf[2*i+1])<<8
		}
		return windows.UTF16ToString(u16)
	}
	if idx := indexByte(buf, 0); idx >= 0 {
		buf = buf[:idx]
	}
	return string(buf)
}

func indexByte(b []byte, c byte) int {
	for i, v := range b {
		if v == c {
			return i
		}
	}
	return -1
}

// Events returns the channel of decoded debug events. Closed when the
// debuggee exits or Close/Detach is called -- range over it the same way
// you'd loop on pwntools' gdb continuing past each stop.
func (d *Debugger) Events() <-chan DebugEvent {
	return d.events
}

// Continue resumes the debuggee past ev, the consumer-side counterpart of a
// value received from Events(). It picks DBG_CONTINUE vs
// DBG_EXCEPTION_NOT_HANDLED automatically (see DebugEvent.status's doc
// comment) and, if ev was a hit on a breakpoint this Debugger set, performs
// the restore-byte/rewind-Rip/single-step/re-arm dance transparently first.
func (d *Debugger) Continue(ev DebugEvent) error {
	select {
	case d.resume <- continueRequest{threadID: ev.ThreadID, status: ev.status}:
		return nil
	case <-d.closed:
		return fmt.Errorf("debugger: session closed")
	}
}

// Step single-steps thread tid by setting the trap flag and resuming for
// exactly one instruction; the resulting EventSingleStep is delivered
// through the normal Events() channel like any other event.
//
// Documented gap, not a bug: calling Step in response to an EventBreakpoint
// behaves like Continue, not like a single step, because resuming past a
// software breakpoint already requires its own internal single-step (see
// stepPastBreakpoint) before real execution can continue -- there's no way
// to stop *exactly* at "one instruction past a breakpoint" without that
// step happening anyway. If you need single-step granularity starting from
// a breakpoint, Continue past it once, then Step from wherever you land.
func (d *Debugger) Step(tid uint32) error {
	th, err := windows.OpenThread(threadAccessForDebug, false, tid)
	if err != nil {
		return fmt.Errorf("OpenThread(%d): %w", tid, err)
	}
	defer windows.CloseHandle(th)

	ctx := &contextX64{}
	if err := getThreadContext(th, ctx); err != nil {
		return err
	}
	ctx.EFlags |= eflagsTrapFlag
	if err := setThreadContext(th, ctx); err != nil {
		return err
	}

	select {
	case d.resume <- continueRequest{threadID: tid, status: dbgContinue}:
		return nil
	case <-d.closed:
		return fmt.Errorf("debugger: session closed")
	}
}

// SetBreakpoint patches a software breakpoint (INT3) at addr, saving the
// original byte so Continue/RemoveBreakpoint can restore it. addr is an
// absolute virtual address in the debuggee -- typically ImageBase +
// some RVA resolved from the target's own PEFile.
func (d *Debugger) SetBreakpoint(addr uintptr) error {
	var orig [1]byte
	if _, err := d.ReadMemory(addr, orig[:]); err != nil {
		return fmt.Errorf("read original byte at 0x%x: %w", addr, err)
	}
	if err := d.WriteMemory(addr, []byte{0xCC}); err != nil {
		return fmt.Errorf("write breakpoint at 0x%x: %w", addr, err)
	}
	d.bpMu.Lock()
	d.breakpoints[addr] = orig[0]
	d.bpMu.Unlock()
	return nil
}

// RemoveBreakpoint restores the original byte at addr. Safe to call on an
// address that isn't currently the thread's Rip -- only Continue's
// breakpoint-resume path needs the single-step dance; removing one that
// isn't being actively resumed through is a plain memory write.
func (d *Debugger) RemoveBreakpoint(addr uintptr) error {
	d.bpMu.Lock()
	orig, ok := d.breakpoints[addr]
	if ok {
		delete(d.breakpoints, addr)
	}
	d.bpMu.Unlock()
	if !ok {
		return fmt.Errorf("no breakpoint set at 0x%x", addr)
	}
	return d.WriteMemory(addr, []byte{orig})
}

// GetContext reads tid's general-purpose registers + Rip/EFlags.
func (d *Debugger) GetContext(tid uint32) (Registers, error) {
	th, err := windows.OpenThread(threadAccessForDebug, false, tid)
	if err != nil {
		return Registers{}, fmt.Errorf("OpenThread(%d): %w", tid, err)
	}
	defer windows.CloseHandle(th)

	ctx := &contextX64{}
	if err := getThreadContext(th, ctx); err != nil {
		return Registers{}, err
	}
	return registersFromContext(ctx), nil
}

// SetContext writes tid's general-purpose registers + Rip/EFlags, e.g. to
// redirect execution (set Rip to a ROP gadget / shellcode address) once you
// have IP control and want to drive it from the debugger rather than
// letting a corrupted return address do it.
func (d *Debugger) SetContext(tid uint32, regs Registers) error {
	th, err := windows.OpenThread(threadAccessForDebug, false, tid)
	if err != nil {
		return fmt.Errorf("OpenThread(%d): %w", tid, err)
	}
	defer windows.CloseHandle(th)

	ctx := &contextX64{}
	if err := getThreadContext(th, ctx); err != nil {
		return err
	}
	applyRegistersToContext(regs, ctx)
	return setThreadContext(th, ctx)
}

// ReadMemory/WriteMemory read and write the debuggee's address space
// directly, the same ReadProcessMemory/WriteProcessMemory primitive
// ProcessMemory wraps in procmem_windows.go -- duplicated here rather than
// embedding a *ProcessMemory because Debugger already owns the process
// handle's lifetime (closed by eventLoop on exit) and WriteMemory needs the
// extra FlushInstructionCache call SetBreakpoint relies on, which a plain
// WriterAt has no hook for.
func (d *Debugger) ReadMemory(addr uintptr, buf []byte) (int, error) {
	if len(buf) == 0 {
		return 0, nil
	}
	var n uintptr
	err := windows.ReadProcessMemory(d.process, addr, &buf[0], uintptr(len(buf)), &n)
	return int(n), err
}

func (d *Debugger) WriteMemory(addr uintptr, data []byte) error {
	if len(data) == 0 {
		return nil
	}
	var n uintptr
	if err := windows.WriteProcessMemory(d.process, addr, &data[0], uintptr(len(data)), &n); err != nil {
		return err
	}
	// Required for code patches per Microsoft's own documentation for
	// WriteProcessMemory: "the function does not flush the instruction
	// cache... If you need that, call FlushInstructionCache". x86/x64 has a
	// coherent icache in practice, but WOW64/exotic configurations are
	// exactly the case that documentation note exists for -- cheap to call
	// unconditionally rather than rediscover the one config where it matters.
	procFlushInstructionCache.Call(uintptr(d.process), addr, uintptr(len(data)))
	return nil
}

// Close detaches the debugger (DebugActiveProcessStop) without killing the
// debuggee -- idempotent via sync.Once, matching Tube.Close/pipeConn.Close
// elsewhere in this package.
func (d *Debugger) Close() error {
	d.closeOnce.Do(func() {
		close(d.closed)
		r, _, err := procDebugActiveProcessStop.Call(uintptr(d.PID))
		if r == 0 {
			d.closeErr = fmt.Errorf("DebugActiveProcessStop(%d): %w", d.PID, err)
		}
	})
	return d.closeErr
}
