//go:build windows

package winpwn

import (
	"fmt"
	"io"
	"sync"

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

const (
	pipeOutBufSize = 64 * 1024
	pipeInBufSize  = 64 * 1024
)

// pipeConn wraps a duplex named-pipe handle opened with
// FILE_FLAG_OVERLAPPED as an io.ReadWriteCloser, the shape Tube needs for
// its stdin/stdout fields. The handle is genuinely asynchronous -- matching
// how real Windows services hold their pipe ends; a synchronous duplex
// pipe is the rarer case in production code -- but every Read/Write below
// immediately blocks on GetOverlappedResult, so the type behaves like an
// ordinary blocking reader/writer to the rest of the package. That keeps
// Tube's synchronous Send/Recv contract intact while still exercising the
// same overlapped-completion path a real target uses.
type pipeConn struct {
	h     windows.Handle
	event windows.Handle // manual-reset event reused across overlapped calls

	closeOnce sync.Once
	closeErr  error
}

func newPipeConn(h windows.Handle) (*pipeConn, error) {
	ev, err := windows.CreateEvent(nil, 1 /* manual reset */, 0, nil)
	if err != nil {
		windows.CloseHandle(h)
		return nil, fmt.Errorf("CreateEvent: %w", err)
	}
	return &pipeConn{h: h, event: ev}, nil
}

func (p *pipeConn) Read(b []byte) (int, error) {
	if len(b) == 0 {
		return 0, nil
	}
	var ov windows.Overlapped
	ov.HEvent = p.event

	var n uint32
	err := windows.ReadFile(p.h, b, &n, &ov)
	if err != nil && err != windows.ERROR_IO_PENDING {
		if err == windows.ERROR_BROKEN_PIPE || err == windows.ERROR_HANDLE_EOF {
			return 0, io.EOF
		}
		return 0, err
	}

	var transferred uint32
	if err := windows.GetOverlappedResult(p.h, &ov, &transferred, true); err != nil {
		if err == windows.ERROR_BROKEN_PIPE || err == windows.ERROR_HANDLE_EOF {
			return int(transferred), io.EOF
		}
		return int(transferred), err
	}
	if transferred == 0 {
		return 0, io.EOF
	}
	return int(transferred), nil
}

func (p *pipeConn) Write(b []byte) (int, error) {
	if len(b) == 0 {
		return 0, nil
	}
	var ov windows.Overlapped
	ov.HEvent = p.event

	var n uint32
	err := windows.WriteFile(p.h, b, &n, &ov)
	if err != nil && err != windows.ERROR_IO_PENDING {
		return 0, err
	}

	var transferred uint32
	if err := windows.GetOverlappedResult(p.h, &ov, &transferred, true); err != nil {
		return int(transferred), err
	}
	return int(transferred), nil
}

// Close is idempotent (sync.Once) deliberately: Tube.Close calls Close once
// via its stdin field and once via its stdout field, and stdin/stdout are
// the same *pipeConn here -- unlike net.Conn, a raw Windows HANDLE is not
// safe to pass to CloseHandle twice (the numeric value can be reused by an
// unrelated object in between).
func (p *pipeConn) Close() error {
	p.closeOnce.Do(func() {
		_ = windows.CloseHandle(p.event)
		p.closeErr = windows.CloseHandle(p.h)
	})
	return p.closeErr
}

// ServePipe creates a duplex named pipe at \\.\pipe\<name>
// (PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED -- the pattern real Windows
// services use, not the rarer synchronous one) and blocks until exactly
// one client connects -- the named-pipe analogue of Spawn/Remote for the
// IPC-flavored challenges Windows uses far more than Linux pwn does.
// Returns a *Tube wired to the connected pipe end, so Send/Recv*/
// Interactive/SetTimeout all work exactly as they do over a process or TCP
// socket.
func ServePipe(name string) (*Tube, error) {
	fullName, err := windows.UTF16PtrFromString(`\\.\pipe\` + name)
	if err != nil {
		return nil, fmt.Errorf("invalid pipe name %q: %w", name, err)
	}

	h, err := windows.CreateNamedPipe(
		fullName,
		windows.PIPE_ACCESS_DUPLEX|windows.FILE_FLAG_OVERLAPPED,
		windows.PIPE_TYPE_BYTE|windows.PIPE_READMODE_BYTE|windows.PIPE_WAIT,
		1, // maxInstances: one client, matching "spawn one challenge instance"
		pipeOutBufSize, pipeInBufSize,
		0, // default timeout
		nil,
	)
	if err != nil {
		return nil, fmt.Errorf("CreateNamedPipe: %w", err)
	}

	conn, err := newPipeConn(h)
	if err != nil {
		return nil, err
	}

	Info(`Waiting for a client on \\.\pipe\%s`, name)

	var ov windows.Overlapped
	ov.HEvent = conn.event
	err = windows.ConnectNamedPipe(conn.h, &ov)
	if err != nil && err != windows.ERROR_IO_PENDING && err != windows.ERROR_PIPE_CONNECTED {
		conn.Close()
		return nil, fmt.Errorf("ConnectNamedPipe: %w", err)
	}
	if err != windows.ERROR_PIPE_CONNECTED {
		var transferred uint32
		if err := windows.GetOverlappedResult(conn.h, &ov, &transferred, true); err != nil {
			conn.Close()
			return nil, fmt.Errorf("waiting for client connection: %w", err)
		}
	}

	Success(`Client connected to \\.\pipe\%s`, name)
	return newTube(nil, nil, conn, conn), nil
}

// DialPipe connects to a named pipe at \\.\pipe\<name> as a client (the
// CreateFile-based counterpart to ServePipe), returning a *Tube wired to
// it.
func DialPipe(name string) (*Tube, error) {
	fullName, err := windows.UTF16PtrFromString(`\\.\pipe\` + name)
	if err != nil {
		return nil, fmt.Errorf("invalid pipe name %q: %w", name, err)
	}

	h, err := windows.CreateFile(
		fullName,
		windows.GENERIC_READ|windows.GENERIC_WRITE,
		0,
		nil,
		windows.OPEN_EXISTING,
		windows.FILE_FLAG_OVERLAPPED,
		0,
	)
	if err != nil {
		return nil, fmt.Errorf(`CreateFile(\\.\pipe\%s): %w`, name, err)
	}

	conn, err := newPipeConn(h)
	if err != nil {
		return nil, err
	}

	return newTube(nil, nil, conn, conn), nil
}
