package lnd

import (
	"fmt"
	"math"
	"net"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/lightningnetwork/lnd/chanbackup"
	"github.com/lightningnetwork/lnd/channeldb"
	"github.com/lightningnetwork/lnd/contractcourt"
	"github.com/lightningnetwork/lnd/keychain"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/shachain"
)

const (
	// mainnetSCBLaunchBlock is the approximate block height of the bitcoin
	// mainnet chain of the date when SCBs first were released in lnd
	// (v0.6.0-beta). The block date is 4/15/2019, 10:54 PM UTC.
	mainnetSCBLaunchBlock = 571800

	// testnetSCBLaunchBlock is the approximate block height of the bitcoin
	// testnet3 chain of the date when SCBs first were released in lnd
	// (v0.6.0-beta). The block date is 4/16/2019, 08:04 AM UTC.
	testnetSCBLaunchBlock = 1489300
)

// chanDBRestorer is an implementation of the chanbackup.ChannelRestorer
// interface that is able to properly map a Single backup, into a
// channeldb.ChannelShell which is required to fully restore a channel. We also
// need the secret key chain in order obtain the prior shachain root so we can
// verify the DLP protocol as initiated by the remote node.
type chanDBRestorer struct {
	db *channeldb.ChannelStateDB

	secretKeys keychain.SecretKeyRing

	chainArb *contractcourt.ChainArbitrator
}

// openChannelShell maps the static channel back up into an open channel
// "shell". We say shell as this doesn't include all the information required
// to continue to use the channel, only the minimal amount of information to
// insert this shell channel back into the database.
func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) (
	*channeldb.ChannelShell, error) {

	var err error

	// Each of the keys in our local channel config only have their
	// locators populate, so we'll re-derive the raw key now as we'll need
	// it in order to carry out the DLP protocol.
	backup.LocalChanCfg.MultiSigKey, err = c.secretKeys.DeriveKey(
		backup.LocalChanCfg.MultiSigKey.KeyLocator,
	)
	if err != nil {
		return nil, fmt.Errorf("unable to derive multi sig key: %w",
			err)
	}
	backup.LocalChanCfg.RevocationBasePoint, err = c.secretKeys.DeriveKey(
		backup.LocalChanCfg.RevocationBasePoint.KeyLocator,
	)
	if err != nil {
		return nil, fmt.Errorf("unable to derive revocation key: %w",
			err)
	}
	backup.LocalChanCfg.PaymentBasePoint, err = c.secretKeys.DeriveKey(
		backup.LocalChanCfg.PaymentBasePoint.KeyLocator,
	)
	if err != nil {
		return nil, fmt.Errorf("unable to derive payment key: %w", err)
	}
	backup.LocalChanCfg.DelayBasePoint, err = c.secretKeys.DeriveKey(
		backup.LocalChanCfg.DelayBasePoint.KeyLocator,
	)
	if err != nil {
		return nil, fmt.Errorf("unable to derive delay key: %w", err)
	}
	backup.LocalChanCfg.HtlcBasePoint, err = c.secretKeys.DeriveKey(
		backup.LocalChanCfg.HtlcBasePoint.KeyLocator,
	)
	if err != nil {
		return nil, fmt.Errorf("unable to derive htlc key: %w", err)
	}

	// The shachain root that seeds RevocationProducer for this channel.
	// It currently has two possible formats.
	var revRoot *chainhash.Hash

	// If the PubKey field is non-nil, then this shachain root is using the
	// legacy non-ECDH scheme.
	if backup.ShaChainRootDesc.PubKey != nil {
		ltndLog.Debugf("Using legacy revocation producer format for "+
			"channel point %v", backup.FundingOutpoint)

		// Obtain the private key for the shachain root from the
		// encoded public key.
		privKey, err := c.secretKeys.DerivePrivKey(
			backup.ShaChainRootDesc,
		)
		if err != nil {
			return nil, fmt.Errorf("could not derive private key "+
				"for legacy channel revocation root format: "+
				"%v", err)
		}

		revRoot, err = chainhash.NewHash(privKey.Serialize())
		if err != nil {
			return nil, err
		}
	} else {
		ltndLog.Debugf("Using new ECDH revocation producer format "+
			"for channel point %v", backup.FundingOutpoint)

		// This is the scheme in which the shachain root is derived via
		// an ECDH operation on the private key of ShaChainRootDesc and
		// our public multisig key.
		ecdh, err := c.secretKeys.ECDH(
			backup.ShaChainRootDesc,
			backup.LocalChanCfg.MultiSigKey.PubKey,
		)
		if err != nil {
			return nil, fmt.Errorf("unable to derive shachain "+
				"root: %v", err)
		}

		ch := chainhash.Hash(ecdh)
		revRoot = &ch
	}

	shaChainProducer := shachain.NewRevocationProducer(*revRoot)

	var chanType channeldb.ChannelType
	switch backup.Version {
	case chanbackup.DefaultSingleVersion:
		chanType = channeldb.SingleFunderBit

	case chanbackup.TweaklessCommitVersion:
		chanType = channeldb.SingleFunderTweaklessBit

	case chanbackup.AnchorsCommitVersion:
		chanType = channeldb.AnchorOutputsBit
		chanType |= channeldb.SingleFunderTweaklessBit

	case chanbackup.AnchorsZeroFeeHtlcTxCommitVersion:
		chanType = channeldb.ZeroHtlcTxFeeBit
		chanType |= channeldb.AnchorOutputsBit
		chanType |= channeldb.SingleFunderTweaklessBit

	case chanbackup.ScriptEnforcedLeaseVersion:
		chanType = channeldb.LeaseExpirationBit
		chanType |= channeldb.ZeroHtlcTxFeeBit
		chanType |= channeldb.AnchorOutputsBit
		chanType |= channeldb.SingleFunderTweaklessBit

	case chanbackup.SimpleTaprootVersion:
		chanType = channeldb.ZeroHtlcTxFeeBit
		chanType |= channeldb.AnchorOutputsBit
		chanType |= channeldb.SingleFunderTweaklessBit
		chanType |= channeldb.SimpleTaprootFeatureBit

	case chanbackup.TapscriptRootVersion:
		chanType = channeldb.ZeroHtlcTxFeeBit
		chanType |= channeldb.AnchorOutputsBit
		chanType |= channeldb.SingleFunderTweaklessBit
		chanType |= channeldb.SimpleTaprootFeatureBit
		chanType |= channeldb.TapscriptRootBit

	default:
		return nil, fmt.Errorf("unknown Single version: %w", err)
	}

	ltndLog.Infof("SCB Recovery: created channel shell for ChannelPoint"+
		"(%v), chan_type=%v", backup.FundingOutpoint, chanType)

	chanShell := channeldb.ChannelShell{
		NodeAddrs: backup.Addresses,
		Chan: &channeldb.OpenChannel{
			ChanType:                chanType,
			ChainHash:               backup.ChainHash,
			IsInitiator:             backup.IsInitiator,
			Capacity:                backup.Capacity,
			FundingOutpoint:         backup.FundingOutpoint,
			ShortChannelID:          backup.ShortChannelID,
			IdentityPub:             backup.RemoteNodePub,
			IsPending:               false,
			LocalChanCfg:            backup.LocalChanCfg,
			RemoteChanCfg:           backup.RemoteChanCfg,
			RemoteCurrentRevocation: backup.RemoteNodePub,
			RevocationStore:         shachain.NewRevocationStore(),
			RevocationProducer:      shaChainProducer,
			ThawHeight:              backup.LeaseExpiry,
		},
	}

	return &chanShell, nil
}

// RestoreChansFromSingles attempts to map the set of single channel backups to
// channel shells that will be stored persistently. Once these shells have been
// stored on disk, we'll be able to connect to the channel peer an execute the
// data loss recovery protocol.
//
// NOTE: Part of the chanbackup.ChannelRestorer interface.
func (c *chanDBRestorer) RestoreChansFromSingles(backups ...chanbackup.Single) error {
	channelShells := make([]*channeldb.ChannelShell, 0, len(backups))
	firstChanHeight := uint32(math.MaxUint32)
	for _, backup := range backups {
		chanShell, err := c.openChannelShell(backup)
		if err != nil {
			return err
		}

		// Find the block height of the earliest channel in this backup.
		chanHeight := chanShell.Chan.ShortChanID().BlockHeight
		if chanHeight != 0 && chanHeight < firstChanHeight {
			firstChanHeight = chanHeight
		}

		channelShells = append(channelShells, chanShell)
	}

	// In case there were only unconfirmed channels, we will have to scan
	// the chain beginning from the launch date of SCBs.
	if firstChanHeight == math.MaxUint32 {
		chainHash := channelShells[0].Chan.ChainHash
		switch {
		case chainHash.IsEqual(chaincfg.MainNetParams.GenesisHash):
			firstChanHeight = mainnetSCBLaunchBlock

		case chainHash.IsEqual(chaincfg.TestNet3Params.GenesisHash):
			firstChanHeight = testnetSCBLaunchBlock

		default:
			// Worst case: We have no height hint and start at
			// block 1. Should only happen for SCBs in regtest
			// and simnet.
			firstChanHeight = 1
		}
	}

	// If there were channels in the backup that were not confirmed at the
	// time of the backup creation, they won't have a block height in the
	// ShortChanID which would lead to an error in the chain watcher.
	// We want to at least set the funding broadcast height that the chain
	// watcher can use instead. We have two possible fallback values for
	// the broadcast height that we are going to try here.
	for _, chanShell := range channelShells {
		channel := chanShell.Chan

		switch {
		// Fallback case 1: This is an unconfirmed channel from an old
		// backup file where we didn't have any workaround in place and
		// the short channel ID is 0:0:0. Best we can do here is set the
		// funding broadcast height to a reasonable value that we
		// determined earlier.
		case channel.ShortChanID().BlockHeight == 0:
			channel.SetBroadcastHeight(firstChanHeight)

		// Fallback case 2: It is extremely unlikely at this point that
		// a channel we are trying to restore has a coinbase funding TX.
		// Therefore we can be quite certain that if the TxIndex is
		// zero but the block height wasn't, it was an unconfirmed
		// channel where we used the BlockHeight to encode the funding
		// TX broadcast height. To not end up with an invalid short
		// channel ID that looks valid, we restore the "original"
		// unconfirmed one here.
		case channel.ShortChannelID.TxIndex == 0:
			broadcastHeight := channel.ShortChannelID.BlockHeight
			channel.SetBroadcastHeight(broadcastHeight)
			channel.ShortChannelID.BlockHeight = 0
		}
	}

	ltndLog.Infof("Inserting %v SCB channel shells into DB",
		len(channelShells))

	// Now that we have all the backups mapped into a series of Singles,
	// we'll insert them all into the database.
	if err := c.db.RestoreChannelShells(channelShells...); err != nil {
		return err
	}

	ltndLog.Infof("Informing chain watchers of new restored channels")

	// Finally, we'll need to inform the chain arbitrator of these new
	// channels so we'll properly watch for their ultimate closure on chain
	// and sweep them via the DLP.
	for _, restoredChannel := range channelShells {
		err := c.chainArb.WatchNewChannel(restoredChannel.Chan)
		if err != nil {
			return err
		}
	}

	return nil
}

// A compile-time constraint to ensure chanDBRestorer implements
// chanbackup.ChannelRestorer.
var _ chanbackup.ChannelRestorer = (*chanDBRestorer)(nil)

// ConnectPeer attempts to connect to the target node at the set of available
// addresses. Once this method returns with a non-nil error, the connector
// should attempt to persistently connect to the target peer in the background
// as a persistent attempt.
//
// NOTE: Part of the chanbackup.PeerConnector interface.
func (s *server) ConnectPeer(nodePub *btcec.PublicKey, addrs []net.Addr) error {
	// Before we connect to the remote peer, we'll remove any connections
	// to ensure the new connection is created after this new link/channel
	// is known.
	if err := s.DisconnectPeer(nodePub); err != nil {
		ltndLog.Infof("Peer(%v) is already connected, proceeding "+
			"with chan restore", nodePub.SerializeCompressed())
	}

	// For each of the known addresses, we'll attempt to launch a
	// persistent connection to the (pub, addr) pair. In the event that any
	// of them connect, all the other stale requests will be canceled.
	for _, addr := range addrs {
		netAddr := &lnwire.NetAddress{
			IdentityKey: nodePub,
			Address:     addr,
		}

		ltndLog.Infof("Attempting to connect to %v for SCB restore "+
			"DLP", netAddr)

		// Attempt to connect to the peer using this full address. If
		// we're unable to connect to them, then we'll try the next
		// address in place of it.
		err := s.ConnectToPeer(netAddr, true, s.cfg.ConnectionTimeout)

		// If we're already connected to this peer, then we don't
		// consider this an error, so we'll exit here.
		if _, ok := err.(*errPeerAlreadyConnected); ok {
			return nil

		} else if err != nil {
			// Otherwise, something else happened, so we'll try the
			// next address.
			ltndLog.Errorf("unable to connect to %v to "+
				"complete SCB restore: %v", netAddr, err)
			continue
		}

		// If we connected no problem, then we can exit early as our
		// job here is done.
		return nil
	}

	return fmt.Errorf("unable to connect to peer %x for SCB restore",
		nodePub.SerializeCompressed())
}
