package gost

import (
	"context"
	"crypto/tls"
	"net"
	"net/url"
	"time"

	"github.com/go-gost/gosocks5"
)

// Client is a proxy client.
// A client is divided into two layers: connector and transporter.
// Connector is responsible for connecting to the destination address through this proxy.
// Transporter performs a handshake with this proxy.
type Client struct {
	Connector
	Transporter
}

// DefaultClient is a standard HTTP proxy client.
var DefaultClient = &Client{Connector: HTTPConnector(nil), Transporter: TCPTransporter()}

// Dial connects to the address addr via the DefaultClient.
func Dial(addr string, options ...DialOption) (net.Conn, error) {
	return DefaultClient.Dial(addr, options...)
}

// Handshake performs a handshake via the DefaultClient.
func Handshake(conn net.Conn, options ...HandshakeOption) (net.Conn, error) {
	return DefaultClient.Handshake(conn, options...)
}

// Connect connects to the address addr via the DefaultClient.
func Connect(conn net.Conn, addr string) (net.Conn, error) {
	return DefaultClient.Connect(conn, addr)
}

// Connector is responsible for connecting to the destination address.
type Connector interface {
	// Deprecated: use ConnectContext instead.
	Connect(conn net.Conn, address string, options ...ConnectOption) (net.Conn, error)
	ConnectContext(ctx context.Context, conn net.Conn, network, address string, options ...ConnectOption) (net.Conn, error)
}

type autoConnector struct {
	User *url.Userinfo
}

// AutoConnector is a Connector.
func AutoConnector(user *url.Userinfo) Connector {
	return &autoConnector{
		User: user,
	}
}

func (c *autoConnector) Connect(conn net.Conn, address string, options ...ConnectOption) (net.Conn, error) {
	return c.ConnectContext(context.Background(), conn, "tcp", address, options...)
}

func (c *autoConnector) ConnectContext(ctx context.Context, conn net.Conn, network, address string, options ...ConnectOption) (net.Conn, error) {
	var cnr Connector
	switch network {
	case "tcp", "tcp4", "tcp6":
		cnr = &httpConnector{User: c.User}
	default:
		cnr = &socks5UDPTunConnector{User: c.User}
	}

	return cnr.ConnectContext(ctx, conn, network, address, options...)
}

// Transporter is responsible for handshaking with the proxy server.
type Transporter interface {
	Dial(addr string, options ...DialOption) (net.Conn, error)
	Handshake(conn net.Conn, options ...HandshakeOption) (net.Conn, error)
	// Indicate that the Transporter supports multiplex
	Multiplex() bool
}

// DialOptions describes the options for Transporter.Dial.
type DialOptions struct {
	Timeout time.Duration
	Chain   *Chain
	Host    string
}

// DialOption allows a common way to set DialOptions.
type DialOption func(opts *DialOptions)

// TimeoutDialOption specifies the timeout used by Transporter.Dial
func TimeoutDialOption(timeout time.Duration) DialOption {
	return func(opts *DialOptions) {
		opts.Timeout = timeout
	}
}

// ChainDialOption specifies a chain used by Transporter.Dial
func ChainDialOption(chain *Chain) DialOption {
	return func(opts *DialOptions) {
		opts.Chain = chain
	}
}

// HostDialOption specifies the host used by Transporter.Dial
func HostDialOption(host string) DialOption {
	return func(opts *DialOptions) {
		opts.Host = host
	}
}

// HandshakeOptions describes the options for handshake.
type HandshakeOptions struct {
	Addr       string
	Host       string
	User       *url.Userinfo
	Timeout    time.Duration
	Interval   time.Duration
	Retry      int
	TLSConfig  *tls.Config
	WSOptions  *WSOptions
	KCPConfig  *KCPConfig
	QUICConfig *QUICConfig
	SSHConfig  *SSHConfig
}

// HandshakeOption allows a common way to set HandshakeOptions.
type HandshakeOption func(opts *HandshakeOptions)

// AddrHandshakeOption specifies the server address
func AddrHandshakeOption(addr string) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.Addr = addr
	}
}

// HostHandshakeOption specifies the hostname
func HostHandshakeOption(host string) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.Host = host
	}
}

// UserHandshakeOption specifies the user used by Transporter.Handshake
func UserHandshakeOption(user *url.Userinfo) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.User = user
	}
}

// TimeoutHandshakeOption specifies the timeout used by Transporter.Handshake
func TimeoutHandshakeOption(timeout time.Duration) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.Timeout = timeout
	}
}

// IntervalHandshakeOption specifies the interval time used by Transporter.Handshake
func IntervalHandshakeOption(interval time.Duration) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.Interval = interval
	}
}

// RetryHandshakeOption specifies the times of retry used by Transporter.Handshake
func RetryHandshakeOption(retry int) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.Retry = retry
	}
}

// TLSConfigHandshakeOption specifies the TLS config used by Transporter.Handshake
func TLSConfigHandshakeOption(config *tls.Config) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.TLSConfig = config
	}
}

// WSOptionsHandshakeOption specifies the websocket options used by websocket handshake
func WSOptionsHandshakeOption(options *WSOptions) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.WSOptions = options
	}
}

// KCPConfigHandshakeOption specifies the KCP config used by KCP handshake
func KCPConfigHandshakeOption(config *KCPConfig) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.KCPConfig = config
	}
}

// QUICConfigHandshakeOption specifies the QUIC config used by QUIC handshake
func QUICConfigHandshakeOption(config *QUICConfig) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.QUICConfig = config
	}
}

// SSHConfigHandshakeOption specifies the ssh config used by SSH client handshake.
func SSHConfigHandshakeOption(config *SSHConfig) HandshakeOption {
	return func(opts *HandshakeOptions) {
		opts.SSHConfig = config
	}
}

// ConnectOptions describes the options for Connector.Connect.
type ConnectOptions struct {
	Addr      string
	Timeout   time.Duration
	User      *url.Userinfo
	Selector  gosocks5.Selector
	UserAgent string
	NoTLS     bool
	NoDelay   bool
}

// ConnectOption allows a common way to set ConnectOptions.
type ConnectOption func(opts *ConnectOptions)

// AddrConnectOption specifies the corresponding address of the target.
func AddrConnectOption(addr string) ConnectOption {
	return func(opts *ConnectOptions) {
		opts.Addr = addr
	}
}

// TimeoutConnectOption specifies the timeout for connecting to target.
func TimeoutConnectOption(timeout time.Duration) ConnectOption {
	return func(opts *ConnectOptions) {
		opts.Timeout = timeout
	}
}

// UserConnectOption specifies the user info for authentication.
func UserConnectOption(user *url.Userinfo) ConnectOption {
	return func(opts *ConnectOptions) {
		opts.User = user
	}
}

// SelectorConnectOption specifies the SOCKS5 client selector.
func SelectorConnectOption(s gosocks5.Selector) ConnectOption {
	return func(opts *ConnectOptions) {
		opts.Selector = s
	}
}

// UserAgentConnectOption specifies the HTTP user-agent header.
func UserAgentConnectOption(ua string) ConnectOption {
	return func(opts *ConnectOptions) {
		opts.UserAgent = ua
	}
}

// NoTLSConnectOption specifies the SOCKS5 method without TLS.
func NoTLSConnectOption(b bool) ConnectOption {
	return func(opts *ConnectOptions) {
		opts.NoTLS = b
	}
}

// NoDelayConnectOption specifies the NoDelay option for ss.Connect.
func NoDelayConnectOption(b bool) ConnectOption {
	return func(opts *ConnectOptions) {
		opts.NoDelay = b
	}
}