package keeper

import (
	"errors"
	"fmt"
	"time"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

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

// StartSurplusAuction starts a new surplus (forward) auction.
func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, error) {
	auction := types.NewSurplusAuction(
		seller,
		lot,
		bidDenom,
		types.DistantFuture,
	)

	// NOTE: for the duration of the auction the auction module account holds the lot
	err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
	if err != nil {
		return 0, err
	}

	auctionID, err := k.StoreNewAuction(ctx, &auction)
	if err != nil {
		return 0, err
	}

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeAuctionStart,
			sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auctionID)),
			sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
			sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
			sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
		),
	)
	return auctionID, nil
}

// StartDebtAuction starts a new debt (reverse) auction.
func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, error) {
	auction := types.NewDebtAuction(
		buyer,
		bid,
		initialLot,
		types.DistantFuture,
		debt,
	)

	// This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker.
	macc := k.accountKeeper.GetModuleAccount(ctx, buyer)
	if !macc.HasPermission(authtypes.Minter) {
		panic(fmt.Errorf("module '%s' does not have '%s' permission", buyer, authtypes.Minter))
	}

	// NOTE: for the duration of the auction the auction module account holds the debt
	err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, buyer, types.ModuleName, sdk.NewCoins(debt))
	if err != nil {
		return 0, err
	}

	auctionID, err := k.StoreNewAuction(ctx, &auction)
	if err != nil {
		return 0, err
	}

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeAuctionStart,
			sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auctionID)),
			sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
			sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
			sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
		),
	)
	return auctionID, nil
}

// StartCollateralAuction starts a new collateral (2-phase) auction.
func (k Keeper) StartCollateralAuction(
	ctx sdk.Context, seller string, lot, maxBid sdk.Coin,
	lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin,
) (uint64, error) {
	weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights)
	if err != nil {
		return 0, err
	}
	auction := types.NewCollateralAuction(
		seller,
		lot,
		types.DistantFuture,
		maxBid,
		weightedAddresses,
		debt,
	)

	// NOTE: for the duration of the auction the auction module account holds the debt and the lot
	err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
	if err != nil {
		return 0, err
	}
	err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(debt))
	if err != nil {
		return 0, err
	}

	auctionID, err := k.StoreNewAuction(ctx, &auction)
	if err != nil {
		return 0, err
	}

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeAuctionStart,
			sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auctionID)),
			sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
			sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
			sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
			sdk.NewAttribute(types.AttributeKeyMaxBid, auction.MaxBid.String()),
		),
	)
	return auctionID, nil
}

// PlaceBid places a bid on any auction.
func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, newAmount sdk.Coin) error {
	auction, found := k.GetAuction(ctx, auctionID)
	if !found {
		return sdkerrors.Wrapf(types.ErrAuctionNotFound, "%d", auctionID)
	}

	// validation common to all auctions
	if ctx.BlockTime().After(auction.GetEndTime()) {
		return sdkerrors.Wrapf(types.ErrAuctionHasExpired, "%d", auctionID)
	}

	// move coins and return updated auction
	var (
		err            error
		updatedAuction types.Auction
	)
	switch auctionType := auction.(type) {
	case *types.SurplusAuction:
		updatedAuction, err = k.PlaceBidSurplus(ctx, auctionType, bidder, newAmount)
	case *types.DebtAuction:
		updatedAuction, err = k.PlaceBidDebt(ctx, auctionType, bidder, newAmount)
	case *types.CollateralAuction:
		if !auctionType.IsReversePhase() {
			updatedAuction, err = k.PlaceForwardBidCollateral(ctx, auctionType, bidder, newAmount)
		} else {
			updatedAuction, err = k.PlaceReverseBidCollateral(ctx, auctionType, bidder, newAmount)
		}
	default:
		err = sdkerrors.Wrap(types.ErrUnrecognizedAuctionType, auction.GetType())
	}

	if err != nil {
		return err
	}

	k.SetAuction(ctx, updatedAuction)

	return nil
}

// PlaceBidSurplus places a forward bid on a surplus auction, moving coins and returning the updated auction.
func (k Keeper) PlaceBidSurplus(ctx sdk.Context, auction *types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (*types.SurplusAuction, error) {
	// Validate new bid
	if bid.Denom != auction.Bid.Denom {
		return auction, sdkerrors.Wrapf(types.ErrInvalidBidDenom, "%s ≠ %s", bid.Denom, auction.Bid.Denom)
	}
	minNewBidAmt := auction.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost
		sdk.MaxInt(
			sdk.NewInt(1),
			sdk.NewDecFromInt(auction.Bid.Amount).Mul(k.GetParams(ctx).IncrementSurplus).RoundInt(),
		),
	)
	if bid.Amount.LT(minNewBidAmt) {
		return auction, sdkerrors.Wrapf(types.ErrBidTooSmall, "%s < %s%s", bid, minNewBidAmt, auction.Bid.Denom)
	}

	// New bidder pays back old bidder
	// Catch edge cases of a bidder replacing their own bid, or the amount being zero (sending zero coins produces meaningless send events).
	if !bidder.Equals(auction.Bidder) && !auction.Bid.IsZero() { // bidder isn't same as before AND previous auction bid must exist
		err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(auction.Bid))
		if err != nil {
			return auction, err
		}
		err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, auction.Bidder, sdk.NewCoins(auction.Bid))
		if err != nil {
			return auction, err
		}
	}

	err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, bidder, auction.Initiator, sdk.NewCoins(bid.Sub(auction.Bid)))
	if err != nil {
		return auction, err
	}

	// Received bid amount is burned from the module account
	err = k.bankKeeper.BurnCoins(ctx, auction.Initiator, sdk.NewCoins(bid.Sub(auction.Bid)))
	if err != nil {
		return auction, err
	}

	// Update Auction
	auction.Bidder = bidder
	auction.Bid = bid
	if !auction.HasReceivedBids {
		auction.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
		auction.HasReceivedBids = true
	}
	auction.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).ForwardBidDuration), auction.MaxEndTime) // increment timeout, up to MaxEndTime

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeAuctionBid,
			sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.ID)),
			sdk.NewAttribute(types.AttributeKeyBidder, auction.Bidder.String()),
			sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
			sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", auction.EndTime.Unix())),
		),
	)

	return auction, nil
}

// PlaceForwardBidCollateral places a forward bid on a collateral auction, moving coins and returning the updated auction.
func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, auction *types.CollateralAuction, bidder sdk.AccAddress, bid sdk.Coin) (*types.CollateralAuction, error) {
	// Validate new bid
	if bid.Denom != auction.Bid.Denom {
		return auction, sdkerrors.Wrapf(types.ErrInvalidBidDenom, "%s ≠ %s", bid.Denom, auction.Bid.Denom)
	}
	if auction.IsReversePhase() {
		panic("cannot place reverse bid on auction in forward phase")
	}
	minNewBidAmt := auction.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost
		sdk.MaxInt(
			sdk.NewInt(1),
			sdk.NewDecFromInt(auction.Bid.Amount).Mul(k.GetParams(ctx).IncrementCollateral).RoundInt(),
		),
	)
	minNewBidAmt = sdk.MinInt(minNewBidAmt, auction.MaxBid.Amount) // allow new bids to hit MaxBid even though it may be less than the increment %
	if bid.Amount.LT(minNewBidAmt) {
		return auction, sdkerrors.Wrapf(types.ErrBidTooSmall, "%s < %s%s", bid, minNewBidAmt, auction.Bid.Denom)
	}
	if auction.MaxBid.IsLT(bid) {
		return auction, sdkerrors.Wrapf(types.ErrBidTooLarge, "%s > %s", bid, auction.MaxBid)
	}

	// New bidder pays back old bidder
	// Catch edge cases of a bidder replacing their own bid, and the amount being zero (sending zero coins produces meaningless send events).
	if !bidder.Equals(auction.Bidder) && !auction.Bid.IsZero() {
		err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(auction.Bid))
		if err != nil {
			return auction, err
		}
		err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, auction.Bidder, sdk.NewCoins(auction.Bid))
		if err != nil {
			return auction, err
		}
	}
	// Increase in bid sent to auction initiator
	bidIncrement := bid.Sub(auction.Bid)
	err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, bidder, auction.Initiator, sdk.NewCoins(bidIncrement))
	if err != nil {
		return auction, err
	}
	// Debt coins are sent to liquidator (until there is no CorrespondingDebt left). Amount sent is equal to bidIncrement (or whatever is left if < bidIncrement).
	if auction.CorrespondingDebt.IsPositive() {

		debtAmountToReturn := sdk.MinInt(bidIncrement.Amount, auction.CorrespondingDebt.Amount)
		debtToReturn := sdk.NewCoin(auction.CorrespondingDebt.Denom, debtAmountToReturn)

		err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, auction.Initiator, sdk.NewCoins(debtToReturn))
		if err != nil {
			return auction, err
		}
		auction.CorrespondingDebt = auction.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ auction.CorrespondingDebt from the MinInt above
	}

	// Update Auction
	auction.Bidder = bidder
	auction.Bid = bid
	if !auction.HasReceivedBids {
		auction.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
		auction.HasReceivedBids = true
	}

	// If this forward bid converts this to a reverse, increase timeout with ReverseBidDuration
	if auction.IsReversePhase() {
		auction.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).ReverseBidDuration), auction.MaxEndTime) // increment timeout, up to MaxEndTime
	} else {
		auction.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).ForwardBidDuration), auction.MaxEndTime) // increment timeout, up to MaxEndTime
	}

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeAuctionBid,
			sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.ID)),
			sdk.NewAttribute(types.AttributeKeyBidder, auction.Bidder.String()),
			sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
			sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", auction.EndTime.Unix())),
		),
	)

	return auction, nil
}

// PlaceReverseBidCollateral places a reverse bid on a collateral auction, moving coins and returning the updated auction.
func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, auction *types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (*types.CollateralAuction, error) {
	// Validate new bid
	if lot.Denom != auction.Lot.Denom {
		return auction, sdkerrors.Wrapf(types.ErrInvalidLotDenom, "%s ≠ %s", lot.Denom, auction.Lot.Denom)
	}
	if !auction.IsReversePhase() {
		panic("cannot place forward bid on auction in reverse phase")
	}
	maxNewLotAmt := auction.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost
		sdk.MaxInt(
			sdk.NewInt(1),
			sdk.NewDecFromInt(auction.Lot.Amount).Mul(k.GetParams(ctx).IncrementCollateral).RoundInt(),
		),
	)
	if lot.Amount.GT(maxNewLotAmt) {
		return auction, sdkerrors.Wrapf(types.ErrLotTooLarge, "%s > %s%s", lot, maxNewLotAmt, auction.Lot.Denom)
	}
	if lot.IsNegative() {
		return auction, sdkerrors.Wrapf(types.ErrLotTooSmall, "%s < 0%s", lot, auction.Lot.Denom)
	}

	// New bidder pays back old bidder
	// Catch edge cases of a bidder replacing their own bid
	if !bidder.Equals(auction.Bidder) {
		err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(auction.Bid))
		if err != nil {
			return auction, err
		}
		err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, auction.Bidder, sdk.NewCoins(auction.Bid))
		if err != nil {
			return auction, err
		}
	}

	// Decrease in lot is sent to weighted addresses (normally the CDP depositors)
	// Note: splitting an integer amount across weighted buckets results in small errors.
	lotPayouts, err := splitCoinIntoWeightedBuckets(auction.Lot.Sub(lot), auction.LotReturns.Weights)
	if err != nil {
		return auction, err
	}
	for i, payout := range lotPayouts {
		// if the payout amount is 0, don't send 0 coins
		if !payout.IsPositive() {
			continue
		}
		err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, auction.LotReturns.Addresses[i], sdk.NewCoins(payout))
		if err != nil {
			return auction, err
		}
	}

	// Update Auction
	auction.Bidder = bidder
	auction.Lot = lot
	if !auction.HasReceivedBids {
		auction.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
		auction.HasReceivedBids = true
	}
	auction.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).ReverseBidDuration), auction.MaxEndTime) // increment timeout, up to MaxEndTime

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeAuctionBid,
			sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.ID)),
			sdk.NewAttribute(types.AttributeKeyBidder, auction.Bidder.String()),
			sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
			sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", auction.EndTime.Unix())),
		),
	)

	return auction, nil
}

// PlaceBidDebt places a reverse bid on a debt auction, moving coins and returning the updated auction.
func (k Keeper) PlaceBidDebt(ctx sdk.Context, auction *types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (*types.DebtAuction, error) {
	// Validate new bid
	if lot.Denom != auction.Lot.Denom {
		return auction, sdkerrors.Wrapf(types.ErrInvalidLotDenom, "%s ≠ %s", lot.Denom, auction.Lot.Denom)
	}
	maxNewLotAmt := auction.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost
		sdk.MaxInt(
			sdk.NewInt(1),
			sdk.NewDecFromInt(auction.Lot.Amount).Mul(k.GetParams(ctx).IncrementDebt).RoundInt(),
		),
	)
	if lot.Amount.GT(maxNewLotAmt) {
		return auction, sdkerrors.Wrapf(types.ErrLotTooLarge, "%s > %s%s", lot, maxNewLotAmt, auction.Lot.Denom)
	}
	if lot.IsNegative() {
		return auction, sdkerrors.Wrapf(types.ErrLotTooSmall, "%s ≤ %s%s", lot, sdk.ZeroInt(), auction.Lot.Denom)
	}

	// New bidder pays back old bidder
	// Catch edge cases of a bidder replacing their own bid
	if !bidder.Equals(auction.Bidder) {
		// Bidder sends coins to module
		err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(auction.Bid))
		if err != nil {
			return auction, err
		}
		// Coins are sent from module to old bidder
		oldBidder := auction.Bidder
		if oldBidder.Equals(authtypes.NewModuleAddress(auction.Initiator)) { // First bid on auction (where there is no previous bidder)
			err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, auction.Initiator, sdk.NewCoins(auction.Bid))
		} else { // Second and later bids on auction (where previous bidder is a user account)
			err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, oldBidder, sdk.NewCoins(auction.Bid))
		}
		if err != nil {
			return auction, err
		}
	}

	// Debt coins are sent to liquidator the first time a bid is placed. Amount sent is equal to min of Bid and amount of debt.
	if auction.Bidder.Equals(authtypes.NewModuleAddress(auction.Initiator)) {
		// Handle debt coins for first bid
		debtAmountToReturn := sdk.MinInt(auction.Bid.Amount, auction.CorrespondingDebt.Amount)
		debtToReturn := sdk.NewCoin(auction.CorrespondingDebt.Denom, debtAmountToReturn)

		err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, auction.Initiator, sdk.NewCoins(debtToReturn))
		if err != nil {
			return auction, err
		}
		auction.CorrespondingDebt = auction.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ auction.CorrespondingDebt from the MinInt above
	}

	// Update Auction
	auction.Bidder = bidder
	auction.Lot = lot
	if !auction.HasReceivedBids {
		auction.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
		auction.HasReceivedBids = true
	}
	auction.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).ForwardBidDuration), auction.MaxEndTime) // increment timeout, up to MaxEndTime

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeAuctionBid,
			sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.ID)),
			sdk.NewAttribute(types.AttributeKeyBidder, auction.Bidder.String()),
			sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
			sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", auction.EndTime.Unix())),
		),
	)

	return auction, nil
}

// CloseAuction closes an auction and distributes funds to the highest bidder.
func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) error {
	auction, found := k.GetAuction(ctx, auctionID)
	if !found {
		return sdkerrors.Wrapf(types.ErrAuctionNotFound, "%d", auctionID)
	}

	if ctx.BlockTime().Before(auction.GetEndTime()) {
		return sdkerrors.Wrapf(types.ErrAuctionHasNotExpired, "block time %s, auction end time %s", ctx.BlockTime().UTC(), auction.GetEndTime().UTC())
	}

	// payout to the last bidder
	var err error
	switch auc := auction.(type) {
	case *types.SurplusAuction:
		err = k.PayoutSurplusAuction(ctx, auc)
	case *types.DebtAuction:
		err = k.PayoutDebtAuction(ctx, auc)
	case *types.CollateralAuction:
		err = k.PayoutCollateralAuction(ctx, auc)
	default:
		err = sdkerrors.Wrap(types.ErrUnrecognizedAuctionType, auc.GetType())
	}

	if err != nil {
		return err
	}

	k.DeleteAuction(ctx, auctionID)

	ctx.EventManager().EmitEvent(
		sdk.NewEvent(
			types.EventTypeAuctionClose,
			sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auctionID)),
			sdk.NewAttribute(types.AttributeKeyCloseBlock, fmt.Sprintf("%d", ctx.BlockHeight())),
		),
	)
	return nil
}

// PayoutDebtAuction pays out the proceeds for a debt auction, first minting the coins.
func (k Keeper) PayoutDebtAuction(ctx sdk.Context, auction *types.DebtAuction) error {
	// create the coins that are needed to pay off the debt
	err := k.bankKeeper.MintCoins(ctx, auction.Initiator, sdk.NewCoins(auction.Lot))
	if err != nil {
		panic(fmt.Errorf("could not mint coins: %w", err))
	}
	// send the new coins from the initiator module to the bidder
	err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, auction.Initiator, auction.Bidder, sdk.NewCoins(auction.Lot))
	if err != nil {
		return err
	}
	// if there is remaining debt, return it to the calling module to manage
	if !auction.CorrespondingDebt.IsPositive() {
		return nil
	}

	return k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, auction.Initiator, sdk.NewCoins(auction.CorrespondingDebt))
}

// PayoutSurplusAuction pays out the proceeds for a surplus auction.
func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, auction *types.SurplusAuction) error {
	// Send the tokens from the auction module account where they are being managed to the bidder who won the auction
	return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, auction.Bidder, sdk.NewCoins(auction.Lot))
}

// PayoutCollateralAuction pays out the proceeds for a collateral auction.
func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, auction *types.CollateralAuction) error {
	// Send the tokens from the auction module account where they are being managed to the bidder who won the auction
	err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, auction.Bidder, sdk.NewCoins(auction.Lot))
	if err != nil {
		return err
	}

	// if there is remaining debt after the auction, send it back to the initiating module for management
	if !auction.CorrespondingDebt.IsPositive() {
		return nil
	}

	return k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, auction.Initiator, sdk.NewCoins(auction.CorrespondingDebt))
}

// CloseExpiredAuctions iterates over all the auctions stored by until the current
// block timestamp and that are past (or at) their ending times and closes them,
// paying out to the highest bidder.
func (k Keeper) CloseExpiredAuctions(ctx sdk.Context) error {
	var err error
	k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) (stop bool) {
		err = k.CloseAuction(ctx, id)
		if err != nil && !errors.Is(err, types.ErrAuctionNotFound) {
			// stop iteration
			return true
		}
		// reset error in case the last element had an ErrAuctionNotFound
		err = nil
		return false
	})

	return err
}

// earliestTime returns the earliest of two times.
func earliestTime(t1, t2 time.Time) time.Time {
	if t1.Before(t2) {
		return t1
	}
	return t2 // also returned if times are equal
}

// splitCoinIntoWeightedBuckets divides up some amount of coins according to some weights.
func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, error) {
	amounts := splitIntIntoWeightedBuckets(coin.Amount, buckets)
	result := make([]sdk.Coin, len(amounts))
	for i, a := range amounts {
		result[i] = sdk.NewCoin(coin.Denom, a)
	}
	return result, nil
}