package channeldb

import (
	"bytes"
	"errors"
	"fmt"

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

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

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

	// dbVersionKey is a boltdb key and it's used for storing/retrieving
	// a list of optional migrations that have been applied.
	optionalVersionKey = []byte("ovk")

	// TombstoneKey is the key under which we add a tag in the source DB
	// after we've successfully and completely migrated it to the target/
	// destination DB.
	TombstoneKey = []byte("data-migration-tombstone")

	// ErrMarkerNotPresent is the error that is returned if the queried
	// marker is not present in the given database.
	ErrMarkerNotPresent = errors.New("marker not present")
)

// Meta structure holds the database meta information.
type Meta struct {
	// DbVersionNumber is the current schema version of the database.
	DbVersionNumber uint32
}

// FetchMeta fetches the metadata from boltdb and returns filled meta structure.
func (d *DB) FetchMeta() (*Meta, error) {
	var meta *Meta

	err := kvdb.View(d, func(tx kvdb.RTx) error {
		return FetchMeta(meta, tx)
	}, func() {
		meta = &Meta{}
	})
	if err != nil {
		return nil, err
	}

	return meta, nil
}

// FetchMeta is a helper function used in order to allow callers to re-use a
// database transaction.
func FetchMeta(meta *Meta, tx kvdb.RTx) error {
	metaBucket := tx.ReadBucket(metaBucket)
	if metaBucket == nil {
		return ErrMetaNotFound
	}

	data := metaBucket.Get(dbVersionKey)
	if data == nil {
		meta.DbVersionNumber = getLatestDBVersion(dbVersions)
	} else {
		meta.DbVersionNumber = byteOrder.Uint32(data)
	}

	return nil
}

// PutMeta writes the passed instance of the database met-data struct to disk.
func (d *DB) PutMeta(meta *Meta) error {
	return kvdb.Update(d, func(tx kvdb.RwTx) error {
		return putMeta(meta, tx)
	}, func() {})
}

// putMeta is an internal helper function used in order to allow callers to
// re-use a database transaction. See the publicly exported PutMeta method for
// more information.
func putMeta(meta *Meta, tx kvdb.RwTx) error {
	metaBucket, err := tx.CreateTopLevelBucket(metaBucket)
	if err != nil {
		return err
	}

	return putDbVersion(metaBucket, meta)
}

func putDbVersion(metaBucket kvdb.RwBucket, meta *Meta) error {
	scratch := make([]byte, 4)
	byteOrder.PutUint32(scratch, meta.DbVersionNumber)
	return metaBucket.Put(dbVersionKey, scratch)
}

// OptionalMeta structure holds the database optional migration information.
type OptionalMeta struct {
	// Versions is a set that contains the versions that have been applied.
	// When saved to disk, only the indexes are stored.
	Versions map[uint64]string
}

func (om *OptionalMeta) String() string {
	s := ""
	for index, name := range om.Versions {
		s += fmt.Sprintf("%d: %s", index, name)
	}
	if s == "" {
		s = "empty"
	}
	return s
}

// fetchOptionalMeta reads the optional meta from the database.
func (d *DB) fetchOptionalMeta() (*OptionalMeta, error) {
	om := &OptionalMeta{
		Versions: make(map[uint64]string),
	}

	err := kvdb.View(d, func(tx kvdb.RTx) error {
		metaBucket := tx.ReadBucket(metaBucket)
		if metaBucket == nil {
			return ErrMetaNotFound
		}

		vBytes := metaBucket.Get(optionalVersionKey)
		// Exit early if nothing found.
		if vBytes == nil {
			return nil
		}

		// Read the versions' length.
		r := bytes.NewReader(vBytes)
		vLen, err := tlv.ReadVarInt(r, &[8]byte{})
		if err != nil {
			return err
		}

		// Write the version index.
		for i := uint64(0); i < vLen; i++ {
			version, err := tlv.ReadVarInt(r, &[8]byte{})
			if err != nil {
				return err
			}
			om.Versions[version] = optionalVersions[i].name
		}

		return nil
	}, func() {})
	if err != nil {
		return nil, err
	}

	return om, nil
}

// putOptionalMeta writes an optional meta to the database.
func (d *DB) putOptionalMeta(om *OptionalMeta) error {
	return kvdb.Update(d, func(tx kvdb.RwTx) error {
		metaBucket, err := tx.CreateTopLevelBucket(metaBucket)
		if err != nil {
			return err
		}

		var b bytes.Buffer

		// Write the total length.
		err = tlv.WriteVarInt(&b, uint64(len(om.Versions)), &[8]byte{})
		if err != nil {
			return err
		}

		// Write the version indexes.
		for v := range om.Versions {
			err := tlv.WriteVarInt(&b, v, &[8]byte{})
			if err != nil {
				return err
			}
		}

		return metaBucket.Put(optionalVersionKey, b.Bytes())
	}, func() {})
}

// CheckMarkerPresent returns the marker under the requested key or
// ErrMarkerNotFound if either the root bucket or the marker key within that
// bucket does not exist.
func CheckMarkerPresent(tx kvdb.RTx, markerKey []byte) ([]byte, error) {
	markerBucket := tx.ReadBucket(markerKey)
	if markerBucket == nil {
		return nil, ErrMarkerNotPresent
	}

	val := markerBucket.Get(markerKey)

	// If we wrote the marker correctly, we created a bucket _and_ created a
	// key with a non-empty value. It doesn't matter to us whether the key
	// exists or whether its value is empty, to us, it just means the marker
	// isn't there.
	if len(val) == 0 {
		return nil, ErrMarkerNotPresent
	}

	return val, nil
}

// EnsureNoTombstone returns an error if there is a tombstone marker in the DB
// of the given transaction.
func EnsureNoTombstone(tx kvdb.RTx) error {
	marker, err := CheckMarkerPresent(tx, TombstoneKey)
	if err == ErrMarkerNotPresent {
		// No marker present, so no tombstone. The DB is still alive.
		return nil
	}
	if err != nil {
		return err
	}

	// There was no error so there is a tombstone marker/tag. We cannot use
	// this DB anymore.
	return fmt.Errorf("refusing to use db, it was marked with a tombstone "+
		"after successful data migration; tombstone reads: %s",
		string(marker))
}

// AddMarker adds the marker with the given key into a top level bucket with the
// same name. So the structure will look like:
//
//	marker-key (top level bucket)
//	    |->   marker-key:marker-value (key/value pair)
func AddMarker(tx kvdb.RwTx, markerKey, markerValue []byte) error {
	if len(markerValue) == 0 {
		return fmt.Errorf("marker value cannot be empty")
	}

	markerBucket, err := tx.CreateTopLevelBucket(markerKey)
	if err != nil {
		return err
	}

	return markerBucket.Put(markerKey, markerValue)
}
