package keeper

import (
	"fmt"

	sdk "github.com/cosmos/cosmos-sdk/types"

	"github.com/kava-labs/kava/x/earn/types"
)

// Deposit adds the provided amount from a depositor to a vault. The vault is
// specified by the denom in the amount.
func (k *Keeper) Deposit(
	ctx sdk.Context,
	depositor sdk.AccAddress,
	amount sdk.Coin,
	depositStrategy types.StrategyType,
) error {
	// Get AllowedVault, if not found (not a valid vault), return error
	allowedVault, found := k.GetAllowedVault(ctx, amount.Denom)
	if !found {
		return types.ErrInvalidVaultDenom
	}

	if amount.IsZero() {
		return types.ErrInsufficientAmount
	}

	// Check if deposit strategy is supported by vault
	if !allowedVault.IsStrategyAllowed(depositStrategy) {
		return types.ErrInvalidVaultStrategy
	}

	// Check if account can deposit -- this checks if the vault is private
	// and if so, if the depositor is in the AllowedDepositors list
	if !allowedVault.IsAccountAllowed(depositor) {
		return types.ErrAccountDepositNotAllowed
	}

	// Check if VaultRecord exists, create if not exist
	vaultRecord, found := k.GetVaultRecord(ctx, amount.Denom)
	if !found {
		// Create a new VaultRecord with 0 supply
		vaultRecord = types.NewVaultRecord(amount.Denom, sdk.ZeroDec())
	}

	// Get the strategy for the vault
	// NOTE: Currently always uses the first one, AllowedVaults are currently
	// only valid with 1 and only 1 strategy so this is safe.
	// If/When multiple strategies are supported and users can specify specific
	// strategies, shares should be issued per-strategy instead of per-vault.
	strategy, err := k.GetStrategy(allowedVault.Strategies[0])
	if err != nil {
		return err
	}

	// Transfer amount to module account
	if err := k.bankKeeper.SendCoinsFromAccountToModule(
		ctx,
		depositor,
		types.ModuleName,
		sdk.NewCoins(amount),
	); err != nil {
		return err
	}

	// Get VaultShareRecord for account, create if account has no deposits.
	// This can still be found if the account has deposits for other vaults.
	vaultShareRecord, found := k.GetVaultShareRecord(ctx, depositor)
	if !found {
		// Create a new empty VaultShareRecord with 0 supply
		vaultShareRecord = types.NewVaultShareRecord(depositor, types.NewVaultShares())
	}

	shares, err := k.ConvertToShares(ctx, amount)
	if err != nil {
		return fmt.Errorf("failed to convert assets to shares: %w", err)
	}

	isNew := vaultShareRecord.Shares.AmountOf(amount.Denom).IsZero()
	if !isNew {
		// If deposits for this vault already exists, call hook with user's existing shares
		k.BeforeVaultDepositModified(ctx, amount.Denom, depositor, vaultShareRecord.Shares.AmountOf(amount.Denom))
	}

	// Increment VaultRecord total shares and account shares
	vaultRecord.TotalShares = vaultRecord.TotalShares.Add(shares)
	vaultShareRecord.Shares = vaultShareRecord.Shares.Add(shares)

	// Update VaultRecord and VaultShareRecord
	k.SetVaultRecord(ctx, vaultRecord)
	k.SetVaultShareRecord(ctx, vaultShareRecord)

	if isNew {
		// If first deposit in this vault
		k.AfterVaultDepositCreated(ctx, amount.Denom, depositor, shares.Amount)
	}

	// Deposit to the strategy
	if err := strategy.Deposit(ctx, amount); err != nil {
		return err
	}

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeVaultDeposit,
			sdk.NewAttribute(types.AttributeKeyVaultDenom, amount.Denom),
			sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
			sdk.NewAttribute(types.AttributeKeyShares, shares.Amount.String()),
			sdk.NewAttribute(sdk.AttributeKeyAmount, amount.Amount.String()),
		),
	)

	return nil
}

// DepositFromModuleAccount adds the provided amount from a depositor module
// account to a vault. The vault is specified by the denom in the amount.
func (k *Keeper) DepositFromModuleAccount(
	ctx sdk.Context,
	from string,
	wantAmount sdk.Coin,
	withdrawStrategy types.StrategyType,
) error {
	addr := k.accountKeeper.GetModuleAddress(from)
	return k.Deposit(ctx, addr, wantAmount, withdrawStrategy)
}
