import asyncio
from typing import List
from datetime import datetime
from gql.transport.exceptions import TransportQueryError

from wton.tonsdk.utils import to_nano, TonCurrencyEnum, from_nano, Address, b64str_to_bytes, bytes_to_b64str
from wton.tonsdk.boc import Cell
from wton.tonsdk.contract.wallet import SendModeEnum, WalletContract
from wton.config import Config
from wton.tonsdk.provider.dapp import DAppClient, DAppWrongResult, BroadcastQuery
from ..._exceptions import TonDappError
from .._base import TonClient, AddressInfoResult, BroadcastResult, BroadcastStatusEnum
from ._queries import DAppQueries


class DAppTonClient(TonClient):
    def __init__(self, config: Config):
        self.config = config
        self.loop = asyncio.get_event_loop()
        self._provider: DAppClient = None

    @property
    def provider(self):
        if self._provider is None:
            self._provider = DAppClient(graphql_url=self.config.provider.dapp.graphql_url,
                                        broadcast_url=self.config.provider.dapp.broadcast_url,
                                        api_key=self.config.provider.dapp.api_key)

        return self._provider

    def get_address_information(self, address: str,
                                currency_to_show: TonCurrencyEnum = TonCurrencyEnum.ton):
        return self.get_addresses_information([address], currency_to_show)[0]

    def get_addresses_information(self, addresses: List[str],
                                  currency_to_show: TonCurrencyEnum = TonCurrencyEnum.ton):
        if not addresses:
            return []

        address_ids = [Address(addr).to_string(
            False, False, False) for addr in addresses]
        query = DAppQueries.accounts(address_ids)
        results = self._run(self.provider.query(
            [query]), single_query=True)['accounts']

        address_infos = [None] * len(address_ids)
        for result in results:
            idx = address_ids.index(result['id'])
            address_infos[idx] = self._parse_addr_info(result)
        for i in range(len(address_infos)):
            if address_infos[i] is None:
                address_infos[i] = AddressInfoResult(
                    address=addresses[i], state="Uninit", balance="0")

        return address_infos

    def seqno(self, addr: str):
        if self.get_address_information(addr).seqno:
            return int(self.get_address_information(addr).seqno)

        return 0

    def deploy_wallet(self, wallet: WalletContract, wait_for_result=False):
        timeout = 30 if wait_for_result else 0
        query = wallet.create_init_external_message()
        base64_boc = bytes_to_b64str(query["message"].to_boc(False))
        result = self._run(self.provider.broadcast(
            [BroadcastQuery(boc=base64_boc, timeout=timeout)]))

        return self._parse_broadcast_result(result, wait_for_result)

    def transfer(self, from_wallet: WalletContract, to_addr: str, amount: TonCurrencyEnum.ton, payload=None,
                 send_mode=SendModeEnum.ignore_errors | SendModeEnum.pay_gas_separately, wait_for_result=False):
        timeout = 30 if wait_for_result else 0
        seqno = self.seqno(from_wallet.address.to_string())
        query = from_wallet.create_transfer_message(
            to_addr=Address(to_addr).to_string(False, False, True),  # TODO: set is bounceable or not
            amount=to_nano(amount, TonCurrencyEnum.ton),
            seqno=seqno,
            payload=payload,
            send_mode=send_mode,
        )
        msg_boc = query["message"].to_boc(False)
        base64_boc = bytes_to_b64str(msg_boc)
        result = self._run(self.provider.broadcast(
            [BroadcastQuery(boc=base64_boc, timeout=timeout)]))

        return self._parse_broadcast_result(result, wait_for_result)

    def _run(self, to_run, *, single_query=True):
        try:
            results = self.loop.run_until_complete(to_run)
        except TransportQueryError as e:
            raise TonDappError(str(e))
        except DAppWrongResult as e:
            raise TonDappError(str(e))

        if single_query:
            return results[0]

        return results

    def _parse_addr_info(self, result: dict, currency_to_show: TonCurrencyEnum = TonCurrencyEnum.ton):
        return AddressInfoResult(
            address=result['address'],
            # contract_type='wallet v3r2',  # FIXME
            seqno=self._get_seqno(result),
            state=result['acc_type_name'],
            balance=self._get_balance(result, currency_to_show),
            last_activity=self._get_last_paid(result),
            code=result['code'],
            data=result['data'],
        )

    def _get_seqno(self, result):
        if result['acc_type_name'] in ["Active", "Frozen"]:
            # TODO: check contract type and version
            data_cell = Cell.one_from_boc(b64str_to_bytes(result["data"]))
            if len(data_cell.bits) > 32:
                seqno = 0
                for bit in data_cell.bits[:32]:
                    seqno = (seqno << 1) | bit
                return seqno

        return 0

    def _get_balance(self, result: dict, currency_to_show):
        if "balance" in result and result["balance"]:
            if int(result["balance"]) < 0:
                balance = 0
            else:
                balance = from_nano(
                    float(result["balance"]), currency_to_show)
        else:
            balance = 0

        return balance

    def _get_last_paid(self, result: dict):
        if "last_paid" in result and result["last_paid"]:
            return str(datetime.utcfromtimestamp(
                result['last_paid']).strftime('%Y-%m-%d %H:%M:%S'))

    def _parse_broadcast_result(self, result, waited) -> BroadcastResult:
        if "status" in result:
            if result["status"] == 1 and waited:
                status = BroadcastStatusEnum.commited
            else:
                status = BroadcastStatusEnum.broadcasted
        else:
            status = BroadcastStatusEnum.failed

        return BroadcastResult(waited=waited, status=status)
