package migration13

import (
	"encoding/binary"
	"fmt"

	"github.com/lightningnetwork/lnd/channeldb/kvdb"
)

var (
	paymentsRootBucket = []byte("payments-root-bucket")

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

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

	// paymentAttemptInfoKey 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.
	paymentAttemptInfoKey = []byte("payment-attempt-info")

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

	// paymentHtlcsBucket is a bucket where we'll store the information
	// about the HTLCs that were attempted for a payment.
	paymentHtlcsBucket = []byte("payment-htlcs-bucket")

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

	// htlcSettleInfoKey is a key used in a HTLC's sub-bucket to store the
	// settle info, if any.
	htlcSettleInfoKey = []byte("htlc-settle-info")

	// htlcFailInfoKey is a key used in a HTLC's sub-bucket to store
	// failure information, if any.
	htlcFailInfoKey = []byte("htlc-fail-info")

	byteOrder = binary.BigEndian
)

// MigrateMPP migrates the payments to a new structure that accommodates for mpp
// payments.
func MigrateMPP(tx kvdb.RwTx) error {
	log.Infof("Migrating payments to mpp structure")

	// Iterate over all payments and store their indexing keys. This is
	// needed, because no modifications are allowed inside a Bucket.ForEach
	// loop.
	paymentsBucket := tx.ReadWriteBucket(paymentsRootBucket)
	if paymentsBucket == nil {
		return nil
	}

	var paymentKeys [][]byte
	err := paymentsBucket.ForEach(func(k, v []byte) error {
		paymentKeys = append(paymentKeys, k)
		return nil
	})
	if err != nil {
		return err
	}

	// With all keys retrieved, start the migration.
	for _, k := range paymentKeys {
		bucket := paymentsBucket.NestedReadWriteBucket(k)

		// We only expect sub-buckets to be found in
		// this top-level bucket.
		if bucket == nil {
			return fmt.Errorf("non bucket element in " +
				"payments bucket")
		}

		// Fetch old format creation info.
		creationInfo := bucket.Get(paymentCreationInfoKey)
		if creationInfo == nil {
			return fmt.Errorf("creation info not found")
		}

		// Make a copy because bbolt doesn't allow this value to be
		// changed in-place.
		newCreationInfo := make([]byte, len(creationInfo))
		copy(newCreationInfo, creationInfo)

		// Convert to nano seconds.
		timeBytes := newCreationInfo[32+8 : 32+8+8]
		time := byteOrder.Uint64(timeBytes)
		timeNs := time * 1000000000
		byteOrder.PutUint64(timeBytes, timeNs)

		// Write back new format creation info.
		err := bucket.Put(paymentCreationInfoKey, newCreationInfo)
		if err != nil {
			return err
		}

		// No migration needed if there is no attempt stored.
		attemptInfo := bucket.Get(paymentAttemptInfoKey)
		if attemptInfo == nil {
			continue
		}

		// Delete attempt info on the payment level.
		if err := bucket.Delete(paymentAttemptInfoKey); err != nil {
			return err
		}

		// Save attempt id for later use.
		attemptID := attemptInfo[:8]

		// Discard attempt id. It will become a bucket key in the new
		// structure.
		attemptInfo = attemptInfo[8:]

		// Append unknown (zero) attempt time.
		var zero [8]byte
		attemptInfo = append(attemptInfo, zero[:]...)

		// Create bucket that contains all htlcs.
		htlcsBucket, err := bucket.CreateBucket(paymentHtlcsBucket)
		if err != nil {
			return err
		}

		// Create an htlc for this attempt.
		htlcBucket, err := htlcsBucket.CreateBucket(attemptID)
		if err != nil {
			return err
		}

		// Save migrated attempt info.
		err = htlcBucket.Put(htlcAttemptInfoKey, attemptInfo)
		if err != nil {
			return err
		}

		// Migrate settle info.
		settleInfo := bucket.Get(paymentSettleInfoKey)
		if settleInfo != nil {
			// Payment-level settle info can be deleted.
			err := bucket.Delete(paymentSettleInfoKey)
			if err != nil {
				return err
			}

			// Append unknown (zero) settle time.
			settleInfo = append(settleInfo, zero[:]...)

			// Save settle info.
			err = htlcBucket.Put(htlcSettleInfoKey, settleInfo)
			if err != nil {
				return err
			}

			// Migration for settled htlc completed.
			continue
		}

		// If there is no payment-level failure reason, the payment is
		// still in flight and nothing else needs to be migrated.
		// Otherwise the payment-level failure reason can remain
		// unchanged.
		inFlight := bucket.Get(paymentFailInfoKey) == nil
		if inFlight {
			continue
		}

		// The htlc failed. Add htlc fail info with reason unknown. We
		// don't have access to the original failure reason anymore.
		failInfo := []byte{
			// Fail time unknown.
			0, 0, 0, 0, 0, 0, 0, 0,

			// Zero length wire message.
			0,

			// Failure reason unknown.
			0,

			// Failure source index zero.
			0, 0, 0, 0,
		}

		// Save fail info.
		err = htlcBucket.Put(htlcFailInfoKey, failInfo)
		if err != nil {
			return err
		}
	}

	log.Infof("Migration of payments to mpp structure complete!")

	return nil
}