package invoices

import (
	"errors"
	"fmt"
	"time"

	"github.com/lightningnetwork/lnd/channeldb/models"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwire"
)

// updateHtlcsAmp takes an invoice, and a new HTLC to be added (along with its
// set ID), and updates the internal AMP state of an invoice, and also tallies
// the set of HTLCs to be updated on disk.
func acceptHtlcsAmp(invoice *Invoice, setID SetID,
	circuitKey models.CircuitKey, htlc *InvoiceHTLC,
	updater InvoiceUpdater) error {

	newAmpState, err := getUpdatedInvoiceAmpState(
		invoice, setID, circuitKey, HtlcStateAccepted, htlc.Amt,
	)
	if err != nil {
		return err
	}

	invoice.AMPState[setID] = newAmpState

	// Mark the updates as needing to be written to disk.
	return updater.UpdateAmpState(setID, newAmpState, circuitKey)
}

// cancelHtlcsAmp processes a cancellation of an HTLC that belongs to an AMP
// HTLC set. We'll need to update the meta data in the  main invoice, and also
// apply the new update to the update MAP, since all the HTLCs for a given HTLC
// set need to be written in-line with each other.
func cancelHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey,
	htlc *InvoiceHTLC, updater InvoiceUpdater) error {

	setID := htlc.AMP.Record.SetID()

	// First, we'll update the state of the entire HTLC set
	// to cancelled.
	newAmpState, err := getUpdatedInvoiceAmpState(
		invoice, setID, circuitKey, HtlcStateCanceled,
		htlc.Amt,
	)
	if err != nil {
		return err
	}

	invoice.AMPState[setID] = newAmpState

	// Mark the updates as needing to be written to disk.
	err = updater.UpdateAmpState(setID, newAmpState, circuitKey)
	if err != nil {
		return err
	}

	// We'll only decrement the total amount paid if the invoice was
	// already in the accepted state.
	if invoice.AmtPaid != 0 {
		return updateInvoiceAmtPaid(
			invoice, invoice.AmtPaid-htlc.Amt, updater,
		)
	}

	return nil
}

// settleHtlcsAmp processes a new settle operation on an HTLC set for an AMP
// invoice. We'll update some meta data in the main invoice, and also signal
// that this HTLC set needs to be re-written back to disk.
func settleHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey,
	htlc *InvoiceHTLC, updater InvoiceUpdater) error {

	setID := htlc.AMP.Record.SetID()

	// Next update the main AMP meta-data to indicate that this HTLC set
	// has been fully settled.
	newAmpState, err := getUpdatedInvoiceAmpState(
		invoice, setID, circuitKey, HtlcStateSettled, 0,
	)
	if err != nil {
		return err
	}

	invoice.AMPState[setID] = newAmpState

	// Mark the updates as needing to be written to disk.
	return updater.UpdateAmpState(setID, newAmpState, circuitKey)
}

// UpdateInvoice fetches the invoice, obtains the update descriptor from the
// callback and applies the updates in a single db transaction.
func UpdateInvoice(hash *lntypes.Hash, invoice *Invoice,
	updateTime time.Time, callback InvoiceUpdateCallback,
	updater InvoiceUpdater) (*Invoice, error) {

	// Create deep copy to prevent any accidental modification in the
	// callback.
	invoiceCopy, err := CopyInvoice(invoice)
	if err != nil {
		return nil, err
	}

	// Call the callback and obtain the update descriptor.
	update, err := callback(invoiceCopy)
	if err != nil {
		return invoice, err
	}

	// If there is nothing to update, return early.
	if update == nil {
		return invoice, nil
	}

	switch update.UpdateType {
	case CancelHTLCsUpdate:
		err := cancelHTLCs(invoice, updateTime, update, updater)
		if err != nil {
			return nil, err
		}

	case AddHTLCsUpdate:
		err := addHTLCs(invoice, hash, updateTime, update, updater)
		if err != nil {
			return nil, err
		}

	case SettleHodlInvoiceUpdate:
		err := settleHodlInvoice(
			invoice, hash, updateTime, update.State, updater,
		)
		if err != nil {
			return nil, err
		}

	case CancelInvoiceUpdate:
		err := cancelInvoice(
			invoice, hash, updateTime, update.State, updater,
		)
		if err != nil {
			return nil, err
		}

	default:
		return nil, fmt.Errorf("unknown update type: %s",
			update.UpdateType)
	}

	if err := updater.Finalize(update.UpdateType); err != nil {
		return nil, err
	}

	return invoice, nil
}

// cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc.
//
// NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the
// InvoiceUpdateDesc.
func cancelHTLCs(invoice *Invoice, updateTime time.Time,
	update *InvoiceUpdateDesc, updater InvoiceUpdater) error {

	for key := range update.CancelHtlcs {
		htlc, exists := invoice.Htlcs[key]

		// Verify that we don't get an action for htlcs that are not
		// present on the invoice.
		if !exists {
			return fmt.Errorf("cancel of non-existent htlc")
		}

		err := canCancelSingleHtlc(htlc, invoice.State)
		if err != nil {
			return err
		}

		err = resolveHtlc(
			key, htlc, HtlcStateCanceled, updateTime,
			updater,
		)
		if err != nil {
			return err
		}

		// Tally this into the set of HTLCs that need to be updated on
		// disk, but once again, only if this is an AMP invoice.
		if invoice.IsAMP() {
			err := cancelHtlcsAmp(invoice, key, htlc, updater)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc.
//
//nolint:funlen
func addHTLCs(invoice *Invoice, hash *lntypes.Hash, updateTime time.Time,
	update *InvoiceUpdateDesc, updater InvoiceUpdater) error {

	var setID *[32]byte
	invoiceIsAMP := invoice.IsAMP()
	if invoiceIsAMP && update.State != nil {
		setID = update.State.SetID
	}

	for key, htlcUpdate := range update.AddHtlcs {
		if _, exists := invoice.Htlcs[key]; exists {
			return fmt.Errorf("duplicate add of htlc %v", key)
		}

		// Force caller to supply htlc without custom records in a
		// consistent way.
		if htlcUpdate.CustomRecords == nil {
			return errors.New("nil custom records map")
		}

		htlc := &InvoiceHTLC{
			Amt:           htlcUpdate.Amt,
			MppTotalAmt:   htlcUpdate.MppTotalAmt,
			Expiry:        htlcUpdate.Expiry,
			AcceptHeight:  uint32(htlcUpdate.AcceptHeight),
			AcceptTime:    updateTime,
			State:         HtlcStateAccepted,
			CustomRecords: htlcUpdate.CustomRecords,
		}

		if invoiceIsAMP {
			if htlcUpdate.AMP == nil {
				return fmt.Errorf("unable to add htlc "+
					"without AMP data to AMP invoice(%v)",
					invoice.AddIndex)
			}

			htlc.AMP = htlcUpdate.AMP.Copy()
		}

		if err := updater.AddHtlc(key, htlc); err != nil {
			return err
		}

		invoice.Htlcs[key] = htlc

		// Collect the set of new HTLCs so we can write them properly
		// below, but only if this is an AMP invoice.
		if invoiceIsAMP {
			err := acceptHtlcsAmp(
				invoice, htlcUpdate.AMP.Record.SetID(), key,
				htlc, updater,
			)
			if err != nil {
				return err
			}
		}
	}

	// At this point, the set of accepted HTLCs should be fully
	// populated with added HTLCs or removed of canceled ones. Update
	// invoice state if the update descriptor indicates an invoice state
	// change, which depends on having an accurate view of the accepted
	// HTLCs.
	if update.State != nil {
		newState, err := getUpdatedInvoiceState(
			invoice, hash, *update.State,
		)
		if err != nil {
			return err
		}

		// If this isn't an AMP invoice, then we'll go ahead and update
		// the invoice state directly here. For AMP invoices, we instead
		// will keep the top-level invoice open, and update the state of
		// each _htlc set_ instead. However, we'll allow the invoice to
		// transition to the cancelled state regardless.
		if !invoiceIsAMP || *newState == ContractCanceled {
			err := updater.UpdateInvoiceState(*newState, nil)
			if err != nil {
				return err
			}
			invoice.State = *newState
		}
	}

	// The set of HTLC pre-images will only be set if we were actually able
	// to reconstruct all the AMP pre-images.
	var settleEligibleAMP bool
	if update.State != nil {
		settleEligibleAMP = len(update.State.HTLCPreimages) != 0
	}

	// With any invoice level state transitions recorded, we'll now
	// finalize the process by updating the state transitions for
	// individual HTLCs
	var amtPaid lnwire.MilliSatoshi

	for key, htlc := range invoice.Htlcs {
		// Set the HTLC preimage for any AMP HTLCs.
		if setID != nil && update.State != nil {
			preimage, ok := update.State.HTLCPreimages[key]
			switch {
			// If we don't already have a preimage for this HTLC, we
			// can set it now.
			case ok && htlc.AMP.Preimage == nil:
				err := updater.AddAmpHtlcPreimage(
					htlc.AMP.Record.SetID(), key, preimage,
				)
				if err != nil {
					return err
				}
				htlc.AMP.Preimage = &preimage

			// Otherwise, prevent over-writing an existing
			// preimage.  Ignore the case where the preimage is
			// identical.
			case ok && *htlc.AMP.Preimage != preimage:
				return ErrHTLCPreimageAlreadyExists
			}
		}

		// The invoice state may have changed and this could have
		// implications for the states of the individual htlcs. Align
		// the htlc state with the current invoice state.
		//
		// If we have all the pre-images for an AMP invoice, then we'll
		// act as if we're able to settle the entire invoice. We need
		// to do this since it's possible for us to settle AMP invoices
		// while the contract state (on disk) is still in the accept
		// state.
		htlcContextState := invoice.State
		if settleEligibleAMP {
			htlcContextState = ContractSettled
		}
		htlcStateChanged, htlcState, err := getUpdatedHtlcState(
			htlc, htlcContextState, setID,
		)
		if err != nil {
			return err
		}

		if htlcStateChanged {
			err = resolveHtlc(
				key, htlc, htlcState, updateTime, updater,
			)
			if err != nil {
				return err
			}
		}

		htlcSettled := htlcStateChanged &&
			htlcState == HtlcStateSettled

		// If the HTLC has being settled for the first time, and this
		// is an AMP invoice, then we'll need to update some additional
		// meta data state.
		if htlcSettled && invoiceIsAMP {
			err = settleHtlcsAmp(invoice, key, htlc, updater)
			if err != nil {
				return err
			}
		}

		accepted := htlc.State == HtlcStateAccepted
		settled := htlc.State == HtlcStateSettled
		invoiceStateReady := accepted || settled

		if !invoiceIsAMP {
			// Update the running amount paid to this invoice. We
			// don't include accepted htlcs when the invoice is
			// still open.
			if invoice.State != ContractOpen &&
				invoiceStateReady {

				amtPaid += htlc.Amt
			}
		} else {
			// For AMP invoices, since we won't always be reading
			// out the total invoice set each time, we'll instead
			// accumulate newly added invoices to the total amount
			// paid.
			if _, ok := update.AddHtlcs[key]; !ok {
				continue
			}

			// Update the running amount paid to this invoice. AMP
			// invoices never go to the settled state, so if it's
			// open, then we tally the HTLC.
			if invoice.State == ContractOpen &&
				invoiceStateReady {

				amtPaid += htlc.Amt
			}
		}
	}

	// For non-AMP invoices we recalculate the amount paid from scratch
	// each time, while for AMP invoices, we'll accumulate only based on
	// newly added HTLCs.
	if invoiceIsAMP {
		amtPaid += invoice.AmtPaid
	}

	return updateInvoiceAmtPaid(invoice, amtPaid, updater)
}

func resolveHtlc(circuitKey models.CircuitKey, htlc *InvoiceHTLC,
	state HtlcState, resolveTime time.Time,
	updater InvoiceUpdater) error {

	err := updater.ResolveHtlc(circuitKey, state, resolveTime)
	if err != nil {
		return err
	}
	htlc.State = state
	htlc.ResolveTime = resolveTime

	return nil
}

func updateInvoiceAmtPaid(invoice *Invoice, amt lnwire.MilliSatoshi,
	updater InvoiceUpdater) error {

	err := updater.UpdateInvoiceAmtPaid(amt)
	if err != nil {
		return err
	}
	invoice.AmtPaid = amt

	return nil
}

// settleHodlInvoice marks a hodl invoice as settled.
//
// NOTE: Currently it is not possible to have HODL AMP invoices.
func settleHodlInvoice(invoice *Invoice, hash *lntypes.Hash,
	updateTime time.Time, update *InvoiceStateUpdateDesc,
	updater InvoiceUpdater) error {

	if !invoice.HodlInvoice {
		return fmt.Errorf("unable to settle hodl invoice: %v is "+
			"not a hodl invoice", invoice.AddIndex)
	}

	// TODO(positiveblue): because NewState can only be ContractSettled we
	// can remove it from the API and set it here directly.
	switch {
	case update == nil:
		fallthrough

	case update.NewState != ContractSettled:
		return fmt.Errorf("unable to settle hodl invoice: "+
			"not valid InvoiceUpdateDesc.State: %v", update)

	case update.Preimage == nil:
		return fmt.Errorf("unable to settle hodl invoice: " +
			"preimage is nil")
	}

	newState, err := getUpdatedInvoiceState(
		invoice, hash, *update,
	)
	if err != nil {
		return err
	}

	if newState == nil || *newState != ContractSettled {
		return fmt.Errorf("unable to settle hodl invoice: "+
			"new computed state is not settled: %s", newState)
	}

	err = updater.UpdateInvoiceState(
		ContractSettled, update.Preimage,
	)
	if err != nil {
		return err
	}

	invoice.State = ContractSettled
	invoice.Terms.PaymentPreimage = update.Preimage

	// TODO(positiveblue): this logic can be further simplified.
	var amtPaid lnwire.MilliSatoshi
	for key, htlc := range invoice.Htlcs {
		settled, _, err := getUpdatedHtlcState(
			htlc, ContractSettled, nil,
		)
		if err != nil {
			return err
		}

		if settled {
			err = resolveHtlc(
				key, htlc, HtlcStateSettled, updateTime,
				updater,
			)
			if err != nil {
				return err
			}

			amtPaid += htlc.Amt
		}
	}

	return updateInvoiceAmtPaid(invoice, amtPaid, updater)
}

// cancelInvoice attempts to cancel the given invoice. That includes changing
// the invoice state and the state of any relevant HTLC.
func cancelInvoice(invoice *Invoice, hash *lntypes.Hash,
	updateTime time.Time, update *InvoiceStateUpdateDesc,
	updater InvoiceUpdater) error {

	switch {
	case update == nil:
		fallthrough

	case update.NewState != ContractCanceled:
		return fmt.Errorf("unable to cancel invoice: "+
			"InvoiceUpdateDesc.State not valid: %v", update)
	}

	var (
		setID        *[32]byte
		invoiceIsAMP bool
	)

	invoiceIsAMP = invoice.IsAMP()
	if invoiceIsAMP {
		setID = update.SetID
	}

	newState, err := getUpdatedInvoiceState(invoice, hash, *update)
	if err != nil {
		return err
	}

	if newState == nil || *newState != ContractCanceled {
		return fmt.Errorf("unable to cancel invoice(%v): new "+
			"computed state is not canceled: %s", invoice.AddIndex,
			newState)
	}

	err = updater.UpdateInvoiceState(ContractCanceled, nil)
	if err != nil {
		return err
	}
	invoice.State = ContractCanceled

	for key, htlc := range invoice.Htlcs {
		canceled, _, err := getUpdatedHtlcState(
			htlc, ContractCanceled, setID,
		)
		if err != nil {
			return err
		}

		if canceled {
			err = resolveHtlc(
				key, htlc, HtlcStateCanceled, updateTime,
				updater,
			)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// getUpdatedInvoiceState validates and processes an invoice state update. The
// new state to transition to is returned, so the caller is able to select
// exactly how the invoice state is updated. Note that for AMP invoices this
// function is only used to validate the state transition if we're cancelling
// the invoice.
func getUpdatedInvoiceState(invoice *Invoice, hash *lntypes.Hash,
	update InvoiceStateUpdateDesc) (*ContractState, error) {

	// Returning to open is never allowed from any state.
	if update.NewState == ContractOpen {
		return nil, ErrInvoiceCannotOpen
	}

	switch invoice.State {
	// Once a contract is accepted, we can only transition to settled or
	// canceled. Forbid transitioning back into this state. Otherwise this
	// state is identical to ContractOpen, so we fallthrough to apply the
	// same checks that we apply to open invoices.
	case ContractAccepted:
		if update.NewState == ContractAccepted {
			return nil, ErrInvoiceCannotAccept
		}

		fallthrough

	// If a contract is open, permit a state transition to accepted, settled
	// or canceled. The only restriction is on transitioning to settled
	// where we ensure the preimage is valid.
	case ContractOpen:
		if update.NewState == ContractCanceled {
			return &update.NewState, nil
		}

		// Sanity check that the user isn't trying to settle or accept a
		// non-existent HTLC set.
		set := invoice.HTLCSet(update.SetID, HtlcStateAccepted)
		if len(set) == 0 {
			return nil, ErrEmptyHTLCSet
		}

		// For AMP invoices, there are no invoice-level preimage checks.
		// However, we still sanity check that we aren't trying to
		// settle an AMP invoice with a preimage.
		if update.SetID != nil {
			if update.Preimage != nil {
				return nil, errors.New("AMP set cannot have " +
					"preimage")
			}

			return &update.NewState, nil
		}

		switch {
		// If an invoice-level preimage was supplied, but the InvoiceRef
		// doesn't specify a hash (e.g. AMP invoices) we fail.
		case update.Preimage != nil && hash == nil:
			return nil, ErrUnexpectedInvoicePreimage

		// Validate the supplied preimage for non-AMP invoices.
		case update.Preimage != nil:
			if update.Preimage.Hash() != *hash {
				return nil, ErrInvoicePreimageMismatch
			}

		// Permit non-AMP invoices to be accepted without knowing the
		// preimage. When trying to settle we'll have to pass through
		// the above check in order to not hit the one below.
		case update.NewState == ContractAccepted:

		// Fail if we still don't have a preimage when transitioning to
		// settle the non-AMP invoice.
		case update.NewState == ContractSettled &&
			invoice.Terms.PaymentPreimage == nil:

			return nil, errors.New("unknown preimage")
		}

		return &update.NewState, nil

	// Once settled, we are in a terminal state.
	case ContractSettled:
		return nil, ErrInvoiceAlreadySettled

	// Once canceled, we are in a terminal state.
	case ContractCanceled:
		return nil, ErrInvoiceAlreadyCanceled

	default:
		return nil, errors.New("unknown state transition")
	}
}

// getUpdatedInvoiceAmpState returns the AMP state of an invoice (without
// applying it), given the new state, and the amount of the HTLC that is
// being updated.
func getUpdatedInvoiceAmpState(invoice *Invoice, setID SetID,
	circuitKey models.CircuitKey, state HtlcState,
	amt lnwire.MilliSatoshi) (InvoiceStateAMP, error) {

	// Retrieve the AMP state for this set ID.
	ampState, ok := invoice.AMPState[setID]

	// If the state is accepted then we may need to create a new entry for
	// this set ID, otherwise we expect that the entry already exists and
	// we can update it.
	if !ok && state != HtlcStateAccepted {
		return InvoiceStateAMP{},
			fmt.Errorf("unable to update AMP state for setID=%x ",
				setID)
	}

	switch state {
	case HtlcStateAccepted:
		if !ok {
			// If an entry for this set ID doesn't already exist,
			// then we'll need to create it.
			ampState = InvoiceStateAMP{
				State: HtlcStateAccepted,
				InvoiceKeys: make(
					map[models.CircuitKey]struct{},
				),
			}
		}

		ampState.AmtPaid += amt

	case HtlcStateCanceled:
		ampState.State = HtlcStateCanceled
		ampState.AmtPaid -= amt

	case HtlcStateSettled:
		ampState.State = HtlcStateSettled
	}

	ampState.InvoiceKeys[circuitKey] = struct{}{}

	return ampState, nil
}

// canCancelSingleHtlc validates cancellation of a single HTLC. If nil is
// returned, then the HTLC can be cancelled.
func canCancelSingleHtlc(htlc *InvoiceHTLC,
	invoiceState ContractState) error {

	// It is only possible to cancel individual htlcs on an open invoice.
	if invoiceState != ContractOpen {
		return fmt.Errorf("htlc canceled on invoice in state %v",
			invoiceState)
	}

	// It is only possible if the htlc is still pending.
	if htlc.State != HtlcStateAccepted {
		return fmt.Errorf("htlc canceled in state %v", htlc.State)
	}

	return nil
}

// getUpdatedHtlcState aligns the state of an htlc with the given invoice state.
// A boolean indicating whether the HTLCs state need to be updated, along with
// the new state (or old state if no change is needed) is returned.
func getUpdatedHtlcState(htlc *InvoiceHTLC,
	invoiceState ContractState, setID *[32]byte) (
	bool, HtlcState, error) {

	trySettle := func(persist bool) (bool, HtlcState, error) {
		if htlc.State != HtlcStateAccepted {
			return false, htlc.State, nil
		}

		// Settle the HTLC if it matches the settled set id. If
		// there're other HTLCs with distinct setIDs, then we'll leave
		// them, as they may eventually be settled as we permit
		// multiple settles to a single pay_addr for AMP.
		settled := false
		if htlc.IsInHTLCSet(setID) {
			// Non-AMP HTLCs can be settled immediately since we
			// already know the preimage is valid due to checks at
			// the invoice level. For AMP HTLCs, verify that the
			// per-HTLC preimage-hash pair is valid.
			switch {
			// Non-AMP HTLCs can be settle immediately since we
			// already know the preimage is valid due to checks at
			// the invoice level.
			case setID == nil:

			// At this point, the setID is non-nil, meaning this is
			// an AMP HTLC. We know that htlc.AMP cannot be nil,
			// otherwise IsInHTLCSet would have returned false.
			//
			// Fail if an accepted AMP HTLC has no preimage.
			case htlc.AMP.Preimage == nil:
				return false, htlc.State,
					ErrHTLCPreimageMissing

			// Fail if the accepted AMP HTLC has an invalid
			// preimage.
			case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash):
				return false, htlc.State,
					ErrHTLCPreimageMismatch
			}

			settled = true
		}

		// Only persist the changes if the invoice is moving to the
		// settled state, and we're actually updating the state to
		// settled.
		newState := htlc.State
		if settled {
			newState = HtlcStateSettled
		}

		return persist && settled, newState, nil
	}

	if invoiceState == ContractSettled {
		// Check that we can settle the HTLCs. For legacy and MPP HTLCs
		// this will be a NOP, but for AMP HTLCs this asserts that we
		// have a valid hash/preimage pair. Passing true permits the
		// method to update the HTLC to HtlcStateSettled.
		return trySettle(true)
	}

	// We should never find a settled HTLC on an invoice that isn't in
	// ContractSettled.
	if htlc.State == HtlcStateSettled {
		return false, htlc.State, ErrHTLCAlreadySettled
	}

	switch invoiceState {
	case ContractCanceled:
		htlcAlreadyCanceled := htlc.State == HtlcStateCanceled
		return !htlcAlreadyCanceled, HtlcStateCanceled, nil

	// TODO(roasbeef): never fully passed thru now?
	case ContractAccepted:
		// Check that we can settle the HTLCs. For legacy and MPP HTLCs
		// this will be a NOP, but for AMP HTLCs this asserts that we
		// have a valid hash/preimage pair. Passing false prevents the
		// method from putting the HTLC in HtlcStateSettled, leaving it
		// in HtlcStateAccepted.
		return trySettle(false)

	case ContractOpen:
		return false, htlc.State, nil

	default:
		return false, htlc.State, errors.New("unknown state transition")
	}
}
