package loop

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/mempool"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/lndclient"
	"github.com/lightninglabs/loop/labels"
	"github.com/lightninglabs/loop/loopdb"
	"github.com/lightninglabs/loop/swap"
	"github.com/lightninglabs/loop/utils"
	"github.com/lightningnetwork/lnd/chainntnfs"
	invpkg "github.com/lightningnetwork/lnd/invoices"
	"github.com/lightningnetwork/lnd/keychain"
	"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
	"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
)

var (
	// MaxLoopInAcceptDelta configures the maximum acceptable number of
	// remaining blocks until the on-chain htlc expires. This value is used
	// to decide whether we want to continue with the swap parameters as
	// proposed by the server. It is a protection to prevent the server from
	// getting us to lock up our funds to an arbitrary point in the future.
	MaxLoopInAcceptDelta = int32(1500)

	// MinLoopInExpiryDelta defines the minimum number of remaining blocks
	// that we accept until htlc expiry path that opens up for us to sweep.
	MinLoopInExpiryDelta = int32(100)

	// MinLoopInPublishDelta defines the minimum number of remaining blocks
	// until on-chain htlc expiry required to proceed to publishing the htlc
	// tx. This value isn't critical, as we could even safely publish the
	// htlc after expiry. The reason we do implement this check is to
	// prevent us from publishing an htlc that the server surely wouldn't
	// follow up to.
	MinLoopInPublishDelta = int32(10)

	// TimeoutTxConfTarget defines the confirmation target for the loop in
	// timeout tx.
	TimeoutTxConfTarget = int32(2)

	// ErrSwapFinalized is returned when a to be executed swap is already in
	// a final state.
	ErrSwapFinalized = errors.New("swap is in a final state")
)

// loopInSwap contains all the in-memory state related to a pending loop in
// swap.
type loopInSwap struct {
	swapKit

	executeConfig

	loopdb.LoopInContract

	htlc *swap.Htlc

	htlcP2WSH *swap.Htlc

	htlcP2TR *swap.Htlc

	// htlcTxHash is the confirmed htlc tx id.
	htlcTxHash *chainhash.Hash

	timeoutAddr btcutil.Address

	abandonChan chan struct{}

	wg sync.WaitGroup
}

// loopInInitResult contains information about a just-initiated loop in swap.
type loopInInitResult struct {
	swap          *loopInSwap
	serverMessage string
}

// newLoopInSwap initiates a new loop in swap.
func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
	currentHeight int32, request *LoopInRequest) (*loopInInitResult,
	error) {

	var err error

	// Private and route hints are mutually exclusive as setting private
	// means we retrieve our own route hints from the connected node.
	if len(request.RouteHints) != 0 && request.Private {
		return nil, fmt.Errorf("private and route_hints both set")
	}

	// If Private is set, we generate route hints.
	if request.Private {
		// If last_hop is set, we'll only add channels with peers set to
		// the last_hop parameter.
		includeNodes := make(map[route.Vertex]struct{})
		if request.LastHop != nil {
			includeNodes[*request.LastHop] = struct{}{}
		}

		// Because the Private flag is set, we'll generate our own set
		// of hop hints.
		request.RouteHints, err = SelectHopHints(
			globalCtx, cfg.lnd.Client, request.Amount,
			DefaultMaxHopHints, includeNodes,
		)
		if err != nil {
			return nil, err
		}
	}

	// Request current server loop in terms and use these to calculate the
	// swap fee that we should subtract from the swap amount in the payment
	// request that we send to the server. We pass nil as optional route
	// hints as hop hint selection when generating invoices with private
	// channels is an LND side black box feature. Advanced users will quote
	// directly anyway and there they have the option to add specific route
	// hints.
	numDeposits := uint32(0)
	fast := false
	quote, err := cfg.server.GetLoopInQuote(
		globalCtx, request.Amount, cfg.lnd.NodePubkey, request.LastHop,
		request.RouteHints, request.Initiator, numDeposits, fast,
	)
	if err != nil {
		return nil, wrapGrpcError("loop in terms", err)
	}

	swapFee := quote.SwapFee

	if swapFee > request.MaxSwapFee {
		log.Warnf("Swap fee %v exceeding maximum of %v",
			swapFee, request.MaxSwapFee)

		return nil, ErrSwapFeeTooHigh
	}

	// Calculate the swap invoice amount. The prepay is added which
	// effectively forces the server to pay us back our prepayment on a
	// successful swap.
	swapInvoiceAmt := request.Amount - swapFee

	// Generate random preimage.
	var swapPreimage lntypes.Preimage
	if _, err := rand.Read(swapPreimage[:]); err != nil {
		log.Error("Cannot generate preimage")
	}
	swapHash := lntypes.Hash(sha256.Sum256(swapPreimage[:]))

	// Derive a sender key for this swap.
	keyDesc, err := cfg.lnd.WalletKit.DeriveNextKey(
		globalCtx, swap.KeyFamily,
	)
	if err != nil {
		return nil, err
	}
	var senderKey [33]byte
	copy(senderKey[:], keyDesc.PubKey.SerializeCompressed())

	// Create the swap invoice in lnd.
	_, swapInvoice, err := cfg.lnd.Client.AddInvoice(
		globalCtx, &invoicesrpc.AddInvoiceData{
			Preimage:   &swapPreimage,
			Value:      lnwire.NewMSatFromSatoshis(swapInvoiceAmt),
			Memo:       "swap",
			Expiry:     3600 * 24 * 365,
			RouteHints: request.RouteHints,
			Private:    true,
		},
	)
	if err != nil {
		return nil, err
	}

	// Create the probe invoice in lnd. Derive the payment hash
	// deterministically from the swap hash in such a way that the server
	// can be sure that we don't know the preimage.
	probeHash := lntypes.Hash(sha256.Sum256(swapHash[:]))
	probeHash[0] ^= 1

	log.Infof("Creating probe invoice %v", probeHash)
	probeInvoice, err := cfg.lnd.Invoices.AddHoldInvoice(
		globalCtx, &invoicesrpc.AddInvoiceData{
			Hash:       &probeHash,
			Value:      lnwire.NewMSatFromSatoshis(swapInvoiceAmt),
			Memo:       "loop in probe",
			Expiry:     3600,
			RouteHints: request.RouteHints,
			Private:    true,
		},
	)
	if err != nil {
		return nil, err
	}

	// Default the HTLC internal key to our sender key.
	senderInternalPubKey := senderKey

	// If this is a MuSig2 swap then we'll generate a brand new key pair and
	// will use that as the internal key for the HTLC.
	if loopdb.CurrentProtocolVersion() >= loopdb.ProtocolVersionMuSig2 {
		secret, err := sharedSecretFromHash(
			globalCtx, cfg.lnd.Signer, swapHash,
		)
		if err != nil {
			return nil, err
		}

		_, pubKey := btcec.PrivKeyFromBytes(secret[:])
		copy(senderInternalPubKey[:], pubKey.SerializeCompressed())
	}

	// Create a cancellable context that is used for monitoring the probe.
	probeWaitCtx, probeWaitCancel := context.WithCancel(globalCtx)

	// Launch a goroutine to monitor the probe.
	probeResult, err := awaitProbe(probeWaitCtx, *cfg.lnd, probeHash)
	if err != nil {
		probeWaitCancel()
		return nil, fmt.Errorf("probe failed: %v", err)
	}

	// Post the swap parameters to the swap server. The response contains
	// the server success key and the expiry height of the on-chain swap
	// htlc.
	log.Infof("Initiating swap request at height %v", currentHeight)
	swapResp, err := cfg.server.NewLoopInSwap(globalCtx, swapHash,
		request.Amount, senderKey, senderInternalPubKey, swapInvoice,
		probeInvoice, request.LastHop, request.Initiator,
	)

	probeWaitCancel()
	if err != nil {
		return nil, wrapGrpcError("cannot initiate swap", err)
	}

	// Because the context is cancelled, it is guaranteed that we will be
	// able to read from the probeResult channel.
	err = <-probeResult
	if err != nil {
		return nil, fmt.Errorf("probe error: %v", err)
	}

	// Validate if the response parameters are outside our allowed range
	// preventing us from continuing with a swap.
	err = ValidateLoopInContract(currentHeight, swapResp.expiry)
	if err != nil {
		return nil, err
	}

	// Instantiate a struct that contains all required data to start the
	// swap.
	initiationTime := time.Now()

	contract := loopdb.LoopInContract{
		HtlcConfTarget: request.HtlcConfTarget,
		LastHop:        request.LastHop,
		ExternalHtlc:   request.ExternalHtlc,
		SwapContract: loopdb.SwapContract{
			InitiationHeight: currentHeight,
			InitiationTime:   initiationTime,
			HtlcKeys: loopdb.HtlcKeys{
				SenderScriptKey:        senderKey,
				SenderInternalPubKey:   senderKey,
				ReceiverScriptKey:      swapResp.receiverKey,
				ReceiverInternalPubKey: swapResp.receiverKey,
				ClientScriptKeyLocator: keyDesc.KeyLocator,
			},
			Preimage:        swapPreimage,
			AmountRequested: request.Amount,
			CltvExpiry:      swapResp.expiry,
			MaxMinerFee:     request.MaxMinerFee,
			MaxSwapFee:      request.MaxSwapFee,
			Label:           request.Label,
			ProtocolVersion: loopdb.CurrentProtocolVersion(),
		},
	}

	// For MuSig2 swaps we store the proper internal keys that we generated
	// and received from the server.
	if loopdb.CurrentProtocolVersion() >= loopdb.ProtocolVersionMuSig2 {
		contract.HtlcKeys.SenderInternalPubKey = senderInternalPubKey
		contract.HtlcKeys.ReceiverInternalPubKey = swapResp.receiverInternalKey
	}

	swapKit := newSwapKit(
		swapHash, swap.TypeIn,
		cfg, &contract.SwapContract,
	)

	swapKit.lastUpdateTime = initiationTime

	swap := &loopInSwap{
		LoopInContract: contract,
		swapKit:        *swapKit,
	}

	if err := swap.initHtlcs(); err != nil {
		return nil, err
	}

	// Persist the data before exiting this function, so that the caller can
	// trust that this swap will be resumed on restart.
	err = cfg.store.CreateLoopIn(globalCtx, swapHash, &swap.LoopInContract)
	if err != nil {
		return nil, fmt.Errorf("cannot store swap: %v", err)
	}

	if swapResp.serverMessage != "" {
		swap.log.Infof("Server message: %v", swapResp.serverMessage)
	}

	swap.abandonChan = make(chan struct{}, 1)

	return &loopInInitResult{
		swap:          swap,
		serverMessage: swapResp.serverMessage,
	}, nil
}

// awaitProbe waits for a probe payment to arrive and cancels it. This is a
// workaround for the current lack of multi-path probing.
func awaitProbe(ctx context.Context, lnd lndclient.LndServices,
	probeHash lntypes.Hash) (chan error, error) {

	// Subscribe to the probe invoice.
	updateChan, errChan, err := lnd.Invoices.SubscribeSingleInvoice(
		ctx, probeHash,
	)
	if err != nil {
		return nil, err
	}

	// Wait in the background for the probe to arrive.
	probeResult := make(chan error, 1)

	go func() {
		for {
			select {
			case update := <-updateChan:
				switch update.State {
				case invpkg.ContractAccepted:
					log.Infof("Server probe successful")

					probeResult <- nil

					// Cancel probe invoice so that the
					// server will know that its probe was
					// successful.
					err := lnd.Invoices.CancelInvoice(
						ctx, probeHash,
					)
					if err != nil {
						log.Errorf("Cancel probe "+
							"invoice: %v", err)
					}

					return

				case invpkg.ContractCanceled:
					probeResult <- errors.New(
						"probe invoice expired")

					return

				case invpkg.ContractSettled:
					probeResult <- errors.New(
						"impossible that probe " +
							"invoice was settled")

					return
				}

			case err := <-errChan:
				probeResult <- err
				return

			case <-ctx.Done():
				probeResult <- ctx.Err()
				return
			}
		}
	}()

	return probeResult, nil
}

// resumeLoopInSwap returns a swap object representing a pending swap that has
// been restored from the database.
func resumeLoopInSwap(_ context.Context, cfg *swapConfig,
	pend *loopdb.LoopIn) (*loopInSwap, error) {

	hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:]))

	log.Infof("Resuming loop in swap %v", hash)

	swapKit := newSwapKit(
		hash, swap.TypeIn, cfg,
		&pend.Contract.SwapContract,
	)

	swap := &loopInSwap{
		LoopInContract: *pend.Contract,
		swapKit:        *swapKit,
	}

	if err := swap.initHtlcs(); err != nil {
		return nil, err
	}

	lastUpdate := pend.LastUpdate()
	if lastUpdate == nil {
		swap.lastUpdateTime = pend.Contract.InitiationTime
	} else {
		swap.state = lastUpdate.State
		swap.lastUpdateTime = lastUpdate.Time
		swap.htlcTxHash = lastUpdate.HtlcTxHash
		swap.cost = lastUpdate.Cost
	}

	// Upon restoring the swap we also need to assign a new abandon channel
	// that the client can use to signal that the swap should be abandoned.
	swap.abandonChan = make(chan struct{}, 1)

	return swap, nil
}

// ValidateLoopInContract validates the contract parameters against our
// configured maximum values.
func ValidateLoopInContract(height int32, htlcExpiry int32) error {
	// Verify that we are not forced to publish a htlc that locks up our
	// funds for too long in case the server doesn't follow through.
	if htlcExpiry-height > MaxLoopInAcceptDelta {
		return ErrExpiryTooFar
	}

	// Ensure that the expiry height is in the future.
	if htlcExpiry < height+MinLoopInExpiryDelta {
		return ErrExpiryTooSoon
	}

	return nil
}

// initHtlcs creates and updates the native and nested segwit htlcs of the
// loopInSwap.
func (s *loopInSwap) initHtlcs() error {
	htlc, err := utils.GetHtlc(
		s.hash, &s.SwapContract, s.swapKit.lnd.ChainParams,
	)
	if err != nil {
		return err
	}

	switch htlc.OutputType {
	case swap.HtlcP2WSH:
		s.htlcP2WSH = htlc

	case swap.HtlcP2TR:
		s.htlcP2TR = htlc

	default:
		return fmt.Errorf("invalid output type")
	}

	s.swapKit.log.Infof("Htlc address (%s): %v", htlc.OutputType,
		htlc.Address)

	return nil
}

// sendUpdate reports an update to the swap state.
func (s *loopInSwap) sendUpdate(ctx context.Context) error {
	info := s.swapInfo()
	s.log.Infof("Loop in swap state: %v", info.State)

	if IsTaprootSwap(&s.SwapContract) {
		info.HtlcAddressP2TR = s.htlcP2TR.Address
	} else {
		info.HtlcAddressP2WSH = s.htlcP2WSH.Address
	}

	info.ExternalHtlc = s.ExternalHtlc

	// In order to avoid potentially dangerous ownership sharing we copy the
	// last hop vertex.
	if s.LastHop != nil {
		lastHop := &route.Vertex{}
		copy(lastHop[:], s.LastHop[:])
		info.LastHop = lastHop
	}

	select {
	case s.statusChan <- *info:
	case <-ctx.Done():
		return ctx.Err()
	}

	return nil
}

// execute starts/resumes the swap. It is a thin wrapper around executeSwap to
// conveniently handle the error case.
func (s *loopInSwap) execute(mainCtx context.Context,
	cfg *executeConfig, height int32) error {

	defer s.wg.Wait()

	s.executeConfig = *cfg
	s.height = height

	// Create context for our state subscription which we will cancel once
	// swap execution has completed, ensuring that we kill the subscribe
	// goroutine.
	subCtx, cancel := context.WithCancel(mainCtx)
	defer cancel()

	s.wg.Add(1)
	go func() {
		defer s.wg.Done()
		subscribeAndLogUpdates(
			subCtx, s.hash, s.log, s.server.SubscribeLoopInUpdates,
		)
	}()

	// Announce swap by sending out an initial update.
	err := s.sendUpdate(mainCtx)
	if err != nil {
		return err
	}

	// Execute the swap until it either reaches a final state or a temporary
	// error occurs.
	err = s.executeSwap(mainCtx)

	// If there are insufficient confirmed funds to publish the swap, we
	// finalize its state so a new swap will be published if funds become
	// available.
	if errors.Is(err, ErrInsufficientBalance) {
		return err
	}

	// Stop the execution if the swap has been abandoned.
	if err != nil && s.state == loopdb.StateFailAbandoned {
		return err
	}

	// Sanity check. If there is no error, the swap must be in a final
	// state.
	if err == nil && s.state.Type() == loopdb.StateTypePending {
		err = fmt.Errorf("swap in non-final state %v", s.state)
	}

	// If an unexpected error happened, report a temporary failure but don't
	// persist the error. Otherwise, for example a connection error could
	// lead to abandoning the swap permanently and losing funds.
	if err != nil {
		s.log.Errorf("Swap error: %v", err)
		s.setState(loopdb.StateFailTemporary)

		// If we cannot send out this update, there is nothing we can
		// do.
		_ = s.sendUpdate(mainCtx)

		return err
	}

	s.log.Infof("Loop in swap completed: %v "+
		"(final cost: server %v, onchain %v, offchain %v)",
		s.state,
		s.cost.Server,
		s.cost.Onchain,
		s.cost.Offchain,
	)

	return nil
}

// executeSwap executes the swap.
func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
	var err error

	// If the swap is already in a final state, we can return immediately.
	if s.state.IsFinal() {
		return ErrSwapFinalized
	}

	// For loop in, the client takes the first step by publishing the
	// on-chain htlc. Only do this if we haven't already done so in a
	// previous run.
	if s.state == loopdb.StateInitiated {
		if s.ExternalHtlc {
			// If an external htlc was indicated, we can move to the
			// HtlcPublished state directly and wait for
			// confirmation.
			s.setState(loopdb.StateHtlcPublished)
			err = s.persistAndAnnounceState(globalCtx)
			if err != nil {
				return err
			}
		} else {
			published, err := s.publishOnChainHtlc(globalCtx)
			if err != nil {
				return err
			}
			if !published {
				return nil
			}
		}
	}

	// Wait for the htlc to confirm. After a restart, this will pick up a
	// previously published tx.
	conf, err := s.waitForHtlcConf(globalCtx)
	if err != nil {
		return err
	}

	// Determine the htlc outpoint by inspecting the htlc tx.
	htlcOutpoint, htlcValue, err := swap.GetScriptOutput(
		conf.Tx, s.htlc.PkScript,
	)
	if err != nil {
		return err
	}

	// Verify that the confirmed (external) htlc value matches the swap
	// amount. If the amounts mismatch we update the swap state to indicate
	// this, but end processing the swap. Instead, we continue to wait for
	// the htlc to expire and publish a timeout tx to reclaim the funds. We
	// skip this part if the swap was recovered from this state.
	if s.state != loopdb.StateFailIncorrectHtlcAmt &&
		htlcValue != s.LoopInContract.AmountRequested {

		s.setState(loopdb.StateFailIncorrectHtlcAmt)
		err = s.persistAndAnnounceState(globalCtx)
		if err != nil {
			log.Errorf("Error persisting state: %v", err)
		}
	}

	// The server is expected to see the htlc on-chain and know that it can
	// sweep that htlc with the preimage, it should pay our swap invoice,
	// receive the preimage and sweep the htlc. We are waiting for this to
	// happen and simultaneously watch the htlc expiry height. When the htlc
	// expires, we will publish a timeout tx to reclaim the funds.
	err = s.waitForSwapComplete(globalCtx, htlcOutpoint, htlcValue)
	if err != nil {
		return err
	}

	// Persist swap outcome.
	if err := s.persistAndAnnounceState(globalCtx); err != nil {
		return err
	}

	return nil
}

// waitForHtlcConf watches the chain until the htlc confirms.
func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) (
	*chainntnfs.TxConfirmation, error) {

	// Register for confirmation of the htlc. It is essential to specify not
	// just the pk script, because an attacker may publish the same htlc
	// with a lower value, and we don't want to follow through with that tx.
	// In the unlikely event that our call to SendOutputs crashes, and we
	// restart, htlcTxHash will be nil at this point. Then only register
	// with PkScript and accept the risk that the call triggers on a
	// different htlc outpoint.
	s.log.Infof("Register for htlc conf (hh=%v, txid=%v)",
		s.InitiationHeight, s.htlcTxHash)

	if s.htlcTxHash == nil {
		s.log.Warnf("No htlc tx hash available, registering with " +
			"just the pkscript")
	}

	ctx, cancel := context.WithCancel(globalCtx)
	defer cancel()

	notifier := s.lnd.ChainNotifier

	notifyConfirmation := func(htlc *swap.Htlc) (
		chan *chainntnfs.TxConfirmation, chan error, error) {

		if htlc == nil {
			return nil, nil, nil
		}

		return notifier.RegisterConfirmationsNtfn(
			ctx, s.htlcTxHash, htlc.PkScript, 1, s.InitiationHeight,
		)
	}

	confChanP2WSH, confErrP2WSH, err := notifyConfirmation(s.htlcP2WSH)
	if err != nil {
		return nil, err
	}

	confChanP2TR, confErrP2TR, err := notifyConfirmation(s.htlcP2TR)
	if err != nil {
		return nil, err
	}

	var conf *chainntnfs.TxConfirmation
	for conf == nil {
		select {
		// P2WSH htlc confirmed.
		case conf = <-confChanP2WSH:
			s.htlc = s.htlcP2WSH
			s.log.Infof("P2WSH htlc confirmed")

		// P2TR htlc confirmed.
		case conf = <-confChanP2TR:
			s.htlc = s.htlcP2TR
			s.log.Infof("P2TR htlc confirmed")

		// Conf ntfn error.
		case err := <-confErrP2WSH:
			return nil, err

		// Conf ntfn error.
		case err := <-confErrP2TR:
			return nil, err

		// Keep up with block height.
		case notification := <-s.blockEpochChan:
			s.height = notification.(int32)

		// If the client requested the swap to be abandoned, we override
		// the status in the database.
		case <-s.abandonChan:
			return nil, s.setStateAbandoned(ctx)

		// Cancel.
		case <-globalCtx.Done():
			return nil, globalCtx.Err()
		}
	}

	// Store htlc tx hash for accounting purposes. Usually this call is a
	// no-op because the htlc tx hash was already known. Exceptions are:
	//
	// - Old pending swaps that were initiated before we persisted the htlc
	//   tx hash directly after publish.
	//
	// - Swaps that experienced a crash during their call to SendOutputs. In
	//   that case, we weren't able to record the tx hash.
	txHash := conf.Tx.TxHash()
	s.htlcTxHash = &txHash

	return conf, nil
}

// publishOnChainHtlc checks whether there are still enough blocks left and if
// so, it publishes the htlc and advances the swap state.
func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
	var err error

	blocksRemaining := s.CltvExpiry - s.height
	s.log.Infof("Blocks left until on-chain expiry: %v", blocksRemaining)

	// Verify whether it still makes sense to publish the htlc.
	if blocksRemaining < MinLoopInPublishDelta {
		s.setState(loopdb.StateFailTimeout)
		return false, s.persistAndAnnounceState(ctx)
	}

	// Get fee estimate from lnd.
	feeRate, err := s.lnd.WalletKit.EstimateFeeRate(
		ctx, s.LoopInContract.HtlcConfTarget,
	)
	if err != nil {
		return false, fmt.Errorf("estimate fee: %v", err)
	}

	// Transition to state HtlcPublished before calling SendOutputs to
	// prevent us from ever paying multiple times after a crash.
	s.setState(loopdb.StateHtlcPublished)
	err = s.persistAndAnnounceState(ctx)
	if err != nil {
		return false, err
	}

	s.log.Infof("Publishing on chain HTLC with fee rate %v", feeRate)

	var pkScript []byte
	if IsTaprootSwap(&s.SwapContract) {
		pkScript = s.htlcP2TR.PkScript
	} else {
		pkScript = s.htlcP2WSH.PkScript
	}

	tx, err := s.lnd.WalletKit.SendOutputs(
		ctx, []*wire.TxOut{{
			PkScript: pkScript,
			Value:    int64(s.LoopInContract.AmountRequested),
		}}, feeRate, labels.LoopInHtlcLabel(swap.ShortHash(&s.hash)),
	)
	if err != nil {
		s.log.Errorf("send outputs: %v", err)
		s.setState(loopdb.StateFailInsufficientConfirmedBalance)

		// If we cannot send out this update, there is nothing we can
		// do.
		_ = s.persistAndAnnounceState(ctx)

		return false, ErrInsufficientBalance
	}

	txHash := tx.TxHash()
	fee := getTxFee(tx, feeRate.FeePerKVByte())

	s.log.Infof("Published on chain HTLC tx %v, fee: %v", txHash, fee)

	// Persist the htlc hash so that after a restart we are still waiting
	// for our own htlc. We don't need to announce to clients, because the
	// state remains unchanged.
	s.htlcTxHash = &txHash

	// We do not expect any on-chain fees to be recorded yet, and we only
	// publish our htlc once, so we set our total on-chain costs to equal
	// the fee for publishing the htlc.
	s.cost.Onchain = fee

	s.lastUpdateTime = time.Now()
	if err := s.persistState(ctx); err != nil {
		return false, fmt.Errorf("persist htlc tx: %v", err)
	}

	return true, nil
}

// getTxFee calculates our fee for a transaction that we have broadcast. We use
// sat per kvbyte because this is what lnd uses, and we will run into rounding
// issues if we do not use the same fee rate as lnd.
func getTxFee(tx *wire.MsgTx, fee chainfee.SatPerKVByte) btcutil.Amount {
	btcTx := btcutil.NewTx(tx)
	vsize := mempool.GetTxVirtualSize(btcTx)

	return fee.FeeForVSize(lntypes.VByte(vsize))
}

// waitForSwapComplete waits until a spending tx of the HTLC gets confirmed and
// the swap invoice is either settled or canceled. If the HTLC times out and the
// invoice is still not paid, the timeout tx will be published. If the HTLC
// times out and the invoice is paid, the function stops waiting and returns
// clearly.
func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
	htlcOutpoint *wire.OutPoint, htlcValue btcutil.Amount) error {

	// Register the htlc spend notification.
	rpcCtx, cancel := context.WithCancel(ctx)
	defer cancel()
	spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
		rpcCtx, htlcOutpoint, s.htlc.PkScript, s.InitiationHeight,
	)
	if err != nil {
		return fmt.Errorf("register spend ntfn: %v", err)
	}

	// Register for swap invoice updates.
	rpcCtx, cancel = context.WithCancel(ctx)
	defer cancel()
	s.log.Infof("Subscribing to swap invoice %v", s.hash)
	swapInvoiceChan, swapInvoiceErr, err := s.lnd.Invoices.SubscribeSingleInvoice(
		rpcCtx, s.hash,
	)
	if err != nil {
		return fmt.Errorf("subscribe to swap invoice: %v", err)
	}

	// publishTxOnTimeout publishes the timeout tx if the contract has
	// expired and invoice has not been settled.
	publishTxOnTimeout := func() (btcutil.Amount, error) {
		// Don't publish the timeout tx if the invoice was settled.
		if s.state == loopdb.StateInvoiceSettled {
			return 0, nil
		}

		// Don't publish the timeout tx if the swap succeeded.
		if s.state == loopdb.StateSuccess {
			return 0, nil
		}

		if s.height >= s.LoopInContract.CltvExpiry {
			return s.publishTimeoutTx(ctx, htlcOutpoint, htlcValue)
		}

		return 0, nil
	}

	// Check timeout at current height. After a restart we may want to
	// publish the tx immediately.
	var sweepFee btcutil.Amount
	sweepFee, err = publishTxOnTimeout()
	if err != nil {
		return err
	}

	htlcSpend := false
	invoiceFinalized := false
	htlcKeyRevealed := false
	for {
		// Check stop conditions.
		if htlcSpend && invoiceFinalized {
			break
		}
		if s.state == loopdb.StateInvoiceSettled {
			if s.height >= s.LoopInContract.CltvExpiry {
				s.setState(loopdb.StateSuccess)
				break
			}
		}

		select {
		// If the client requested the swap to be abandoned, we override
		// the status in the database.
		case <-s.abandonChan:
			return s.setStateAbandoned(ctx)

		// Spend notification error.
		case err := <-spendErr:
			return err

		// Receive block epochs and start publishing the timeout tx
		// whenever possible.
		case notification := <-s.blockEpochChan:
			s.height = notification.(int32)

			sweepFee, err = publishTxOnTimeout()
			if err != nil {
				return err
			}

			if invoiceFinalized && !htlcKeyRevealed {
				htlcKeyRevealed = s.tryPushHtlcKey(ctx)
			}

		// The htlc spend is confirmed. Inspect the spending tx to
		// determine the final swap state.
		case spendDetails := <-spendChan:
			s.log.Infof("Htlc spend by tx: %v",
				spendDetails.SpenderTxHash)

			err := s.processHtlcSpend(ctx, spendDetails, sweepFee)
			if err != nil {
				return err
			}

			htlcSpend = true

		// Swap invoice ntfn error.
		case err, ok := <-swapInvoiceErr:
			// If the channel has been closed, the server has
			// finished sending updates, so we set the channel to
			// nil because we don't want to constantly select this
			// case.
			if !ok {
				swapInvoiceErr = nil
				continue
			}

			return err

		// An update to the swap invoice occurred. Check the new state
		// and update the swap state accordingly.
		case update, ok := <-swapInvoiceChan:
			// If the channel has been closed, the server has
			// finished sending updates, so we set the channel to
			// nil because we don't want to constantly select this
			// case.
			if !ok {
				swapInvoiceChan = nil
				continue
			}

			s.log.Infof("Received swap invoice update: %v",
				update.State)

			switch update.State {
			// Swap invoice was paid, so update server cost balance.
			case invpkg.ContractSettled:
				// If invoice settlement and htlc spend happen
				// in the expected order, move the swap to an
				// intermediate state that indicates that the
				// swap is complete from the user point of view,
				// but still incomplete with regards to
				// accounting data.
				if s.state == loopdb.StateHtlcPublished {
					s.setState(loopdb.StateInvoiceSettled)
					err := s.persistAndAnnounceState(ctx)
					if err != nil {
						return err
					}
				}

				invoiceFinalized = true
				htlcKeyRevealed = s.tryPushHtlcKey(ctx)
				s.cost.Server = s.AmountRequested -
					update.AmtPaid

			// Canceled invoice has no effect on server cost
			// balance.
			case invpkg.ContractCanceled:
				invoiceFinalized = true
			}

		case <-ctx.Done():
			return ctx.Err()
		}
	}

	return nil
}

// tryPushHtlcKey attempts to push the htlc key to the server. If the server
// returns an error of any kind we'll log it as a warning but won't act as the
// swap execution can just go on without the server gaining knowledge of our
// internal key.
func (s *loopInSwap) tryPushHtlcKey(ctx context.Context) bool {
	if s.ProtocolVersion < loopdb.ProtocolVersionMuSig2 {
		return false
	}

	log.Infof("Attempting to reveal internal HTLC key to the server")

	internalPrivKey, err := sharedSecretFromHash(
		ctx, s.swapConfig.lnd.Signer, s.hash,
	)
	if err != nil {
		s.log.Warnf("Unable to derive HTLC internal private key: %v",
			err)

		return false
	}

	err = s.server.PushKey(ctx, s.ProtocolVersion, s.hash, internalPrivKey)
	if err != nil {
		s.log.Warnf("Internal HTLC key reveal failed: %v", err)
		return false
	}

	return true
}

func (s *loopInSwap) processHtlcSpend(ctx context.Context,
	spend *chainntnfs.SpendDetail, sweepFee btcutil.Amount) error {

	// Determine the htlc input of the spending tx and inspect the witness
	// to find out whether a success or a timeout tx spent the htlc.
	htlcInput := spend.SpendingTx.TxIn[spend.SpenderInputIndex]

	if s.htlc.IsSuccessWitness(htlcInput.Witness) {
		s.setState(loopdb.StateSuccess)
	} else {
		// We needed another on chain tx to sweep the timeout clause,
		// which we now include in our costs.
		s.cost.Onchain += sweepFee

		// If the swap is in state StateFailIncorrectHtlcAmt we know
		// that the deposited htlc amount wasn't equal to the contract
		// amount. We can finalize the swap by setting an appropriate
		// state.
		if s.state == loopdb.StateFailIncorrectHtlcAmt {
			s.setState(loopdb.StateFailIncorrectHtlcAmtSwept)
		} else {
			s.setState(loopdb.StateFailTimeout)
		}

		// Now that the timeout tx confirmed, we can safely cancel the
		// swap invoice. We still need to query the final invoice state.
		// This is not a hodl invoice, so it may be that the invoice was
		// already settled. This means that the server didn't succeed in
		// sweeping the htlc after paying the invoice.
		err := s.lnd.Invoices.CancelInvoice(ctx, s.hash)
		if err != nil && err != invpkg.ErrInvoiceAlreadySettled {
			return err
		}
	}

	return nil
}

// publishTimeoutTx publishes a timeout tx after the on-chain htlc has expired,
// returning the fee that is paid by the sweep tx. We cannot update our swap
// costs in this function because it is called multiple times. The swap failed
// and we are reclaiming our funds.
func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
	htlcOutpoint *wire.OutPoint, htlcValue btcutil.Amount) (btcutil.Amount,
	error) {

	if s.timeoutAddr == nil {
		var err error
		s.timeoutAddr, err = s.lnd.WalletKit.NextAddr(
			ctx, "", walletrpc.AddressType_WITNESS_PUBKEY_HASH,
			false,
		)
		if err != nil {
			return 0, err
		}
	}

	label := fmt.Sprintf("loopin-timeout-%x", s.hash[:6])

	// Calculate sweep tx fee.
	fee, err := s.sweeper.GetSweepFee(
		ctx, s.htlc.AddTimeoutToEstimator, s.timeoutAddr,
		TimeoutTxConfTarget, label,
	)
	if err != nil {
		return 0, err
	}

	// Create a function that will assemble our timeout witness.
	witnessFunc := func(sig []byte) (wire.TxWitness, error) {
		return s.htlc.GenTimeoutWitness(sig)
	}

	// Retrieve the full script required to unlock the output.
	redeemScript := s.htlc.TimeoutScript()

	sequence := uint32(0)
	timeoutTx, err := s.sweeper.CreateSweepTx(
		ctx, s.height, sequence, s.htlc, *htlcOutpoint,
		s.HtlcKeys.SenderScriptKey, redeemScript, witnessFunc,
		htlcValue, fee, s.timeoutAddr,
	)
	if err != nil {
		return 0, err
	}

	timeoutTxHash := timeoutTx.TxHash()
	s.log.Infof("Publishing timeout tx %v with fee %v to addr %v",
		timeoutTxHash, fee, s.timeoutAddr)

	err = s.lnd.WalletKit.PublishTransaction(
		ctx, timeoutTx,
		labels.LoopInSweepTimeout(swap.ShortHash(&s.hash)),
	)
	if err != nil {
		s.log.Warnf("publish timeout: %v", err)
	}

	return fee, nil
}

// setStateAbandoned stores the abandoned state and announces it. It also
// cancels the swap invoice so the server can't settle it.
func (s *loopInSwap) setStateAbandoned(ctx context.Context) error {
	s.log.Infof("Abandoning swap %v...", s.hash)

	if !s.state.IsPending() {
		return fmt.Errorf("cannot abandon swap in state %v", s.state)
	}

	s.setState(loopdb.StateFailAbandoned)

	err := s.persistAndAnnounceState(ctx)
	if err != nil {
		return err
	}

	// If the invoice is already settled or canceled, this is a nop.
	_ = s.lnd.Invoices.CancelInvoice(ctx, s.hash)

	return fmt.Errorf("swap hash "+
		"abandoned by client, "+
		"swap ID: %v, %v",
		s.hash, err)
}

// persistAndAnnounceState updates the swap state on disk and sends out an
// update notification.
func (s *loopInSwap) persistAndAnnounceState(ctx context.Context) error {
	// Update state in store.
	if err := s.persistState(ctx); err != nil {
		return err
	}

	// Send out swap update
	return s.sendUpdate(ctx)
}

// persistState updates the swap state on disk.
func (s *loopInSwap) persistState(ctx context.Context) error {
	return s.store.UpdateLoopIn(
		ctx, s.hash, s.lastUpdateTime,
		loopdb.SwapStateData{
			State:      s.state,
			Cost:       s.cost,
			HtlcTxHash: s.htlcTxHash,
		},
	)
}

// setState updates the swap state and last update timestamp.
func (s *loopInSwap) setState(state loopdb.SwapState) {
	s.lastUpdateTime = time.Now()
	s.state = state
}

// sharedSecretFromHash derives the shared secret from the swap hash using the
// swap.KeyFamily family and zero as index.
func sharedSecretFromHash(ctx context.Context, signer lndclient.SignerClient,
	hash lntypes.Hash) ([32]byte, error) {

	_, hashPubKey := btcec.PrivKeyFromBytes(hash[:])

	return signer.DeriveSharedKey(
		ctx, hashPubKey, &keychain.KeyLocator{
			Family: keychain.KeyFamily(swap.KeyFamily),
			Index:  0,
		},
	)
}
