package winpwn

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"os/signal"
	"regexp"
	"sync"
	"syscall"
	"time"
)

// Tube is the core abstraction for talking to a target: a spawned local
// process or a remote TCP connection. It mirrors pwntools' tube class and
// is the type every transport (Spawn, Remote, ...) returns.
//
// Every blocking call returns an error instead of killing the process --
// callers decide what a timeout/EOF/closed-connection means for their
// script, the same way pwntools raises (and lets you catch) EOFError /
// PwnlibException instead of the library calling sys.exit().
type Tube struct {
	cmd    *exec.Cmd
	conn   net.Conn
	stdin  io.WriteCloser
	stdout io.ReadCloser
	reader *bufio.Reader

	// timeout overrides Context.Timeout for this tube specifically; zero
	// means "fall back to Context.Timeout" (which is itself zero/forever
	// by default). Set via SetTimeout.
	timeout time.Duration

	closeOnce sync.Once
	closeErr  error
}

// newTube wraps a writer/reader pair into a Tube with a buffered reader.
func newTube(cmd *exec.Cmd, conn net.Conn, stdin io.WriteCloser, stdout io.ReadCloser) *Tube {
	return &Tube{
		cmd:    cmd,
		conn:   conn,
		stdin:  stdin,
		stdout: stdout,
		reader: bufio.NewReader(stdout),
	}
}

// SetTimeout overrides Context.Timeout for this tube's Recv*/Send* calls.
// Zero means block forever (the default), matching pwntools' per-tube
// timeout= override of the global context.timeout.
func (t *Tube) SetTimeout(d time.Duration) {
	t.timeout = d
}

func (t *Tube) effectiveTimeout() time.Duration {
	if t.timeout > 0 {
		return t.timeout
	}
	return Context.Timeout
}

// PID returns the OS process ID for a locally spawned process, or 0 for
// remote tubes. Use it to feed winpwn.NewProcessSymbols or
// winpwn.ListProcessHeaps without having to track the PID separately.
func (t *Tube) PID() uint32 {
	if t.cmd != nil && t.cmd.Process != nil {
		return uint32(t.cmd.Process.Pid)
	}
	return 0
}

// Close tears down the underlying process/connection. Safe to call more
// than once (idempotent) -- Interactive relies on this to make a pending
// stdin write fail fast instead of dangling.
func (t *Tube) Close() error {
	t.closeOnce.Do(func() {
		if t.stdin != nil {
			_ = t.stdin.Close()
		}
		if t.stdout != nil {
			_ = t.stdout.Close()
		}
		if t.conn != nil {
			t.closeErr = t.conn.Close()
			return
		}
		if t.cmd != nil && t.cmd.Process != nil {
			t.closeErr = t.cmd.Process.Kill()
		}
	})
	return t.closeErr
}

// withTimeout runs fn on its own goroutine and races it against this tube's
// effective timeout. If fn doesn't return in time, withTimeout returns a
// timeout error immediately -- but fn's goroutine is *not* killed (the
// underlying pipe/socket reader has no native per-call deadline), so it
// keeps running in the background until the blocking I/O it's stuck in
// eventually completes or errors. That's the standard, and only portable,
// way to bolt a deadline onto an arbitrary io.Reader/Writer in Go.
func withTimeout[T any](t *Tube, fn func() (T, error)) (T, error) {
	timeout := t.effectiveTimeout()
	if timeout <= 0 {
		return fn()
	}
	type result struct {
		v   T
		err error
	}
	ch := make(chan result, 1)
	go func() {
		v, err := fn()
		ch <- result{v, err}
	}()
	select {
	case r := <-ch:
		return r.v, r.err
	case <-time.After(timeout):
		var zero T
		return zero, fmt.Errorf("winpwn: operation timed out after %s", timeout)
	}
}

// Recv reads up to n bytes from the tube, blocking until n bytes have
// arrived. On a clean EOF after at least one byte it returns the partial
// read with a nil error (mirroring the old behavior); on EOF with nothing
// read yet, it returns io.EOF.
func (t *Tube) Recv(n int) ([]byte, error) {
	return withTimeout(t, func() ([]byte, error) {
		buf := make([]byte, n)
		read, err := io.ReadFull(t.reader, buf)
		if err != nil {
			if err == io.ErrUnexpectedEOF || err == io.EOF {
				if read > 0 {
					return buf[:read], nil
				}
				return nil, io.EOF
			}
			return buf[:read], err
		}
		return buf, nil
	})
}

// RecvUntil reads from the tube until delim is seen (inclusive of delim).
func (t *Tube) RecvUntil(delim []byte) ([]byte, error) {
	return withTimeout(t, func() ([]byte, error) {
		var out []byte
		for {
			b, err := t.reader.ReadByte()
			if err != nil {
				return out, err
			}
			out = append(out, b)
			if bytes.HasSuffix(out, delim) {
				return out, nil
			}
		}
	})
}

// RecvLine reads a single line, including the trailing newline
// (Context.Newline, "\n" by default).
func (t *Tube) RecvLine() ([]byte, error) {
	return t.RecvUntil(Context.Newline)
}

// RecvPred reads one byte at a time until pred(accumulated) reports true,
// the analogue of pwntools' recvpred.
func (t *Tube) RecvPred(pred func([]byte) bool) ([]byte, error) {
	return withTimeout(t, func() ([]byte, error) {
		var out []byte
		for {
			b, err := t.reader.ReadByte()
			if err != nil {
				return out, err
			}
			out = append(out, b)
			if pred(out) {
				return out, nil
			}
		}
	})
}

// RecvRegex reads one byte at a time until the accumulated buffer matches
// re, the analogue of pwntools' recvregex.
func (t *Tube) RecvRegex(re *regexp.Regexp) ([]byte, error) {
	return t.RecvPred(func(buf []byte) bool {
		return re.Match(buf)
	})
}

// Send writes raw bytes to the tube.
func (t *Tube) Send(data []byte) error {
	_, err := withTimeout(t, func() (int, error) {
		return t.stdin.Write(data)
	})
	return err
}

// SendLine writes data followed by Context.Newline.
func (t *Tube) SendLine(data []byte) error {
	return t.Send(append(append([]byte{}, data...), Context.Newline...))
}

// SendAfter waits for delim, then sends data (no trailing newline).
func (t *Tube) SendAfter(delim []byte, data []byte) error {
	if _, err := t.RecvUntil(delim); err != nil {
		return err
	}
	return t.Send(data)
}

// SendLineAfter waits for delim, then sends data followed by a newline.
func (t *Tube) SendLineAfter(delim []byte, data []byte) error {
	if _, err := t.RecvUntil(delim); err != nil {
		return err
	}
	return t.SendLine(data)
}

// Interactive hands the tube's stdin/stdout over to the user's terminal,
// the Go analogue of pwntools' tube.interactive(). Ctrl+C cleanly tears
// down the local process or remote connection.
//
// The stdin-forwarding goroutine below has no portable way to be cancelled
// in Go (os.Stdin.Read blocks with no deadline support), so it keeps
// running until the next keystroke/EOF even after Interactive returns;
// the deferred Close() at least makes its next Write fail fast instead of
// leaving the target side dangling. This is a known, deliberate limitation,
// not an oversight -- don't call Interactive() in a tight loop expecting
// the goroutine to be gone before the next iteration.
func (t *Tube) Interactive() {
	defer t.Close()

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
	defer signal.Stop(sigChan)

	stdoutDone := make(chan struct{})
	go func() {
		_, _ = io.Copy(os.Stdout, t.reader)
		close(stdoutDone)
	}()

	go func() {
		_, _ = io.Copy(t.stdin, os.Stdin)
	}()

	Info("Switching to interactive mode")

	if t.cmd != nil {
		// Local process branch.
		waitCh := make(chan error, 1)
		go func() {
			waitCh <- t.cmd.Wait()
		}()

		select {
		case err := <-waitCh:
			if err == nil {
				Info("Process exited normally (code 0)")
			} else {
				var exitErr *exec.ExitError
				if errors.As(err, &exitErr) {
					code := exitErr.ExitCode()
					// 0xC0000005 (Access Violation) is the Windows analogue of SIGSEGV.
					Error("Process crashed/terminated with code: 0x%X", uint32(code))
				} else {
					Error("Process execution error: %v", err)
				}
			}
		case <-sigChan:
			Info("Interrupted by user, killing process...")
		}
	} else {
		// Remote connection branch.
		select {
		case <-stdoutDone:
			Info("Connection closed by foreign host")
		case <-sigChan:
			Info("Interrupted by user, closing connection...")
		}
	}
}
