package socks5

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/url"
	"time"
)

// Dialer is a SOCKS5 dialer.
type Dialer struct {
	// ProxyNetwork network between a proxy server and a client
	ProxyNetwork string
	// ProxyAddress proxy server address
	ProxyAddress string
	// ProxyDial specifies the optional dial function for
	// establishing the transport connection.
	ProxyDial func(ctx context.Context, network string, address string) (net.Conn, error)
	// ProxyPacketDial specifies the optional proxyPacketDial function for
	// establishing the transport connection.
	ProxyPacketDial func(ctx context.Context, network string, address string) (net.PacketConn, error)
	// Username use username authentication if not empty
	Username string
	// Password use password authentication if not empty,
	// only valid if username is set
	Password string
	// IsResolve resolve domain name on locally
	IsResolve bool
	// Resolver optionally specifies an alternate resolver to use
	Resolver *net.Resolver
	// Timeout is the maximum amount of time a dial will wait for
	// a connect to complete. The default is no timeout
	Timeout time.Duration
}

// NewDialer returns a new Dialer that dials through the provided
// proxy server's network and address.
func NewDialer(addr string) (*Dialer, error) {
	d := &Dialer{
		ProxyNetwork: "tcp",
		Timeout:      time.Minute,
	}
	u, err := url.Parse(addr)
	if err != nil {
		return nil, err
	}
	switch u.Scheme {
	case "socks5":
		d.IsResolve = true
	case "socks5h":
	default:
		return nil, fmt.Errorf("unsupported protocol '%s'", u.Scheme)
	}
	host := u.Host
	port := u.Port()
	if port == "" {
		port = "1080"
		hostname := u.Hostname()
		host = net.JoinHostPort(hostname, port)
	}
	if u.User != nil {
		d.Username = u.User.Username()
		d.Password, _ = u.User.Password()
	}
	d.ProxyAddress = host
	return d, nil
}

// DialContext connects to the provided address on the provided network.
func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	switch network {
	default:
		return nil, fmt.Errorf("unsupported network %q", network)
	case "tcp", "tcp4", "tcp6":
		return d.do(ctx, ConnectCommand, address)
	case "udp", "udp4", "udp6":
		return d.do(ctx, AssociateCommand, address)
	}
}

// Dial connects to the provided address on the provided network.
func (d *Dialer) Dial(network, address string) (net.Conn, error) {
	return d.DialContext(context.Background(), network, address)
}

func (d *Dialer) Listen(ctx context.Context, network, address string) (net.Listener, error) {
	switch network {
	default:
		return nil, fmt.Errorf("unsupported network %q", network)
	case "tcp", "tcp4", "tcp6":
	}
	return &listener{ctx: ctx, d: d, address: address}, nil
}

func (d *Dialer) do(ctx context.Context, cmd Command, address string) (net.Conn, error) {
	if d.IsResolve {
		host, port, err := net.SplitHostPort(address)
		if err != nil {
			return nil, err
		}
		if host != "" {
			ip := net.ParseIP(host)
			if ip == nil {
				ipaddr, err := d.resolver().LookupIP(ctx, "ip4", host)
				if err != nil {
					ipaddr, err = d.resolver().LookupIP(ctx, "ip", host)
					if err != nil {
						return nil, err
					}
				}
				host := ipaddr[0].String()
				address = net.JoinHostPort(host, port)
			}
		}
	}

	conn, err := d.proxyDial(ctx, d.ProxyNetwork, d.ProxyAddress)
	if err != nil {
		return nil, err
	}

	return d.connect(ctx, conn, cmd, address)
}

func (d *Dialer) connect(ctx context.Context, conn net.Conn, cmd Command, address string) (net.Conn, error) {
	if d.Timeout != 0 {
		deadline := time.Now().Add(d.Timeout)
		if d, ok := ctx.Deadline(); !ok || deadline.Before(d) {
			subCtx, cancel := context.WithDeadline(ctx, deadline)
			defer cancel()
			ctx = subCtx
		}
	}
	if deadline, ok := ctx.Deadline(); ok && !deadline.IsZero() {
		conn.SetDeadline(deadline)
		defer conn.SetDeadline(time.Time{})
	}

	err := d.connectAuth(conn)
	if err != nil {
		return nil, err
	}

	switch cmd {
	default:
		return nil, fmt.Errorf("unsupported Command %s", cmd)
	case ConnectCommand:
		_, err := d.connectCommand(conn, ConnectCommand, address)
		if err != nil {
			return nil, err
		}
		return conn, nil
	case BindCommand:
		_, err := d.connectCommand(conn, BindCommand, address)
		if err != nil {
			return nil, err
		}
		return conn, nil
	case AssociateCommand:
		targetIP, targetPort, err := splitHostPort(address)
		if err != nil {
			return nil, err
		}

		addr, err := d.connectCommand(conn, AssociateCommand, ":0")
		if err != nil {
			return nil, err
		}

		proxyIP, proxyPort, err := splitHostPort(addr.String())
		if err != nil {
			return nil, err
		}

		udpConn, err := d.proxyPacketDial(ctx, "udp", ":0")
		if err != nil {
			return nil, err
		}

		targetAddr := &net.UDPAddr{
			IP:   net.ParseIP(targetIP),
			Port: targetPort,
		}
		proxyAddr := &net.UDPAddr{
			IP:   net.ParseIP(proxyIP),
			Port: proxyPort,
		}
		wrapConn, err := NewUDPConn(udpConn, proxyAddr, targetAddr)
		if err != nil {
			return nil, err
		}

		go func() {
			var buf [1]byte
			for {
				_, err := conn.Read(buf[:])
				if err != nil {
					wrapConn.Close()
					break
				}
			}
		}()
		return wrapConn, nil
	}

}

func (d *Dialer) connectAuth(conn net.Conn) error {
	_, err := conn.Write([]byte{socks5Version})
	if err != nil {
		return err
	}
	if d.Username == "" {
		err = writeBytes(conn, []byte{byte(noAuth)})
		if err != nil {
			return err
		}
	} else {
		err = writeBytes(conn, []byte{byte(noAuth), byte(userAuth)})
		if err != nil {
			return err
		}
	}

	var header [2]byte
	_, err = io.ReadFull(conn, header[:])
	if err != nil {
		return err
	}
	if header[0] != socks5Version {
		return fmt.Errorf("unexpected protocol version %d", header[0])
	}
	if authMethod(header[1]) == noAcceptable {
		return fmt.Errorf("no acceptable authentication methods %d", authMethod(header[1]))
	}
	switch authMethod(header[1]) {
	default:
		return fmt.Errorf("authentication method not supported %d", authMethod(header[1]))
	case noAuth:
	case userAuth:
		if d.Username == "" {
			return errors.New("need username/password")
		}

		if len(d.Username) == 0 || len(d.Username) > 255 || len(d.Password) == 0 || len(d.Password) > 255 {
			return errors.New("invalid username/password")
		}
		_, err = conn.Write([]byte{userAuthVersion})
		if err != nil {
			return err
		}
		err = writeBytes(conn, []byte(d.Username))
		if err != nil {
			return err
		}
		err = writeBytes(conn, []byte(d.Password))
		if err != nil {
			return err
		}

		_, err := io.ReadFull(conn, header[:])
		if err != nil {
			return err
		}
		if header[0] != userAuthVersion {
			return fmt.Errorf("invalid username/password version %d", header[0])
		}
		if header[1] != authSuccess {
			return fmt.Errorf("username/password authentication failed %d", header[1])
		}
	}
	return nil
}

func (d *Dialer) connectCommand(conn net.Conn, cmd Command, address string) (net.Addr, error) {
	_, err := conn.Write([]byte{socks5Version, byte(cmd), 0})
	if err != nil {
		return nil, err
	}
	err = writeAddrWithStr(conn, address)
	if err != nil {
		return nil, err
	}

	return d.readReply(conn)
}

func (d *Dialer) readReply(conn net.Conn) (net.Addr, error) {
	var header [3]byte
	_, err := io.ReadFull(conn, header[:])
	if err != nil {
		return nil, err
	}

	if header[0] != socks5Version {
		return nil, fmt.Errorf("unexpected protocol version %d", header[0])
	}

	if reply(header[1]) != successReply {
		return nil, fmt.Errorf("unknown error %s", reply(header[1]).String())
	}

	return readAddr(conn)
}

func (d *Dialer) resolver() *net.Resolver {
	if d.Resolver == nil {
		return net.DefaultResolver
	}
	return d.Resolver
}

func (d *Dialer) proxyDial(ctx context.Context, network, address string) (net.Conn, error) {
	proxyDial := d.ProxyDial
	if proxyDial == nil {
		var dialer net.Dialer
		proxyDial = dialer.DialContext
	}
	return proxyDial(ctx, network, address)
}

func (d *Dialer) proxyPacketDial(ctx context.Context, network, address string) (net.PacketConn, error) {
	proxyPacketDial := d.ProxyPacketDial
	if proxyPacketDial == nil {
		var listener net.ListenConfig
		proxyPacketDial = listener.ListenPacket
	}
	return proxyPacketDial(ctx, network, address)
}

type listener struct {
	ctx     context.Context
	d       *Dialer
	address string
}

// Accept waits for and returns the next connection to the listener.
func (l *listener) Accept() (net.Conn, error) {
	conn, err := l.d.do(l.ctx, BindCommand, l.address)
	if err != nil {
		return nil, err
	}
	addr, err := l.d.readReply(conn)
	if err != nil {
		return nil, err
	}
	return &connect{Conn: conn, remoteAddr: addr}, nil
}

// Close closes the listener.
func (l *listener) Close() error {
	return nil
}

// address returns the listener's network address.
func (l *listener) Addr() net.Addr {
	return nil
}

type connect struct {
	net.Conn
	remoteAddr net.Addr
}

func (c *connect) RemoteAddr() net.Addr {
	return c.remoteAddr
}
