package lndc

import (
	"bytes"
	"fmt"
	"net"

	"github.com/btcsuite/fastsha256"
	"github.com/codahale/chacha20poly1305"
	"github.com/roasbeef/btcd/btcec"
	"github.com/roasbeef/btcutil"
)

// Listener...
type Listener struct {
	longTermPriv *btcec.PrivateKey

	tcp *net.TCPListener
}

var _ net.Listener = (*Listener)(nil)

// NewListener...
func NewListener(localPriv *btcec.PrivateKey, listenAddr string) (*Listener, error) {
	addr, err := net.ResolveTCPAddr("tcp", listenAddr)
	if err != nil {
		return nil, err
	}

	l, err := net.ListenTCP("tcp", addr)
	if err != nil {
		return nil, err
	}

	return &Listener{localPriv, l}, nil
}

// Accept waits for and returns the next connection to the listener.
// Part of the net.Listener interface.
func (l *Listener) Accept() (c net.Conn, err error) {
	conn, err := l.tcp.Accept()
	if err != nil {
		return nil, err
	}

	lndc := NewConn(conn)

	// Exchange an ephemeral public key with the remote connection in order
	// to establish a confidential connection before we attempt to
	// authenticated.
	ephemeralKey, err := l.createCipherConn(lndc)
	if err != nil {
		return nil, err
	}

	// Now that we've established an encrypted connection, authenticate the
	// identity of the remote host.
	ephemeralPub := ephemeralKey.PubKey().SerializeCompressed()
	if err := l.authenticateConnection(l.longTermPriv, lndc, ephemeralPub); err != nil {
		lndc.Close()
		return nil, err
	}

	return lndc, nil
}

// createCipherConn....
func (l *Listener) createCipherConn(lnConn *LNDConn) (*btcec.PrivateKey, error) {
	var err error
	var theirEphPubBytes []byte

	// First, read and deserialize their ephemeral public key.
	theirEphPubBytes, err = readClear(lnConn.Conn)
	if err != nil {
		return nil, err
	}
	if len(theirEphPubBytes) != 33 {
		return nil, fmt.Errorf("Got invalid %d byte eph pubkey %x\n",
			len(theirEphPubBytes), theirEphPubBytes)
	}
	theirEphPub, err := btcec.ParsePubKey(theirEphPubBytes, btcec.S256())
	if err != nil {
		return nil, err
	}

	// Once we've parsed and verified their key, generate, and send own
	// ephemeral key pair for use within this session.
	myEph, err := btcec.NewPrivateKey(btcec.S256())
	if err != nil {
		return nil, err
	}
	if _, err := writeClear(lnConn.Conn, myEph.PubKey().SerializeCompressed()); err != nil {
		return nil, err
	}

	// Now that we have both keys, do non-interactive diffie with ephemeral
	// pubkeys, sha256 for good luck.
	sessionKey := fastsha256.Sum256(
		btcec.GenerateSharedSecret(myEph, theirEphPub),
	)

	lnConn.chachaStream, err = chacha20poly1305.New(sessionKey[:])

	lnConn.remoteNonceInt = 1 << 63
	lnConn.myNonceInt = 0

	lnConn.RemotePub = theirEphPub
	lnConn.Authed = false

	return myEph, nil
}

// AuthListen...
func (l *Listener) authenticateConnection(
	myId *btcec.PrivateKey, lnConn *LNDConn, localEphPubBytes []byte) error {
	var err error

	// TODO(roasbeef): should be using read/write clear here?
	slice := make([]byte, 73)
	n, err := lnConn.Conn.Read(slice)
	if err != nil {
		return err
	}

	authmsg := slice[:n]
	if len(authmsg) != 53 && len(authmsg) != 73 {
		return fmt.Errorf("got auth message of %d bytes, "+
			"expect 53 or 73", len(authmsg))
	}

	myPKH := btcutil.Hash160(l.longTermPriv.PubKey().SerializeCompressed())
	if !bytes.Equal(myPKH, authmsg[33:53]) {
		return fmt.Errorf(
			"remote host asking for PKH %x, that's not me", authmsg[33:53])
	}

	// do DH with id keys
	theirPub, err := btcec.ParsePubKey(authmsg[:33], btcec.S256())
	if err != nil {
		return err
	}
	idDH := fastsha256.Sum256(
		btcec.GenerateSharedSecret(l.longTermPriv, theirPub),
	)

	myDHproof := btcutil.Hash160(
		append(lnConn.RemotePub.SerializeCompressed(), idDH[:]...),
	)
	theirDHproof := btcutil.Hash160(append(localEphPubBytes, idDH[:]...))

	// If they already know our public key, then execute the fast path.
	// Verify their DH proof, and send our own.
	if len(authmsg) == 73 {
		// Verify their DH proof.
		if !bytes.Equal(authmsg[53:], theirDHproof) {
			return fmt.Errorf("invalid DH proof from %s",
				lnConn.RemoteAddr().String())
		}

		// Their DH proof checks out, so send ours now.
		if _, err = lnConn.Conn.Write(myDHproof); err != nil {
			return err
		}
	} else {
		// Otherwise, they don't yet know our public key. So we'll send
		// it over to them, so we can both compute the DH proof.
		msg := append(l.longTermPriv.PubKey().SerializeCompressed(), myDHproof...)
		if _, err = lnConn.Conn.Write(msg); err != nil {
			return err
		}

		resp := make([]byte, 20)
		if _, err := lnConn.Conn.Read(resp); err != nil {
			return err
		}

		// Verify their DH proof.
		if bytes.Equal(resp, theirDHproof) == false {
			return fmt.Errorf("Invalid DH proof %x", theirDHproof)
		}
	}

	theirAdr := btcutil.Hash160(theirPub.SerializeCompressed())
	copy(lnConn.RemoteLNId[:], theirAdr[:16])
	lnConn.RemotePub = theirPub
	lnConn.Authed = true

	return nil
}

// Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
// Part of the net.Listener interface.
func (l *Listener) Close() error {
	return l.tcp.Close()
}

// Addr returns the listener's network address.
// Part of the net.Listener interface.
func (l *Listener) Addr() net.Addr {
	return l.tcp.Addr()
}
