package server

import (
	"encoding/binary"
	"fmt"
	"net"
	"strconv"
	"strings"
	"time"

	pb "github.com/lbryio/herald.go/protobuf/go"
)

const maxBufferSize = 1024

// genesis blocktime (which is actually wrong)
// magic constant for the UDPPing protocol. The above comment is taken from
// the python code this was implemented off of.
// https://github.com/lbryio/lbry-sdk/blob/7d49b046d44a4b7067d5dc1d6cd65ff0475c71c8/lbry/wallet/server/udp.py#L12
const magic = 1446058291
const protocolVersion = 1
const defaultFlags = 0b00000000
const availableFlag = 0b00000001

// SPVPing is a struct for the format of how to ping another hub over udp.
// format b'!lB64s'
type SPVPing struct {
	magic   uint32
	version byte
	padding []byte //64
}

// SPVPong is a struct for the return pong from another hub server.
// format b'!BBL32s4sH'
type SPVPong struct {
	protocolVersion byte
	flags           byte
	height          uint32
	tip             []byte // 32
	srcAddrRaw      []byte // 4
	country         uint16
}

// encodeSPVPing creates a slice of bytes to ping another hub with
// over udp.
func encodeSPVPing() []byte {
	data := make([]byte, 69)

	binary.BigEndian.PutUint32(data, magic)
	data[4] = protocolVersion

	return data
}

// decodeSPVPing takes a slice of bytes and decodes an SPVPing struct from them.
func decodeSPVPing(data []byte) *SPVPing {
	if len(data) < 69 {
		return nil
	}

	parsedMagic := binary.BigEndian.Uint32(data)
	parsedProtocalVersion := data[4]
	return &SPVPing{
		magic:   parsedMagic,
		version: parsedProtocalVersion,
	}
}

// Encode is a function for SPVPong structs to encode them into bytes for
// sending over udp.
func (pong *SPVPong) Encode() []byte {
	data := make([]byte, 44)

	data[0] = pong.protocolVersion
	data[1] = pong.flags
	binary.BigEndian.PutUint32(data[2:], pong.height)
	copy(data[6:], pong.tip)
	copy(data[38:], pong.srcAddrRaw)
	binary.BigEndian.PutUint16(data[42:], pong.country)

	return data
}

// makeSPVPong creates an SPVPong struct according to given parameters.
func makeSPVPong(flags int, height int, tip []byte, sourceAddr string, country string) *SPVPong {
	byteAddr := EncodeAddress(sourceAddr)
	var countryInt int32
	var ok bool
	if countryInt, ok = pb.Location_Country_value[country]; !ok {
		countryInt = int32(pb.Location_UNKNOWN_COUNTRY)
	}
	return &SPVPong{
		protocolVersion: protocolVersion,
		flags:           byte(flags),
		height:          uint32(height),
		tip:             tip,
		srcAddrRaw:      byteAddr,
		country:         uint16(countryInt),
	}
}

// decodeSPVPong takes a slice of bytes and decodes an SPVPong struct
// from it.
func decodeSPVPong(data []byte) *SPVPong {
	if len(data) < 44 {
		return nil
	}

	parsedProtocalVersion := data[0]
	flags := data[1]
	height := binary.BigEndian.Uint32(data[2:])
	tip := make([]byte, 32)
	copy(tip, data[6:38])
	srcRawAddr := make([]byte, 4)
	copy(srcRawAddr, data[38:42])
	country := binary.BigEndian.Uint16(data[42:])
	return &SPVPong{
		protocolVersion: parsedProtocalVersion,
		flags:           flags,
		height:          height,
		tip:             tip,
		srcAddrRaw:      srcRawAddr,
		country:         country,
	}
}

// EncodeAddress takes an ipv4 address and encodes it into bytes for the hub
// Ping/Pong protocol.
func EncodeAddress(addr string) []byte {
	parts := strings.Split(addr, ".")

	if len(parts) != 4 {
		return []byte{}
	}

	data := make([]byte, 4)
	for i, part := range parts {
		x, err := strconv.Atoi(part)
		if err != nil || x > 255 {
			return []byte{}
		}
		data[i] = byte(x)
	}

	return data
}

// DecodeAddress gets the string ipv4 address from an SPVPong struct.
func (pong *SPVPong) DecodeAddress() net.IP {
	return net.IPv4(
		pong.srcAddrRaw[0],
		pong.srcAddrRaw[1],
		pong.srcAddrRaw[2],
		pong.srcAddrRaw[3],
	)
}

func (pong *SPVPong) DecodeCountry() string {
	return pb.Location_Country_name[int32(pong.country)]
}

func (pong *SPVPong) DecodeProtocolVersion() int {
	return int(pong.protocolVersion)
}

func (pong *SPVPong) DecodeHeight() int {
	return int(pong.height)
}

func (pong *SPVPong) DecodeTip() []byte {
	return pong.tip
}

func (pong *SPVPong) DecodeFlags() byte {
	return pong.flags
}

// UDPPing sends a ping over udp to another hub and returns the ip address of
// this hub.
func UDPPing(ip, port string) (*SPVPong, error) {
	address := ip + ":" + port
	addr, err := net.ResolveUDPAddr("udp", address)
	if err != nil {
		return nil, err
	}

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

	defer conn.Close()

	_, err = conn.Write(encodeSPVPing())
	if err != nil {
		return nil, err
	}

	buffer := make([]byte, maxBufferSize)
	deadline := time.Now().Add(time.Second)
	err = conn.SetReadDeadline(deadline)
	if err != nil {
		return nil, err
	}
	n, _, err := conn.ReadFromUDP(buffer)
	if err != nil {
		return nil, err
	}

	pong := decodeSPVPong(buffer[:n])

	if pong == nil {
		return nil, fmt.Errorf("Pong decoding failed")
	}

	return pong, nil
}

// UDPServer is a goroutine that starts an udp server that implements the hubs
// Ping/Pong protocol to find out about each other without making full TCP
// connections.
func (s *Server) UDPServer(port int) error {
	address := ":" + strconv.Itoa(port)

	tip := make([]byte, 32)
	addr, err := net.ResolveUDPAddr("udp", address)
	if err != nil {
		return err
	}

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

	defer conn.Close()

	buffer := make([]byte, maxBufferSize)
	for {
		//TODO verify ping
		_, addr, err := conn.ReadFromUDP(buffer)
		if err != nil {
			return err
		}

		sAddr := addr.IP.String()
		pong := makeSPVPong(defaultFlags|availableFlag, 0, tip, sAddr, s.Args.Country)
		data := pong.Encode()

		_, err = conn.WriteToUDP(data, addr)
		if err != nil {
			return err
		}
	}
}
