from dataclasses import dataclass, field
from logging import Logger
import random
import json
import asyncio

from sudoku_py import SudokuGenerator, Sudoku

from rollbot import as_command, initialize_data, RollbotFailure, Attachment, Response
from rollbot.injection import (
    Data,
    Sender,
    Config,
    Const,
    Arg,
    Reply,
    OriginAdmin,
    Args,
    Attachments,
    Origin,
    Request,
    Lazy,
    Channel,
    SenderName,
)

# View
#   !wallet - shows your number of rollcoins, NFTs (Non-Functional Tamagotchis), and market balance
#   !nfts - shows your entire list of NFTs
#   !blockchain - shows the contents of all wallets and the rollcoins in the treasury
# Generate
#   !mine - provides a sudoku that can be solved for rollcoins, which also adds to the treasury
#   !appraise - rollbot will purchase a post, based indirectly on number of likes received
# Spend
#   !tip - transfer rollcoins from one person to another
#   !gacha - insert a rollcoin (which enters the treasury), receive a random NFT
#   !donate - split an amount and donate it to everyone
# Market
#   !bet - put some amount of rollcoins into the market, some portion of which will be put in the treasury, evolves market
#   !cash - take some amount of rollcoins from the market, up to investment + the number of coins in the treasury, evolves market
# Admin
#   !deflate - deflate all coins by a power of 10 (negative to inflate economy)
#   !brrr - print some number of coins into the treasury (negative to delete coins)
#   !tax - take some number of coins from a wallet and put it in the treasury (uses wallet id, negative to give coins)
#   !mine skip - skip the current mining puzzle
#   !market - force the market to transition to a given state


@initialize_data
@dataclass
class RollcoinState:
    treasury: float
    mining_puzzle: list[list[int | None]]
    market_state: int
    appraised: list[str] = field(default_factory=list)


@initialize_data
@dataclass
class RollcoinWallet:
    balance: float = 10  # new wallets start with 10 coins
    holdings: float = 0
    cost_basis: float = 0
    nfts: list[str] = field(default_factory=list)

    def __str__(self):
        s = f"Wallet: {self.balance}\n"
        if self.holdings > 0 or self.cost_basis > 0:
            s += f"Investments: {self.holdings}\n\t(cost basis {self.cost_basis})\n"
        if (num_nfts := len(self.nfts)) > 0:
            s += f"NFTs: {num_nfts}\n"
        return s.strip()

    def get_valuation(self):
        return self.balance + self.holdings - self.cost_basis, len(self.nfts)

    def get_nft_ratios(self):
        if (num_nfts := len(self.nfts)) == 0:
            return ""
        # writing out all of them produces a TON of spam, so just write ratios
        counts = {
            rarity: len([nft for nft in self.nfts if rarity in nft])
            for rarity in [NFT_MAX_RARITY, *NFT_RARITY[1:]]
        }
        counts[NFT_RARITY[0]] = num_nfts - sum(counts.values())
        return "\n\t".join(
            f"{rarity.title()}: {c} ({100 * c / num_nfts:.02f}%)"
            for rarity in [*NFT_RARITY, NFT_MAX_RARITY]
            if (c := counts[rarity]) > 0
        )


SPECIAL_AMOUNTS = {
    "all": lambda limit: limit,
    "half": lambda limit: limit / 2,
    "frac": lambda limit: limit - int(limit),
}


def convert_amount(amount):
    if (conv := SPECIAL_AMOUNTS.get(amount.lower(), None)) is not None:
        return conv
    return float(amount)


# constants
GLOBAL_STATE_KEY = "ROLLCOIN_GLOBAL_STATE"
NAME_URL = "https://randommer.io/api/Name?nameType=firstname&quantity=1"
ICON_URL = (
    "https://app.pixelencounter.com/api/basic/svgmonsters/image/png?size=256&fillType=2"
)
NFT_COLORS = ["red", "blue", "yellow", "green", "orange", "purple", "brown", "gray"]
NFT_UNCOMMON = "silver"
NFT_RARE = "gold"
NFT_ULTRA = "black"
NFT_RARITY_LOOKUP = {
    NFT_UNCOMMON: 1,
    NFT_RARE: 2,
    NFT_ULTRA: 3,
}
NFT_RARITY = ["common", "uncommon", "rare!", "ultra rare!"]
NFT_MAX_RARITY = "legendary!"

# injection values
State = Data(RollcoinState).For(
    Const(GLOBAL_STATE_KEY), treasury=0, mining_puzzle=None, market_state=0
)
SenderWallet = Data(RollcoinWallet).For(Sender)
WalletLookup = Config("rollcoin.wallet_names")
MarketTransitions = Config("rollcoin.market.transitions")
MarketMultipliers = Config("rollcoin.market.multipliers")
MarketMessages = Config("rollcoin.market.messages")
NameAPIKey = Config("rollcoin.gacha.name_api_key")
GachaSleep = Config("rollcoin.gacha.sleep_time")
AppraiseChannel = Config("rollcoin.appraise.channel")
AppraiseAllowSelfLike = Config("rollcoin.appraise.allow_self")
AppraiseRequireOwnership = Config("rollcoin.appraise.require_own")


@as_command
def wallet(sender_wallet: SenderWallet, reply: Reply):
    """
    View the contents of your wallet. Using this command once will initialize your wallet.
    """
    return (
        f"You currently own...\n{sender_wallet}\n\t{sender_wallet.get_nft_ratios()}".strip(),
        reply,
    )


@as_command
def nfts(sender_wallet: SenderWallet, reply: Reply):
    """
    View the full list of your NFTs.
    """
    nfts = "\n\t".join(sender_wallet.nfts)
    return f"You currently own...\n\t{nfts}".strip(), reply


@as_command
async def blockchain(
    origin: Origin,
    wallets: Data(RollcoinWallet),
    wallet_lookup: WalletLookup,
    state: State,
):
    """
    View the contents of everyone's wallets
    """
    response = f"Blockchain:\n\tTreasury: {state.treasury}\n\n"
    names = {v: k.title() for k, v in wallet_lookup[origin].items()}
    wallets = [(sender_id, wallet) async for (sender_id, wallet) in wallets.all()]
    for sender_id, wallet in sorted(
        wallets, key=lambda p: p[1].get_valuation(), reverse=True
    ):
        if (name := names.get(sender_id, None)) is not None:
            response += f"{name}:\n"
            response += "\n".join("\t" + s for s in str(wallet).split("\n")) + "\n\n"
    return response.strip()


@as_command
async def tip(
    origin: Origin,
    wallets: Data(RollcoinWallet),
    sender_id: Sender,
    sender_wallet: SenderWallet,
    wallet_lookup: WalletLookup,
    target_name: Arg(0, missing_msg="You must tell me who to tip!"),
    amount: Arg(
        1,
        convert=convert_amount,
        missing_msg="You must provide an amount to tip!",
        fail_msg="Could not parse {} as value",
    ),
):
    """
    Send RollCoins to another person, as in !tip [person] [amount]
    """
    if not isinstance(amount, float):
        # handle special converters
        amount = amount(sender_wallet.balance)

    if sender_wallet.balance == 0:
        return "Sorry! You don't have any rollcoins right now - try mining!"

    if amount > sender_wallet.balance:
        return f"Sorry! You only have {sender_wallet.balance} RollCoins available - try mining for more!"

    if amount <= 0:
        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
            f"Amount must be positive, not {amount}"
        )

    if (target_id := wallet_lookup[origin].get(target_name.lower(), None)) is None:
        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
            f"Cannot find wallet belonging to {target_name}"
        )

    target_wallet = await wallets.load_or(target_id)
    sender_wallet.balance -= amount
    target_wallet.balance += amount
    await wallets.save(target_id, target_wallet)
    await wallets.save(sender_id, sender_wallet)

    return f"Done! {target_name} now has {target_wallet.balance} RollCoins!"


@as_command
async def donate(
    wallets: Data(RollcoinWallet),
    sender_id: Sender,
    sender_wallet: SenderWallet,
    wallet_store: Data(RollcoinWallet),
    amount: Arg(
        0,
        convert=convert_amount,
        missing_msg="You must provide an amount to donate!",
        fail_msg="Could not parse {} as value",
    ),
):
    """
    Donate RollCoins to everyone who has a wallet, the number of coins you specify will be divided amongst everyone else.
    """
    if not isinstance(amount, float):
        # handle special converters
        amount = amount(sender_wallet.balance)

    if sender_wallet.balance == 0:
        return "Sorry! You don't have any rollcoins right now - try mining!"

    if amount > sender_wallet.balance:
        return f"Sorry! You only have {sender_wallet.balance} RollCoins available - try mining for more!"

    if amount <= 0:
        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
            f"Amount must be positive, not {amount}"
        )

    sender_wallet.balance -= amount
    await wallet_store.save(sender_id, sender_wallet)

    wallets = [(wid, w) async for wid, w in wallet_store.all() if wid != sender_id]
    to_donate = amount / len(wallets)
    for wallet_id, wallet in wallets:
        wallet.balance += to_donate
        await wallet_store.save(wallet_id, wallet)

    return f"Done! Donated {to_donate} to each other wallet, leaving you with {sender_wallet.balance} RollCoins!"


async def evolve_market(state, transitions, multipliers, wallet_store, state_store):
    state.market_state = random.choices(
        range(len(transitions)), weights=transitions[state.market_state], k=1
    )[0]
    await state_store.save(GLOBAL_STATE_KEY, state)
    multiplier = multipliers[state.market_state]
    async for (wallet_id, wallet) in wallet_store.all():
        wallet.holdings *= multiplier
        await wallet_store.save(wallet_id, wallet)


@as_command
async def bet(
    sender_id: Sender,
    sender_wallet: SenderWallet,
    wallet_store: Data(RollcoinWallet),
    state: State,
    state_store: Data(RollcoinState),
    transitions: MarketTransitions,
    multipliers: MarketMultipliers,
    messages: MarketMessages,
    amount: Arg(
        0,
        convert=convert_amount,
        missing_msg="You must provide an amount to bet!",
        fail_msg="Could not parse {} as value",
    ),
    reply: Reply,
):
    """
    Bet some number of RollCoins on the market. These coins will leave your wallet.
    """
    if not isinstance(amount, float):
        # handle special converters
        amount = amount(sender_wallet.balance)

    if sender_wallet.balance == 0:
        yield "Sorry! You don't have any rollcoins right now - try mining!"
        return

    if amount > sender_wallet.balance:
        yield f"Sorry! You only have {sender_wallet.balance} RollCoins available - try mining for more!"
        return

    if amount <= 0:
        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
            f"Amount must be positive, not {amount}"
        )

    sender_wallet.balance -= amount
    sender_wallet.holdings += amount
    sender_wallet.cost_basis += amount
    await wallet_store.save(sender_id, sender_wallet)

    # coins enter treasury, allow evolve_market to save state
    state.treasury += amount
    await evolve_market(state, transitions, multipliers, wallet_store, state_store)

    yield f"Trade complete!\n{await wallet_store.load(sender_id)}", reply
    yield f"Market status: {messages[state.market_state]}"


@as_command
async def cash(
    sender_id: Sender,
    sender_wallet: SenderWallet,
    wallet_store: Data(RollcoinWallet),
    state: State,
    state_store: Data(RollcoinState),
    transitions: MarketTransitions,
    multipliers: MarketMultipliers,
    messages: MarketMessages,
    amount: Arg(
        0,
        convert=convert_amount,
        missing_msg="You must provide an amount to bet!",
        fail_msg="Could not parse {} as value",
    ),
    reply: Reply,
):
    """
    Cash some number of RollCoins out of the market.
    Because RollCoins are the one true fiat currency, you can only cash out up to the number of coins available in the treasury.
    """
    if not isinstance(amount, float):
        # handle special converters
        amount = amount(sender_wallet.holdings)

    if sender_wallet.holdings == 0:
        yield "Sorry! You don't have any rollcoins invested right now - try betting!"
        return

    if amount > sender_wallet.holdings:
        yield f"Sorry! You only have {sender_wallet.holdings} RollCoins in the market - try betting more first!"
        return

    if amount <= 0:
        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
            f"Amount must be positive, not {amount}"
        )

    if state.treasury == 0:
        yield f"Sorry! The treasury is actually empty right now so uh... no one can sell..."
        return

    if amount <= state.treasury:
        response = "Successfully sold!"
        actual_amount = amount
    else:
        response = f"Funny story, I'm actually out of coins! I gave you what I could, draining the treasury, which was {state.treasury}."
        actual_amount = state.treasury

    sender_wallet.balance += actual_amount
    sender_wallet.holdings -= actual_amount
    sender_wallet.cost_basis = max(sender_wallet.cost_basis - actual_amount, 0)
    await wallet_store.save(sender_id, sender_wallet)
    # coins exit treasury, allow evolve_market to save state
    state.treasury -= actual_amount
    await evolve_market(state, transitions, multipliers, wallet_store, state_store)

    yield f"{response}\n{await wallet_store.load(sender_id)}", reply
    yield f"Market status: {messages[state.market_state]}"


@as_command
async def mine(
    args: Args,
    admin: OriginAdmin,
    state: State,
    sender_id: Sender,
    get_sender_wallet: Lazy(SenderWallet),
    wallet_store: Data(RollcoinWallet),
    state_store: Data(RollcoinState),
    reply: Reply,
):
    """
    Trade solved Sudokus for RollCoins. Admins can skip the current puzzle with !mine skip
    """

    async def generate_puzzle():
        exchange = list(zip("abcdefghi", range(10)))
        random.shuffle(exchange)
        gen = SudokuGenerator(9)
        gen.generate(0)
        gen.board_exchange_values({k: v + 1 for k, v in exchange})
        gen.generate(random.randint(10, 50))
        state.mining_puzzle = gen.board
        await state_store.save(GLOBAL_STATE_KEY, state)

    if args is None or len(args) == 0:
        if state.mining_puzzle is None:
            await generate_puzzle()
        yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
        return

    if "skip" in args.strip(" !/").lower():
        if not admin:
            RollbotFailure.PERMISSIONS.raise_exc(
                "Only admins can skip the current mining challenge"
            )
        await generate_puzzle()
        yield f"Admin has skipped the previous mining challenge. The new challenge is:\n{Sudoku(board=state.mining_puzzle)}"
        return

    # best effort parse input into a sudoku grid
    parsed = []
    row = []
    for c in args:
        if not c.isdigit():
            continue
        row.append(int(c))
        if len(row) == 9:
            parsed.append(row)
            row = []

    try:
        Sudoku(board=parsed, block_width=3, block_height=3)
    except:
        RollbotFailure.INVALID_ARGUMENTS.raise_exc("Could not parse that solution")

    for i, row in enumerate(state.mining_puzzle):
        for j, cell in enumerate(row):
            if cell not in (0, parsed[i][j]):
                yield "Sorry, that solution doesn't match the puzzle!", reply
                yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
                return

    for row in parsed:
        if set(row) != set(range(1, 10)):
            yield "Sorry, that solution isn't valid!", reply
            yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
            return

    for i in range(9):
        if set(r[i] for r in parsed) != set(range(1, 10)):
            yield "Sorry, that solution isn't valid!", reply
            yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
            return

    for i in range(0, 9, 3):
        for j in range(0, 9, 3):
            if set(x for r in parsed[i : i + 3] for x in r[j : j + 3]) != set(
                range(1, 10)
            ):
                yield "Sorry, that solution isn't valid!", reply
                yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"
                return

    zeroes = len([y for x in state.mining_puzzle for y in x if y == 0])
    difficulty = (zeroes - 10) / 4
    value = round(abs(random.gauss(difficulty * 200, 25)), 2)

    sender_wallet = await get_sender_wallet()
    sender_wallet.balance += value
    await wallet_store.save(sender_id, sender_wallet)

    # allow generate_puzzle to save the state
    state.treasury += value / 10
    await generate_puzzle()

    yield f"Looks right to me! You earned {value} RollCoins (and generated some for the treasury). That brings your balance to {sender_wallet.balance}", reply
    yield f"The current mining challenge is\n{Sudoku(board=state.mining_puzzle)}"


@as_command
async def appraise(
    logger: Logger,
    origin: Origin,
    attachments: Attachments,
    sender_id: Sender,
    sender_name: SenderName,
    get_sender_wallet: Lazy(SenderWallet),
    wallet_store: Data(RollcoinWallet),
    state: State,
    state_store: Data(RollcoinState),
    received_channel: Channel,
    appraise_channel: AppraiseChannel,
    allow_self_likes: AppraiseAllowSelfLike,
    require_ownership: AppraiseRequireOwnership,
):
    """
    Use this command while replying to a popular post, and I will purchase it for some RollCoins!
    """

    def make_response(text):
        return Response(
            origin_id=origin,
            channel_id=appraise_channel or received_channel,
            text=text,
        )

    if origin == "GROUPME":
        try:
            reply_attachment = next(a for a in attachments if a.name == "reply")
            target_msg = json.loads(await reply_attachment.body())
        except:
            logger.exception("Failed appraisal, logging for debugging")
            RollbotFailure.INVALID_ARGUMENTS.raise_exc(
                "Reply to a message to have it appraised, I could not read that one"
            )
        appraisal_id = f"GROUPME-{target_msg['group_id']}-{target_msg['id']}"
        if require_ownership and target_msg["sender_id"] != sender_id:
            RollbotFailure.INVALID_ARGUMENTS.raise_exc(
                "You can only appraise your own posts!"
            )
        score_base = len(
            [
                t
                for t in target_msg["favorited_by"]
                if allow_self_likes or t != target_msg["sender_id"]
            ]
        )
    elif origin == "DISCORD":
        try:
            target_msg = next(a for a in attachments if a.name == "reply").body
            score_base = 0
            for r in target_msg.reactions:
                for u in await r.users().flatten():
                    if allow_self_likes or str(u.id) != sender_id:
                        score_base += 1
        except:
            logger.exception("Failed appraisal, logging for debugging")
            RollbotFailure.INVALID_ARGUMENTS.raise_exc(
                "Reply to a message to have it appraised, I could not read that one"
            )
        appraisal_id = f"DISCORD-{target_msg.channel.id}-{target_msg.id}"
        if require_ownership and str(target_msg.author.id) != sender_id:
            RollbotFailure.INVALID_ARGUMENTS.raise_exc(
                "You can only appraise your own posts!"
            )
    # other origins can be handled here
    else:
        RollbotFailure.INVALID_COMMAND.raise_exc(
            f"Message appraisal is not implemented in this platform (origin was {origin})"
        )

    if appraisal_id in state.appraised:
        return make_response(
            f"Sorry {sender_name}, I've already purchased that message!"
        )

    if score_base <= 2:
        return make_response(
            f"Sorry {sender_name}, I don't think that message is worth very much..."
        )

    value = round(abs(random.gauss(score_base * 50, 15)), 2)
    sender_wallet = await get_sender_wallet()
    sender_wallet.balance += value
    await wallet_store.save(sender_id, sender_wallet)
    state.appraised.append(appraisal_id)
    await state_store.save(GLOBAL_STATE_KEY, state)

    return make_response(
        f"That post is very interesting, {sender_name}!\nI will purchase it for {value} RollCoins!\nThat brings your balance to {sender_wallet.balance}"
    )


def validate_pulls(pulls, balance):
    if not (1 <= pulls <= 10):
        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
            "Number of pulls must be between 1 and 10"
        )

    if balance < pulls:
        return f"You only have {balance} RollCoins available, and gacha pulls cost one each!"


def pull_gacha_color():
    pull = random.randint(1, 100)
    if pull == 100:  # 1%
        return NFT_ULTRA
    if pull >= 90:  # 10%
        return NFT_RARE
    if pull >= 70:  # 20%
        return NFT_UNCOMMON
    return random.choice(NFT_COLORS)  # nice%


async def pull_gacha(req, name_api_key, logger):
    color1 = pull_gacha_color()
    color2 = pull_gacha_color()

    try:
        async with req.get(NAME_URL, headers={"X-Api-Key": name_api_key}) as res:
            name = (await res.json())[0]
        async with req.get(
            ICON_URL + f"&primaryColor={color1}&secondaryColor={color2}"
        ) as res:
            img = Attachment(name="image", body=await res.read())
    except:
        logger.exception("Failed gacha, logging for debugging")
        RollbotFailure.SERVICE_DOWN.raise_exc(
            "Failed to pull from gachapon, your coin has not been deducted"
        )

    # calculate rarity
    rarity = (NFT_RARITY_LOOKUP.get(color1, 0) + NFT_RARITY_LOOKUP.get(color2, 0)) // 2
    if color1 == color2:
        color_info = f"double {color1}!"
        rarity += 1
    else:
        color_info = f"{color1} and {color2}"
    rarity_info = NFT_MAX_RARITY if rarity >= len(NFT_RARITY) else NFT_RARITY[rarity]

    return f"{name} ({color_info}) ({rarity_info})", img


async def run_gacha_pulls(receiver_name, req, name_api_key, sleep_time, logger, pulls):
    info, img = await pull_gacha(req, name_api_key, logger)
    if pulls == 1:
        yield f"{receiver_name} received...\n\t{info}", img
        return
    yield f"{receiver_name} received (1/{pulls})...\n\t{info}", img
    for i in range(pulls - 1):
        await asyncio.sleep(sleep_time)
        try:
            info, img = await pull_gacha(req, name_api_key, logger)
            yield f"{receiver_name} received ({i + 2}/{pulls})...\n\t{info}", img
        except:
            logger.exception("Failed to pull")
            yield f"Failed to pull from gachapon! You will not be charged the coin for this pull.",


@as_command
async def gacha(
    sender_wallet: SenderWallet,
    get_sender_wallet: Lazy(
        SenderWallet
    ),  # used for re-querying to mitigate race condition
    sender_id: Sender,
    wallet_store: Data(RollcoinWallet),
    get_state: Lazy(State),
    state_store: Data(RollcoinState),
    sleep_time: GachaSleep,
    logger: Logger,
    req: Request,
    name_api_key: NameAPIKey,
    pulls: Arg(
        0,
        convert=int,
        required=False,
        default=1,
        fail_msg="Number of pulls must be an integer, not {}",
    ),
    reply: Reply,
):
    """
    Spend one RollCoin to pull from the gachapon! It dispenses the hottest new commodity - broken virtual pets from the 90s!
    These "Non-Functional Tamagotchis" or "NFTs" have varying rarities - good luck!
    """
    if (invalid := validate_pulls(pulls, sender_wallet.balance)) is not None:
        yield invalid, reply
        return

    pulled = []
    async for result in run_gacha_pulls(
        "You", req, name_api_key, sleep_time, logger, pulls
    ):
        pulled.append(result[0])
        yield *result, reply

    sender_wallet = await get_sender_wallet()
    sender_wallet.balance -= len(pulled)
    sender_wallet.nfts += pulled
    await wallet_store.save(sender_id, sender_wallet)

    state = await get_state()
    state.treasury += len(pulled)
    await state_store.save(GLOBAL_STATE_KEY, state)


@as_command
async def giftcha(
    origin: Origin,
    sender_wallet: SenderWallet,
    get_sender_wallet: Lazy(
        SenderWallet
    ),  # used for re-querying to mitigate race condition
    sender_id: Sender,
    wallet_store: Data(RollcoinWallet),
    get_state: Lazy(State),
    state_store: Data(RollcoinState),
    sleep_time: GachaSleep,
    logger: Logger,
    req: Request,
    name_api_key: NameAPIKey,
    wallet_lookup: WalletLookup,
    target_name: Arg(0, missing_msg="Must provide a target to tax"),
    pulls: Arg(
        1,
        convert=int,
        required=False,
        default=1,
        fail_msg="Number of pulls must be an integer, not {}",
    ),
    reply: Reply,
):
    """
    Spend one RollCoin to pull from the gachapon, but selflessly gift the resulting NFT to someone else!
    """
    if (invalid := validate_pulls(pulls, sender_wallet.balance)) is not None:
        yield invalid, reply
        return

    if (target_id := wallet_lookup[origin].get(target_name.lower(), None)) is None:
        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
            f"Could not find a wallet for {target_name}"
        )

    pulled = []
    async for result in run_gacha_pulls(
        target_name.title(), req, name_api_key, sleep_time, logger, pulls
    ):
        pulled.append(result[0])
        yield result

    sender_wallet = await get_sender_wallet()
    sender_wallet.balance -= len(pulled)
    await wallet_store.save(sender_id, sender_wallet)

    target_wallet = await wallet_store.load_or(target_id)
    target_wallet.nfts += pulled
    await wallet_store.save(target_id, target_wallet)

    state = await get_state()
    state.treasury += len(pulled)
    await state_store.save(GLOBAL_STATE_KEY, state)


# ADMIN COMMANDS


@as_command
async def deflate(
    origin_admin: OriginAdmin,
    power: Arg(
        0,
        convert=int,
        missing_msg="Must provide power to deflate by",
        fail_msg="Power to deflate by must be an integer, not {}",
    ),
    wallet_store: Data(RollcoinWallet),
    state_store: Data(RollcoinState),
):
    """
    Admins only: Deflate the value of the RollCoin by a factor of 10 to the given power, which can be negative to inflate.
    """
    if not origin_admin:
        RollbotFailure.PERMISSIONS.raise_exc("Only admins can deflate the currency")

    factor = 10**power

    state = await state_store.load(GLOBAL_STATE_KEY)
    state.treasury /= factor
    await state_store.save(GLOBAL_STATE_KEY, state)

    async for (wallet_id, wallet) in wallet_store.all():
        wallet.balance /= factor
        wallet.holdings /= factor
        wallet.cost_basis /= factor
        await wallet_store.save(wallet_id, wallet)

    return f"Economy deflated by a factor of 10^{power}"


@as_command
async def brrr(
    origin_admin: OriginAdmin,
    coins: Arg(
        0,
        convert=float,
        missing_msg="Must provide coins to mint",
        fail_msg="Coins to mint by must be a number, not {}",
    ),
    state_store: Data(RollcoinState),
):
    """
    Admins only: Print more RollCoins into the treasury, which can be useful to help the economy! You can also delete them with a negative number
    """
    if not origin_admin:
        RollbotFailure.PERMISSIONS.raise_exc("Only admins can mint coins")

    if coins <= 0:
        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
            "Can only mint a positive number of coins"
        )

    state = await state_store.load(GLOBAL_STATE_KEY)
    state.treasury += coins
    await state_store.save(GLOBAL_STATE_KEY, state)

    return f"{coins} new RollCoins have been added to the treasury!"


@as_command
async def tax(
    origin: Origin,
    origin_admin: OriginAdmin,
    wallet_lookup: WalletLookup,
    target_name: Arg(0, missing_msg="Must provide a target to tax"),
    coins: Arg(
        1,
        convert=float,
        missing_msg="Must provide coins to tax",
        fail_msg="Coins to tax by must be a number, not {}",
    ),
    wallet_store: Data(RollcoinWallet),
    state_store: Data(RollcoinState),
):
    """
    Admins only: Tax a user to put some of their RollCoins into the treasury, or use a negative number to provide a rebate
    """
    if not origin_admin:
        RollbotFailure.PERMISSIONS.raise_exc("Only admins can tax someone")

    if (target_id := wallet_lookup[origin].get(target_name.lower(), None)) is None or (
        wallet := await wallet_store.load(target_id)
    ) is None:
        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
            f"Could not find a wallet for {target_name}"
        )

    state = await state_store.load(GLOBAL_STATE_KEY)
    state.treasury += coins
    wallet.balance -= coins
    await state_store.save(GLOBAL_STATE_KEY, state)
    await wallet_store.save(target_id, wallet)

    return f"{coins} RollCoins were taken from {target_name} and put in the treasury!"


@as_command
async def market(
    origin_admin: OriginAdmin,
    target_state: Arg(
        0,
        convert=int,
        missing_msg="Must provide target state for market",
        fail_msg="Target market state must be an integer, not {}",
    ),
    state_store: Data(RollcoinState),
    messages: MarketMessages,
):
    """
    Admins only: Force the market into a certain state
    """
    if not origin_admin:
        RollbotFailure.PERMISSIONS.raise_exc("Only admins can manipulate the market")

    if target_state < 0 or target_state >= len(messages):
        RollbotFailure.INVALID_ARGUMENTS.raise_exc("Market state invalid")

    state = await state_store.load(GLOBAL_STATE_KEY)
    state.market_state = target_state
    await state_store.save(GLOBAL_STATE_KEY, state)

    return f"Market status: {messages[state.market_state]}"
