//go:build windows

package winpwn

import (
	"debug/pe"
	"errors"
	"fmt"
	"io"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"
	"unsafe"

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

// ProcessMemory is an io.ReaderAt/io.WriterAt over a remote process's
// address space, anchored at a base address -- the live-memory analogue of
// reading bytes off disk. Offset 0 in ReadAt/WriteAt means "Base itself",
// matching how a memory-backed PEFile's RVAToFileOffset treats RVAs as
// identical to read offsets: that's exactly what an RVA means once an
// image is actually loaded.
type ProcessMemory struct {
	Handle windows.Handle
	Base   uintptr
}

// OpenProcessMemory opens pid for VM read/write, anchored at base -- the
// building block behind OpenPEFromProcess, but also useful standalone for
// any arbitrary-read/write-shaped primitive once you have a target address
// (Phase 8's ARW interface is satisfied directly by *ProcessMemory).
func OpenProcessMemory(pid uint32, base uintptr) (*ProcessMemory, error) {
	h, err := windows.OpenProcess(
		windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ|windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION,
		false, pid)
	if err != nil {
		return nil, err
	}
	return &ProcessMemory{Handle: h, Base: base}, nil
}

func (m *ProcessMemory) Close() error {
	return windows.CloseHandle(m.Handle)
}

func (m *ProcessMemory) ReadAt(p []byte, off int64) (int, error) {
	if off < 0 {
		return 0, errors.New("ProcessMemory.ReadAt: negative offset")
	}
	if len(p) == 0 {
		return 0, nil
	}
	var n uintptr
	err := windows.ReadProcessMemory(m.Handle, m.Base+uintptr(off), &p[0], uintptr(len(p)), &n)
	if err != nil {
		return int(n), err
	}
	if int(n) < len(p) {
		return int(n), io.ErrUnexpectedEOF
	}
	return int(n), nil
}

func (m *ProcessMemory) WriteAt(p []byte, off int64) (int, error) {
	if off < 0 {
		return 0, errors.New("ProcessMemory.WriteAt: negative offset")
	}
	if len(p) == 0 {
		return 0, nil
	}
	var n uintptr
	err := windows.WriteProcessMemory(m.Handle, m.Base+uintptr(off), &p[0], uintptr(len(p)), &n)
	return int(n), err
}

// OpenPEFromProcess opens the PE module loaded at base inside pid's address
// space, the live-process analogue of OpenPE. Every PEFile accessor
// (Checksec, ListExports/ListImports, the ROP gadget scanner, PatchBytes)
// works against it exactly as it does against a disk-backed PEFile -- the
// whole point of routing everything through PEFile.r/.w/.RVAToFileOffset.
// Find base with ResolveModuleBase, or read it straight from a leaked
// pointer once you have one.
func OpenPEFromProcess(pid uint32, base uintptr) (*PEFile, error) {
	mem, err := OpenProcessMemory(pid, base)
	if err != nil {
		return nil, err
	}
	f, err := pe.NewFile(mem)
	if err != nil {
		mem.Close()
		return nil, err
	}
	pf := &PEFile{File: f, r: mem, w: mem, closer: mem, live: true}
	if h, herr := pf.header(); herr == nil {
		pf.size = int64(h.sizeOfImage)
	}
	return pf, nil
}

// ResolveModuleBase walks pid's PEB -> Ldr -> InMemoryOrderModuleList to
// find moduleName's load address (e.g. "kernel32.dll", "ntdll.dll", or the
// process's own main-module file name), matched against the loader's
// FullDllName case-insensitively by base name.
//
// This is the live, cross-process twin of
// shellcode/asm/resolver.inc's get_kernel32_base: that NASM walks the exact
// same PEB/Ldr chain from *inside* the target process at IP-control time
// with no Windows API calls available; this does it from *outside*, with a
// debugger/exploit-tooling process's full WinAPI access, which is why it
// can resolve any module by name instead of relying on a fixed loader
// order. It's the real Windows analogue of how pwntools' DynELF defeats
// ASLR by walking ELF structures through a leak oracle -- here the "oracle"
// is ReadProcessMemory itself.
func ResolveModuleBase(pid uint32, moduleName string) (uintptr, error) {
	h, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, pid)
	if err != nil {
		return 0, err
	}
	defer windows.CloseHandle(h)

	var pbi windows.PROCESS_BASIC_INFORMATION
	var retLen uint32
	if err := windows.NtQueryInformationProcess(h, windows.ProcessBasicInformation,
		unsafe.Pointer(&pbi), uint32(unsafe.Sizeof(pbi)), &retLen); err != nil {
		return 0, fmt.Errorf("NtQueryInformationProcess(ProcessBasicInformation): %w", err)
	}
	pebAddr := uintptr(unsafe.Pointer(pbi.PebBaseAddress))
	if pebAddr == 0 {
		return 0, errors.New("PEB address is null")
	}

	peb, err := readRemoteStruct[windows.PEB](h, pebAddr)
	if err != nil {
		return 0, fmt.Errorf("read PEB: %w", err)
	}
	ldrAddr := uintptr(unsafe.Pointer(peb.Ldr))
	if ldrAddr == 0 {
		return 0, errors.New("PEB.Ldr is null -- ntdll's loader (LdrInitializeThunk) hasn't run in this process yet; " +
			"this is normal for a CREATE_SUSPENDED process before it's resumed, retry shortly after ResumeMainThread")
	}

	ldr, err := readRemoteStruct[windows.PEB_LDR_DATA](h, ldrAddr)
	if err != nil {
		return 0, fmt.Errorf("read PEB_LDR_DATA: %w", err)
	}

	// Flink/Blink point at the InMemoryOrderLinks *field* of each entry, not
	// at the start of its LDR_DATA_TABLE_ENTRY -- the field sits at a
	// nonzero offset (0x10 on x64: it's the second of three LIST_ENTRYs at
	// the head of the real struct), so every address read off the list has
	// to be corrected by that offset before it's used as an entry address.
	entryLinksOffset := unsafe.Offsetof(windows.LDR_DATA_TABLE_ENTRY{}.InMemoryOrderLinks)
	headAddr := ldrAddr + unsafe.Offsetof(windows.PEB_LDR_DATA{}.InMemoryOrderModuleList)

	cur := uintptr(unsafe.Pointer(ldr.InMemoryOrderModuleList.Flink))
	for cur != 0 && cur != headAddr {
		entryAddr := cur - entryLinksOffset

		entry, err := readRemoteStruct[windows.LDR_DATA_TABLE_ENTRY](h, entryAddr)
		if err != nil {
			return 0, fmt.Errorf("read LDR_DATA_TABLE_ENTRY: %w", err)
		}

		if name, err := readRemoteUTF16(h, entry.FullDllName); err == nil {
			if strings.EqualFold(moduleBaseName(name), moduleName) {
				return entry.DllBase, nil
			}
		}

		cur = uintptr(unsafe.Pointer(entry.InMemoryOrderLinks.Flink))
	}

	return 0, fmt.Errorf("module %q not found in process %d's loaded module list", moduleName, pid)
}

// readRemoteStruct copies sizeof(T) bytes from h's address space at addr
// into a T, by raw memcpy through ReadProcessMemory -- safe specifically
// because T's fields here are always fixed-width (uintptr/pointer-shaped)
// values being read as bit patterns, never dereferenced as if they were
// local pointers.
func readRemoteStruct[T any](h windows.Handle, addr uintptr) (T, error) {
	var v T
	size := int(unsafe.Sizeof(v))
	buf := make([]byte, size)
	var n uintptr
	if err := windows.ReadProcessMemory(h, addr, &buf[0], uintptr(size), &n); err != nil {
		return v, err
	}
	if int(n) < size {
		return v, io.ErrUnexpectedEOF
	}
	return *(*T)(unsafe.Pointer(&buf[0])), nil
}

func readRemoteUTF16(h windows.Handle, s windows.NTUnicodeString) (string, error) {
	if s.Buffer == nil || s.Length == 0 {
		return "", errors.New("empty NTUnicodeString")
	}
	buf := make([]uint16, s.Length/2)
	var n uintptr
	if err := windows.ReadProcessMemory(h, uintptr(unsafe.Pointer(s.Buffer)),
		(*byte)(unsafe.Pointer(&buf[0])), uintptr(s.Length), &n); err != nil {
		return "", err
	}
	return windows.UTF16ToString(buf), nil
}

func moduleBaseName(path string) string {
	if idx := strings.LastIndexAny(path, `/\`); idx >= 0 {
		path = path[idx+1:]
	}
	return path
}

// SpawnSuspended launches target with CREATE_SUSPENDED, returning the Tube
// (stdin/stdout wired exactly like Spawn) and the new process's PID, with
// the main thread parked before it executes a single instruction.
//
// Verified by testing, worth recording because the obvious assumption is
// wrong: you can NOT resolve module bases via ResolveModuleBase while the
// thread is still suspended. CREATE_SUSPENDED only pins the thread before
// its start routine runs, and that start routine *is*
// ntdll!LdrInitializeThunk -- the loader code that populates
// PEB->Ldr->InMemoryOrderModuleList in the first place. Until it runs,
// PEB.Ldr reads back as null (confirmed empirically: see the smoke test in
// this package's history). What this primitive is actually for is pausing
// *before the loader and entry point run*, e.g. to let Phase 4's debugger
// attach and plant a breakpoint before any application/TLS-callback code
// executes. Call ResumeMainThread(pid), then poll ResolveModuleBase for a
// few milliseconds (the loader runs fast, but it isn't instant) once you
// actually need module bases.
func SpawnSuspended(target string) (tube *Tube, pid uint32, err error) {
	if abs, aerr := filepath.Abs(target); aerr == nil {
		target = abs
	}

	cmd := exec.Command(target)
	cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: windows.CREATE_SUSPENDED}

	stdin, err := cmd.StdinPipe()
	if err != nil {
		return nil, 0, fmt.Errorf("StdinPipe: %w", err)
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, 0, fmt.Errorf("StdoutPipe: %w", err)
	}
	if err := cmd.Start(); err != nil {
		return nil, 0, fmt.Errorf("Start: %w", err)
	}

	return newTube(cmd, nil, stdin, stdout), uint32(cmd.Process.Pid), nil
}

// ResumeMainThread resumes a process started with SpawnSuspended. A
// CREATE_SUSPENDED process has exactly one thread (its initial thread)
// until something resumes it, so finding "the" thread to resume is just
// finding the one thread Toolhelp reports for pid -- there's no race with
// the target spawning more threads first, because it hasn't run yet.
func ResumeMainThread(pid uint32) error {
	snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPTHREAD, 0)
	if err != nil {
		return fmt.Errorf("CreateToolhelp32Snapshot: %w", err)
	}
	defer windows.CloseHandle(snap)

	var entry windows.ThreadEntry32
	entry.Size = uint32(unsafe.Sizeof(entry))
	for err = windows.Thread32First(snap, &entry); err == nil; err = windows.Thread32Next(snap, &entry) {
		if entry.OwnerProcessID != pid {
			continue
		}
		th, err := windows.OpenThread(windows.THREAD_SUSPEND_RESUME, false, entry.ThreadID)
		if err != nil {
			return fmt.Errorf("OpenThread(%d): %w", entry.ThreadID, err)
		}
		_, err = windows.ResumeThread(th)
		windows.CloseHandle(th)
		return err
	}
	return fmt.Errorf("no thread found for process %d", pid)
}
