// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package yggconn

import (
	"context"
	"crypto/ed25519"
	"crypto/tls"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/lucas-clemente/quic-go"
	"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/convert"
	"github.com/matrix-org/gomatrixserverlib"
	"go.uber.org/atomic"

	yggdrasilconfig "github.com/yggdrasil-network/yggdrasil-go/src/config"
	yggdrasilmulticast "github.com/yggdrasil-network/yggdrasil-go/src/multicast"
	"github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"

	gologme "github.com/gologme/log"
)

type Node struct {
	core         *yggdrasil.Core
	config       *yggdrasilconfig.NodeConfig
	state        *yggdrasilconfig.NodeState
	multicast    *yggdrasilmulticast.Multicast
	log          *gologme.Logger
	listener     quic.Listener
	tlsConfig    *tls.Config
	quicConfig   *quic.Config
	sessions     sync.Map // string -> *session
	sessionCount atomic.Uint32
	sessionFunc  func(address string)
	coords       sync.Map // string -> yggdrasil.Coords
	incoming     chan QUICStream
	NewSession   func(remote gomatrixserverlib.ServerName)
}

func (n *Node) Dialer(_, address string) (net.Conn, error) {
	tokens := strings.Split(address, ":")
	raw, err := hex.DecodeString(tokens[0])
	if err != nil {
		return nil, fmt.Errorf("hex.DecodeString: %w", err)
	}
	converted := convert.Ed25519PublicKeyToCurve25519(ed25519.PublicKey(raw))
	convhex := hex.EncodeToString(converted)
	return n.Dial("curve25519", convhex)
}

func (n *Node) DialerContext(ctx context.Context, network, address string) (net.Conn, error) {
	return n.Dialer(network, address)
}

func Setup(instanceName, storageDirectory string) (*Node, error) {
	n := &Node{
		core:      &yggdrasil.Core{},
		config:    yggdrasilconfig.GenerateConfig(),
		multicast: &yggdrasilmulticast.Multicast{},
		log:       gologme.New(os.Stdout, "YGG ", log.Flags()),
		incoming:  make(chan QUICStream),
	}

	yggfile := fmt.Sprintf("%s/%s-yggdrasil.conf", storageDirectory, instanceName)
	if _, err := os.Stat(yggfile); !os.IsNotExist(err) {
		yggconf, e := ioutil.ReadFile(yggfile)
		if e != nil {
			panic(err)
		}
		if err := json.Unmarshal([]byte(yggconf), &n.config); err != nil {
			panic(err)
		}
	}

	n.core.SetCoordChangeCallback(func(old, new yggdrasil.Coords) {
		fmt.Println("COORDINATE CHANGE!")
		fmt.Println("Old:", old)
		fmt.Println("New:", new)
		n.sessions.Range(func(k, v interface{}) bool {
			if s, ok := v.(*session); ok {
				fmt.Println("Killing session", k)
				s.kill()
			}
			return true
		})
	})

	n.config.Peers = []string{}
	n.config.AdminListen = "none"
	n.config.MulticastInterfaces = []string{}
	n.config.EncryptionPrivateKey = hex.EncodeToString(n.EncryptionPrivateKey())
	n.config.EncryptionPublicKey = hex.EncodeToString(n.EncryptionPublicKey())

	j, err := json.MarshalIndent(n.config, "", "  ")
	if err != nil {
		panic(err)
	}
	if e := ioutil.WriteFile(yggfile, j, 0600); e != nil {
		n.log.Printf("Couldn't write private key to file '%s': %s\n", yggfile, e)
	}

	n.log.EnableLevel("error")
	n.log.EnableLevel("warn")
	n.log.EnableLevel("info")
	n.state, err = n.core.Start(n.config, n.log)
	if err != nil {
		panic(err)
	}
	if err = n.multicast.Init(n.core, n.state, n.log, nil); err != nil {
		panic(err)
	}
	if err = n.multicast.Start(); err != nil {
		panic(err)
	}

	n.tlsConfig = n.generateTLSConfig()
	n.quicConfig = &quic.Config{
		MaxIncomingStreams:    0,
		MaxIncomingUniStreams: 0,
		KeepAlive:             true,
		MaxIdleTimeout:        time.Minute * 30,
		HandshakeTimeout:      time.Second * 15,
	}
	copy(n.quicConfig.StatelessResetKey, n.EncryptionPublicKey())

	n.log.Println("Public curve25519:", n.core.EncryptionPublicKey())
	n.log.Println("Public ed25519:", n.core.SigningPublicKey())

	go func() {
		time.Sleep(time.Second)
		n.listenFromYgg()
	}()

	return n, nil
}

func (n *Node) Stop() {
	if err := n.multicast.Stop(); err != nil {
		n.log.Println("Error stopping multicast:", err)
	}
	n.core.Stop()
}

func (n *Node) DerivedServerName() string {
	return hex.EncodeToString(n.SigningPublicKey())
}

func (n *Node) DerivedSessionName() string {
	return hex.EncodeToString(n.EncryptionPublicKey())
}

func (n *Node) EncryptionPublicKey() []byte {
	edkey := n.SigningPublicKey()
	return convert.Ed25519PublicKeyToCurve25519(edkey)
}

func (n *Node) EncryptionPrivateKey() []byte {
	edkey := n.SigningPrivateKey()
	return convert.Ed25519PrivateKeyToCurve25519(edkey)
}

func (n *Node) SigningPublicKey() ed25519.PublicKey {
	pubBytes, _ := hex.DecodeString(n.config.SigningPublicKey)
	return ed25519.PublicKey(pubBytes)
}

func (n *Node) SigningPrivateKey() ed25519.PrivateKey {
	privBytes, _ := hex.DecodeString(n.config.SigningPrivateKey)
	return ed25519.PrivateKey(privBytes)
}

func (n *Node) SetSessionFunc(f func(address string)) {
	n.sessionFunc = f
}

func (n *Node) PeerCount() int {
	return len(n.core.GetPeers()) - 1
}

func (n *Node) SessionCount() int {
	return int(n.sessionCount.Load())
}

func (n *Node) KnownNodes() []gomatrixserverlib.ServerName {
	nodemap := map[string]struct{}{
		//"b5ae50589e50991dd9dd7d59c5c5f7a4521e8da5b603b7f57076272abc58b374": {},
	}
	for _, peer := range n.core.GetSwitchPeers() {
		nodemap[hex.EncodeToString(peer.SigPublicKey[:])] = struct{}{}
	}
	n.sessions.Range(func(_, v interface{}) bool {
		session, ok := v.(quic.Session)
		if !ok {
			return true
		}
		if len(session.ConnectionState().PeerCertificates) != 1 {
			return true
		}
		subjectName := session.ConnectionState().PeerCertificates[0].Subject.CommonName
		nodemap[subjectName] = struct{}{}
		return true
	})
	var nodes []gomatrixserverlib.ServerName
	for node := range nodemap {
		nodes = append(nodes, gomatrixserverlib.ServerName(node))
	}
	return nodes
}

func (n *Node) SetMulticastEnabled(enabled bool) {
	if enabled {
		n.config.MulticastInterfaces = []string{".*"}
	} else {
		n.config.MulticastInterfaces = []string{}
	}
	n.multicast.UpdateConfig(n.config)
	if !enabled {
		n.DisconnectMulticastPeers()
	}
}

func (n *Node) DisconnectMulticastPeers() {
	for _, sp := range n.core.GetSwitchPeers() {
		if !strings.HasPrefix(sp.Endpoint, "fe80") {
			continue
		}
		if err := n.core.DisconnectPeer(sp.Port); err != nil {
			n.log.Printf("Failed to disconnect port %d: %s", sp.Port, err)
		}
	}
}

func (n *Node) DisconnectNonMulticastPeers() {
	for _, sp := range n.core.GetSwitchPeers() {
		if strings.HasPrefix(sp.Endpoint, "fe80") {
			continue
		}
		if err := n.core.DisconnectPeer(sp.Port); err != nil {
			n.log.Printf("Failed to disconnect port %d: %s", sp.Port, err)
		}
	}
}

func (n *Node) SetStaticPeer(uri string) error {
	n.config.Peers = []string{}
	n.core.UpdateConfig(n.config)
	n.DisconnectNonMulticastPeers()
	if uri != "" {
		n.log.Infoln("Adding static peer", uri)
		if err := n.core.AddPeer(uri, ""); err != nil {
			n.log.Warnln("Adding static peer failed:", err)
			return err
		}
		if err := n.core.CallPeer(uri, ""); err != nil {
			n.log.Warnln("Calling static peer failed:", err)
			return err
		}
	}
	return nil
}
