package autopilot

import (
	"context"
	"encoding/hex"
	"net"
	"sort"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
	"github.com/btcsuite/btcd/btcutil/v2"
	graphdb "github.com/lightningnetwork/lnd/graph/db"
	"github.com/lightningnetwork/lnd/graph/db/models"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
)

var (
	testRBytes, _ = hex.DecodeString("8ce2bc69281ce27da07e6683571319d18e949ddfa2965fb6caa1bf0314f882d7")
	testSBytes, _ = hex.DecodeString("299105481d63e0f4bc2a88121167221b6700d72a0ead154c03be696a292d24ae")
	testRScalar   = new(btcec.ModNScalar)
	testSScalar   = new(btcec.ModNScalar)
	_             = testRScalar.SetByteSlice(testRBytes)
	_             = testSScalar.SetByteSlice(testSBytes)
	testSig       = ecdsa.NewSignature(testRScalar, testSScalar)

	chanIDCounter uint64 // To be used atomically.
)

// databaseChannelGraph wraps a channeldb.ChannelGraph instance with the
// necessary API to properly implement the autopilot.ChannelGraph interface.
//
// TODO(roasbeef): move inmpl to main package?
type databaseChannelGraph struct {
	db GraphSource
}

// A compile time assertion to ensure databaseChannelGraph meets the
// autopilot.ChannelGraph interface.
var _ ChannelGraph = (*databaseChannelGraph)(nil)

// ChannelGraphFromDatabase returns an instance of the autopilot.ChannelGraph
// backed by a GraphSource.
func ChannelGraphFromDatabase(db GraphSource) ChannelGraph {
	return &databaseChannelGraph{
		db: db,
	}
}

// type dbNode is a wrapper struct around a database transaction an
// channeldb.Node. The wrapper method implement the autopilot.Node
// interface.
type dbNode struct {
	pub   [33]byte
	addrs []net.Addr
}

// A compile time assertion to ensure dbNode meets the autopilot.Node
// interface.
var _ Node = (*dbNode)(nil)

// PubKey is the identity public key of the node. This will be used to attempt
// to target a node for channel opening by the main autopilot agent. The key
// will be returned in serialized compressed format.
//
// NOTE: Part of the autopilot.Node interface.
func (d *dbNode) PubKey() [33]byte {
	return d.pub
}

// Addrs returns a slice of publicly reachable public TCP addresses that the
// peer is known to be listening on.
//
// NOTE: Part of the autopilot.Node interface.
func (d *dbNode) Addrs() []net.Addr {
	return d.addrs
}

// ForEachNode is a higher-order function that should be called once for each
// connected node within the channel graph. If the passed callback returns an
// error, then execution should be terminated.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (d *databaseChannelGraph) ForEachNode(ctx context.Context,
	cb func(context.Context, Node) error, reset func()) error {

	return d.db.ForEachNode(ctx, func(n *models.Node) error {
		// We'll skip over any node that doesn't have any advertised
		// addresses. As we won't be able to reach them to actually
		// open any channels.
		if len(n.Addresses) == 0 {
			return nil
		}

		node := &dbNode{
			pub:   n.PubKeyBytes,
			addrs: n.Addresses,
		}

		return cb(ctx, node)
	}, reset)
}

// ForEachNodesChannels iterates through all connected nodes, and for each
// node, all the channels that connect to it. The passed callback will be
// called with the context, the node's pubkey, and a slice of ChannelEdge
// that connect to the node.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (d *databaseChannelGraph) ForEachNodesChannels(ctx context.Context,
	cb func(context.Context, NodeID, []*ChannelEdge) error,
	reset func()) error {

	// The channel-scoring callers only need topology data here. Address
	// filtering happens through ForEachNode before connecting to peers.
	return d.db.ForEachNodeCached(
		ctx, func(ctx context.Context, node route.Vertex,
			chans map[uint64]*graphdb.DirectedChannel) error {

			if len(chans) == 0 {
				return nil
			}

			edges := make([]*ChannelEdge, 0, len(chans))
			for _, channel := range chans {
				edges = append(edges, &ChannelEdge{
					ChanID: lnwire.NewShortChanIDFromInt(
						channel.ChannelID,
					),
					Capacity: channel.Capacity,
					Peer:     channel.OtherNode,
				})
			}

			return cb(ctx, NodeID(node), edges)
		}, reset,
	)
}

// databaseChannelGraphCached wraps a channeldb.ChannelGraph instance with the
// necessary API to properly implement the autopilot.ChannelGraph interface.
type databaseChannelGraphCached struct {
	db GraphSource
}

// A compile time assertion to ensure databaseChannelGraphCached meets the
// autopilot.ChannelGraph interface.
var _ ChannelGraph = (*databaseChannelGraphCached)(nil)

// ChannelGraphFromCachedDatabase returns an instance of the
// autopilot.ChannelGraph backed by a live, open channeldb instance.
func ChannelGraphFromCachedDatabase(db GraphSource) ChannelGraph {
	return &databaseChannelGraphCached{
		db: db,
	}
}

// dbNodeCached is a wrapper struct around a database transaction for a
// channeldb.Node. The wrapper methods implement the autopilot.Node
// interface.
type dbNodeCached struct {
	node     route.Vertex
	channels map[uint64]*graphdb.DirectedChannel
}

// A compile time assertion to ensure dbNodeCached meets the autopilot.Node
// interface.
var _ Node = (*dbNodeCached)(nil)

// PubKey is the identity public key of the node.
//
// NOTE: Part of the autopilot.Node interface.
func (nc dbNodeCached) PubKey() [33]byte {
	return nc.node
}

// Addrs returns a slice of publicly reachable public TCP addresses that the
// peer is known to be listening on.
//
// NOTE: Part of the autopilot.Node interface.
func (nc dbNodeCached) Addrs() []net.Addr {
	// TODO: Add addresses to be usable by autopilot.
	return []net.Addr{}
}

// ForEachNode is a higher-order function that should be called once for each
// connected node within the channel graph. If the passed callback returns an
// error, then execution should be terminated.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (dc *databaseChannelGraphCached) ForEachNode(ctx context.Context,
	cb func(context.Context, Node) error, reset func()) error {

	return dc.db.ForEachNodeCached(ctx, func(ctx context.Context,
		n route.Vertex,
		channels map[uint64]*graphdb.DirectedChannel) error {

		if len(channels) > 0 {
			node := dbNodeCached{
				node:     n,
				channels: channels,
			}

			return cb(ctx, node)
		}

		return nil
	}, reset)
}

// ForEachNodesChannels iterates through all connected nodes, and for each
// node, all the channels that connect to it. The passed callback will be
// called with the context, the node's pubkey, and a slice of ChannelEdge
// that connect to the node.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (dc *databaseChannelGraphCached) ForEachNodesChannels(ctx context.Context,
	cb func(context.Context, NodeID, []*ChannelEdge) error,
	reset func()) error {

	return dc.db.ForEachNodeCached(ctx, func(ctx context.Context,
		n route.Vertex,
		channels map[uint64]*graphdb.DirectedChannel) error {

		if len(channels) == 0 {
			return nil
		}

		edges := make([]*ChannelEdge, 0, len(channels))
		for cid, channel := range channels {
			edges = append(edges, &ChannelEdge{
				ChanID:   lnwire.NewShortChanIDFromInt(cid),
				Capacity: channel.Capacity,
				Peer:     channel.OtherNode,
			})
		}

		return cb(ctx, NodeID(n), edges)
	}, reset)
}

// memNode is a purely in-memory implementation of the autopilot.Node
// interface.
type memNode struct {
	pub *btcec.PublicKey

	chans []ChannelEdge

	addrs []net.Addr
}

// A compile time assertion to ensure memNode meets the autopilot.Node
// interface.
var _ Node = (*memNode)(nil)

// PubKey is the identity public key of the node. This will be used to attempt
// to target a node for channel opening by the main autopilot agent.
//
// NOTE: Part of the autopilot.Node interface.
func (m memNode) PubKey() [33]byte {
	var n [33]byte
	copy(n[:], m.pub.SerializeCompressed())

	return n
}

// Addrs returns a slice of publicly reachable public TCP addresses that the
// peer is known to be listening on.
//
// NOTE: Part of the autopilot.Node interface.
func (m memNode) Addrs() []net.Addr {
	return m.addrs
}

// Median returns the median value in the slice of Amounts.
func Median(vals []btcutil.Amount) btcutil.Amount {
	sort.Slice(vals, func(i, j int) bool {
		return vals[i] < vals[j]
	})

	num := len(vals)
	switch {
	case num == 0:
		return 0

	case num%2 == 0:
		return (vals[num/2-1] + vals[num/2]) / 2

	default:
		return vals[num/2]
	}
}
