package contractcourt

import (
	"io"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/lightningnetwork/lnd/channeldb"
	"github.com/lightningnetwork/lnd/fn/v2"
	"github.com/lightningnetwork/lnd/lnwallet"
)

// htlcOutgoingContestResolver is a ContractResolver that's able to resolve an
// outgoing HTLC that is still contested. An HTLC is still contested, if at the
// time that we broadcast the commitment transaction, it isn't able to be fully
// resolved. In this case, we'll either wait for the HTLC to timeout, or for
// us to learn of the preimage.
type htlcOutgoingContestResolver struct {
	// htlcTimeoutResolver is the inner solver that this resolver may turn
	// into. This only happens if the HTLC expires on-chain.
	*htlcTimeoutResolver
}

// newOutgoingContestResolver instantiates a new outgoing contested htlc
// resolver.
func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution,
	broadcastHeight uint32, htlc channeldb.HTLC,
	chanType channeldb.ChannelType,
	resCfg ResolverConfig) *htlcOutgoingContestResolver {

	timeout := newTimeoutResolver(
		res, broadcastHeight, htlc, chanType, resCfg,
	)

	return &htlcOutgoingContestResolver{
		htlcTimeoutResolver: timeout,
	}
}

// Launch will call the inner resolver's launch method if the expiry height has
// been reached, otherwise it's a no-op.
func (h *htlcOutgoingContestResolver) Launch() error {
	// NOTE: we don't mark this resolver as launched as the inner resolver
	// will set it when it's launched.
	if h.isLaunched() {
		h.log.Tracef("already launched")
		return nil
	}

	h.log.Debugf("launching contest resolver...")

	_, bestHeight, err := h.ChainIO.GetBestBlock()
	if err != nil {
		return err
	}

	if uint32(bestHeight) < h.htlcResolution.Expiry {
		return nil
	}

	// If the current height is >= expiry, then a timeout path spend will
	// be valid to be included in the next block, and we can immediately
	// return the resolver.
	h.log.Infof("expired (height=%v, expiry=%v), transforming into "+
		"timeout resolver and launching it", bestHeight,
		h.htlcResolution.Expiry)

	return h.htlcTimeoutResolver.Launch()
}

// Resolve commences the resolution of this contract. As this contract hasn't
// yet timed out, we'll wait for one of two things to happen
//
//  1. The HTLC expires. In this case, we'll sweep the funds and send a clean
//     up cancel message to outside sub-systems.
//
//  2. The remote party sweeps this HTLC on-chain, in which case we'll add the
//     pre-image to our global cache, then send a clean up settle message
//     backwards.
//
// When either of these two things happens, we'll create a new resolver which
// is able to handle the final resolution of the contract. We're only the pivot
// point.
func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
	// If we're already full resolved, then we don't have anything further
	// to do.
	if h.IsResolved() {
		h.log.Errorf("already resolved")
		return nil, nil
	}

	// Otherwise, we'll watch for two external signals to decide if we'll
	// morph into another resolver, or fully resolve the contract.
	//
	// The output we'll be watching for is the *direct* spend from the HTLC
	// output. If this isn't our commitment transaction, it'll be right on
	// the resolution. Otherwise, we fetch this pointer from the input of
	// the time out transaction.
	outPointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
	if err != nil {
		return nil, err
	}

	// First, we'll register for a spend notification for this output. If
	// the remote party sweeps with the pre-image, we'll be notified.
	spendNtfn, err := h.Notifier.RegisterSpendNtfn(
		outPointToWatch, scriptToWatch, h.broadcastHeight,
	)
	if err != nil {
		return nil, err
	}

	// We'll quickly check to see if the output has already been spent.
	select {
	// If the output has already been spent, then we can stop early and
	// sweep the pre-image from the output.
	case commitSpend, ok := <-spendNtfn.Spend:
		if !ok {
			return nil, errResolverShuttingDown
		}

		return nil, h.claimCleanUp(commitSpend)

	// If it hasn't, then we'll watch for both the expiration, and the
	// sweeping out this output.
	default:
	}

	// If we reach this point, then we can't fully act yet, so we'll await
	// either of our signals triggering: the HTLC expires, or we learn of
	// the preimage.
	blockEpochs, err := h.Notifier.RegisterBlockEpochNtfn(nil)
	if err != nil {
		return nil, err
	}
	defer blockEpochs.Cancel()

	for {
		select {

		// A new block has arrived, we'll check to see if this leads to
		// HTLC expiration.
		case newBlock, ok := <-blockEpochs.Epochs:
			if !ok {
				return nil, errResolverShuttingDown
			}

			// If the current height is >= expiry, then a timeout
			// path spend will be valid to be included in the next
			// block, and we can immediately return the resolver.
			//
			// NOTE: when broadcasting this transaction, btcd will
			// check the timelock in `CheckTransactionStandard`,
			// which requires `expiry < currentHeight+1`. If the
			// check doesn't pass, error `transaction is not
			// finalized` will be returned and the broadcast will
			// fail.
			newHeight := uint32(newBlock.Height)
			expiry := h.htlcResolution.Expiry

			// Check if the expiry height is about to be reached.
			// We offer this HTLC one block earlier to make sure
			// when the next block arrives, the sweeper will pick
			// up this input and sweep it immediately. The sweeper
			// will handle the waiting for the one last block till
			// expiry.
			if newHeight >= expiry-1 {
				h.log.Infof("HTLC about to expire "+
					"(height=%v, expiry=%v), transforming "+
					"into timeout resolver", newHeight,
					h.htlcResolution.Expiry)

				return h.htlcTimeoutResolver, nil
			}

		// The output has been spent! This means the preimage has been
		// revealed on-chain.
		case commitSpend, ok := <-spendNtfn.Spend:
			if !ok {
				return nil, errResolverShuttingDown
			}

			// The only way this output can be spent by the remote
			// party is by revealing the preimage. So we'll perform
			// our duties to clean up the contract once it has been
			// claimed.
			return nil, h.claimCleanUp(commitSpend)

		case <-h.quit:
			return nil, errResolverShuttingDown
		}
	}
}

// report returns a report on the resolution state of the contract.
func (h *htlcOutgoingContestResolver) report() *ContractReport {
	// No locking needed as these values are read-only.

	finalAmt := h.htlc.Amt.ToSatoshis()
	if h.htlcResolution.SignedTimeoutTx != nil {
		finalAmt = btcutil.Amount(
			h.htlcResolution.SignedTimeoutTx.TxOut[0].Value,
		)
	}

	return &ContractReport{
		Outpoint:       h.htlcResolution.ClaimOutpoint,
		Type:           ReportOutputOutgoingHtlc,
		Amount:         finalAmt,
		MaturityHeight: h.htlcResolution.Expiry,
		LimboBalance:   finalAmt,
		Stage:          1,
	}
}

// Stop signals the resolver to cancel any current resolution processes, and
// suspend.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcOutgoingContestResolver) Stop() {
	h.log.Debugf("stopping...")
	defer h.log.Debugf("stopped")
	close(h.quit)
}

// Encode writes an encoded version of the ContractResolver into the passed
// Writer.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcOutgoingContestResolver) Encode(w io.Writer) error {
	return h.htlcTimeoutResolver.Encode(w)
}

// SupplementDeadline forwards the incoming HTLC's expiry height to the inner
// timeout resolver. This resolver morphs into that timeout resolver once the
// outgoing HTLC expires on-chain, so the deadline is retained across the
// transition.
//
// NOTE: Part of the htlcContractResolver interface.
func (h *htlcOutgoingContestResolver) SupplementDeadline(d fn.Option[int32]) {
	h.htlcTimeoutResolver.SupplementDeadline(d)
}

// newOutgoingContestResolverFromReader attempts to decode an encoded ContractResolver
// from the passed Reader instance, returning an active ContractResolver
// instance.
func newOutgoingContestResolverFromReader(r io.Reader, resCfg ResolverConfig) (
	*htlcOutgoingContestResolver, error) {

	h := &htlcOutgoingContestResolver{}
	timeoutResolver, err := newTimeoutResolverFromReader(r, resCfg)
	if err != nil {
		return nil, err
	}
	h.htlcTimeoutResolver = timeoutResolver
	return h, nil
}

// A compile time assertion to ensure htlcOutgoingContestResolver meets the
// ContractResolver interface.
var _ htlcContractResolver = (*htlcOutgoingContestResolver)(nil)
