package proxy

import (
	"context"
	"errors"
	"log"
	"net"
	"net/url"
	"strconv"
	"time"

	"github.com/miekg/dns"
	"github.com/pion/transport/v2"
	"github.com/txthinking/socks5"
)

func NewSocks5UDPClient(addr *url.URL) SocksClient {
	return SocksClient{addr: addr}
}

type SocksClient struct {
	addr *url.URL
}

type SocksConn struct {
	net.Conn
	socks5Client *socks5.Client
}

func (s SocksConn) SetReadBuffer(bytes int) error {
	return nil
}

func (s SocksConn) SetWriteBuffer(bytes int) error {
	return nil
}

func (s SocksConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) {
	var buf [2000]byte
	n, err = s.Conn.Read(buf[:])
	if err != nil {
		return 0, nil, err
	}
	Datagram, err := socks5.NewDatagramFromBytes(buf[:n])
	if err != nil {
		return 0, nil, err
	}
	addr, err = net.ResolveUDPAddr("udp", Datagram.Address())
	if err != nil {
		return 0, nil, err
	}
	n = copy(b, Datagram.Data)
	if n < len(Datagram.Data) {
		return 0, nil, errors.New("short buffer")
	}
	return len(Datagram.Data), addr, nil
}

func (s SocksConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) {
	panic("unimplemented")
}

func (s SocksConn) WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) {

	a, addrb, portb, err := socks5.ParseAddress(addr.String())
	if err != nil {
		return 0, err
	}
	packet := socks5.NewDatagram(a, addrb, portb, b)
	_, err = s.Conn.Write(packet.Bytes())
	if err != nil {
		return 0, err
	}
	return len(b), nil
}

func (s SocksConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) {
	panic("unimplemented")
}

func (sc *SocksClient) ListenPacket(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) {
	conn, err := sc.listenPacket()
	if err != nil {
		log.Println("[SOCKS5 Client Error] cannot listen packet", err)
	}
	return conn, err
}

func (sc *SocksClient) listenPacket() (transport.UDPConn, error) {
	var username, password string
	if sc.addr.User != nil {
		username = sc.addr.User.Username()
		password, _ = sc.addr.User.Password()
	}
	client, err := socks5.NewClient(
		sc.addr.Host,
		username, password, 300, 300)
	if err != nil {
		return nil, err
	}

	err = client.Negotiate(nil)
	if err != nil {
		return nil, err
	}

	udpRequest := socks5.NewRequest(socks5.CmdUDP, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00})

	reply, err := client.Request(udpRequest)
	if err != nil {
		return nil, err
	}

	udpServerAddr := socks5.ToAddress(reply.Atyp, reply.BndAddr, reply.BndPort)

	conn, err := net.Dial("udp", udpServerAddr)
	if err != nil {
		return nil, err
	}

	return &SocksConn{conn, client}, nil
}

func (s SocksConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
	return s.WriteToUDP(p, addr.(*net.UDPAddr))
}

func (s SocksConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
	return s.ReadFromUDP(p)
}

func (s SocksConn) Read(b []byte) (int, error) {
	panic("implement me")
}

func (s SocksConn) RemoteAddr() net.Addr {
	panic("implement me")
}

func (s SocksConn) Write(b []byte) (int, error) {
	panic("implement me")
}

func (sc *SocksClient) ResolveUDPAddr(network string, address string) (*net.UDPAddr, error) {
	dnsServer, err := net.ResolveUDPAddr("udp", "1.1.1.1:53")
	if err != nil {
		return nil, err
	}
	proxiedResolver := newDnsResolver(sc, dnsServer)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	host, port, err := net.SplitHostPort(address)
	if err != nil {
		return nil, err
	}
	ip, err := proxiedResolver.lookupIPAddr(ctx, host, network == "udp6")
	if err != nil {
		return nil, err
	}
	if len(ip) <= 0 {
		return nil, errors.New("cannot resolve hostname: NXDOMAIN")
	}
	switch network {
	case "udp4":
		var v4IPAddr []net.IPAddr
		for _, v := range ip {
			if v.IP.To4() != nil {
				v4IPAddr = append(v4IPAddr, v)
			}
		}
		ip = v4IPAddr
	case "udp6":
		var v6IPAddr []net.IPAddr
		for _, v := range ip {
			if v.IP.To4() == nil {
				v6IPAddr = append(v6IPAddr, v)
			}
		}
		ip = v6IPAddr
	case "udp":
	default:
		return nil, errors.New("unknown network")
	}

	if len(ip) <= 0 {
		return nil, errors.New("cannot resolve hostname: so suitable address")
	}

	portInInt, err := strconv.ParseInt(port, 10, 32)
	return &net.UDPAddr{
		IP:   ip[0].IP,
		Port: int(portInInt),
		Zone: "",
	}, nil
}

func newDnsResolver(sc *SocksClient,
	serverAddress net.Addr) *dnsResolver {
	return &dnsResolver{sc: sc, serverAddress: serverAddress}
}

type dnsResolver struct {
	sc            *SocksClient
	serverAddress net.Addr
}

func (r *dnsResolver) lookupIPAddr(ctx context.Context, host string, ipv6 bool) ([]net.IPAddr, error) {
	packetConn, err := r.sc.listenPacket()
	if err != nil {
		return nil, err
	}
	msg := new(dns.Msg)
	if !ipv6 {
		msg.SetQuestion(dns.Fqdn(host), dns.TypeA)
	} else {
		msg.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)
	}
	encodedMsg, err := msg.Pack()
	if err != nil {
		log.Println(err.Error())
	}
	for i := 2; i >= 0; i-- {
		_, err := packetConn.WriteTo(encodedMsg, r.serverAddress)
		if err != nil {
			log.Println(err.Error())
		}
	}
	ctx, cancel := context.WithTimeout(ctx, time.Second)
	defer cancel()
	go func() {
		<-ctx.Done()
		packetConn.Close()
	}()
	var dataBuf [1600]byte
	n, _, err := packetConn.ReadFrom(dataBuf[:])
	if err != nil {
		return nil, err
	}
	err = msg.Unpack(dataBuf[:n])
	if err != nil {
		return nil, err
	}
	var returnedIPs []net.IPAddr
	for _, resp := range msg.Answer {
		switch respTyped := resp.(type) {
		case *dns.A:
			returnedIPs = append(returnedIPs, net.IPAddr{IP: respTyped.A})
		case *dns.AAAA:
			returnedIPs = append(returnedIPs, net.IPAddr{IP: respTyped.AAAA})
		}
	}
	return returnedIPs, nil
}

func NewTransportWrapper(sc *SocksClient, innerNet transport.Net) transport.Net {
	return &transportWrapper{sc: sc, Net: innerNet}
}

type transportWrapper struct {
	transport.Net
	sc *SocksClient
}

func (t *transportWrapper) ListenUDP(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) {
	return t.sc.ListenPacket(network, nil)
}

func (t *transportWrapper) ListenPacket(network string, address string) (net.PacketConn, error) {
	return t.sc.ListenPacket(network, nil)
}

func (t *transportWrapper) ResolveUDPAddr(network string, address string) (*net.UDPAddr, error) {
	return t.sc.ResolveUDPAddr(network, address)
}
