package socks5

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

// Config carries connection-time SOCKS5 settings.
type Config struct {
	ProxyAddr string // "host:port"
	UseAuth   bool
	Login     string
	Password  string
}

// Dial opens a TCP connection to the SOCKS5 proxy, runs the greeting,
// optionally authenticates with username/password (RFC 1929), and
// issues a CONNECT to host:port (sent as ATYP=03 domain so the proxy
// resolves on its side). Returns the established net.Conn ready for
// bidirectional traffic.
//
// The given ctx bounds dial + handshake; once Dial returns, the conn
// has its own deadline-free I/O state.
func Dial(ctx context.Context, cfg Config, host string, port uint16) (net.Conn, error) {
	d := net.Dialer{}
	conn, err := d.DialContext(ctx, "tcp", cfg.ProxyAddr)
	if err != nil {
		return nil, fmt.Errorf("dial proxy: %w", err)
	}
	if dl, ok := ctx.Deadline(); ok {
		conn.SetDeadline(dl)
	}
	if err := handshake(conn, cfg, host, port); err != nil {
		conn.Close()
		return nil, err
	}
	conn.SetDeadline(time.Time{})
	return conn, nil
}

func handshake(conn net.Conn, cfg Config, host string, port uint16) error {
	// Greeting
	if cfg.UseAuth {
		if _, err := conn.Write([]byte{0x05, 0x02, 0x00, 0x02}); err != nil {
			return fmt.Errorf("greet write: %w", err)
		}
	} else {
		if _, err := conn.Write([]byte{0x05, 0x01, 0x00}); err != nil {
			return fmt.Errorf("greet write: %w", err)
		}
	}
	var rep [2]byte
	if _, err := io.ReadFull(conn, rep[:]); err != nil {
		return fmt.Errorf("greet read: %w", err)
	}
	if rep[0] != 0x05 {
		return fmt.Errorf("greet: server version %#x is not SOCKS5", rep[0])
	}
	if rep[1] == 0xff {
		return errors.New("greet: proxy rejected all offered auth methods")
	}
	method := rep[1]

	// Auth subneg
	if method == 0x02 {
		if !cfg.UseAuth {
			return errors.New("proxy requires auth but Config.UseAuth is false")
		}
		if len(cfg.Login) > 255 || len(cfg.Password) > 255 {
			return errors.New("login or password too long")
		}
		buf := make([]byte, 0, 3+len(cfg.Login)+len(cfg.Password))
		buf = append(buf, 0x01, byte(len(cfg.Login)))
		buf = append(buf, []byte(cfg.Login)...)
		buf = append(buf, byte(len(cfg.Password)))
		buf = append(buf, []byte(cfg.Password)...)
		if _, err := conn.Write(buf); err != nil {
			return fmt.Errorf("auth write: %w", err)
		}
		if _, err := io.ReadFull(conn, rep[:]); err != nil {
			return fmt.Errorf("auth read: %w", err)
		}
		if rep[1] != 0x00 {
			return errors.New("auth: invalid login or password")
		}
	}

	// CONNECT
	if len(host) > 255 {
		return errors.New("host too long")
	}
	req := make([]byte, 0, 7+len(host))
	req = append(req, 0x05, 0x01, 0x00, 0x03, byte(len(host)))
	req = append(req, []byte(host)...)
	pBuf := make([]byte, 2)
	binary.BigEndian.PutUint16(pBuf, port)
	req = append(req, pBuf...)
	if _, err := conn.Write(req); err != nil {
		return fmt.Errorf("connect write: %w", err)
	}
	var creply [10]byte
	if _, err := io.ReadFull(conn, creply[:]); err != nil {
		return fmt.Errorf("connect read: %w", err)
	}
	if creply[0] != 0x05 {
		return fmt.Errorf("connect: server version %#x is not SOCKS5", creply[0])
	}
	if creply[1] != 0x00 {
		return fmt.Errorf("connect: REP=%#02x", creply[1])
	}
	return nil
}
