# -*- coding: utf-8 -*-
import logging
import math
from datetime import date, datetime, timezone

from nectar.blockchaininstance import BlockChainInstance
from nectar.constants import STEEM_100_PERCENT
from nectargraphenebase.chains import known_chains

from .amount import Amount
from .utils import formatToTimeStamp

log = logging.getLogger(__name__)


class Hive(BlockChainInstance):
    """Connect to the Hive network.

    :param str node: Node to connect to *(optional)*
    :param str rpcuser: RPC user *(optional)*
    :param str rpcpassword: RPC password *(optional)*
    :param bool nobroadcast: Do **not** broadcast a transaction!
        *(optional)*
    :param bool unsigned: Do **not** sign a transaction! *(optional)*
    :param bool debug: Enable Debugging *(optional)*
    :param keys: Predefine the wif keys to shortcut the
        wallet database *(optional)*
    :type keys: array, dict, string
    :param wif: Predefine the wif keys to shortcut the
            wallet database *(optional)*
    :type wif: array, dict, string
    :param bool offline: Boolean to prevent connecting to network (defaults
        to ``False``) *(optional)*
    :param int expiration: Delay in seconds until transactions are supposed
        to expire *(optional)* (default is 30)
    :param str blocking: Wait for broadcasted transactions to be included
        in a block and return full transaction (can be "head" or
        "irreversible")
    :param bool bundle: Do not broadcast transactions right away, but allow
        to bundle operations. It is not possible to send out more than one
        vote operation and more than one comment operation in a single broadcast *(optional)*
    :param bool appbase: Use the new appbase rpc protocol on nodes with version
        0.19.4 or higher. The settings has no effect on nodes with version of 0.19.3 or lower.
    :param int num_retries: Set the maximum number of reconnects to the nodes before
        NumRetriesReached is raised. Disabled for -1. (default is -1)
    :param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5)
    :param int timeout: Timeout setting for https nodes (default is 60)
    :param bool use_hs: When True, a hivesigner object is created. Can be used for
        broadcast posting op or creating hot_links (default is False)
    :param HiveSigner hivesigner: A HiveSigner object can be set manually, set use_hs to True
    :param dict custom_chains: custom chain which should be added to the known chains

    Three wallet operation modes are possible:

    * **Wallet Database**: Here, the nectarlibs load the keys from the
      locally stored wallet SQLite database (see ``storage.py``).
      To use this mode, simply call ``Hive()`` without the
      ``keys`` parameter
    * **Providing Keys**: Here, you can provide the keys for
      your accounts manually. All you need to do is add the wif
      keys for the accounts you want to use as a simple array
      using the ``keys`` parameter to ``Steem()``.
    * **Force keys**: This more is for advanced users and
      requires that you know what you are doing. Here, the
      ``keys`` parameter is a dictionary that overwrite the
      ``active``, ``owner``, ``posting`` or ``memo`` keys for
      any account. This mode is only used for *foreign*
      signatures!

    If no node is provided, it will connect to default nodes from
    nectar.NodeList. Default settings can be changed with:

    .. code-block:: python

        hive = Hive(<host>)

    where ``<host>`` starts with ``https://``, ``ws://`` or ``wss://``.

    The purpose of this class it to simplify interaction with
    Hive.

    The idea is to have a class that allows to do this:

    .. code-block:: python

        >>> from nectar import Hive
        >>> hive = Hive()
        >>> print(hive.get_blockchain_version())  # doctest: +SKIP

    This class also deals with edits, votes and reading content.

    Example for adding a custom chain:

    .. code-block:: python

        from nectar import Hive
        stm = Hive(node=["https://mytstnet.com"], custom_chains={"MYTESTNET":
            {'chain_assets': [{'asset': 'HBD', 'id': 0, 'precision': 3, 'symbol': 'HBD'},
                              {'asset': 'STEEM', 'id': 1, 'precision': 3, 'symbol': 'STEEM'},
                              {'asset': 'VESTS', 'id': 2, 'precision': 6, 'symbol': 'VESTS'}],
             'chain_id': '79276aea5d4877d9a25892eaa01b0adf019d3e5cb12a97478df3298ccdd01674',
             'min_version': '0.0.0',
             'prefix': 'MTN'}
            }
        )

    """

    def get_network(self, use_stored_data=True, config=None):
        """Identify the network

        :param bool use_stored_data: if True, stored data will be returned. If stored data are
                                     empty or old, refresh_data() is used.

        :returns: Network parameters
        :rtype: dictionary
        """
        if use_stored_data:
            self.refresh_data("config")
            return self.data["network"]

        if self.rpc is None:
            return known_chains["HIVE"]
        try:
            return self.rpc.get_network(props=config)
        except Exception:
            return known_chains["HIVE"]

    def rshares_to_token_backed_dollar(
        self, rshares, not_broadcasted_vote=False, use_stored_data=True
    ):
        return self.rshares_to_hbd(
            rshares, not_broadcasted_vote=not_broadcasted_vote, use_stored_data=use_stored_data
        )

    def rshares_to_hbd(self, rshares, not_broadcasted_vote=False, use_stored_data=True):
        """Calculates the current HBD value of a vote"""
        payout = float(rshares) * self.get_hbd_per_rshares(
            use_stored_data=use_stored_data,
            not_broadcasted_vote_rshares=rshares if not_broadcasted_vote else 0,
        )
        return payout

    def get_hbd_per_rshares(self, not_broadcasted_vote_rshares=0, use_stored_data=True):
        """Returns the current rshares to HBD ratio"""
        reward_fund = self.get_reward_funds(use_stored_data=use_stored_data)
        reward_balance = float(Amount(reward_fund["reward_balance"], blockchain_instance=self))
        recent_claims = float(reward_fund["recent_claims"]) + not_broadcasted_vote_rshares
        if recent_claims == 0:
            return 0
        fund_per_share = reward_balance / (recent_claims)
        median_price = self.get_median_price(use_stored_data=use_stored_data)
        if median_price is None:
            return 0
        HBD_price = float(median_price * (Amount(1, self.hive_symbol, blockchain_instance=self)))
        return fund_per_share * HBD_price

    def get_token_per_mvest(self, time_stamp=None, use_stored_data=True):
        return self.get_hive_per_mvest(time_stamp=time_stamp, use_stored_data=use_stored_data)

    def get_hive_per_mvest(self, time_stamp=None, use_stored_data=True):
        """Returns the MVEST to HIVE ratio

        :param int time_stamp: (optional) if set, return an estimated
            HIVE per MVEST ratio for the given time stamp. If unset the
            current ratio is returned (default). (can also be a datetime object)
        """
        if self.offline and time_stamp is None:
            time_stamp = datetime.now(timezone.utc)

        if time_stamp is not None:
            if isinstance(time_stamp, (datetime, date)):
                time_stamp = formatToTimeStamp(time_stamp)
            a = 2.1325476281078992e-05
            b = -31099.685481490847
            a2 = 2.9019227739473682e-07
            b2 = 48.41432402074669

            if time_stamp < (b2 - b) / (a - a2):
                return a * time_stamp + b
            else:
                return a2 * time_stamp + b2
        global_properties = self.get_dynamic_global_properties(use_stored_data=use_stored_data)
        return float(
            Amount(global_properties["total_vesting_fund_hive"], blockchain_instance=self)
        ) / (
            float(Amount(global_properties["total_vesting_shares"], blockchain_instance=self)) / 1e6
        )

    def vests_to_hp(self, vests, timestamp=None, use_stored_data=True):
        """Converts vests to HP

        :param amount.Amount vests/float vests: Vests to convert
        :param int timestamp: (Optional) Can be used to calculate
            the conversion rate from the past

        """
        if isinstance(vests, Amount):
            vests = float(vests)
        return (
            float(vests) / 1e6 * self.get_hive_per_mvest(timestamp, use_stored_data=use_stored_data)
        )

    def vests_to_token_power(self, vests, timestamp=None, use_stored_data=True):
        return self.vests_to_hp(vests, timestamp=timestamp, use_stored_data=use_stored_data)

    def hp_to_vests(self, hp, timestamp=None, use_stored_data=True):
        """Converts HP to vests

        :param float hp: Hive power to convert
        :param datetime timestamp: (Optional) Can be used to calculate
            the conversion rate from the past
        """
        return hp * 1e6 / self.get_hive_per_mvest(timestamp, use_stored_data=use_stored_data)

    def token_power_to_vests(self, token_power, timestamp=None, use_stored_data=True):
        return self.hp_to_vests(token_power, timestamp=timestamp, use_stored_data=use_stored_data)

    def token_power_to_token_backed_dollar(
        self,
        token_power,
        post_rshares=0,
        voting_power=STEEM_100_PERCENT,
        vote_pct=STEEM_100_PERCENT,
        not_broadcasted_vote=True,
        use_stored_data=True,
    ):
        return self.hp_to_hbd(
            token_power,
            post_rshares=post_rshares,
            voting_power=voting_power,
            vote_pct=vote_pct,
            not_broadcasted_vote=not_broadcasted_vote,
            use_stored_data=use_stored_data,
        )

    def hp_to_hbd(
        self,
        hp,
        post_rshares=0,
        voting_power=STEEM_100_PERCENT,
        vote_pct=STEEM_100_PERCENT,
        not_broadcasted_vote=True,
        use_stored_data=True,
    ):
        """Obtain the resulting HBD vote value from Hive power

        :param number hive_power: Hive Power
        :param int post_rshares: rshares of post which is voted
        :param int voting_power: voting power (100% = 10000)
        :param int vote_pct: voting percentage (100% = 10000)
        :param bool not_broadcasted_vote: not_broadcasted or already broadcasted vote (True = not_broadcasted vote).

        Only impactful for very big votes. Slight modification to the value calculation, as the not_broadcasted
        vote rshares decreases the reward pool.
        """
        vesting_shares = int(self.hp_to_vests(hp, use_stored_data=use_stored_data))
        return self.vests_to_hbd(
            vesting_shares,
            post_rshares=post_rshares,
            voting_power=voting_power,
            vote_pct=vote_pct,
            not_broadcasted_vote=not_broadcasted_vote,
            use_stored_data=use_stored_data,
        )

    def vests_to_hbd(
        self,
        vests,
        post_rshares=0,
        voting_power=STEEM_100_PERCENT,
        vote_pct=STEEM_100_PERCENT,
        not_broadcasted_vote=True,
        use_stored_data=True,
    ):
        """Obtain the resulting HBD vote value from vests

        :param number vests: vesting shares
        :param int post_rshares: rshares of post which is voted
        :param int voting_power: voting power (100% = 10000)
        :param int vote_pct: voting percentage (100% = 10000)
        :param bool not_broadcasted_vote: not_broadcasted or already broadcasted vote (True = not_broadcasted vote).

        Only impactful for very big votes. Slight modification to the value calculation, as the not_broadcasted
        vote rshares decreases the reward pool.
        """
        vote_rshares = self.vests_to_rshares(
            vests, post_rshares=post_rshares, voting_power=voting_power, vote_pct=vote_pct
        )
        return self.rshares_to_hbd(
            vote_rshares, not_broadcasted_vote=not_broadcasted_vote, use_stored_data=use_stored_data
        )

    def hp_to_rshares(
        self,
        hive_power,
        post_rshares=0,
        voting_power=STEEM_100_PERCENT,
        vote_pct=STEEM_100_PERCENT,
        use_stored_data=True,
    ):
        """Obtain the r-shares from Hive power

        :param number hive_power: Hive Power
        :param int post_rshares: rshares of post which is voted
        :param int voting_power: voting power (100% = 10000)
        :param int vote_pct: voting percentage (100% = 10000)

        """
        # calculate our account voting shares (from vests)
        vesting_shares = int(self.hp_to_vests(hive_power, use_stored_data=use_stored_data))
        rshares = self.vests_to_rshares(
            vesting_shares,
            post_rshares=post_rshares,
            voting_power=voting_power,
            vote_pct=vote_pct,
            use_stored_data=use_stored_data,
        )
        return rshares

    def vests_to_rshares(
        self,
        vests,
        post_rshares=0,
        voting_power=STEEM_100_PERCENT,
        vote_pct=STEEM_100_PERCENT,
        subtract_dust_threshold=True,
        use_stored_data=True,
    ):
        """Obtain the r-shares from vests

        :param number vests: vesting shares
        :param int post_rshares: rshares of post which is voted
        :param int voting_power: voting power (100% = 10000)
        :param int vote_pct: voting percentage (100% = 10000)

        """
        used_power = self._calc_resulting_vote(
            voting_power=voting_power, vote_pct=vote_pct, use_stored_data=use_stored_data
        )
        # calculate vote rshares
        rshares = int(math.copysign(vests * 1e6 * used_power / STEEM_100_PERCENT, vote_pct))
        if subtract_dust_threshold:
            if abs(rshares) <= self.get_dust_threshold(use_stored_data=use_stored_data):
                return 0
            rshares -= math.copysign(
                self.get_dust_threshold(use_stored_data=use_stored_data), vote_pct
            )
        rshares = self._calc_vote_claim(rshares, post_rshares)
        return rshares

    def hbd_to_rshares(self, hbd, not_broadcasted_vote=False, use_stored_data=True):
        """Obtain the r-shares from HBD

        :param hbd: HBD
        :type hbd: str, int, amount.Amount
        :param bool not_broadcasted_vote: not_broadcasted or already broadcasted vote (True = not_broadcasted vote).
         Only impactful for very high amounts of HBD. Slight modification to the value calculation, as the not_broadcasted
         vote rshares decreases the reward pool.

        """
        if isinstance(hbd, Amount):
            hbd = Amount(hbd, blockchain_instance=self)
        elif isinstance(hbd, str):
            hbd = Amount(hbd, blockchain_instance=self)
        else:
            hbd = Amount(hbd, self.hbd_symbol, blockchain_instance=self)
        if hbd["symbol"] != self.hbd_symbol:
            raise AssertionError("Should input HBD, not any other asset!")

        # If the vote was already broadcasted we can assume the blockchain values to be true
        if not not_broadcasted_vote:
            return int(float(hbd) / self.get_hbd_per_rshares(use_stored_data=use_stored_data))

        # If the vote wasn't broadcasted (yet), we have to calculate the rshares while considering
        # the change our vote is causing to the recent_claims. This is more important for really
        # big votes which have a significant impact on the recent_claims.
        reward_fund = self.get_reward_funds(use_stored_data=use_stored_data)
        median_price = self.get_median_price(use_stored_data=use_stored_data)
        recent_claims = int(reward_fund["recent_claims"])
        reward_balance = Amount(reward_fund["reward_balance"], blockchain_instance=self)
        reward_pool_hbd = median_price * reward_balance
        if hbd > reward_pool_hbd:
            raise ValueError("Provided more HBD than available in the reward pool.")

        # This is the formula we can use to determine the "true" rshares.
        # We get this formula by some math magic using the previous used formulas
        # FundsPerShare = (balance / (claims + newShares)) * Price
        # newShares = amount / FundsPerShare
        # We can now resolve both formulas for FundsPerShare and set the formulas to be equal
        # (balance / (claims + newShares)) * price = amount / newShares
        # Now we resolve for newShares resulting in:
        # newShares = claims * amount / (balance * price - amount)
        rshares = (
            recent_claims
            * float(hbd)
            / ((float(reward_balance) * float(median_price)) - float(hbd))
        )
        return int(rshares)

    def rshares_to_vote_pct(
        self,
        rshares,
        post_rshares=0,
        hive_power=None,
        vests=None,
        voting_power=STEEM_100_PERCENT,
        use_stored_data=True,
    ):
        """Obtain the voting percentage for a desired rshares value
        for a given Hive Power or vesting shares and voting_power
        Give either hive_power or vests, not both.
        When the output is greater than 10000 or less than -10000,
        the given absolute rshares are too high

        Returns the required voting percentage (100% = 10000)

        :param number rshares: desired rshares value
        :param number hive_power: Hive Power
        :param number vests: vesting shares
        :param int voting_power: voting power (100% = 10000)

        """
        if hive_power is None and vests is None:
            raise ValueError("Either hive_power or vests has to be set!")
        if hive_power is not None and vests is not None:
            raise ValueError("Either hive_power or vests has to be set. Not both!")
        if hive_power is not None:
            vests = int(self.hp_to_vests(hive_power, use_stored_data=use_stored_data) * 1e6)

        if self.hardfork >= 20:
            rshares += math.copysign(
                self.get_dust_threshold(use_stored_data=use_stored_data), rshares
            )

        if post_rshares >= 0 and rshares > 0:
            rshares = math.copysign(
                self._calc_revert_vote_claim(abs(rshares), post_rshares), rshares
            )
        elif post_rshares < 0 and rshares < 0:
            rshares = math.copysign(
                self._calc_revert_vote_claim(abs(rshares), abs(post_rshares)), rshares
            )
        elif post_rshares < 0 and rshares > 0:
            rshares = math.copysign(self._calc_revert_vote_claim(abs(rshares), 0), rshares)
        elif post_rshares > 0 and rshares < 0:
            rshares = math.copysign(
                self._calc_revert_vote_claim(abs(rshares), post_rshares), rshares
            )

        max_vote_denom = self._max_vote_denom(use_stored_data=use_stored_data)

        used_power = int(math.ceil(abs(rshares) * STEEM_100_PERCENT / vests))
        used_power = used_power * max_vote_denom

        vote_pct = used_power * STEEM_100_PERCENT / (60 * 60 * 24) / voting_power
        return int(math.copysign(vote_pct, rshares))

    def hbd_to_vote_pct(
        self,
        hbd,
        post_rshares=0,
        hive_power=None,
        vests=None,
        voting_power=STEEM_100_PERCENT,
        not_broadcasted_vote=True,
        use_stored_data=True,
    ):
        """Obtain the voting percentage for a desired HBD value
        for a given Hive Power or vesting shares and voting power
        Give either Hive Power or vests, not both.
        When the output is greater than 10000 or smaller than -10000,
        the HBD value is too high.

        Returns the required voting percentage (100% = 10000)

        :param hbd: desired HBD value
        :type hbd: str, int, amount.Amount
        :param number hive_power: Hive Power
        :param number vests: vesting shares
        :param bool not_broadcasted_vote: not_broadcasted or already broadcasted vote (True = not_broadcasted vote).
         Only impactful for very high amounts of HBD. Slight modification to the value calculation, as the not_broadcasted
         vote rshares decreases the reward pool.

        """
        if isinstance(hbd, Amount):
            hbd = Amount(hbd, blockchain_instance=self)
        elif isinstance(hbd, str):
            hbd = Amount(hbd, blockchain_instance=self)
        else:
            hbd = Amount(hbd, self.hbd_symbol, blockchain_instance=self)
        if hbd["symbol"] != self.hbd_symbol:
            raise AssertionError()
        rshares = self.hbd_to_rshares(
            hbd, not_broadcasted_vote=not_broadcasted_vote, use_stored_data=use_stored_data
        )
        return self.rshares_to_vote_pct(
            rshares,
            post_rshares=post_rshares,
            hive_power=hive_power,
            vests=vests,
            voting_power=voting_power,
            use_stored_data=use_stored_data,
        )

    @property
    def chain_params(self):
        if self.offline or self.rpc is None:
            return known_chains["HIVE"]
        else:
            return self.get_network()

    @property
    def hardfork(self):
        if self.offline or self.rpc is None:
            versions = known_chains["HIVE"]["min_version"]
        else:
            hf_prop = self.get_hardfork_properties()
            if "current_hardfork_version" in hf_prop:
                versions = hf_prop["current_hardfork_version"]
            else:
                versions = self.get_blockchain_version()
        return int(versions.split(".")[1])

    @property
    def is_hive(self):
        config = self.get_config()
        if config is None:
            return True
        return "HIVE_CHAIN_ID" in self.get_config()

    @property
    def hbd_symbol(self):
        """get the current chains symbol for HBD (e.g. "TBD" on testnet)"""
        return self.backed_token_symbol

    @property
    def hive_symbol(self):
        """get the current chains symbol for HIVE (e.g. "TESTS" on testnet)"""
        return self.token_symbol

    @property
    def vests_symbol(self):
        """get the current chains symbol for VESTS"""
        return self.vest_token_symbol
