from __future__ import annotations

import sys
from decimal import Decimal
from typing import Any, Dict, List, Tuple, Union

from chik.cmds.units import units
from chik.cmds.wallet_funcs import get_mojo_per_unit, get_wallet_type, print_balance
from chik.rpc.wallet_rpc_client import WalletRpcClient
from chik.types.blockchain_format.coin import Coin
from chik.types.blockchain_format.sized_bytes import bytes32
from chik.types.coin_record import CoinRecord
from chik.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
from chik.util.ints import uint64, uint128
from chik.wallet.transaction_record import TransactionRecord
from chik.wallet.util.wallet_types import WalletType


async def async_list(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
    wallet_id: int = args["id"]
    min_coin_amount = Decimal(args["min_coin_amount"])
    max_coin_amount = Decimal(args["max_coin_amount"])
    excluded_coin_ids = args["excluded_coin_ids"]
    excluded_amounts = args["excluded_amounts"]
    addr_prefix = args["addr_prefix"]
    show_unconfirmed = args["show_unconfirmed"]
    paginate = args["paginate"]
    if paginate is None:
        paginate = sys.stdout.isatty()
    try:
        wallet_type = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client)
        mojo_per_unit = get_mojo_per_unit(wallet_type)
    except LookupError:
        print(f"Wallet id: {wallet_id} not found.")
        return
    if not await wallet_client.get_synced():
        print("Wallet not synced. Please wait.")
        return
    final_min_coin_amount: uint64 = uint64(int(min_coin_amount * mojo_per_unit))
    final_max_coin_amount: uint64 = uint64(int(max_coin_amount * mojo_per_unit))
    final_excluded_amounts: List[uint64] = [uint64(int(Decimal(amount) * mojo_per_unit)) for amount in excluded_amounts]
    conf_coins, unconfirmed_removals, unconfirmed_additions = await wallet_client.get_spendable_coins(
        wallet_id=wallet_id,
        max_coin_amount=final_max_coin_amount,
        min_coin_amount=final_min_coin_amount,
        excluded_amounts=final_excluded_amounts,
        excluded_coin_ids=excluded_coin_ids,
    )
    print(f"There are a total of {len(conf_coins) + len(unconfirmed_additions)} coins in wallet {wallet_id}.")
    print(f"{len(conf_coins)} confirmed coins.")
    print(f"{len(unconfirmed_additions)} unconfirmed additions.")
    print(f"{len(unconfirmed_removals)} unconfirmed removals.")
    print("Confirmed coins:")
    print_coins(
        "\tAddress: {} Amount: {}, Confirmed in block: {}\n",
        [(cr.coin, str(cr.confirmed_block_index)) for cr in conf_coins],
        mojo_per_unit,
        addr_prefix,
        paginate,
    )
    if show_unconfirmed:
        print("\nUnconfirmed Removals:")
        print_coins(
            "\tPrevious Address: {} Amount: {}, Confirmed in block: {}\n",
            [(cr.coin, str(cr.confirmed_block_index)) for cr in unconfirmed_removals],
            mojo_per_unit,
            addr_prefix,
            paginate,
        )
        print("\nUnconfirmed Additions:")
        print_coins(
            "\tNew Address: {} Amount: {}, Not yet confirmed in a block.{}\n",
            [(coin, "") for coin in unconfirmed_additions],
            mojo_per_unit,
            addr_prefix,
            paginate,
        )


def print_coins(
    target_string: str, coins: List[Tuple[Coin, str]], mojo_per_unit: int, addr_prefix: str, paginate: bool
) -> None:
    if len(coins) == 0:
        print("\tNo Coins.")
        return
    num_per_screen = 5 if paginate else len(coins)
    for i in range(0, len(coins), num_per_screen):
        for j in range(0, num_per_screen):
            if i + j >= len(coins):
                break
            coin, conf_height = coins[i + j]
            address = encode_puzzle_hash(coin.puzzle_hash, addr_prefix)
            amount_str = print_balance(coin.amount, mojo_per_unit, "", decimal_only=True)
            print(f"Coin ID: 0x{coin.name().hex()}")
            print(target_string.format(address, amount_str, conf_height))

        if i + num_per_screen >= len(coins):
            return None
        print("Press q to quit, or c to continue")
        while True:
            entered_key = sys.stdin.read(1)
            if entered_key == "q":
                return None
            elif entered_key == "c":
                break


async def async_combine(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
    wallet_id: int = args["id"]
    min_coin_amount = Decimal(args["min_coin_amount"])
    excluded_amounts = args["excluded_amounts"]
    number_of_coins = args["number_of_coins"]
    max_amount = Decimal(args["max_amount"])
    target_coin_amount = Decimal(args["target_coin_amount"])
    target_coin_ids: List[bytes32] = [bytes32.from_hexstr(coin_id) for coin_id in args["target_coin_ids"]]
    largest = bool(args["largest"])
    final_fee = uint64(int(Decimal(args["fee"]) * units["chik"]))
    if number_of_coins > 500:
        raise ValueError(f"{number_of_coins} coins is greater then the maximum limit of 500 coins.")
    try:
        wallet_type = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client)
        mojo_per_unit = get_mojo_per_unit(wallet_type)
    except LookupError:
        print(f"Wallet id: {wallet_id} not found.")
        return
    if not await wallet_client.get_synced():
        print("Wallet not synced. Please wait.")
        return
    is_xck: bool = wallet_type == WalletType.STANDARD_WALLET  # this lets us know if we are directly combining Chik
    final_max_amount = uint64(int(max_amount * mojo_per_unit)) if not target_coin_ids else uint64(0)
    final_min_coin_amount: uint64 = uint64(int(min_coin_amount * mojo_per_unit))
    final_excluded_amounts: List[uint64] = [uint64(int(Decimal(amount) * mojo_per_unit)) for amount in excluded_amounts]
    final_target_coin_amount = uint64(int(target_coin_amount * mojo_per_unit))
    if final_target_coin_amount != 0:  # if we have a set target, just use standard coin selection.
        removals: List[Coin] = await wallet_client.select_coins(
            amount=(final_target_coin_amount + final_fee) if is_xck else final_target_coin_amount,
            wallet_id=wallet_id,
            max_coin_amount=final_max_amount,
            min_coin_amount=final_min_coin_amount,
            excluded_amounts=final_excluded_amounts + [final_target_coin_amount],  # dont reuse coins of same amount.
        )
    else:
        conf_coins, _, _ = await wallet_client.get_spendable_coins(
            wallet_id=wallet_id,
            max_coin_amount=final_max_amount,
            min_coin_amount=final_min_coin_amount,
            excluded_amounts=final_excluded_amounts,
        )
        if len(target_coin_ids) > 0:
            conf_coins = [cr for cr in conf_coins if cr.name in target_coin_ids]
        if len(conf_coins) == 0:
            print("No coins to combine.")
            return
        if len(conf_coins) == 1:
            print("Only one coin found, you need at least two coins to combine.")
            return
        if largest:
            conf_coins.sort(key=lambda r: r.coin.amount, reverse=True)
        else:
            conf_coins.sort(key=lambda r: r.coin.amount)  # sort the smallest first
        if number_of_coins < len(conf_coins):
            conf_coins = conf_coins[:number_of_coins]
        removals = [cr.coin for cr in conf_coins]
    print(f"Combining {len(removals)} coins.")
    if input("Would you like to Continue? (y/n): ") != "y":
        return
    total_amount: uint128 = uint128(sum(coin.amount for coin in removals))
    if is_xck and total_amount - final_fee <= 0:
        print("Total amount is less than 0 after fee, exiting.")
        return
    target_ph: bytes32 = decode_puzzle_hash(await wallet_client.get_next_address(wallet_id, False))
    additions = [{"amount": (total_amount - final_fee) if is_xck else total_amount, "puzzle_hash": target_ph}]
    transaction: TransactionRecord = await wallet_client.send_transaction_multi(
        wallet_id, additions, removals, final_fee
    )
    tx_id = transaction.name.hex()
    print(f"Transaction sent: {tx_id}")
    print(f"To get status, use command: chik wallet get_transaction -f {fingerprint} -tx 0x{tx_id}")


async def async_split(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
    wallet_id: int = args["id"]
    number_of_coins = args["number_of_coins"]
    final_fee = uint64(int(Decimal(args["fee"]) * units["chik"]))
    # new args
    amount_per_coin = Decimal(args["amount_per_coin"])
    target_coin_id: bytes32 = bytes32.from_hexstr(args["target_coin_id"])
    if number_of_coins > 500:
        print(f"{number_of_coins} coins is greater then the maximum limit of 500 coins.")
        return
    try:
        wallet_type = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client)
        mojo_per_unit = get_mojo_per_unit(wallet_type)
    except LookupError:
        print(f"Wallet id: {wallet_id} not found.")
        return
    if not await wallet_client.get_synced():
        print("Wallet not synced. Please wait.")
        return
    is_xck: bool = wallet_type == WalletType.STANDARD_WALLET  # this lets us know if we are directly spitting Chik
    final_amount_per_coin = uint64(int(amount_per_coin * mojo_per_unit))
    total_amount = final_amount_per_coin * number_of_coins
    if is_xck:
        total_amount += final_fee
    # get full coin record from name, and validate information about it.
    removal_coin_record: CoinRecord = (await wallet_client.get_coin_records_by_names([target_coin_id]))[0]
    if removal_coin_record.coin.amount < total_amount:
        print(
            f"Coin amount: {removal_coin_record.coin.amount/ mojo_per_unit} "
            f"is less than the total amount of the split: {total_amount/mojo_per_unit}, exiting."
        )
        print("Try using a smaller fee or amount.")
        return
    additions: List[Dict[str, Union[uint64, bytes32]]] = []
    for i in range(number_of_coins):  # for readability.
        # we always use new addresses
        target_ph: bytes32 = decode_puzzle_hash(await wallet_client.get_next_address(wallet_id, new_address=True))
        additions.append({"amount": final_amount_per_coin, "puzzle_hash": target_ph})
    transaction: TransactionRecord = await wallet_client.send_transaction_multi(
        wallet_id, additions, [removal_coin_record.coin], final_fee
    )
    tx_id = transaction.name.hex()
    print(f"Transaction sent: {tx_id}")
    print(f"To get status, use command: chik wallet get_transaction -f {fingerprint} -tx 0x{tx_id}")
