package tor

import (
	"context"
	"fmt"
	"git.openprivacy.ca/openprivacy/bine/control"
	"net"
	"strings"

	"golang.org/x/net/proxy"
)

// Dialer is a wrapper around a proxy.Dialer for dialing connections.
type Dialer struct {
	proxy.Dialer
}

// Authenticator provides a facade over various Tor control port authentication methods.
type Authenticator interface {
	Authenticate(controlport *control.Conn) error
}

// DialConf is the configuration used for Dialer.
type DialConf struct {
	// ProxyAddress is the address for the SOCKS5 proxy. If empty, it is looked
	// up.
	ProxyAddress string

	// ProxyNetwork is the network for the SOCKS5 proxy. If ProxyAddress is
	// empty, this value is ignored and overridden by what is looked up. If this
	// is empty and ProxyAddress is not empty, it defaults to "tcp".
	ProxyNetwork string

	// ProxyAuth is the auth for the proxy. Since Tor's SOCKS5 proxy is
	// unauthenticated, this is rarely needed. It can be used when
	// IsolateSOCKSAuth is set to ensure separate circuits.
	//
	// This should not be confused with downstream SOCKS proxy authentication
	// which is set via Tor values for Socks5ProxyUsername and
	// Socks5ProxyPassword when Socks5Proxy is set.
	ProxyAuth *proxy.Auth

	// SkipEnableNetwork, if true, will skip the enable network step in Dialer.
	SkipEnableNetwork bool

	// Forward is the dialer to forward to. If nil, just uses normal net dialer.
	Forward proxy.Dialer

	Authenticator Authenticator
}

// Dialer creates a new Dialer for the given configuration. Context can be nil.
// If conf is nil, a default is used.
func (t *Tor) Dialer(ctx context.Context, conf *DialConf) (*Dialer, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	if conf == nil {
		conf = &DialConf{}
	}

	conf.Authenticator.Authenticate(t.Control)

	// Enable the network if requested
	if !conf.SkipEnableNetwork {
		if err := t.EnableNetwork(ctx, true); err != nil {
			return nil, err
		}
	}
	// Lookup proxy address as needed
	proxyNetwork := conf.ProxyNetwork
	proxyAddress := conf.ProxyAddress
	if proxyAddress == "" {
		info, err := t.Control.GetInfo("net/listeners/socks")
		if err != nil {
			return nil, err
		}
		if len(info) != 1 || info[0].Key != "net/listeners/socks" {
			return nil, fmt.Errorf("Unable to get socks proxy address")
		}

		if strings.HasPrefix(info[0].Val, "\"") {
			options := strings.Split(info[0].Val, " ")
			// we get this kind of response from Tails / OnionGrater, multiple potential listeners
			// formatted. Simply choose the first one.
			// NOTE: This logic is probably not the best...
			proxyAddress = options[0][1 : len(options[0])-1]
			proxyNetwork = "tcp"
		} else {

			proxyAddress = info[0].Val
			if strings.HasPrefix(proxyAddress, "unix:") {
				proxyAddress = proxyAddress[5:]
				proxyNetwork = "unix"
			} else {
				proxyNetwork = "tcp"
			}
		}
	} else if proxyNetwork == "" {
		proxyNetwork = "tcp"
	}

	dialer, err := proxy.SOCKS5(proxyNetwork, proxyAddress, conf.ProxyAuth, conf.Forward)
	if err != nil {
		return nil, err
	}
	return &Dialer{dialer}, nil
}

// DialContext is the equivalent of net.DialContext.
//
// TODO: Remove when https://github.com/golang/go/issues/17759 is released.
func (d *Dialer) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
	errCh := make(chan error, 1)
	connCh := make(chan net.Conn, 1)
	go func() {
		if conn, err := d.Dial(network, addr); err != nil {
			errCh <- err
		} else if ctx.Err() != nil {
			conn.Close()
		} else {
			connCh <- conn
		}
	}()
	select {
	case err := <-errCh:
		return nil, err
	case conn := <-connCh:
		return conn, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}
