package leveldb

import (
	"errors"
	"fmt"
	"strings"

	"github.com/ethereum/go-ethereum/common"
)

var (
	errMissingCurrentSchema = errors.New("could not find current db schema")
	errMissingTargetSchema  = errors.New("could not find target db schema")
)

const (
	dbSchemaKey = "statestore_schema"

	dbSchemaGrace         = "grace"
	dbSchemaDrain         = "drain"
	dbSchemaCleanInterval = "clean-interval"
	dbSchemaNoStamp       = "no-stamp"
	dbSchemaFlushBlock    = "flushblock"
	dbSchemaSwapAddr      = "swapaddr"
)

var (
	dbSchemaCurrent = dbSchemaFlushBlock
)

type migration struct {
	name string               // name of the schema
	fn   func(s *store) error // the migration function that needs to be performed in order to get to the current schema name
}

// schemaMigrations contains an ordered list of the database schemes, that is
// in order to run data migrations in the correct sequence
var schemaMigrations = []migration{
	{name: dbSchemaGrace, fn: func(s *store) error { return nil }},
	{name: dbSchemaDrain, fn: migrateGrace},
	{name: dbSchemaCleanInterval, fn: migrateGrace},
	{name: dbSchemaNoStamp, fn: migrateStamp},
	{name: dbSchemaFlushBlock, fn: migrateFB},
	{name: dbSchemaSwapAddr, fn: migrateSwap},
}

func migrateFB(s *store) error {
	collectedKeys, err := collectKeys(s, "blocklist-")
	if err != nil {
		return err
	}
	return deleteKeys(s, collectedKeys)
}

func migrateStamp(s *store) error {
	for _, pfx := range []string{"postage", "batchstore", "addressbook_entry_"} {
		collectedKeys, err := collectKeys(s, pfx)
		if err != nil {
			return err
		}
		if err := deleteKeys(s, collectedKeys); err != nil {
			return err
		}
	}

	return nil
}

func migrateGrace(s *store) error {
	var collectedKeys []string
	mgfn := func(k, v []byte) (bool, error) {
		stk := string(k)
		if strings.Contains(stk, "|") &&
			len(k) > 32 &&
			!strings.Contains(stk, "swap") &&
			!strings.Contains(stk, "peer") {
			log.Debugf("found key designated to deletion %s", k)
			collectedKeys = append(collectedKeys, stk)
		}

		return false, nil
	}

	_ = s.Iterate("", mgfn)

	for _, v := range collectedKeys {
		err := s.Delete(v)
		if err != nil {
			log.Debugf("error deleting key %s", v)
			continue
		}
		log.Debugf("deleted key %s", v)
	}
	log.Debugf("deleted keys: %d", len(collectedKeys))

	return nil
}

func migrateSwap(s *store) error {
	migratePrefix := func(prefix string) error {
		keys, err := collectKeys(s, prefix)
		if err != nil {
			return err
		}

		for _, key := range keys {
			split := strings.SplitAfter(key, prefix)
			if len(split) != 2 {
				return errors.New("no peer in key")
			}

			addr := common.BytesToAddress([]byte(split[1]))
			fixed := fmt.Sprintf("%s%x", prefix, addr)

			var chequebookAddress common.Address
			if err = s.Get(key, &chequebookAddress); err != nil {
				return err
			}

			if err = s.Put(fixed, chequebookAddress); err != nil {
				return err
			}

			if err = s.Delete(key); err != nil {
				return err
			}
		}
		return nil
	}

	if err := migratePrefix("swap_peer_chequebook_"); err != nil {
		return err
	}

	return migratePrefix("swap_beneficiary_peer_")
}

func (s *store) migrate(schemaName string) error {
	migrations, err := getMigrations(schemaName, dbSchemaCurrent, schemaMigrations, s)
	if err != nil {
		return fmt.Errorf("error getting migrations for current schema (%s): %w", schemaName, err)
	}

	// no migrations to run
	if migrations == nil {
		return nil
	}

	log.Debugf("statestore: need to run %d data migrations to schema %s", len(migrations), schemaName)
	for i := 0; i < len(migrations); i++ {
		err := migrations[i].fn(s)
		if err != nil {
			return err
		}
		err = s.putSchemaName(migrations[i].name) // put the name of the current schema
		if err != nil {
			return err
		}
		schemaName, err = s.getSchemaName()
		if err != nil {
			return err
		}
		log.Debugf("statestore: successfully ran migration: id %d current schema: %s", i, schemaName)
	}
	return nil
}

// getMigrations returns an ordered list of migrations that need be executed
// with no errors in order to bring the statestore to the most up-to-date
// schema definition
func getMigrations(currentSchema, targetSchema string, allSchemeMigrations []migration, store *store) (migrations []migration, err error) {
	foundCurrent := false
	foundTarget := false
	if currentSchema == dbSchemaCurrent {
		return nil, nil
	}
	for i, v := range allSchemeMigrations {
		switch v.name {
		case currentSchema:
			if foundCurrent {
				return nil, errors.New("found schema name for the second time when looking for migrations")
			}
			foundCurrent = true
			log.Debugf("statestore migration: found current schema %s, migrate to %s, total migrations %d", currentSchema, dbSchemaCurrent, len(allSchemeMigrations)-i)
			continue // current schema migration should not be executed (already has been when schema was migrated to)
		case targetSchema:
			foundTarget = true
		}
		if foundCurrent {
			migrations = append(migrations, v)
		}
	}
	if !foundCurrent {
		return nil, errMissingCurrentSchema
	}
	if !foundTarget {
		return nil, errMissingTargetSchema
	}
	return migrations, nil
}

func collectKeys(s *store, prefix string) (keys []string, err error) {
	if err := s.Iterate(prefix, func(k, v []byte) (bool, error) {
		stk := string(k)
		if strings.HasPrefix(stk, prefix) {
			keys = append(keys, stk)
		}
		return false, nil
	}); err != nil {
		return nil, err
	}
	return keys, nil
}

func deleteKeys(s *store, keys []string) error {
	for _, v := range keys {
		err := s.Delete(v)
		if err != nil {
			return fmt.Errorf("error deleting key %s: %w", v, err)
		}
		log.Debugf("deleted key %s", v)
	}
	log.Debugf("deleted keys: %d", len(keys))
	return nil
}
