package openchannel

import (
	"bytes"
	"context"
	"crypto/rand"
	"errors"
	"fmt"
	"io"
	"strings"
	"sync"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/lndclient"
	"github.com/lightninglabs/loop/fsm"
	"github.com/lightninglabs/loop/staticaddr/deposit"
	"github.com/lightninglabs/loop/staticaddr/staticutil"
	"github.com/lightninglabs/loop/staticaddr/withdraw"
	serverrpc "github.com/lightninglabs/loop/swapserverrpc"
	"github.com/lightningnetwork/lnd/lnrpc"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
	"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
	"google.golang.org/protobuf/proto"
)

const (
	// The minimum number of confirmations lnd requires inputs to have for a
	// channel opening.
	defaultUtxoMinConf = 1

	// Is the default confirmation target for a channel open transaction.
	defaultConfTarget int32 = 3
)

var (
	ErrOpeningChannelUnavailableDeposits = errors.New("some deposits are " +
		"not usable to open a channel with")

	// errPsbtFinalized is returned when the PSBT finalize step was already
	// sent to lnd. After this point the funding transaction may have been
	// broadcast, so deposits must not be rolled back to Deposited.
	errPsbtFinalized = errors.New("PSBT finalize already sent")
)

// Config is the configuration struct for the open channel manager.
type Config struct {
	// StaticAddressServerClient is the client that calls the swap server
	// rpcs to negotiate static address withdrawals.
	Server serverrpc.StaticAddressServerClient

	// AddressManager gives the withdrawal manager access to static address
	// parameters.
	AddressManager AddressManager

	// DepositManager gives the withdrawal manager access to the deposits
	// enabling it to create and manage withdrawals.
	DepositManager DepositManager

	// WithdrawalManager is used to create the withdrawal transaction into
	// the channel funding address.
	WithdrawalManager WithdrawalManager

	// WalletKit is the wallet client that is used to derive new keys from
	// lnd's wallet.
	WalletKit lndclient.WalletKitClient

	// ChainParams is the chain configuration(mainnet, testnet...) this
	// manager uses.
	ChainParams *chaincfg.Params

	// ChainNotifier is the chain notifier that is used to listen for new
	// blocks.
	ChainNotifier lndclient.ChainNotifierClient

	// Signer is the signer client that is used to sign transactions.
	Signer lndclient.SignerClient

	// LightningClient is the lnd client that is used to open channels.
	LightningClient lndclient.LightningClient
}

type newOpenChannelRequest struct {
	request  *lnrpc.OpenChannelRequest
	respChan chan *newOpenChannelResponse
}

type newOpenChannelResponse struct {
	// ChanOutpoint is the outpoint of the channel open transaction.
	ChanOutpoint *wire.OutPoint

	// Err is the error that occurred during the channel open process.
	err error
}

// Manager is the main struct that handles the open channel manager.
type Manager struct {
	cfg *Config

	newOpenChannelRequestChan chan newOpenChannelRequest

	// exitChan signals subroutines that the open channel is exiting.
	exitChan chan struct{}

	// errChan forwards errors from the open channel to the server.
	errChan chan error
}

// NewManager creates a new manager instance.
func NewManager(cfg *Config) *Manager {
	m := &Manager{
		cfg:                       cfg,
		exitChan:                  make(chan struct{}),
		newOpenChannelRequestChan: make(chan newOpenChannelRequest),
		errChan:                   make(chan error),
	}

	return m
}

// Run runs the open channel manager.
func (m *Manager) Run(ctx context.Context) error {
	err := m.recoverOpeningChannelDeposits(ctx)
	if err != nil {
		return err
	}

	for {
		select {
		case req := <-m.newOpenChannelRequestChan:
			chanOutpoint, err := m.OpenChannel(ctx, req.request)
			resp := &newOpenChannelResponse{
				ChanOutpoint: chanOutpoint,
				err:          err,
			}

			select {
			case req.respChan <- resp:

			case <-ctx.Done():
				// Notify subroutines that the main loop has
				// been canceled.
				close(m.exitChan)

				return ctx.Err()
			}

		case <-ctx.Done():
			// Signal subroutines that the manager is exiting.
			close(m.exitChan)

			return ctx.Err()
		}
	}
}

// recoverOpeningChannelDeposits resolves deposits that were left in
// OpeningChannel after a client restart. If a deposit input is still unspent,
// the channel open did not publish and we move back to Deposited. If the input
// is no longer unspent, it was spent on-chain and we finalize it as
// ChannelPublished.
func (m *Manager) recoverOpeningChannelDeposits(ctx context.Context) error {
	openingDeposits, err := m.cfg.DepositManager.GetActiveDepositsInState(
		deposit.OpeningChannel,
	)
	if err != nil {
		return fmt.Errorf("unable to fetch opening channel deposits: %w",
			err)
	}

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

	log.Infof("Recovering %d deposits in OpeningChannel state",
		len(openingDeposits))

	utxos, err := m.cfg.WalletKit.ListUnspent(
		ctx, 0, 0,
	)
	if err != nil {
		return fmt.Errorf("unable to list unspent outputs for recovery: %w",
			err)
	}

	unspentOutpoints := make(map[wire.OutPoint]struct{}, len(utxos))
	for _, utxo := range utxos {
		unspentOutpoints[utxo.OutPoint] = struct{}{}
	}

	var (
		deposited        []*deposit.Deposit
		channelPublished []*deposit.Deposit
	)

	for _, d := range openingDeposits {
		_, stillUnspent := unspentOutpoints[d.OutPoint]
		if stillUnspent {
			deposited = append(deposited, d)
			continue
		}

		channelPublished = append(channelPublished, d)
	}

	if len(deposited) > 0 {
		err = m.cfg.DepositManager.TransitionDeposits(
			ctx, deposited, fsm.OnError, deposit.Deposited,
		)
		if err != nil {
			return fmt.Errorf("unable to recover unspent opening "+
				"deposits: %w", err)
		}
	}

	if len(channelPublished) > 0 {
		err = m.cfg.DepositManager.TransitionDeposits(
			ctx, channelPublished, deposit.OnChannelPublished,
			deposit.ChannelPublished,
		)
		if err != nil {
			return fmt.Errorf("unable to recover spent opening "+
				"deposits: %w", err)
		}
	}

	log.Infof("Recovered opening channel deposits: %d returned to Deposited, "+
		"%d marked ChannelPublished", len(deposited),
		len(channelPublished))

	return nil
}

// OpenChannel transitions the requested deposits into the OpeningChannel state
// and then starts the open channel psbt flow between the client's lnd instance
// and the server.
func (m *Manager) OpenChannel(ctx context.Context,
	req *lnrpc.OpenChannelRequest) (*wire.OutPoint, error) {

	var (
		outpoints []wire.OutPoint
		deposits  []*deposit.Deposit
		allActive bool
		feeRate   chainfee.SatPerKWeight
		err       error
	)

	if req.LocalFundingAmount == 0 && !req.FundMax {
		return nil, fmt.Errorf("either local funding amount or " +
			"fundmax must be set")
	}

	if req.LocalFundingAmount != 0 && req.FundMax {
		return nil, fmt.Errorf("local funding amount and fundmax " +
			"cannot be set at the same time")
	}

	// Validate PSBT-incompatible flags early, before locking deposits.
	// We accept MinConfs=0 here because that's the proto default for unset
	// values and normalize to defaultUtxoMinConf later.
	if err := validateInitialPsbtFlags(req); err != nil {
		return nil, err
	}

	// Determine the commitment type for the channel.
	chanCommitmentType, err := resolveCommitmentType(req.CommitmentType)
	if err != nil {
		return nil, err
	}

	// Estimate the fee rate before deposit selection so that we can verify
	// the selected deposits cover the funding amount plus fees.
	if req.SatPerVbyte == 0 {
		feeRate, err = m.cfg.WalletKit.EstimateFeeRate(
			ctx, defaultConfTarget,
		)
		if err != nil {
			return nil, fmt.Errorf("error estimating fee rate: %w",
				err)
		}
	} else {
		feeRate = chainfee.SatPerKVByte(
			req.SatPerVbyte * 1000,
		).FeePerKWeight()
	}

	// There are three ways in which we select deposits to open a channel
	// with. 1.) The user manually selects the deposits. 2.) The user only
	// selects a local channel amount in which case we coin-select deposits
	// to cover for it. 3.) The user selects the fundmax flag, in which case
	// we select all deposits to fund the channel.
	if len(req.Outpoints) > 0 {
		// Ensure that the deposits are in a state in which they are
		// available for a channel open.
		outpoints, err = staticutil.ToWireOutpoints(req.Outpoints)
		if err != nil {
			return nil, fmt.Errorf("error parsing outpoints: %w",
				err)
		}

		// Check for duplicate outpoints which would lead to fee
		// miscalculation and an invalid PSBT with the same input
		// listed twice.
		seen := make(map[wire.OutPoint]struct{}, len(outpoints))
		for _, op := range outpoints {
			if _, ok := seen[op]; ok {
				return nil, fmt.Errorf("duplicate outpoint "+
					"%v in request", op)
			}
			seen[op] = struct{}{}
		}

		deposits, allActive =
			m.cfg.DepositManager.AllOutpointsActiveDeposits(
				outpoints, deposit.Deposited,
			)
		if !allActive {
			return nil, ErrOpeningChannelUnavailableDeposits
		}
	} else {
		// We have to select the deposits that are used to fund the
		// channel.
		deposits, err = m.cfg.DepositManager.GetActiveDepositsInState(
			deposit.Deposited,
		)
		if err != nil {
			return nil, err
		}

		if req.LocalFundingAmount != 0 {
			deposits, err = staticutil.SelectDeposits(
				deposits, req.LocalFundingAmount,
				feeRate, chanCommitmentType,
			)
			if err != nil {
				return nil, fmt.Errorf("error selecting "+
					"deposits: %w", err)
			}
		} else {
			// The fundmax flag is set, hence we select all deposits
			// for funding the channel.
		}
	}

	// Pre-check: calculate the channel funding amount and the optional
	// change before locking deposits. This ensures the selected deposits
	// can cover the funding amount plus fees.
	chanFundingAmt, _, calcErr := withdraw.CalculateWithdrawalTxValues(
		deposits, btcutil.Amount(req.LocalFundingAmount), feeRate, nil,
		chanCommitmentType,
	)
	if calcErr != nil {
		return nil, fmt.Errorf("error calculating funding tx "+
			"values: %w", calcErr)
	}

	// We need to transition the deposits to the opening channel state
	// before we start the channel open process. This is important to
	// ensure that the deposits are not used for other purposes while we
	// are opening the channel.
	err = m.cfg.DepositManager.TransitionDeposits(
		ctx, deposits, deposit.OnOpeningChannel, deposit.OpeningChannel,
	)
	if err != nil {
		return nil, err
	}

	// Clone the request message and set mandatory parameters.
	reqClone := proto.Clone(req).(*lnrpc.OpenChannelRequest)

	// Override fields consumed locally or incompatible with PSBT funding.
	reqClone.LocalFundingAmount = int64(chanFundingAmt)

	// TODO: Once lnd's PSBT channel open flow supports fundmax natively,
	// we can pass FundMax through to lnd and remove Loop's local coin
	// selection for the fundmax case.
	reqClone.FundMax = false
	reqClone.MinConfs = defaultUtxoMinConf
	reqClone.SpendUnconfirmed = false
	// In the lnd PSBT flow, fee estimation params on the request are
	// explicitly disallowed.
	reqClone.SatPerVbyte = 0
	reqClone.CommitmentType = chanCommitmentType

	chanOutpoint, err := m.openChannelPsbt(ctx, reqClone, deposits, feeRate)
	if err == nil {
		return chanOutpoint, nil
	}

	log.Infof("error opening channel: %v", err)

	// If the PSBT was already finalized and sent to lnd, the
	// funding transaction may have been broadcast. In that case
	// we must not blindly roll back. Instead, try to recover
	// the deposits now so they don't remain stuck in
	// OpeningChannel until the next restart.
	if errors.Is(err, errPsbtFinalized) {
		recoverErr := m.recoverOpeningChannelDeposits(ctx)
		if recoverErr != nil {
			log.Errorf("failed recovering deposits "+
				"after PSBT finalize: %v",
				recoverErr)
		}
	} else {
		err2 := m.cfg.DepositManager.TransitionDeposits(
			ctx, deposits, fsm.OnError,
			deposit.Deposited,
		)
		if err2 != nil {
			log.Errorf("failed transitioning deposits "+
				"after failed channel open: %v",
				err2)
		}
	}

	return nil, err
}

// openChannelPsbt starts an interactive channel open protocol that uses a
// partially signed bitcoin transaction (PSBT) to fund the channel output. The
// protocol involves several steps between the loop client and the server:
//
// RPC server                           CLI client
//
//	|                                    |
//	|  |<------open channel (stream)-----|
//	|  |-------ready for funding----->|  |
//	|  |------------------------------|  | create psbt from deposits
//	|  |<------PSBT verify------------|  |
//	|  |-------ready for signing----->|  |
//	|  |------------------------------|  | request server co-sig
//	|  |------------------------------|  | sign psbt with combined sig
//	|  |<------PSBT finalize----------|  |
//	|  |-------channel pending------->|  |
//	|  |-------channel open------------->|
//	|                                    |
func (m *Manager) openChannelPsbt(ctx context.Context,
	req *lnrpc.OpenChannelRequest, deposits []*deposit.Deposit,
	feeRate chainfee.SatPerKWeight) (*wire.OutPoint, error) {

	var (
		pendingChanID [32]byte
		shimMu        sync.Mutex
		shimPending   = true
		psbtFinalized bool
		basePsbtBytes []byte
		quit          = make(chan struct{})
		quitCause     error
		closeQuitOnce sync.Once
		srvMsg        = make(chan *lnrpc.OpenStatusUpdate, 1)
		srvErr        = make(chan error, 1)
	)

	// closeQuit safely closes the quit channel using sync.Once to prevent
	// panic from closing an already-closed channel.
	closeQuit := func() {
		closeQuitOnce.Do(func() {
			close(quit)
		})
	}

	// Make sure the user didn't supply any command line flags that are
	// incompatible with PSBT funding.
	err := checkPsbtFlags(req)
	if err != nil {
		return nil, err
	}

	// Generate a new, random pending channel ID that we'll use as the main
	// identifier when sending update messages to the RPC server.
	if _, err := rand.Read(pendingChanID[:]); err != nil {
		return nil, fmt.Errorf("unable to generate random chan ID: "+
			"%w", err)
	}
	log.Infof("Starting PSBT funding flow with pending channel ID %x.\n",
		pendingChanID)

	// maybeCancelShim is a helper function that cancels the funding shim
	// with the RPC server in case we end up aborting early.
	maybeCancelShim := func() {
		shimMu.Lock()
		defer shimMu.Unlock()

		// If the user canceled while there was still a shim registered
		// with the wallet, release the resources now.
		if shimPending {
			log.Infof("Canceling PSBT funding flow for pending "+
				"channel ID %x.\n", pendingChanID)

			cancelMsg := &lnrpc.FundingTransitionMsg{
				Trigger: &lnrpc.FundingTransitionMsg_ShimCancel{
					ShimCancel: &lnrpc.FundingShimCancel{
						PendingChanId: pendingChanID[:],
					},
				},
			}
			_, err := m.cfg.LightningClient.FundingStateStep(
				ctx, cancelMsg,
			)
			if err != nil {
				log.Errorf("Error canceling shim: %v\n", err)
			}
			shimPending = false
		}
	}
	defer maybeCancelShim()

	// Create the PSBT funding shim that will tell the funding manager we
	// want to use a PSBT.
	req.FundingShim = &lnrpc.FundingShim{
		Shim: &lnrpc.FundingShim_PsbtShim{
			PsbtShim: &lnrpc.PsbtShim{
				PendingChanId: pendingChanID[:],
				BasePsbt:      basePsbtBytes,
				// Setting this to false since we don't batch
				// open channels.
				NoPublish: false,
			},
		},
	}

	// Start the interactive process by opening the stream connection to the
	// daemon. If the user cancels by pressing <Ctrl+C> we need to cancel
	// the shim. To not just kill the process on interrupt, we need to
	// explicitly capture the signal.
	rawCtx, _, rawClient := m.cfg.LightningClient.RawClientWithMacAuth(ctx)
	stream, err := rawClient.OpenChannel(rawCtx, req)
	if err != nil {
		return nil, fmt.Errorf("opening stream to server "+
			"failed: %w", err)
	}

	// We also need to spawn a goroutine that reads from the server. This
	// will copy the messages to the channel as long as they come in or add
	// exactly one error to the error stream and then bail out.
	go func() {
		for {
			// Recv blocks until a message or error arrives.
			resp, err := stream.Recv()
			if err == io.EOF {
				srvErr <- fmt.Errorf("loop shutting down: %w",
					err)

				return
			} else if err != nil {
				srvErr <- fmt.Errorf("got error from server: "+
					"%v", err)

				return
			}

			// Don't block on sending in case of shutting down.
			select {
			case srvMsg <- resp:
			case <-quit:

				return
			}
		}
	}()

	// Spawn another goroutine that only handles loop server shutdown or
	// errors from the lnd server. Both will trigger an attempt to cancel
	// the shim with the server.
	go func() {
		select {
		case <-ctx.Done():
			log.Infof("OpenChannel context cancel.")
			closeQuit()

		case err := <-srvErr:
			log.Errorf("OpenChannel lnd server error received: "+
				"%v\n", err)

			// If the remote peer canceled on us, the reservation
			// has already been deleted. We don't need to try to
			// remove it again, this would just produce another
			// error.
			cancelErr := chanfunding.ErrRemoteCanceled.Error()
			if err != nil && strings.Contains(
				err.Error(), cancelErr,
			) {

				shimMu.Lock()
				shimPending = false
				shimMu.Unlock()
			}

			quitCause = err
			closeQuit()

		case <-quit:
		}
	}()

	for {
		var srvResponse *lnrpc.OpenStatusUpdate
		select {
		case srvResponse = <-srvMsg:
		case <-quit:
			cancelErr := fmt.Errorf("open channel flow canceled")
			if quitCause != nil {
				cancelErr = quitCause
			}
			if psbtFinalized {
				return nil, fmt.Errorf("%w: %v",
					errPsbtFinalized, cancelErr)
			}
			return nil, cancelErr
		}

		switch update := srvResponse.Update.(type) {
		case *lnrpc.OpenStatusUpdate_PsbtFund:
			fundingAmount := update.PsbtFund.FundingAmount
			if req.LocalFundingAmount != fundingAmount {
				err := fmt.Errorf("funding amount "+
					"%v doesn't match local "+
					"funding amount %v",
					fundingAmount,
					req.LocalFundingAmount)

				return nil, err
			}

			addr := update.PsbtFund.FundingAddress

			log.Infof("PSBT funding initiated with peer "+
				"%x, funding amount %v, funding "+
				"address %v", req.NodePubkey,
				fundingAmount, addr)

			// Create the psbt funding transaction for the
			// channel. Ensure the selected deposits amount
			// to the psbt funding amount.
			channelFundingAddress, err := btcutil.DecodeAddress(
				addr, m.cfg.ChainParams,
			)
			if err != nil {
				return nil, fmt.Errorf("decoding funding "+
					"address: %w", err)
			}

			//nolint:ll
			signedTx, unsignedPsbt, err := m.cfg.WithdrawalManager.CreateFinalizedWithdrawalTx(
				ctx, deposits, channelFundingAddress, feeRate,
				fundingAmount, req.CommitmentType,
			)
			if err != nil {
				return nil, fmt.Errorf("creating PSBT "+
					"failed: %w", err)
			}

			// Verify that the psbt contains the correct outputs.
			verifyMsg := &lnrpc.FundingTransitionMsg{
				Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{
					PsbtVerify: &lnrpc.FundingPsbtVerify{
						FundedPsbt:    unsignedPsbt,
						PendingChanId: pendingChanID[:],
					},
				},
			}
			_, err = m.cfg.LightningClient.FundingStateStep(
				ctx, verifyMsg,
			)
			if err != nil {
				return nil, fmt.Errorf("verifying PSBT by lnd "+
					"failed: %v", err)
			}

			// Now that we have the final transaction, we can
			// finalize the PSBT and publish the channel open
			// transaction.
			var buffer bytes.Buffer
			err = signedTx.Serialize(&buffer)
			if err != nil {
				return nil, fmt.Errorf("error serializing "+
					"tx: %w", err)
			}
			transitionMsg := &lnrpc.FundingTransitionMsg{
				Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{
					PsbtFinalize: &lnrpc.FundingPsbtFinalize{
						FinalRawTx:    buffer.Bytes(),
						PendingChanId: pendingChanID[:],
					},
				},
			}
			_, err = m.cfg.LightningClient.FundingStateStep(
				ctx, transitionMsg,
			)
			if err != nil {
				return nil, fmt.Errorf("finalizing PSBT "+
					"funding flow failed: %v", err)
			}

			// The finalize step succeeded. From this point
			// on the funding tx may have been broadcast, so
			// deposits must not be rolled back.
			psbtFinalized = true

		case *lnrpc.OpenStatusUpdate_ChanPending:
			// As soon as the channel is pending, there is no more
			// shim that needs to be canceled. If the user
			// interrupts now, we don't need to clean up anything.
			shimMu.Lock()
			shimPending = false
			shimMu.Unlock()

			hash, err := chainhash.NewHash(
				update.ChanPending.Txid,
			)
			if err != nil {
				return nil, fmt.Errorf("error creating "+
					"hash for channel open tx: %w", err)
			}

			chanOutpoint := &wire.OutPoint{
				Hash:  *hash,
				Index: update.ChanPending.OutputIndex,
			}

			log.Infof("Channel transaction pending: %v",
				chanOutpoint)
			log.Infof("Please monitor the channel from lnd")

			err = m.cfg.DepositManager.TransitionDeposits(
				ctx, deposits, deposit.OnChannelPublished,
				deposit.ChannelPublished,
			)
			if err != nil {
				log.Errorf("error transitioning deposits to "+
					"ChannelPublished: %v", err)
			}

			// We can now close the quit channel to stop the
			// goroutine that reads from the server.
			closeQuit()

			return chanOutpoint, nil
		}
	}
}

// validateInitialPsbtFlags validates request fields that are incompatible with
// the interactive PSBT channel funding flow.
func validateInitialPsbtFlags(req *lnrpc.OpenChannelRequest) error {
	if req.MinConfs != 0 && req.MinConfs != defaultUtxoMinConf {
		return fmt.Errorf("custom MinConfs not supported for PSBT " +
			"funding, only the default is allowed")
	}

	if req.SpendUnconfirmed {
		return fmt.Errorf("SpendUnconfirmed is not supported " +
			"for PSBT funding")
	}

	if req.TargetConf != 0 {
		return fmt.Errorf("TargetConf is not supported for PSBT " +
			"funding, use SatPerVbyte to specify fee rate")
	}

	if req.SatPerByte != 0 { //nolint:staticcheck
		return fmt.Errorf("SatPerByte is deprecated and not " +
			"supported for PSBT funding, use SatPerVbyte")
	}

	if req.NodePubkeyString != "" { //nolint:staticcheck
		return fmt.Errorf("NodePubkeyString is not supported, " +
			"use NodePubkey instead")
	}

	if req.FundingShim != nil {
		return fmt.Errorf("FundingShim is not supported, it is " +
			"managed internally for PSBT funding")
	}

	return nil
}

// resolveCommitmentType validates supported channel commitment types and
// normalizes unknown/default to STATIC_REMOTE_KEY.
func resolveCommitmentType(commitmentType lnrpc.CommitmentType) (
	lnrpc.CommitmentType, error) {

	switch commitmentType {
	case lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
		lnrpc.CommitmentType_STATIC_REMOTE_KEY:

		return lnrpc.CommitmentType_STATIC_REMOTE_KEY, nil

	case lnrpc.CommitmentType_ANCHORS:
		return lnrpc.CommitmentType_ANCHORS, nil

	case lnrpc.CommitmentType_SIMPLE_TAPROOT:
		return lnrpc.CommitmentType_SIMPLE_TAPROOT, nil

	default:
		return lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE, fmt.Errorf(
			"unsupported commitment type %v", commitmentType,
		)
	}
}

// checkPsbtFlags make sure a request to open a channel doesn't set any
// parameters that are incompatible with the PSBT funding flow.
func checkPsbtFlags(req *lnrpc.OpenChannelRequest) error {
	if req.MinConfs != defaultUtxoMinConf || req.SpendUnconfirmed {
		return fmt.Errorf("specifying minimum confirmations for PSBT " +
			"funding is not supported")
	}
	if req.TargetConf != 0 || req.SatPerByte != 0 || req.SatPerVbyte != 0 { // nolint:staticcheck
		return fmt.Errorf("setting fee estimation parameters not " +
			"supported for PSBT funding")
	}
	return nil
}

// DeliverOpenChannelRequest forwards a open channel request to the manager main
// loop.
func (m *Manager) DeliverOpenChannelRequest(ctx context.Context,
	req *lnrpc.OpenChannelRequest) (*wire.OutPoint, error) {

	request := newOpenChannelRequest{
		request:  req,
		respChan: make(chan *newOpenChannelResponse),
	}

	// Send the open channel request to the manager run loop.
	select {
	case m.newOpenChannelRequestChan <- request:

	case <-m.exitChan:
		return nil, fmt.Errorf("open channel manager has been " +
			"canceled")

	case <-ctx.Done():
		return nil, fmt.Errorf("context canceled while opening " +
			"channel")
	}

	// Wait for the response from the manager run loop.
	select {
	case resp := <-request.respChan:
		return resp.ChanOutpoint, resp.err

	case <-m.exitChan:
		return nil, fmt.Errorf("open channel manager has been " +
			"canceled")

	case <-ctx.Done():
		return nil, fmt.Errorf("context canceled while waiting " +
			"for open channel response")
	}
}
