import dateutil.parser

from blockapi.services import (
    BlockchainAPI,
    set_default_args_values,
    APIError,
    on_failure_return_none
)


class TzscanAPI(BlockchainAPI):
    """
    Tezos
    API docs: https://tzscan.io/api
    Explorer: https://tzscan.io
    """

    symbol = 'XTZ'
    base_url = None # endpoint is created in runtime
    rate_limit = 0
    coef = 1e-6
    max_items_per_page = 50
    page_offset_step = 1

    supported_requests = {
        'get_balance': '/v3/balance_from_balance_updates/{address}',
        'get_operations': '/v3/operations/{address}?type={type}&p={page_offset}&number={number}',
        'get_rewards': '/v3/rewards_split_cycles/{address}?p={page_offset}&number={number}',
        'get_rewards_split': '/v2/rewards_split/{address}?cycle={cycle}&number=50&p={page}',
        'get_bakings': '/v3/cycle_bakings/{address}?p={page_offset}&number={number}',
        'get_endorsements': '/v3/cycle_endorsements/{address}?p={page_offset}&number={number}'
    }

    def __init__(self, address, api_key=None):
        super().__init__(address)
        # this API is very unstable, add some functionality to prevent fails
        self._base_url_temp = 'https://api{num}.dunscan.io'  # num = 1-6
        self._api_nums = [1, 2, 3, 4, 5, 6]

    @on_failure_return_none()
    def get_balance(self):
        balance = self._safe_request('get_balance', address=self.address)
        return [{'symbol': self.symbol,
                 'amount': float(balance['spendable']) * self.coef}]

    def get_rewards(self, offset=0, limit=50):
        rewards = self._safe_request('get_rewards', address=self.address,
            page_offset=offset, number=limit)

        return [self.get_reward_details(reward, int(reward['cycle'])) for reward in rewards]

    def get_reward_details(self, reward_v3, cycle=None):
        reward = self._safe_request(
            'get_rewards_split', address=self.address, cycle=cycle,page=0)

        total_staking_balance = float(reward['delegate_staking_balance'])
        baker_balance = total_staking_balance
        total_rewards = float(reward['blocks_rewards']) + \
                        float(reward['endorsements_rewards']) + \
                        float(reward['fees']) + \
                        float(reward['future_blocks_rewards']) + \
                        float(reward['future_endorsements_rewards']) + \
                        float(reward['gain_from_denounciation_baking']) - \
                        float(reward['lost_deposit_from_denounciation_baking']) - \
                        float(reward['lost_fees_denounciation_baking']) - \
                        float(reward['lost_rewards_denounciation_baking']) + \
                        float(reward['gain_from_denounciation_endorsement']) - \
                        float(reward['lost_deposit_from_denounciation_endorsement']) - \
                        float(reward['lost_fees_denounciation_endorsement']) - \
                        float(reward['lost_rewards_denounciation_endorsement']) + \
                        float(reward['revelation_rewards']) - \
                        float(reward['lost_revelation_rewards']) - \
                        float(reward['lost_revelation_fees'])

        total_staking_balance = round(float(total_staking_balance) / 1000000, 6)


        total_delegators = int(reward['delegators_nb'])
        pages = total_delegators / 50

        delegations = []
        delegators = 0
        page = 0

        while True:
            for del_balance in reward['delegators_balance']:
                delegator_address = del_balance[0]['tz']
                delegator_balance = int(del_balance[1]) * self.coef
                delegations.append({'address': delegator_address,
                                    'balance': delegator_balance,
                                    'share': (float(delegator_balance) / total_staking_balance) * 100,
                                    'rewards_share': (delegator_balance/total_staking_balance) * (total_rewards * self.coef)})
                delegators += 1

            page += 1
            if page < pages:
                reward = self._safe_request(
                    'get_rewards_split', address=self.address, cycle=cycle,page=page)
            else:
                break

        return { 'staking_balance': total_staking_balance, 
                 'cycle': cycle,
                 'status': reward_v3['status']['status'],
                 'delegators': delegators,
                 'delegations': delegations,
                 'end_rewards': float(reward['endorsements_rewards']) * self.coef,
                 'blocks_rewards': float(reward['blocks_rewards']) * self.coef,
                 'extra_rewards': float(reward['revelation_rewards']) * self.coef,
                 'losses': float(reward['lost_revelation_rewards']) * self.coef,
                 'fees': float(reward['fees']) * self.coef
        }

    def get_txs(self, offset=None, limit=None, unconfirmed=False):
        return self._get_operations(
            'Transaction', self.parse_tx, offset, limit)

    def get_activations(self, offset=None, limit=None, unconfirmed=False):
        return self._get_operations(
            'Activation', self.parse_activation, offset, limit)

    def get_originations(self, offset=None, limit=None, unconfirmed=False):
        return self._get_operations(
            'Origination', self.parse_origination, offset, limit)

    def get_delegations(self, offset=None, limit=None, unconfirmed=False):
        return self._get_operations(
            'Delegation', self.parse_delegation, offset, limit)

    @set_default_args_values
    def _get_operations(self, op_type, parse, offset=None, limit=None):
        """Get all operations by type
        @op_type in [Transaction, Origination, Delegation, Activation, ...]"""

        operations = self._safe_request(
            'get_operations',
            address=self.address,
            type=op_type,
            page_offset=offset,
            number=limit
        )

        parsed_txs = []
        for tx in operations:
            parsed_txs += parse(tx)
        return parsed_txs

    def parse_tx(self, tx):
        # tezos can have multiple txs in single tx (with one hash)
        parsed = []
        for op in tx.get('type', {}).get('operations', []):
            parsed.append({
                'date': dateutil.parser.parse(op['timestamp']),
                'from_address': op['src']['tz'],
                'to_address': op['destination']['tz'],
                'amount': float(op['amount']) * self.coef,
                'fee': None if op['fee'] == -1 else op['fee'] * self.coef,
                'gas_limit': None if int(op['gas_limit']) == -1 else int(op['gas_limit']),
                'hash': tx['hash'],
                'confirmed': None,
                'is_error': op['failed'],
                'type': 'internal' if op['internal'] else 'normal',
                'kind': op['kind'].lower(),
                'direction': 'outgoing' if self.address == op['src']['tz'] else 'incoming',
                'raw': tx
            })
            return parsed

    def parse_delegation(self, tx):
        parsed = []
        for op in tx.get('type', {}).get('operations', []):
            parsed.append({
                'date': dateutil.parser.parse(op['timestamp']),
                'source_address': op['src']['tz'],
                'delegate': op['delegate']['tz'],
                'fee': None if op['fee'] == -1 else op['fee'] * self.coef,
                'gas_limit': None if int(op['gas_limit']) == -1 else int(op['gas_limit']),
                'hash': tx['hash'],
                'is_error': op['failed'],
                'type': 'internal' if op['internal'] else 'normal',
                'kind': op['kind'].lower(),
                'raw': tx
            })
            return parsed

    def parse_activation(self, tx):
        parsed = []
        for op in tx.get('type', {}).get('operations', []):
            parsed.append({
                'date': dateutil.parser.parse(op['timestamp']),
                'secret': op['secret'],
                'balance': op['balance'],
                'hash': tx['hash'],
                'kind': op['kind'].lower(),
                'raw': tx
            })
            return parsed

    def parse_origination(self, tx):
        parsed = []
        for op in tx.get('type', {}).get('operations', []):
            parsed.append({
                'date': dateutil.parser.parse(op['timestamp']),
                'originator': op['src']['tz'],
                'manager': op['managerPubkey']['tz'],
                'balance': op['balance'],
                'spendable': op['spendable'],
                'delegatable': op['delegatable'],
                'delegate': op['delegate']['tz'],
                'delegate_alias': op['delegate']['alias'],
                'burnt': op['burn_tez'] * self.coef,
                'fee': None if op['fee'] == -1 else op['fee'] * self.coef,
                'gas_limit': None if int(op['gas_limit']) == -1 else int(op['gas_limit']),
                'hash': tx['hash'],
                'is_error': op['failed'],
                'type': 'internal' if op['internal'] else 'normal',
                'kind': op['kind'].lower(),
                'raw': tx
            })
            return parsed

    @set_default_args_values
    def get_endorsements(self, offset=None, limit=None):
        ends = self._safe_request(
            'get_endorsements',
            address=self.address,
            page_offset=offset,
            number=limit
        )
        return [self.parse_endorsement(e) for e in ends]

    def parse_endorsement(self, e):
        return {
            'cycle': int(e['cycle']),
            'depth': int(e['depth']),
            'num_slots': {
                'all': int(e['slots']['count_all']),
                'miss': int(e['slots']['count_miss']),
                'steal': int(e['slots']['count_steal'])
            },
            'fee': int(e['tez']['fee']) * self.coef,
            'reward': int(e['tez']['reward']) * self.coef,
            'deposit': int(e['tez']['deposit']) * self.coef,
            'priority': float(e['priority'])
        }

    @set_default_args_values
    def get_bakings(self, offset=None, limit=None):
        baks = self._safe_request(
            'get_bakings',
            address=self.address,
            page_offset=offset,
            number=limit
        )
        return [self.parse_baking(b) for b in baks]

    def parse_baking(self, b):
        return {
            'cycle': int(b['cycle']),
            'depth': int(b['depth']),
            'num_blocks': {
                'all': int(b['count']['count_all']),
                'miss': int(b['count']['count_miss']),
                'steal': int(b['count']['count_steal'])
            },
            'fee': int(b['tez']['fee']) * self.coef,
            'reward': int(b['tez']['reward']) * self.coef,
            'deposit': int(b['tez']['deposit']) * self.coef,
            'priority': float(b['priority']),
            'bake_time': int(b['bake_time'])
        }

    def _safe_request(self, method, **params):
        api_nums = self._api_nums.copy()
        while api_nums:
            try:
                api_num = api_nums.pop(0)
                self.base_url = self._base_url_temp.format(num=api_num)
                response = self.request(method, **params)
            except APIError as e:
                ex = e
                continue
            else:
                return response

        # when all endpoints are checked and none of them is valid, raise exc
        raise ex
