package main

import (
	"errors"
	"fmt"
	"net"

	"github.com/btcsuite/btcd/btcec"
	"github.com/btcsuite/btcd/wire"
	"github.com/btcsuite/btcutil"
	"github.com/davecgh/go-spew/spew"
	"github.com/lightningnetwork/lnd/autopilot"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/tor"
)

// validateAtplConfig is a helper method that makes sure the passed
// configuration is sane. Currently it checks that the heuristic configuration
// makes sense. In case the config is valid, it will return a list of
// WeightedHeuristics that can be combined for use with the autopilot agent.
func validateAtplCfg(cfg *autoPilotConfig) ([]*autopilot.WeightedHeuristic,
	error) {

	var (
		heuristicsStr string
		sum           float64
		heuristics    []*autopilot.WeightedHeuristic
	)

	// Create a help text that we can return in case the config is not
	// correct.
	for _, a := range autopilot.AvailableHeuristics {
		heuristicsStr += fmt.Sprintf(" '%v' ", a.Name())
	}
	availStr := fmt.Sprintf("Avaiblable heuristcs are: [%v]", heuristicsStr)

	// We'll go through the config and make sure all the heuristics exists,
	// and that the sum of their weights is 1.0.
	for name, weight := range cfg.Heuristic {
		a, ok := autopilot.AvailableHeuristics[name]
		if !ok {
			// No heuristic matching this config option was found.
			return nil, fmt.Errorf("Heuristic %v not available. %v",
				name, availStr)
		}

		// If this heuristic was among the registered ones, we add it
		// to the list we'll give to the agent, and keep track of the
		// sum of weights.
		heuristics = append(
			heuristics,
			&autopilot.WeightedHeuristic{
				Weight:              weight,
				AttachmentHeuristic: a,
			},
		)
		sum += weight
	}

	// Check found heuristics. We must have at least one to operate.
	if len(heuristics) == 0 {
		return nil, fmt.Errorf("No active heuristics. %v", availStr)
	}

	if sum != 1.0 {
		return nil, fmt.Errorf("Heuristic weights must sum to 1.0")
	}
	return heuristics, nil
}

// chanController is an implementation of the autopilot.ChannelController
// interface that's backed by a running lnd instance.
type chanController struct {
	server   *server
	private  bool
	minConfs int32
}

// OpenChannel opens a channel to a target peer, with a capacity of the
// specified amount. This function should un-block immediately after the
// funding transaction that marks the channel open has been broadcast.
func (c *chanController) OpenChannel(target *btcec.PublicKey,
	amt btcutil.Amount) error {

	// With the connection established, we'll now establish our connection
	// to the target peer, waiting for the first update before we exit.
	feePerKw, err := c.server.cc.feeEstimator.EstimateFeePerKW(3)
	if err != nil {
		return err
	}

	// TODO(halseth): make configurable?
	minHtlc := lnwire.NewMSatFromSatoshis(1)

	// Construct the open channel request and send it to the server to begin
	// the funding workflow.
	req := &openChanReq{
		targetPubkey:    target,
		chainHash:       *activeNetParams.GenesisHash,
		localFundingAmt: amt,
		pushAmt:         0,
		minHtlc:         minHtlc,
		fundingFeePerKw: feePerKw,
		private:         c.private,
		remoteCsvDelay:  0,
		minConfs:        c.minConfs,
	}

	updateStream, errChan := c.server.OpenChannel(req)
	select {
	case err := <-errChan:
		return err
	case <-updateStream:
		return nil
	case <-c.server.quit:
		return nil
	}
}

func (c *chanController) CloseChannel(chanPoint *wire.OutPoint) error {
	return nil
}
func (c *chanController) SpliceIn(chanPoint *wire.OutPoint,
	amt btcutil.Amount) (*autopilot.Channel, error) {
	return nil, nil
}
func (c *chanController) SpliceOut(chanPoint *wire.OutPoint,
	amt btcutil.Amount) (*autopilot.Channel, error) {
	return nil, nil
}

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

// initAutoPilot initializes a new autopilot.ManagerCfg to manage an
// autopilot.Agent instance based on the passed configuration struct. The agent
// and all interfaces needed to drive it won't be launched before the Manager's
// StartAgent method is called.
func initAutoPilot(svr *server, cfg *autoPilotConfig) (*autopilot.ManagerCfg, error) {
	atplLog.Infof("Instantiating autopilot with cfg: %v", spew.Sdump(cfg))

	// Set up the constraints the autopilot heuristics must adhere to.
	atplConstraints := autopilot.NewConstraints(
		btcutil.Amount(cfg.MinChannelSize),
		btcutil.Amount(cfg.MaxChannelSize),
		uint16(cfg.MaxChannels),
		10,
		cfg.Allocation,
	)
	heuristics, err := validateAtplCfg(cfg)
	if err != nil {
		return nil, err
	}

	weightedAttachment, err := autopilot.NewWeightedCombAttachment(
		heuristics...,
	)
	if err != nil {
		return nil, err
	}

	// With the heuristic itself created, we can now populate the remainder
	// of the items that the autopilot agent needs to perform its duties.
	self := svr.identityPriv.PubKey()
	pilotCfg := autopilot.Config{
		Self:      self,
		Heuristic: weightedAttachment,
		ChanController: &chanController{
			server:   svr,
			private:  cfg.Private,
			minConfs: cfg.MinConfs,
		},
		WalletBalance: func() (btcutil.Amount, error) {
			return svr.cc.wallet.ConfirmedBalance(cfg.MinConfs)
		},
		Graph:       autopilot.ChannelGraphFromDatabase(svr.chanDB.ChannelGraph()),
		Constraints: atplConstraints,
		ConnectToPeer: func(target *btcec.PublicKey, addrs []net.Addr) (bool, error) {
			// First, we'll check if we're already connected to the
			// target peer. If we are, we can exit early. Otherwise,
			// we'll need to establish a connection.
			if _, err := svr.FindPeer(target); err == nil {
				return true, nil
			}

			// We can't establish a channel if no addresses were
			// provided for the peer.
			if len(addrs) == 0 {
				return false, errors.New("no addresses specified")
			}

			atplLog.Tracef("Attempting to connect to %x",
				target.SerializeCompressed())

			lnAddr := &lnwire.NetAddress{
				IdentityKey: target,
				ChainNet:    activeNetParams.Net,
			}

			// We'll attempt to successively connect to each of the
			// advertised IP addresses until we've either exhausted
			// the advertised IP addresses, or have made a
			// connection.
			var connected bool
			for _, addr := range addrs {
				switch addr.(type) {
				case *net.TCPAddr, *tor.OnionAddr:
					lnAddr.Address = addr
				default:
					return false, fmt.Errorf("unknown "+
						"address type %T", addr)
				}

				err := svr.ConnectToPeer(lnAddr, false)
				if err != nil {
					// If we weren't able to connect to the
					// peer at this address, then we'll move
					// onto the next.
					continue
				}

				connected = true
				break
			}

			// If we weren't able to establish a connection at all,
			// then we'll error out.
			if !connected {
				return false, errors.New("exhausted all " +
					"advertised addresses")
			}

			return false, nil
		},
		DisconnectPeer: svr.DisconnectPeer,
	}

	// Create and return the autopilot.ManagerCfg that administrates this
	// agent-pilot instance.
	return &autopilot.ManagerCfg{
		Self:     self,
		PilotCfg: &pilotCfg,
		ChannelState: func() ([]autopilot.Channel, error) {
			// We'll fetch the current state of open
			// channels from the database to use as initial
			// state for the auto-pilot agent.
			activeChannels, err := svr.chanDB.FetchAllChannels()
			if err != nil {
				return nil, err
			}
			chanState := make([]autopilot.Channel,
				len(activeChannels))
			for i, channel := range activeChannels {
				chanState[i] = autopilot.Channel{
					ChanID:   channel.ShortChanID(),
					Capacity: channel.Capacity,
					Node: autopilot.NewNodeID(
						channel.IdentityPub),
				}
			}

			return chanState, nil
		},
		SubscribeTransactions: svr.cc.wallet.SubscribeTransactions,
		SubscribeTopology:     svr.chanRouter.SubscribeTopology,
	}, nil
}