package checker

import (
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"net"
	"time"
)

// Sentinel errors returned by the SOCKS5 primitives.
var (
	ErrSocks5BadVersion      = errors.New("socks5: server returned wrong version")
	ErrSocks5RejectedAllAuth = errors.New("socks5: server rejected all offered auth methods (0xFF)")
	ErrAuthRejected          = errors.New("socks5: user/pass authentication rejected")
	ErrCredentialTooLong     = errors.New("socks5: login or password longer than 255 bytes")
	ErrHostTooLong           = errors.New("socks5: target hostname longer than 255 bytes")
	ErrUnsupportedRelayATYP  = errors.New("socks5: udp associate replied with non-IPv4 ATYP")
	ErrShortReply            = errors.New("socks5: short server reply")
)

// ErrSocks5Reply wraps a non-zero REP code so callers can react to specific
// SOCKS5 reply codes (e.g. REP=0x07 = command not supported, REP=0x05 =
// connection refused).
type ErrSocks5Reply struct{ Code byte }

// Error implements the error interface.
func (e ErrSocks5Reply) Error() string {
	return fmt.Sprintf("socks5: server replied with non-zero REP code 0x%02X", e.Code)
}

// Is reports whether target matches this reply error by Code.
func (e ErrSocks5Reply) Is(target error) bool {
	t, ok := target.(ErrSocks5Reply)
	if !ok {
		return false
	}
	return t.Code == e.Code
}

// applyDeadline applies the deadline from ctx (if any) to conn. Returns a
// function to clear the deadline.
func applyDeadline(ctx context.Context, conn net.Conn) {
	if dl, ok := ctx.Deadline(); ok {
		_ = conn.SetDeadline(dl)
	} else {
		_ = conn.SetDeadline(time.Time{})
	}
}

// joinCtxErr wraps err with ctx.Err() if ctx has been cancelled or expired,
// so that callers see context.Canceled / context.DeadlineExceeded in the
// error chain even when the underlying I/O reported a deadline-based error.
func joinCtxErr(ctx context.Context, err error) error {
	if err == nil {
		return nil
	}
	if cerr := ctx.Err(); cerr != nil {
		return errors.Join(err, cerr)
	}
	return err
}

// socks5Greeting performs the RFC 1928 client greeting on conn.
// useAuth=true sends "05 02 00 02" (offer no-auth and user/pass);
// useAuth=false sends "05 01 00" (offer no-auth only).
func socks5Greeting(ctx context.Context, conn net.Conn, useAuth bool) (method byte, rawReply []byte, err error) {
	applyDeadline(ctx, conn)

	var greet []byte
	if useAuth {
		greet = []byte{0x05, 0x02, 0x00, 0x02}
	} else {
		greet = []byte{0x05, 0x01, 0x00}
	}

	if _, werr := conn.Write(greet); werr != nil {
		return 0, nil, joinCtxErr(ctx, fmt.Errorf("socks5 greeting: write: %w", werr))
	}

	reply := make([]byte, 2)
	n, rerr := io.ReadFull(conn, reply)
	if rerr != nil {
		partial := reply[:n]
		if errors.Is(rerr, io.ErrUnexpectedEOF) || errors.Is(rerr, io.EOF) {
			return 0, partial, joinCtxErr(ctx, fmt.Errorf("socks5 greeting: %w (raw=%x)", ErrShortReply, partial))
		}
		return 0, partial, joinCtxErr(ctx, fmt.Errorf("socks5 greeting: read: %w (raw=%x)", rerr, partial))
	}

	if reply[0] != 0x05 {
		return 0, reply, fmt.Errorf("socks5 greeting: %w (raw=%x)", ErrSocks5BadVersion, reply)
	}
	if reply[1] == 0xFF {
		return reply[1], reply, fmt.Errorf("socks5 greeting: %w (raw=%x)", ErrSocks5RejectedAllAuth, reply)
	}
	return reply[1], reply, nil
}

// socks5Auth performs RFC 1929 user/pass sub-negotiation on conn,
// after greeting selected method 0x02.
func socks5Auth(ctx context.Context, conn net.Conn, login, password string) (rawReply []byte, err error) {
	if len(login) > 255 || len(password) > 255 {
		return nil, ErrCredentialTooLong
	}

	applyDeadline(ctx, conn)

	buf := make([]byte, 0, 3+len(login)+len(password))
	buf = append(buf, 0x01)              // VER
	buf = append(buf, byte(len(login)))  // ULEN
	buf = append(buf, []byte(login)...)  // UNAME
	buf = append(buf, byte(len(password)))
	buf = append(buf, []byte(password)...)

	if _, werr := conn.Write(buf); werr != nil {
		return nil, joinCtxErr(ctx, fmt.Errorf("socks5 auth: write: %w", werr))
	}

	reply := make([]byte, 2)
	n, rerr := io.ReadFull(conn, reply)
	if rerr != nil {
		partial := reply[:n]
		if errors.Is(rerr, io.ErrUnexpectedEOF) || errors.Is(rerr, io.EOF) {
			return partial, joinCtxErr(ctx, fmt.Errorf("socks5 auth: %w (raw=%x)", ErrShortReply, partial))
		}
		return partial, joinCtxErr(ctx, fmt.Errorf("socks5 auth: read: %w (raw=%x)", rerr, partial))
	}

	if reply[0] != 0x01 {
		return reply, fmt.Errorf("socks5 auth: auth subneg version mismatch: got 0x%02X want 0x01 (raw=%x)", reply[0], reply)
	}
	if reply[1] != 0x00 {
		return reply, fmt.Errorf("socks5 auth: %w (raw=%x)", ErrAuthRejected, reply)
	}
	return reply, nil
}

// socks5Connect performs SOCKS5 CONNECT (CMD=01) to host:port using
// ATYP=03 (domain name).
func socks5Connect(ctx context.Context, conn net.Conn, host string, port uint16) (rawReply []byte, err error) {
	if len(host) > 255 {
		return nil, ErrHostTooLong
	}

	applyDeadline(ctx, conn)

	// VER=05 CMD=01 RSV=00 ATYP=03 LEN host port
	req := make([]byte, 0, 7+len(host))
	req = append(req, 0x05, 0x01, 0x00, 0x03)
	req = append(req, byte(len(host)))
	req = append(req, []byte(host)...)
	var portBuf [2]byte
	binary.BigEndian.PutUint16(portBuf[:], port)
	req = append(req, portBuf[:]...)

	if _, werr := conn.Write(req); werr != nil {
		return nil, joinCtxErr(ctx, fmt.Errorf("socks5 connect: write: %w", werr))
	}

	// We always read 10 bytes (assuming ATYP=01 IPv4 reply, the most
	// common case from real proxies). Parsing variable-length BND is
	// out of scope for the diagnostic.
	reply := make([]byte, 10)
	n, rerr := io.ReadFull(conn, reply)
	if rerr != nil {
		partial := reply[:n]
		if errors.Is(rerr, io.ErrUnexpectedEOF) || errors.Is(rerr, io.EOF) {
			return partial, joinCtxErr(ctx, fmt.Errorf("socks5 connect: %w (raw=%x)", ErrShortReply, partial))
		}
		return partial, joinCtxErr(ctx, fmt.Errorf("socks5 connect: read: %w (raw=%x)", rerr, partial))
	}

	if reply[0] != 0x05 {
		return reply, fmt.Errorf("socks5 connect: %w (raw=%x)", ErrSocks5BadVersion, reply)
	}
	if reply[1] != 0x00 {
		return reply, fmt.Errorf("socks5 connect: %w (raw=%x)", ErrSocks5Reply{Code: reply[1]}, reply)
	}
	return reply, nil
}

// socks5UDPAssociate performs SOCKS5 UDP ASSOCIATE (CMD=03) on conn.
func socks5UDPAssociate(ctx context.Context, conn net.Conn) (relay *net.UDPAddr, rawReply []byte, err error) {
	applyDeadline(ctx, conn)

	// VER=05 CMD=03 RSV=00 ATYP=01 DST.ADDR=0.0.0.0 DST.PORT=0
	req := []byte{0x05, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
	if _, werr := conn.Write(req); werr != nil {
		return nil, nil, joinCtxErr(ctx, fmt.Errorf("socks5 udp-associate: write: %w", werr))
	}

	reply := make([]byte, 10)
	n, rerr := io.ReadFull(conn, reply)
	if rerr != nil {
		partial := reply[:n]
		if errors.Is(rerr, io.ErrUnexpectedEOF) || errors.Is(rerr, io.EOF) {
			return nil, partial, joinCtxErr(ctx, fmt.Errorf("socks5 udp-associate: %w (raw=%x)", ErrShortReply, partial))
		}
		return nil, partial, joinCtxErr(ctx, fmt.Errorf("socks5 udp-associate: read: %w (raw=%x)", rerr, partial))
	}

	if reply[0] != 0x05 {
		return nil, reply, fmt.Errorf("socks5 udp-associate: %w (raw=%x)", ErrSocks5BadVersion, reply)
	}
	if reply[1] != 0x00 {
		return nil, reply, fmt.Errorf("socks5 udp-associate: %w (raw=%x)", ErrSocks5Reply{Code: reply[1]}, reply)
	}
	if reply[3] != 0x01 {
		return nil, reply, fmt.Errorf("socks5 udp-associate: %w (atyp=0x%02X raw=%x)", ErrUnsupportedRelayATYP, reply[3], reply)
	}

	ip := net.IPv4(reply[4], reply[5], reply[6], reply[7])
	port := binary.BigEndian.Uint16(reply[8:10])
	relay = &net.UDPAddr{IP: ip, Port: int(port)}
	return relay, reply, nil
}
