from typing import Any, Union, Dict, cast, List

from boa3.builtin import NeoMetadata, metadata, public, CreateNewEvent
from boa3.builtin.contract import Nep17TransferEvent, abort
from boa3.builtin.interop.blockchain import get_contract, Transaction
from boa3.builtin.interop.contract import call_contract, update_contract
from boa3.builtin.interop.runtime import calling_script_hash, check_witness, script_container
from boa3.builtin.interop.storage import delete, get, put
from boa3.builtin.interop.stdlib import serialize, deserialize
from boa3.builtin.type import UInt160

# TODO: Migrate UAC to use the account prop via contract call

# ---------------------------------
# CONTRACT HEADER
# ---------------------------------

CONTRACT_NAME = 'TranslateMe Network'
CONTRACT_VERSION = 'v0.0.1'
AUTHOR = 'COZ, Inc.'
EMAIL = 'contact@coz.io'
DESCRIPTION = 'This is the NEP17 token contract for the TranslateMe Network.'
DESCRIPTION_EXTENDED = ''


@metadata
def manifest_metadata() -> NeoMetadata:
    meta = NeoMetadata()
    meta.author = 'COZ, Inc.'
    meta.email = 'contact@coz.io'
    meta.description = 'This is the NEP17 token contract for the TranslateMe Network.'
    meta.version = "v0.0.1"
    return meta

# ---------------------------------
# CONTRACT GLOBALS
# ---------------------------------


ACCOUNT_PREFIX = b'a'
TOKEN_DECIMALS = 8
TOKEN_PREFIX = b't'
TOKEN_SYMBOL = 'TMN'
ACCOUNT_COUNT = b'!ACCOUNT_COUNT'
DEPLOYED = b'!DEPLOYED'
TOTAL_SUPPLY = b'!TOTAL_SUPPLY'
MAX_SUPPLY = 2000000000

# ---------------------------------
# EVENTS
# ---------------------------------

on_transfer = Nep17TransferEvent

# ---------------------------------
# Methods
# ---------------------------------


@public
def symbol() -> str:
    """
    Gets the symbols of the token.

    :return: a short string representing symbol of the token managed in this contract.
    """
    return TOKEN_SYMBOL


@public
def decimals() -> int:
    """
    Gets the amount of decimals used by the token.

    :return: the number of decimals used by the token.
    """
    return TOKEN_DECIMALS


@public
def totalSupply() -> int:
    """
    Gets the total token supply deployed in the system.

    :return: the total token supply in the system.
    """
    return get(TOTAL_SUPPLY).to_int()


@public
def balanceOf(account: UInt160) -> int:
    """
    Get the current balance of an address

    :param account: the account address to retrieve the balance for

    :return: the accounts balance
    """
    assert len(account) == 20
    user: User = get_user(account)
    return user.get_balance_of()


@public
def transfer(from_address: UInt160, to_address: UInt160, amount: int, data: Any) -> bool:
    """
    Transfers an amount of TMN tokens from one account to another

    If the method succeeds, it must fire the `Transfer` event and must return true, even if the amount is 0,
    or from and to are the same address.

    :param from_address: the address to transfer from
    :param to_address: the address to transfer to
    :param amount: the amount of NEP17 tokens to transfer
    :param data: whatever data is pertinent to the onPayment method

    :return: whether the transfer was successful
    :raise AssertionError: raised if `from_address` or `to_address` length is not 20.
    """
    # the parameters from and to should be 20-byte addresses. If not, this method should throw an exception.
    assert len(from_address) == 20 and len(to_address) == 20

    from_user: User = get_user(from_address)
    to_user: User = get_user(to_address)

    # The function MUST return false if the from account balance does not have enough tokens to spend.
    from_balance = from_user.get_balance_of()
    to_balance = to_user.get_balance_of()

    if from_balance < amount:
        return False

    # The function should check whether the from address equals the caller contract hash.
    # If so, the transfer should be processed;
    # If not, the function should use the check_witness to verify the transfer.
    if from_address != calling_script_hash:
        if not check_witness(from_address):
            return False

    # skip balance changes if transferring to yourself or transferring 0 cryptocurrency
    if from_address != to_address and amount != 0:
        x: bool = from_user.set_balance_of(from_balance - amount)
        x = to_user.set_balance_of(to_balance + amount)
        save_user(from_address, from_user)
        save_user(to_address, to_user)

    # if the method succeeds, it must fire the transfer event
    on_transfer(from_address, to_address, amount)
    # if the to_address is a smart contract, it must call the contracts onPayment
    post_transfer(from_address, to_address, amount, data)
    # and then it must return true
    return True


def post_transfer(from_address: Union[UInt160, None], to_address: Union[UInt160, None], amount: int, data: Any):
    """
    Checks if the one receiving TMN tokens is a smart contract and if is is, the onPayment method will be called.

    :param from_address: the address of the sender
    :param to_address: the address of the receiver
    :param amount: the amount of cryptocurrency that is being sent
    :param data: any pertinent data that might validate the transaction
    """
    if not isinstance(to_address, None):
        contract = get_contract(to_address)
        if not isinstance(contract, None):
            call_contract(to_address, 'onNEP17Payment', [from_address, amount, data])


@public
def mint(account: UInt160, amount: int) -> bool:
    """
    Mints TMN tokens.

    :param account: the address of the account that is sending cryptocurrency to this contract
    :param amount: the amount of gas to be refunded
    :raise AssertionError: raised if amount is less than than 0 or the user does not have permissions
    """

    tx: Transaction = cast(Transaction, script_container)
    signer: UInt160 = tx.sender
    user: User = get_user(signer)

    assert user.get_permissions()["mint"], 'User Permission Denied'
    assert amount >= 0, 'Amount must be greater than 0'

    current_total_supply: int = totalSupply()
    new_total_supply: int = current_total_supply + amount


    # exit if the new total supply would exceed the max supply
    assert new_total_supply <= MAX_SUPPLY, "The new total supply would exceed the max supply"

    internal_mint(account, amount)

    put(TOTAL_SUPPLY, new_total_supply)

    on_transfer(None, account, amount)
    post_transfer(None, account, amount, None)

    return True


@public
def mint_batch(events: [List]) -> bool:
    """
    Executes a mint operation in batch mode
    :param events: A list of events of type Array(account: Uint160, amount: int)
    :return: a boolean indicating success
    :raise AssertionError: raised if the user lacks permissions
    :raise AssertionError: raised if a mint amount is LTE 0
    :raise AssertionError: raised if the total supply would extend beyond the max supply
    """
    tx = cast(Transaction, script_container)
    signer: UInt160 = tx.sender
    user: User = get_user(signer)

    assert user.get_permissions()["mint"], 'User Permission Denied'

    current_total_supply: int = totalSupply()
    new_total_supply: int = current_total_supply
    for event in events:

        account: UInt160 = cast(UInt160, event[0])
        amount: int = cast(int, event[1])
        assert amount >= 0, 'Amount must be greater than 0'


        new_total_supply: int = new_total_supply + amount
        assert new_total_supply <= MAX_SUPPLY, "The new total supply would exceed the max supply"


        internal_mint(account, amount)

        on_transfer(None, account, amount)
        post_transfer(None, account, amount, None)

    put(TOTAL_SUPPLY, new_total_supply)

    return True


def internal_mint(account: UInt160, amount: int) -> bool:
    user_account: User = get_user(account)
    user_account_balance: int = user_account.get_balance_of()
    x: bool = user_account.set_balance_of(user_account_balance + amount)
    save_user(account, user_account)
    return True


@public
def onNEP17Payment(from_address: UInt160, amount: int, data: Any):
    """
    This contract is currently not accepting any transfers.

    :param from_address: the address of the one who is trying to send cryptocurrency to this smart contract
    :param amount: the amount of cryptocurrency that is being sent to the this smart contract
    :param data: any pertinent data that might validate the transaction
    """
    abort()


@public
def deploy() -> bool:
    """
    The contracts initial entry point, on deployment.
    """
    if get(DEPLOYED).to_bool():
        return False

    tx = cast(Transaction, script_container)
    owner: UInt160 = tx.sender

    internal_deploy(owner)
    return True


def internal_deploy(owner: UInt160) -> bool:
    """
    This method executes a deploy operation for the contract, setting initial parameters for operation.
    :param owner: The initial admin account for the contract.
    :return: a boolean indicating success.
    """
    put(DEPLOYED, True)
    put(TOTAL_SUPPLY, 0)
    put(ACCOUNT_COUNT, 0)

    super_user_permissions: Dict[str, bool] = {
        'set_permissions': True,
        'mint': True,
        'update': True
    }
    user: User = User()
    x: bool = user.set_permissions(super_user_permissions)
    save_user(owner, user)
    return True


@public
def update(script: bytes, manifest: bytes, data: Any):
    """
    Updates the smart contract script
    :param script: The new script to update to
    :param manifest: The new manifest to update to
    :param data: additional data field
    :return:
    :raise AssertionError: raised if the user lacks the "update" permission
    """
    tx = cast(Transaction, script_container)
    signer: UInt160 = tx.sender
    invoking_user: User = get_user(signer)
    assert invoking_user.get_permissions()["update"], "User Permission Denied"

    update_contract(script, manifest, data)


class User:

    def __init__(self):
        self._balance: int = 0
        self._account_id: bytes = (total_accounts() + 1).to_bytes()
        self._permissions: Dict[str, bool] = {
            'set_permissions': False,
            'mint': False,
            'update': False
        }

    def export(self) -> Dict[str, Any]:
        exported: Dict[str, Any] = {
            'balance': self._balance,
            'account_id': self._account_id,
            'permissions': self._permissions
        }
        return exported

    def set_permissions(self, permissions: Dict[str, bool]) -> bool:
        perm_clone = self._permissions
        for key in permissions.keys():
            perm_clone[key] = permissions[key]
        self._permissions = perm_clone
        return True

    def get_account_id(self) -> bytes:
        return self._account_id

    def get_balance_of(self) -> int:
        return self._balance

    def set_balance_of(self, amount: int) -> bool:
        self._balance = amount
        return True

    def get_permissions(self) -> Dict[str, bool]:
        return self._permissions


@public
def get_user_json(address: UInt160) -> Dict[str, Any]:
    """
    Gets the json formatted user information
    :param address: The address to get the account information for
    :return: A dictionary representing the user account
    """
    user: User = get_user(address)
    return user.export()

@public
def get_user(address: UInt160) -> User:
    """
    Gets the raw user object
    :param address: The address to get the account information for
    :return: A user object representing the requested user
    """
    user_bytes: bytes = get_user_raw(address)
    if len(user_bytes) != 0:
        user: User = cast(User, deserialize(user_bytes))
        return user

    return User()


def get_user_raw(address: UInt160) -> bytes:
    """
    Gets the raw bytes representing a user object
    :param address: The address being requested
    :return: A bytestring representing the user object
    """
    return get(mk_user_key(address))


def save_user(address: UInt160, user: User) -> bool:
    """
    Saves a user object
    :param address: The address of the user
    :param user: The user object
    :return: A boolean indicating success
    """
    account_id: bytes = user.get_account_id()
    account_count: int = total_accounts()

    if account_id.to_int() > account_count:
        put(ACCOUNT_COUNT, account_id)

    put(mk_user_key(address), serialize(user))
    return True


def mk_user_key(address: UInt160) -> bytes:
    return ACCOUNT_PREFIX + address


@public
def set_user_permission(user: UInt160, permission: str, value: bool) -> bool:
    """
    Upserts a user permission
    :param user: The user account to update
    :param permission: the permission key to update
    :param value: A boolean representing the new account state
    :return: A boolean indicating success
    :raise AssertionError: raised if user lacks the "set_permissions" permissions
    """
    tx = cast(Transaction, script_container)
    signer: UInt160 = tx.sender
    invoking_user: User = get_user(signer)
    assert invoking_user.get_permissions()["set_permissions"], "User Permission Denied"

    impacted_user: User = get_user(user)

    newPerm: Dict[str, bool] = {}
    newPerm[permission] = value
    x: bool = impacted_user.set_permissions(newPerm)
    save_user(user, impacted_user)
    return True


@public
def total_accounts() -> int:
    """
    Gets the number of accounts.

    :return: the number of accounts in the system.
    """
    total: bytes = get(ACCOUNT_COUNT)
    if len(total) == 0:
        return 0
    return total.to_int()