package app

import (
	"fmt"

	sdkmath "cosmossdk.io/math"

	evmutilkeeper "github.com/0glabs/0g-chain/x/evmutil/keeper"
	evmutiltypes "github.com/0glabs/0g-chain/x/evmutil/types"
	precisebankkeeper "github.com/0glabs/0g-chain/x/precisebank/keeper"
	precisebanktypes "github.com/0glabs/0g-chain/x/precisebank/types"
	storetypes "github.com/cosmos/cosmos-sdk/store/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/types/module"
	bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
	upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
)

const (
	UpgradeName_Testnet = "v0.4.0"
)

// RegisterUpgradeHandlers registers the upgrade handlers for the app.
func (app App) RegisterUpgradeHandlers() {
	app.upgradeKeeper.SetUpgradeHandler(
		UpgradeName_Testnet,
		upgradeHandler(app, UpgradeName_Testnet),
	)

	upgradeInfo, err := app.upgradeKeeper.ReadUpgradeInfoFromDisk()
	if err != nil {
		panic(err)
	}

	doUpgrade := upgradeInfo.Name == UpgradeName_Testnet

	if doUpgrade && !app.upgradeKeeper.IsSkipHeight(upgradeInfo.Height) {
		storeUpgrades := storetypes.StoreUpgrades{
			Added: []string{
				precisebanktypes.ModuleName,
			},
		}

		// configure store loader that checks if version == upgradeHeight and applies store upgrades
		app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades))
	}
}

// upgradeHandler returns an UpgradeHandler for the given upgrade parameters.
func upgradeHandler(
	app App,
	name string,
) upgradetypes.UpgradeHandler {
	return func(
		ctx sdk.Context,
		plan upgradetypes.Plan,
		fromVM module.VersionMap,
	) (module.VersionMap, error) {
		logger := app.Logger()
		logger.Info(fmt.Sprintf("running %s upgrade handler", name))

		// Run migrations for all modules and return new consensus version map.
		versionMap, err := app.mm.RunMigrations(ctx, app.configurator, fromVM)
		if err != nil {
			return nil, err
		}

		logger.Info("completed store migrations")

		// Migration of fractional balances from x/evmutil to x/precisebank
		if err := MigrateEvmutilToPrecisebank(
			ctx,
			app.accountKeeper,
			app.bankKeeper,
			app.evmutilKeeper,
			app.precisebankKeeper,
		); err != nil {
			return nil, err
		}

		logger.Info("completed x/evmutil to x/precisebank migration")

		return versionMap, nil
	}
}

// MigrateEvmutilToPrecisebank migrates all required state from x/evmutil to
// x/precisebank and ensures the resulting state is correct.
// This migrates the following state:
// - Fractional balances
// - Fractional balance reserve
// Initializes the following state in x/precisebank:
// - Remainder amount
func MigrateEvmutilToPrecisebank(
	ctx sdk.Context,
	accountKeeper evmutiltypes.AccountKeeper,
	bankKeeper bankkeeper.Keeper,
	evmutilKeeper evmutilkeeper.Keeper,
	precisebankKeeper precisebankkeeper.Keeper,
) error {
	logger := ctx.Logger()

	aggregateSum, err := TransferFractionalBalances(
		ctx,
		evmutilKeeper,
		precisebankKeeper,
	)
	if err != nil {
		return fmt.Errorf("fractional balances transfer: %w", err)
	}
	logger.Info(
		"fractional balances transferred from x/evmutil to x/precisebank",
		"aggregate sum", aggregateSum,
	)

	remainder := InitializeRemainder(ctx, precisebankKeeper, aggregateSum)
	logger.Info("remainder amount initialized in x/precisebank", "remainder", remainder)

	// Migrate fractional balances, reserve, and ensure reserve fully backs all
	// fractional balances.
	if err := TransferFractionalBalanceReserve(
		ctx,
		accountKeeper,
		bankKeeper,
		precisebankKeeper,
	); err != nil {
		return fmt.Errorf("reserve transfer: %w", err)
	}

	return nil
}

// TransferFractionalBalances migrates fractional balances from x/evmutil to
// x/precisebank. It sets the fractional balance in x/precisebank and deletes
// the account from x/evmutil. Returns the aggregate sum of all fractional
// balances.
func TransferFractionalBalances(
	ctx sdk.Context,
	evmutilKeeper evmutilkeeper.Keeper,
	precisebankKeeper precisebankkeeper.Keeper,
) (sdkmath.Int, error) {
	aggregateSum := sdkmath.ZeroInt()

	var iterErr error

	evmutilKeeper.IterateAllAccounts(ctx, func(acc evmutiltypes.Account) bool {
		// Set account balance in x/precisebank
		precisebankKeeper.SetFractionalBalance(ctx, acc.Address, acc.Balance)

		// Delete account from x/evmutil
		iterErr := evmutilKeeper.SetAccount(ctx, evmutiltypes.Account{
			Address: acc.Address,
			// Set balance to 0 to delete it
			Balance: sdkmath.ZeroInt(),
		})

		// Halt iteration if there was an error
		if iterErr != nil {
			return true
		}

		// Aggregate sum of all fractional balances
		aggregateSum = aggregateSum.Add(acc.Balance)

		// Continue iterating
		return false
	})

	return aggregateSum, iterErr
}

// InitializeRemainder initializes the remainder amount in x/precisebank. It
// calculates the remainder amount that is needed to ensure that the sum of all
// fractional balances is a multiple of the conversion factor. The remainder
// amount is stored in the store and returned.
func InitializeRemainder(
	ctx sdk.Context,
	precisebankKeeper precisebankkeeper.Keeper,
	aggregateSum sdkmath.Int,
) sdkmath.Int {
	// Extra fractional coins that exceed the conversion factor.
	// This extra + remainder should equal the conversion factor to ensure
	// (sum(fBalances) + remainder) % conversionFactor = 0
	extraFractionalAmount := aggregateSum.Mod(precisebanktypes.ConversionFactor())
	remainder := precisebanktypes.ConversionFactor().
		Sub(extraFractionalAmount).
		// Mod conversion factor to ensure remainder is valid.
		// If extraFractionalAmount is a multiple of conversion factor, the
		// remainder is 0.
		Mod(precisebanktypes.ConversionFactor())

	// Panics if the remainder is invalid. In a correct chain state and only
	// mint/burns due to transfers, this would be 0.
	precisebankKeeper.SetRemainderAmount(ctx, remainder)

	return remainder
}

// TransferFractionalBalanceReserve migrates the fractional balance reserve from
// x/evmutil to x/precisebank. It transfers the reserve balance from x/evmutil
// to x/precisebank and ensures that the reserve fully backs all fractional
// balances. It mints or burns coins to back the fractional balances exactly.
func TransferFractionalBalanceReserve(
	ctx sdk.Context,
	accountKeeper evmutiltypes.AccountKeeper,
	bankKeeper bankkeeper.Keeper,
	precisebankKeeper precisebankkeeper.Keeper,
) error {
	logger := ctx.Logger()

	// Transfer x/evmutil reserve to x/precisebank.
	evmutilAddr := accountKeeper.GetModuleAddress(evmutiltypes.ModuleName)
	reserveBalance := bankKeeper.GetBalance(ctx, evmutilAddr, precisebanktypes.IntegerCoinDenom)

	if err := bankKeeper.SendCoinsFromModuleToModule(
		ctx,
		evmutiltypes.ModuleName,     // from x/evmutil
		precisebanktypes.ModuleName, // to x/precisebank
		sdk.NewCoins(reserveBalance),
	); err != nil {
		return fmt.Errorf("failed to transfer reserve from x/evmutil to x/precisebank: %w", err)
	}

	logger.Info(fmt.Sprintf("transferred reserve balance: %s", reserveBalance))

	// Ensure x/precisebank reserve fully backs all fractional balances.
	totalFractionalBalances := precisebankKeeper.GetTotalSumFractionalBalances(ctx)

	// Does NOT ensure state is correct, total fractional balances should be a
	// multiple of conversion factor but is not guaranteed due to the remainder.
	// Remainder initialization is handled by InitializeRemainder.

	// Determine how much the reserve is off by, e.g. unbacked amount
	expectedReserveBalance := totalFractionalBalances.Quo(precisebanktypes.ConversionFactor())

	// If there is a remainder (totalFractionalBalances % conversionFactor != 0),
	// then expectedReserveBalance is rounded up to the nearest integer.
	if totalFractionalBalances.Mod(precisebanktypes.ConversionFactor()).IsPositive() {
		expectedReserveBalance = expectedReserveBalance.Add(sdkmath.OneInt())
	}

	unbackedAmount := expectedReserveBalance.Sub(reserveBalance.Amount)
	logger.Info(fmt.Sprintf("total account fractional balances: %s", totalFractionalBalances))

	// Three possible cases:
	// 1. Reserve is not enough, mint coins to back the fractional balances
	// 2. Reserve is too much, burn coins to back the fractional balances exactly
	// 3. Reserve is exactly enough, no action needed
	if unbackedAmount.IsPositive() {
		coins := sdk.NewCoins(sdk.NewCoin(precisebanktypes.IntegerCoinDenom, unbackedAmount))
		if err := bankKeeper.MintCoins(ctx, precisebanktypes.ModuleName, coins); err != nil {
			return fmt.Errorf("failed to mint extra reserve coins: %w", err)
		}

		logger.Info(fmt.Sprintf("unbacked amount minted to reserve: %s", unbackedAmount))
	} else if unbackedAmount.IsNegative() {
		coins := sdk.NewCoins(sdk.NewCoin(precisebanktypes.IntegerCoinDenom, unbackedAmount.Neg()))
		if err := bankKeeper.BurnCoins(ctx, precisebanktypes.ModuleName, coins); err != nil {
			return fmt.Errorf("failed to burn extra reserve coins: %w", err)
		}

		logger.Info(fmt.Sprintf("extra reserve amount burned: %s", unbackedAmount.Neg()))
	} else {
		logger.Info("reserve exactly backs fractional balances, no mint/burn needed")
	}

	return nil
}