package loopdb

import (
	"errors"
	"fmt"

	"github.com/btcsuite/btcd/chaincfg"
	"go.etcd.io/bbolt"
)

var (
	// metaBucket stores all the meta information concerning the state of
	// the database.
	metaBucketKey = []byte("metadata")

	// dbVersionKey is a boltdb key and it's used for storing/retrieving
	// current database version.
	dbVersionKey = []byte("dbp")

	// ErrDBReversion is returned when detecting an attempt to revert to a
	// prior database version.
	ErrDBReversion = fmt.Errorf("channel db cannot revert to prior version")
)

// migration is a function which takes a prior outdated version of the database
// instances and mutates the key/bucket structure to arrive at a more
// up-to-date version of the database.
type migration func(tx *bbolt.Tx, chainParams *chaincfg.Params) error

var (
	// dbVersions is storing all versions of database. If current version
	// of database don't match with latest version this list will be used
	// for retrieving all migration function that are need to apply to the
	// current db.
	migrations = []migration{
		migrateCosts,
		migrateSwapPublicationDeadline,
		migrateLastHop,
		migrateUpdates,
	}

	latestDBVersion = uint32(len(migrations))
)

// getDBVersion retrieves the current db version.
func getDBVersion(db *bbolt.DB) (uint32, error) {
	var version uint32

	err := db.View(func(tx *bbolt.Tx) error {
		metaBucket := tx.Bucket(metaBucketKey)
		if metaBucket == nil {
			return errors.New("bucket does not exist")
		}

		data := metaBucket.Get(dbVersionKey)
		// If no version key found, assume version is 0.
		if data != nil {
			version = byteOrder.Uint32(data)
		}

		return nil
	})
	if err != nil {
		return 0, err
	}

	return version, nil
}

// setDBVersion updates the current db version.
func setDBVersion(tx *bbolt.Tx, version uint32) error {
	metaBucket, err := tx.CreateBucketIfNotExists(metaBucketKey)
	if err != nil {
		return fmt.Errorf("set db version: %v", err)
	}

	scratch := make([]byte, 4)
	byteOrder.PutUint32(scratch, version)
	return metaBucket.Put(dbVersionKey, scratch)
}

// syncVersions function is used for safe db version synchronization. It
// applies migration functions to the current database and recovers the
// previous state of db if at least one error/panic appeared during migration.
func syncVersions(db *bbolt.DB, chainParams *chaincfg.Params) error {
	currentVersion, err := getDBVersion(db)
	if err != nil {
		return err
	}

	log.Infof("Checking for schema update: latest_version=%v, "+
		"db_version=%v", latestDBVersion, currentVersion)

	switch {
	// If the database reports a higher version that we are aware of, the
	// user is probably trying to revert to a prior version of lnd. We fail
	// here to prevent reversions and unintended corruption.
	case currentVersion > latestDBVersion:
		log.Errorf("Refusing to revert from db_version=%d to "+
			"lower version=%d", currentVersion,
			latestDBVersion)

		return ErrDBReversion

	// If the current database version matches the latest version number,
	// then we don't need to perform any migrations.
	case currentVersion == latestDBVersion:
		return nil
	}

	log.Infof("Performing database schema migration")

	// Otherwise we execute the migrations serially within a single
	// database transaction to ensure the migration is atomic.
	return db.Update(func(tx *bbolt.Tx) error {
		for v := currentVersion; v < latestDBVersion; v++ {
			log.Infof("Applying migration #%v", v+1)

			migration := migrations[v]
			if err := migration(tx, chainParams); err != nil {
				log.Infof("Unable to apply migration #%v",
					v+1)
				return err
			}
		}

		return setDBVersion(tx, latestDBVersion)
	})
}
