// Copyright (c) 2015-2020 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package wallet

import (
	"fmt"

	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcwallet/waddrmgr"
	"github.com/btcsuite/btcwallet/walletdb"
	"github.com/btcsuite/btcwallet/wtxmgr"
)

var (
	// bucketTxLabels is the name of the label sub bucket of the wtxmgr
	// top level bucket that stores the mapping between a txid and a
	// user-defined transaction label.
	bucketTxLabels = []byte("l")
)

// DropTransactionHistory completely removes and re-creates the transaction
// manager namespace from the given wallet database. This can be used to force
// a full chain rescan of all wallet transaction and UTXO data. User-defined
// transaction labels can optionally be kept by setting keepLabels to true.
func DropTransactionHistory(db walletdb.DB, keepLabels bool) error {
	log.Infof("Dropping btcwallet transaction history")

	err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
		// If we want to keep our tx labels, we read them out so we
		// can re-add them after we have deleted our wtxmgr.
		var (
			labels map[chainhash.Hash]string
			err    error
		)
		if keepLabels {
			labels, err = fetchAllLabels(tx)
			if err != nil {
				return err
			}
		}

		err = tx.DeleteTopLevelBucket(wtxmgrNamespaceKey)
		if err != nil && err != walletdb.ErrBucketNotFound {
			return err
		}
		ns, err := tx.CreateTopLevelBucket(wtxmgrNamespaceKey)
		if err != nil {
			return err
		}
		err = wtxmgr.Create(ns)
		if err != nil {
			return err
		}

		// If we want to re-add our labels, we do so now.
		if keepLabels {
			if err := putTxLabels(ns, labels); err != nil {
				return err
			}
		}

		ns = tx.ReadWriteBucket(waddrmgrNamespaceKey)
		birthdayBlock, err := waddrmgr.FetchBirthdayBlock(ns)
		if err != nil {
			log.Warnf("Wallet does not have a birthday block " +
				"set, falling back to rescan from genesis")

			startBlock, err := waddrmgr.FetchStartBlock(ns)
			if err != nil {
				return err
			}
			return waddrmgr.PutSyncedTo(ns, startBlock)
		}

		// We'll need to remove our birthday block first because it
		// serves as a barrier when updating our state to detect reorgs
		// due to the wallet not storing all block hashes of the chain.
		if err := waddrmgr.DeleteBirthdayBlock(ns); err != nil {
			return err
		}

		if err := waddrmgr.PutSyncedTo(ns, &birthdayBlock); err != nil {
			return err
		}
		return waddrmgr.PutBirthdayBlock(ns, birthdayBlock)
	})
	if err != nil {
		return fmt.Errorf("failed to drop and re-create namespace: %w",
			err)
	}

	return nil
}

// fetchAllLabels returns a map of hex-encoded txid to label.
func fetchAllLabels(tx walletdb.ReadWriteTx) (map[chainhash.Hash]string,
	error) {

	// Get our top level bucket, if it does not exist we just exit.
	txBucket := tx.ReadBucket(wtxmgrNamespaceKey)
	if txBucket == nil {
		return nil, nil
	}

	// If we do not have a labels bucket, there are no labels so we exit.
	labelsBucket := txBucket.NestedReadBucket(bucketTxLabels)
	if labelsBucket == nil {
		return nil, nil
	}

	labels := make(map[chainhash.Hash]string)
	if err := labelsBucket.ForEach(func(k, v []byte) error {
		txid, err := chainhash.NewHash(k)
		if err != nil {
			return err
		}

		label, err := wtxmgr.DeserializeLabel(v)
		if err != nil {
			return err
		}

		// Add an entry to our map of labels.
		labels[*txid] = label

		return nil
	}); err != nil {
		return nil, err
	}

	return labels, nil
}

// putTxLabels re-adds a nested labels bucket and entries to the bucket provided
// if there are any labels present.
func putTxLabels(ns walletdb.ReadWriteBucket,
	labels map[chainhash.Hash]string) error {

	// If there are no labels, exit early.
	if len(labels) == 0 {
		return nil
	}

	// First, we create a labels bucket which we will add all labels to.
	labelBucket, err := ns.CreateBucketIfNotExists(bucketTxLabels)
	if err != nil {
		return err
	}

	// Next, we re-add every label to the bucket.
	for txid, label := range labels {
		err := wtxmgr.PutTxLabel(labelBucket, txid, label)
		if err != nil {
			return err
		}
	}

	return nil
}
