package main

import (
	"fmt"
	"strings"

	"github.com/davecgh/go-spew/spew"
	"github.com/lightningnetwork/lnd/channeldb"
	"github.com/lightningnetwork/lnd/contractcourt"
	"github.com/lightningnetwork/lnd/htlcswitch"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/roasbeef/btcd/txscript"
	"github.com/roasbeef/btcd/wire"
	"github.com/roasbeef/btcutil"
)

var (
	// ErrChanAlreadyClosing is returned when a channel shutdown is
	// attempted more than once.
	ErrChanAlreadyClosing = fmt.Errorf("channel shutdown already initiated")

	// ErrChanCloseNotFinished is returned when a caller attempts to access
	// a field or function that is continent on the channel closure
	// negotiation already being completed.
	ErrChanCloseNotFinished = fmt.Errorf("close negotiation not finished")

	// ErrInvalidState is returned when the closing state machine receives
	// a message while it is in an unknown state.
	ErrInvalidState = fmt.Errorf("invalid state")
)

// closeState represents all the possible states the channel closer state
// machine can be in. Each message will either advance to the next state, or
// remain at the current state. Once the state machine reaches a state of
// closeFinished, then negotiation is over.
type closeState uint8

const (
	// closeIdle is the initial starting state. In this state, the stat
	// machine has been instantiated, but not state transitions have been
	// attempted. If a state machine receives a message while in this
	// state, then it is the responder to an initiated cooperative channel
	// closure.
	closeIdle closeState = iota

	// closeShutdownInitiated is the state that's transitioned to once the
	// initiator of a closing workflow sends the shutdown message. At this
	// point, they're waiting for the remote party to respond with their
	// own shutdown message. After which, they'll both enter the fee
	// negotiation phase.
	closeShutdownInitiated

	// closeFeeNegotiation is the third, and most persistent state. Both
	// parties enter this state after they've sent and receive a shutdown
	// message. During this phase, both sides will send monotonically
	// increasing fee requests until one side accepts the last fee rate
	// offered by the other party. In this case, the party will broadcast
	// the closing transaction, and send the accepted fee to the remote
	// party. This then causes a shift into the close finished state.
	closeFeeNegotiation

	// closeFinished is the final state of the state machine. In this,
	// state, a side has accepted a fee offer and has broadcast the valid
	// closing transaction to the network. During this phase, the closing
	// transaction becomes available for examination.
	closeFinished
)

// chanCloseCfg holds all the items that a channelCloser requires to carry out
// its duties.
type chanCloseCfg struct {
	// channel is the channel that should be closed.
	channel *lnwallet.LightningChannel

	// unregisterChannel is a function closure that allows the
	// channelCloser to re-register a channel. Once this has been done, no
	// further HTLC's should be routed through the channel.
	unregisterChannel func(lnwire.ChannelID) error

	// broadcastTx broadcasts the passed transaction to the network.
	broadcastTx func(*wire.MsgTx) error

	// quit is a channel that should be sent upon in the occasion the state
	// machine should cease all progress and shutdown.
	quit chan struct{}
}

// channelCloser is a state machine that handles the cooperative channel
// closure procedure. This includes shutting down a channel, marking it
// ineligible for routing HTLC's, negotiating fees with the remote party, and
// finally broadcasting the fully signed closure transaction to the network.
type channelCloser struct {
	// state is the current state of the state machine.
	state closeState

	// cfg holds the configuration for this channelCloser instance.
	cfg chanCloseCfg

	// chanPoint is the full channel point of the target channel.
	chanPoint wire.OutPoint

	// cid is the full channel ID of the target channel.
	cid lnwire.ChannelID

	// negotiationHeight is the height that the fee negotiation begun at.
	negotiationHeight uint32

	// closingTx is the final, fully signed closing transaction. This will
	// only be populated once the state machine shifts to the closeFinished
	// state.
	closingTx *wire.MsgTx

	// idealFeeSat is the ideal fee that the state machine should initially
	// offer when starting negotiation. This will be used as a baseline.
	idealFeeSat btcutil.Amount

	// lastFeeProposal is the last fee that we proposed to the remote
	// party. We'll use this as a pivot point to rachet our next offer up,
	// or down, or simply accept the remote party's prior offer.
	lastFeeProposal btcutil.Amount

	// priorFeeOffers is a map that keeps track of all the proposed fees
	// that we've offered during the fee negotiation. We use this map to
	// cut the negotiation early if the remote party ever sends an offer
	// that we've sent in the past. Once negotiation terminates, we can
	// extract the prior signature of our accepted offer from this map.
	//
	// TODO(roasbeef): need to ensure if they broadcast w/ any of our prior
	// sigs, we are aware of
	priorFeeOffers map[btcutil.Amount]*lnwire.ClosingSigned

	// closeReq is the initial closing request. This will only be populated
	// if we're the initiator of this closing negotiation.
	//
	// TODO(roasbeef): abstract away
	closeReq *htlcswitch.ChanClose

	closeCtx *contractcourt.CooperativeCloseCtx

	// localDeliveryScript is the script that we'll send our settled
	// channel funds to.
	localDeliveryScript []byte

	// remoteDeliveryScript is the script that we'll send the remote
	// party's settled channel funds to.
	remoteDeliveryScript []byte
}

// newChannelCloser creates a new instance of the channel closure given the
// passed configuration, and delivery+fee preference. The final argument should
// only be populated iff, we're the initiator of this closing request.
func newChannelCloser(cfg chanCloseCfg, deliveryScript []byte,
	idealFeePerkw btcutil.Amount, negotiationHeight uint32,
	closeReq *htlcswitch.ChanClose,
	closeCtx *contractcourt.CooperativeCloseCtx) *channelCloser {

	// Given the target fee-per-kw, we'll compute what our ideal _total_
	// fee will be starting at for this fee negotiation.
	//
	// TODO(roasbeef): should factor in minimal commit
	idealFeeSat := btcutil.Amount(
		cfg.channel.CalcFee(uint64(idealFeePerkw)),
	)

	// If this fee is greater than the fee currently present within the
	// commitment transaction, then we'll clamp it down to be within the
	// proper range.
	//
	// TODO(roasbeef): clamp fee func?
	channelCommitFee := cfg.channel.StateSnapshot().CommitFee
	if idealFeeSat > channelCommitFee {
		peerLog.Infof("Ideal starting fee of %v is greater than "+
			"commit fee of %v, clamping", int64(idealFeeSat),
			int64(channelCommitFee))

		idealFeeSat = channelCommitFee
	}

	peerLog.Infof("Ideal fee for closure of ChannelPoint(%v) is: %v sat",
		cfg.channel.ChannelPoint(), int64(idealFeeSat))

	cid := lnwire.NewChanIDFromOutPoint(cfg.channel.ChannelPoint())
	return &channelCloser{
		closeReq:            closeReq,
		state:               closeIdle,
		chanPoint:           *cfg.channel.ChannelPoint(),
		cid:                 cid,
		cfg:                 cfg,
		negotiationHeight:   negotiationHeight,
		idealFeeSat:         idealFeeSat,
		closeCtx:            closeCtx,
		localDeliveryScript: deliveryScript,
		priorFeeOffers:      make(map[btcutil.Amount]*lnwire.ClosingSigned),
	}
}

// initChanShutdown beings the shutdown process by un-registering the channel,
// and creating a valid shutdown message to our target delivery address.
func (c *channelCloser) initChanShutdown() (*lnwire.Shutdown, error) {
	// With both items constructed we'll now send the shutdown message for
	// this particular channel, advertising a shutdown request to our
	// desired closing script.
	shutdown := lnwire.NewShutdown(c.cid, c.localDeliveryScript)

	// TODO(roasbeef): err if channel has htlc's?

	// Before returning the shutdown message, we'll unregister the channel
	// to ensure that it isn't see as usable within the system.
	//
	// TODO(roasbeef): fail if err?
	c.cfg.unregisterChannel(c.cid)

	peerLog.Infof("ChannelPoint(%v): sending shutdown message", c.chanPoint)

	return shutdown, nil
}

// ShutdownChan is the first method that's to be called by the initiator of the
// cooperative channel closure. This message returns the shutdown message to to
// sent to the remote party. Upon completion, we enter the
// closeShutdownInitiated phase as we await a response.
func (c *channelCloser) ShutdownChan() (*lnwire.Shutdown, error) {
	// If we attempt to shutdown the channel for the first time, and we're
	// not in the closeIdle state, then the caller made an error.
	if c.state != closeIdle {
		return nil, ErrChanAlreadyClosing
	}

	peerLog.Infof("ChannelPoint(%v): initiating shutdown of", c.chanPoint)

	shutdownMsg, err := c.initChanShutdown()
	if err != nil {
		return nil, err
	}

	// With the opening steps complete, we'll transition into the
	// closeShutdownInitiated state. In this state, we'll wait until the
	// other party sends their version of the shutdown message.
	c.state = closeShutdownInitiated

	// Finally, we'll return the shutdown message to the caller so it can
	// send it to the remote peer.
	return shutdownMsg, nil
}

// ClosingTx returns the fully signed, final closing transaction.
//
// NOTE: This transaction is only available if the state machine is in the
// closeFinished state.
func (c *channelCloser) ClosingTx() (*wire.MsgTx, error) {
	// If the state machine hasn't finished closing the channel then we'll
	// return an error as we haven't yet computed the closing tx.
	if c.state != closeFinished {
		return nil, ErrChanCloseNotFinished
	}

	return c.closingTx, nil
}

// CloseRequest returns the original close request that prompted the creation
// of the state machine.
//
// NOTE: This will only return a non-nil pointer if we were the initiator of
// the cooperative closure workflow.
func (c *channelCloser) CloseRequest() *htlcswitch.ChanClose {
	return c.closeReq
}

// ProcessCloseMsg attempts to process the next message in the closing series.
// This method will update the state accordingly and return two primary values:
// the next set of messages to be sent, and a bool indicating if the fee
// negotiation process has completed. If the second value is true, then this
// means the channelCloser can be garbage collected.
func (c *channelCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, bool, error) {
	switch c.state {

	// If we're in the close idle state, and we're receiving a channel
	// closure related message, then this indicates that we're on the
	// receiving side of an initiated channel closure.
	case closeIdle:
		// First, we'll assert that we have a channel shutdown message,
		// otherwise, this is an attempted invalid state transition.
		shutDownMsg, ok := msg.(*lnwire.Shutdown)
		if !ok {
			return nil, false, fmt.Errorf("expected lnwire.Shutdown, "+
				"instead have %v", spew.Sdump(msg))
		}

		// Next, we'll note the other party's preference for their
		// delivery address. We'll use this when we craft the closure
		// transaction.
		c.remoteDeliveryScript = shutDownMsg.Address

		// We'll generate a shutdown message of our own to set across
		// the wire.
		localShutdown, err := c.initChanShutdown()
		if err != nil {
			return nil, false, err
		}

		peerLog.Infof("ChannelPoint(%v): Responding to shutdown",
			c.chanPoint)

		msgsToSend := make([]lnwire.Message, 0, 2)
		msgsToSend = append(msgsToSend, localShutdown)

		// After the other party receives this message, we'll actually
		// start the final stage of the closure process: fee
		// negotiation.  So we'll update our internal state to reflect
		// this, so we can handle the next message sent.
		c.state = closeFeeNegotiation

		// We'll also craft our initial close proposal in order to keep
		// the negotiation moving, but only if we're the negotiator.
		if c.cfg.channel.IsInitiator() {
			closeSigned, err := c.proposeCloseSigned(c.idealFeeSat)
			if err != nil {
				return nil, false, err
			}
			msgsToSend = append(msgsToSend, closeSigned)
		}

		// We'll return both sent of messages to sent to the remote
		// party to kick off the fee negotiation process.
		return msgsToSend, false, nil

	// If we just initiated a channel shutdown, and we receive a new
	// message, then this indicates the other party is ready to shutdown as
	// well. In this state we'll send our first signature.
	case closeShutdownInitiated:
		// First, we'll assert that we have a channel shutdown message,
		// otherwise, this is an attempted invalid state transition.
		shutDownMsg, ok := msg.(*lnwire.Shutdown)
		if !ok {
			return nil, false, fmt.Errorf("expected lnwire.Shutdown, "+
				"instead have %v", spew.Sdump(msg))
		}

		// Now that we know this is a valid shutdown message, we'll
		// record their preferred delivery closing script.
		c.remoteDeliveryScript = shutDownMsg.Address

		// At this point, we can now start the fee negotiation state,
		// by constructing and sending our initial signature for what
		// we think the closing transaction should look like.
		c.state = closeFeeNegotiation

		peerLog.Infof("ChannelPoint(%v): shutdown response received, "+
			"entering fee negotiation", c.chanPoint)

		// Starting with our ideal fee rate, we'll create an initial
		// closing proposal, but only if we're the initiator, as
		// otherwise, the other party will send their first proposal
		// first.
		if c.cfg.channel.IsInitiator() {
			closeSigned, err := c.proposeCloseSigned(c.idealFeeSat)
			if err != nil {
				return nil, false, err
			}

			return []lnwire.Message{closeSigned}, false, nil
		}

		return nil, false, nil

	// If we're receiving a message while we're in the fee negotiation
	// phase, then this indicates the remote party is responding a closed
	// signed message we sent, or kicking off the process with their own.
	case closeFeeNegotiation:
		// First, we'll assert that we're actually getting a
		// CloseSigned message, otherwise an invalid state transition
		// was attempted.
		closeSignedMsg, ok := msg.(*lnwire.ClosingSigned)
		if !ok {
			return nil, false, fmt.Errorf("expected lnwire.ClosingSigned, "+
				"instead have %v", spew.Sdump(msg))
		}

		// We'll compare the proposed total fee, to what we've proposed
		// during the negotiations, if it doesn't match any of our
		// prior offers, then we'll attempt to rachet the fee closer to
		remoteProposedFee := closeSignedMsg.FeeSatoshis
		if _, ok := c.priorFeeOffers[remoteProposedFee]; !ok {
			// We'll now attempt to rachet towards a fee deemed
			// acceptable by both parties, factoring in our ideal
			// fee rate, and the last proposed fee by both sides.
			feeProposal := calcCompromiseFee(c.chanPoint,
				c.idealFeeSat, c.lastFeeProposal,
				remoteProposedFee,
			)

			// With our new fee proposal calculated, we'll craft a
			// new close signed signature to send to the other
			// party so we can continue the fee negotiation
			// process.
			closeSigned, err := c.proposeCloseSigned(feeProposal)
			if err != nil {
				return nil, false, err
			}

			// If the compromise fee doesn't match what the peer
			// proposed, then we'll return this latest close signed
			// message so we continue negotiation.
			if feeProposal != remoteProposedFee {
				peerLog.Debugf("ChannelPoint(%v): close tx "+
					"fee disagreement, continuing negotiation",
					c.chanPoint)
				return []lnwire.Message{closeSigned}, false, nil
			}
		}

		peerLog.Infof("ChannelPoint(%v) fee of %v accepted, ending "+
			"negotiation", c.chanPoint, remoteProposedFee)

		// Otherwise, we've agreed on a fee for the closing
		// transaction!  We'll craft the final closing transaction so
		// we can broadcast it to the network.
		matchingSig := c.priorFeeOffers[remoteProposedFee].Signature
		localSigBytes := matchingSig.ToSignatureBytes()
		localSig := append(localSigBytes, byte(txscript.SigHashAll))

		remoteSigBytes := closeSignedMsg.Signature.ToSignatureBytes()
		remoteSig := append(remoteSigBytes, byte(txscript.SigHashAll))

		closeTx, finalLocalBalance, err := c.cfg.channel.CompleteCooperativeClose(
			localSig, remoteSig, c.localDeliveryScript,
			c.remoteDeliveryScript, remoteProposedFee,
		)
		if err != nil {
			return nil, false, err
		}
		c.closingTx = closeTx

		// With the closing transaction crafted, we'll now broadcast it
		// to the network.
		peerLog.Infof("Broadcasting cooperative close tx: %v",
			newLogClosure(func() string {
				return spew.Sdump(closeTx)
			}))
		if err := c.cfg.broadcastTx(closeTx); err != nil {
			// TODO(halseth): add relevant error types to the
			// WalletController interface as this is quite fragile.
			switch {
			case strings.Contains(err.Error(), "already exists"):
				fallthrough
			case strings.Contains(err.Error(), "already have"):
				peerLog.Debugf("channel close tx from "+
					"ChannelPoint(%v) already exist, "+
					"probably broadcast by peer: %v",
					c.chanPoint, err)
			default:
				return nil, false, err
			}
		}

		// Clear out the current channel state, marking the channel as
		// being closed within the database.
		closingTxid := closeTx.TxHash()
		chanInfo := c.cfg.channel.StateSnapshot()
		c.closeCtx.Finalize(&channeldb.ChannelCloseSummary{
			ChanPoint:      c.chanPoint,
			ChainHash:      chanInfo.ChainHash,
			ClosingTXID:    closingTxid,
			CloseHeight:    c.negotiationHeight,
			RemotePub:      &chanInfo.RemoteIdentity,
			Capacity:       chanInfo.Capacity,
			SettledBalance: finalLocalBalance,
			CloseType:      channeldb.CooperativeClose,
			ShortChanID:    c.cfg.channel.ShortChanID(),
			IsPending:      true,
		})

		// TODO(roasbeef): don't need, ChainWatcher will handle

		c.state = closeFinished

		// Finally, we'll transition to the closeFinished state, and
		// also return the final close signed message we sent.
		// Additionally, we return true for the second argument to
		// indicate we're finished with the channel closing
		// negotiation.
		matchingOffer := c.priorFeeOffers[remoteProposedFee]
		return []lnwire.Message{matchingOffer}, true, nil

	// If we receive a message while in the closeFinished state, then this
	// should only be the remote party echoing the last ClosingSigned
	// message that we agreed on.
	case closeFinished:
		if _, ok := msg.(*lnwire.ClosingSigned); !ok {
			return nil, false, fmt.Errorf("expected "+
				"lnwire.ClosingSigned, instead have %v",
				spew.Sdump(msg))
		}

		// There's no more to do as both sides should have already
		// broadcast the closing transaction at this state.
		return nil, true, nil

	// Otherwise, we're in an unknown state, and can't proceed.
	default:
		return nil, false, ErrInvalidState
	}
}

// proposeCloseSigned attempts to propose a new signature for the closing
// transaction for a channel based on the prior fee negotiations and our
// current compromise fee.
func (c *channelCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSigned, error) {

	rawSig, txid, localAmt, err := c.cfg.channel.CreateCloseProposal(
		fee, c.localDeliveryScript, c.remoteDeliveryScript,
	)
	if err != nil {
		return nil, err
	}

	// We'll note our last signature and proposed fee so when the remote
	// party responds we'll be able to decide if we've agreed on fees or
	// not.
	c.lastFeeProposal = fee
	parsedSig, err := lnwire.NewSigFromRawSignature(rawSig)
	if err != nil {
		return nil, err
	}

	peerLog.Infof("ChannelPoint(%v): proposing fee of %v sat to close "+
		"chan", c.chanPoint, int64(fee))

	// We'll assembled a ClosingSigned message using this information and
	// return it to the caller so we can kick off the final stage of the
	// channel closure project.
	closeSignedMsg := lnwire.NewClosingSigned(c.cid, fee, parsedSig)

	// We'll also save this close signed, in the case that the remote party
	// accepts our offer. This way, we don't have to re-sign.
	c.priorFeeOffers[fee] = closeSignedMsg

	chanInfo := c.cfg.channel.StateSnapshot()
	c.closeCtx.LogPotentialClose(&channeldb.ChannelCloseSummary{
		ChanPoint:      c.chanPoint,
		ChainHash:      chanInfo.ChainHash,
		ClosingTXID:    *txid,
		CloseHeight:    c.negotiationHeight,
		RemotePub:      &chanInfo.RemoteIdentity,
		Capacity:       chanInfo.Capacity,
		SettledBalance: localAmt,
		CloseType:      channeldb.CooperativeClose,
		ShortChanID:    c.cfg.channel.ShortChanID(),
		IsPending:      true,
	})

	return closeSignedMsg, nil
}

// feeInAcceptableRange returns true if the passed remote fee is deemed to be
// in an "acceptable" range to our local fee. This is an attempt at a
// compromise and to ensure that the fee negotiation has a stopping point. We
// consider their fee acceptable if it's within 30% of our fee.
func feeInAcceptableRange(localFee, remoteFee btcutil.Amount) bool {
	// If our offer is lower than theirs, then we'll accept their
	// offer if it's no more than 30% *greater* than our current
	// offer.
	if localFee < remoteFee {
		acceptableRange := localFee + ((localFee * 3) / 10)
		return remoteFee <= acceptableRange
	}

	// If our offer is greater than theirs, then we'll accept their offer
	// if it's no more than 30% *less* than our current offer.
	acceptableRange := localFee - ((localFee * 3) / 10)
	return remoteFee >= acceptableRange
}

// rachetFee is our step function used to inch our fee closer to something that
// both sides can agree on. If up is true, then we'll attempt to increase our
// offered fee. Otherwise, if up if false, then we'll attempt to decrease our
// offered fee.
func rachetFee(fee btcutil.Amount, up bool) btcutil.Amount {
	// If we need to rachet up, then we'll increase our fee by 10%.
	if up {
		return fee + ((fee * 1) / 10)
	}

	// Otherwise, we'll *decrease* our fee by 10%.
	return fee - ((fee * 1) / 10)
}

// calcCompromiseFee performs the current fee negotiation algorithm, taking
// into consideration our ideal fee based on current fee environment, the fee
// we last proposed (if any), and the fee proposed by the peer.
func calcCompromiseFee(chanPoint wire.OutPoint,
	ourIdealFee, lastSentFee, remoteFee btcutil.Amount) btcutil.Amount {

	// TODO(roasbeef): take in number of rounds as well?

	peerLog.Infof("ChannelPoint(%v): computing fee compromise, ideal=%v, "+
		"last_sent=%v, remote_offer=%v", chanPoint, int64(ourIdealFee),
		int64(lastSentFee), int64(remoteFee))

	// Otherwise, we'll need to attempt to make a fee compromise if this is
	// the second round, and neither side has agreed on fees.
	switch {

	// If their proposed fee is identical to our ideal fee, then we'll go
	// with that as we can short circuit the fee negotiation. Similarly, if
	// we haven't sent an offer yet, we'll default to our ideal fee.
	case ourIdealFee == remoteFee || lastSentFee == 0:
		return ourIdealFee

	// If the last fee we sent, is equal to the fee the remote party is
	// offering, then we can simply return this fee as the negotiation is
	// over.
	case remoteFee == lastSentFee:
		return lastSentFee

	// If the fee the remote party is offering is less than the last one we
	// sent, then we'll need to rachet down in order to move our offer
	// closer to theirs.
	case remoteFee < lastSentFee:
		// If the fee is lower, but still acceptable, then we'll just
		// return this fee and end the negotiation.
		if feeInAcceptableRange(lastSentFee, remoteFee) {
			peerLog.Infof("ChannelPoint(%v): proposed remote fee "+
				"is close enough, capitulating", chanPoint)
			return remoteFee
		}

		// Otherwise, we'll rachet the fee *down* using our current
		// algorithm.
		return rachetFee(lastSentFee, false)

	// If the fee the remote party is offering is greater than the last one
	// we sent, then we'll rachet up in order to ensure we terminate
	// eventually.
	case remoteFee > lastSentFee:
		// If the fee is greater, but still acceptable, then we'll just
		// return this fee in order to put an end to the negotiation.
		if feeInAcceptableRange(lastSentFee, remoteFee) {
			peerLog.Infof("ChannelPoint(%v): proposed remote fee "+
				"is close enough, capitulating", chanPoint)
			return remoteFee
		}

		// Otherwise, we'll rachet the fee up using our current
		// algorithm.
		return rachetFee(lastSentFee, true)

	default:
		// TODO(roasbeef): fail if their fee isn't in expected range
		return remoteFee
	}
}
