package channeldb

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"time"

	"github.com/btcsuite/btcd/btcec"
	"github.com/lightningnetwork/lnd/kvdb"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
)

var (
	// duplicatePaymentsBucket is the name of a optional sub-bucket within
	// the payment hash bucket, that is used to hold duplicate payments to a
	// payment hash. This is needed to support information from earlier
	// versions of lnd, where it was possible to pay to a payment hash more
	// than once.
	duplicatePaymentsBucket = []byte("payment-duplicate-bucket")

	// duplicatePaymentSettleInfoKey is a key used in the payment's
	// sub-bucket to store the settle info of the payment.
	duplicatePaymentSettleInfoKey = []byte("payment-settle-info")

	// duplicatePaymentAttemptInfoKey is a key used in the payment's
	// sub-bucket to store the info about the latest attempt that was done
	// for the payment in question.
	duplicatePaymentAttemptInfoKey = []byte("payment-attempt-info")

	// duplicatePaymentCreationInfoKey is a key used in the payment's
	// sub-bucket to store the creation info of the payment.
	duplicatePaymentCreationInfoKey = []byte("payment-creation-info")

	// duplicatePaymentFailInfoKey is a key used in the payment's sub-bucket
	// to store information about the reason a payment failed.
	duplicatePaymentFailInfoKey = []byte("payment-fail-info")

	// duplicatePaymentSequenceKey is a key used in the payment's sub-bucket
	// to store the sequence number of the payment.
	duplicatePaymentSequenceKey = []byte("payment-sequence-key")
)

// duplicateHTLCAttemptInfo contains static information about a specific HTLC
// attempt for a payment. This information is used by the router to handle any
// errors coming back after an attempt is made, and to query the switch about
// the status of the attempt.
type duplicateHTLCAttemptInfo struct {
	// attemptID is the unique ID used for this attempt.
	attemptID uint64

	// sessionKey is the ephemeral key used for this attempt.
	sessionKey [btcec.PrivKeyBytesLen]byte

	// route is the route attempted to send the HTLC.
	route route.Route
}

// fetchDuplicatePaymentStatus fetches the payment status of the payment. If the
// payment isn't found, it will default to "StatusUnknown".
func fetchDuplicatePaymentStatus(bucket kvdb.RBucket) PaymentStatus {
	if bucket.Get(duplicatePaymentSettleInfoKey) != nil {
		return StatusSucceeded
	}

	if bucket.Get(duplicatePaymentFailInfoKey) != nil {
		return StatusFailed
	}

	if bucket.Get(duplicatePaymentCreationInfoKey) != nil {
		return StatusInFlight
	}

	return StatusUnknown
}

func deserializeDuplicateHTLCAttemptInfo(r io.Reader) (
	*duplicateHTLCAttemptInfo, error) {

	a := &duplicateHTLCAttemptInfo{}
	err := ReadElements(r, &a.attemptID, &a.sessionKey)
	if err != nil {
		return nil, err
	}
	a.route, err = DeserializeRoute(r)
	if err != nil {
		return nil, err
	}
	return a, nil
}

func deserializeDuplicatePaymentCreationInfo(r io.Reader) (
	*PaymentCreationInfo, error) {

	var scratch [8]byte

	c := &PaymentCreationInfo{}

	if _, err := io.ReadFull(r, c.PaymentIdentifier[:]); err != nil {
		return nil, err
	}

	if _, err := io.ReadFull(r, scratch[:]); err != nil {
		return nil, err
	}
	c.Value = lnwire.MilliSatoshi(byteOrder.Uint64(scratch[:]))

	if _, err := io.ReadFull(r, scratch[:]); err != nil {
		return nil, err
	}
	c.CreationTime = time.Unix(int64(byteOrder.Uint64(scratch[:])), 0)

	if _, err := io.ReadFull(r, scratch[:4]); err != nil {
		return nil, err
	}

	reqLen := byteOrder.Uint32(scratch[:4])
	payReq := make([]byte, reqLen)
	if reqLen > 0 {
		if _, err := io.ReadFull(r, payReq); err != nil {
			return nil, err
		}
	}
	c.PaymentRequest = payReq

	return c, nil
}

func fetchDuplicatePayment(bucket kvdb.RBucket) (*MPPayment, error) {
	seqBytes := bucket.Get(duplicatePaymentSequenceKey)
	if seqBytes == nil {
		return nil, fmt.Errorf("sequence number not found")
	}

	sequenceNum := binary.BigEndian.Uint64(seqBytes)

	// Get the payment status.
	paymentStatus := fetchDuplicatePaymentStatus(bucket)

	// Get the PaymentCreationInfo.
	b := bucket.Get(duplicatePaymentCreationInfoKey)
	if b == nil {
		return nil, fmt.Errorf("creation info not found")
	}

	r := bytes.NewReader(b)
	creationInfo, err := deserializeDuplicatePaymentCreationInfo(r)
	if err != nil {
		return nil, err

	}

	// Get failure reason if available.
	var failureReason *FailureReason
	b = bucket.Get(duplicatePaymentFailInfoKey)
	if b != nil {
		reason := FailureReason(b[0])
		failureReason = &reason
	}

	payment := &MPPayment{
		SequenceNum:   sequenceNum,
		Info:          creationInfo,
		FailureReason: failureReason,
		Status:        paymentStatus,
	}

	// Get the HTLCAttemptInfo. It can be absent.
	b = bucket.Get(duplicatePaymentAttemptInfoKey)
	if b != nil {
		r = bytes.NewReader(b)
		attempt, err := deserializeDuplicateHTLCAttemptInfo(r)
		if err != nil {
			return nil, err
		}

		htlc := HTLCAttempt{
			HTLCAttemptInfo: HTLCAttemptInfo{
				AttemptID:  attempt.attemptID,
				Route:      attempt.route,
				sessionKey: attempt.sessionKey,
			},
		}

		// Get the payment preimage. This is only found for
		// successful payments.
		b = bucket.Get(duplicatePaymentSettleInfoKey)
		if b != nil {
			var preimg lntypes.Preimage
			copy(preimg[:], b)

			htlc.Settle = &HTLCSettleInfo{
				Preimage:   preimg,
				SettleTime: time.Time{},
			}
		} else {
			// Otherwise the payment must have failed.
			htlc.Failure = &HTLCFailInfo{
				FailTime: time.Time{},
			}
		}

		payment.HTLCs = []HTLCAttempt{htlc}
	}

	return payment, nil
}

func fetchDuplicatePayments(paymentHashBucket kvdb.RBucket) ([]*MPPayment,
	error) {

	var payments []*MPPayment

	// For older versions of lnd, duplicate payments to a payment has was
	// possible. These will be found in a sub-bucket indexed by their
	// sequence number if available.
	dup := paymentHashBucket.NestedReadBucket(duplicatePaymentsBucket)
	if dup == nil {
		return nil, nil
	}

	err := dup.ForEach(func(k, v []byte) error {
		subBucket := dup.NestedReadBucket(k)
		if subBucket == nil {
			// We one bucket for each duplicate to be found.
			return fmt.Errorf("non bucket element" +
				"in duplicate bucket")
		}

		p, err := fetchDuplicatePayment(subBucket)
		if err != nil {
			return err
		}

		payments = append(payments, p)
		return nil
	})
	if err != nil {
		return nil, err
	}

	return payments, nil
}