package torsvc

import (
	"fmt"
	"net"
	"strconv"

	"github.com/btcsuite/go-socks/socks"
	"github.com/miekg/dns"
	"github.com/roasbeef/btcd/connmgr"
	"golang.org/x/net/proxy"
)

const (
	localhost = "127.0.0.1"
)

var (
	// DNS Message Response Codes, see
	// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml
	dnsCodes = map[int]string{
		0:  "No Error",
		1:  "Format Error",
		2:  "Server Failure",
		3:  "Non-Existent Domain",
		4:  "Not Implemented",
		5:  "Query Refused",
		6:  "Name Exists when it should not",
		7:  "RR Set Exists when it should not",
		8:  "RR Set that should exist does not",
		9:  "Server Not Authoritative for zone",
		10: "Name not contained in zone",
		// Left out 16: "Bad OPT Version" because of duplicate keys and
		// because miekg/dns does not use this message response code.
		16: "TSIG Signature Failure",
		17: "Key not recognized",
		18: "Signature out of time window",
		19: "Bad TKEY Mode",
		20: "Duplicate key name",
		21: "Algorithm not supported",
		22: "Bad Truncation",
		23: "Bad/missing Server Cookie",
	}
)

// TorDial returns a connection to a remote peer via Tor's socks proxy. Only
// TCP is supported over Tor.
func TorDial(address, socksPort string) (net.Conn, error) {
	p := &socks.Proxy{Addr: localhost + ":" + socksPort}
	return p.Dial("tcp", address)
}

// TorLookupHost performs DNS resolution on a given hostname via Tor's
// native resolver. Only IPv4 addresses are returned.
func TorLookupHost(host, socksPort string) ([]string, error) {
	ip, err := connmgr.TorLookupIP(host, localhost+":"+socksPort)
	if err != nil {
		return nil, err
	}

	var addrs []string
	// Only one IPv4 address is returned by the TorLookupIP function.
	addrs = append(addrs, ip[0].String())
	return addrs, nil
}

// TorLookupSRV uses Tor's socks proxy to route DNS SRV queries. Tor does not
// natively support SRV queries so we must route all SRV queries THROUGH the
// Tor proxy and connect directly to a DNS server and query it.
// NOTE: TorLookupSRV uses golang's proxy package since go-socks will cause
// the SRV request to hang.
func TorLookupSRV(service, proto, name, socksPort, dnsServer string) (string,
	[]*net.SRV, error) {
	// _service._proto.name as described in RFC#2782.
	host := "_" + service + "._" + proto + "." + name + "."

	// Set up golang's proxy dialer - Tor's socks proxy doesn't support
	// authentication.
	dialer, err := proxy.SOCKS5(
		"tcp",
		localhost+":"+socksPort,
		nil,
		proxy.Direct,
	)
	if err != nil {
		return "", nil, err
	}

	// Dial dnsServer via Tor. dnsServer must have TCP resolution enabled
	// for the port we are dialing.
	conn, err := dialer.Dial("tcp", dnsServer)
	if err != nil {
		return "", nil, err
	}

	// Construct the actual SRV request.
	msg := new(dns.Msg)
	msg.SetQuestion(host, dns.TypeSRV)
	msg.RecursionDesired = true

	dnsConn := &dns.Conn{Conn: conn}
	defer dnsConn.Close()

	// Write the SRV request.
	dnsConn.WriteMsg(msg)

	// Read the response.
	resp, err := dnsConn.ReadMsg()
	if err != nil {
		return "", nil, err
	}

	// If the message response code was not the success code, fail.
	if resp.Rcode != dns.RcodeSuccess {
		return "", nil, fmt.Errorf("Unsuccessful SRV request, "+
			"received: %s", dnsCodes[resp.Rcode])
	}

	// Retrieve the RR(s) of the Answer section.
	var rrs []*net.SRV
	for _, rr := range resp.Answer {
		srv := rr.(*dns.SRV)
		rrs = append(rrs, &net.SRV{
			Target:   srv.Target,
			Port:     srv.Port,
			Priority: srv.Priority,
			Weight:   srv.Weight,
		})
	}

	return "", rrs, nil
}

// TorResolveTCP uses Tor's proxy to resolve TCP addresses instead of the
// system resolver that ResolveTCPAddr and related functions use. Only TCP
// resolution is supported.
func TorResolveTCP(address, socksPort string) (*net.TCPAddr, error) {
	// Split host:port since the lookup function does not take a port.
	host, port, err := net.SplitHostPort(address)
	if err != nil {
		return nil, err
	}

	// Look up the host's IP address via Tor.
	ip, err := TorLookupHost(host, socksPort)
	if err != nil {
		return nil, err
	}

	// Convert port to an int.
	p, err := strconv.Atoi(port)
	if err != nil {
		return nil, err
	}

	// Return a *net.TCPAddr exactly like net.ResolveTCPAddr.
	return &net.TCPAddr{
		IP:   net.ParseIP(ip[0]),
		Port: p,
	}, nil
}
