# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from asyncinit import asyncinit

from africuniabase import operations

from .account import Account
from .amount import Amount
from .asset import Asset
from .instance import BlockchainInstance
from .price import FilledOrder, Order, Price
from ..utils import assets_from_string, formatTime, formatTimeFromNow
from ..market import Market as SyncMarket


@asyncinit
@BlockchainInstance.inject
class Market(SyncMarket):
    """
    This class allows to easily access Markets on the blockchain for trading, etc.

    :param africunia.aio.africunia.AfrIcunia blockchain_instance: AfrIcunia instance
    :param africunia.aio.asset.Asset base: Base asset
    :param africunia.aio.asset.Asset quote: Quote asset
    :returns: Blockchain Market
    :rtype: dictionary with overloaded methods

    Instances of this class are dictionaries that come with additional
    methods (see below) that allow dealing with a market and it's
    corresponding functions.

    This class tries to identify **two** assets as provided in the
    parameters in one of the following forms:

    * ``base`` and ``quote`` are valid assets (according to :class:`africunia.asset.Asset`)
    * ``base:quote`` separated with ``:``
    * ``base/quote`` separated with ``/``
    * ``base-quote`` separated with ``-``

    .. note:: Throughout this library, the ``quote`` symbol will be
              presented first (e.g. ``USD:AFCASH`` with ``USD`` being the
              quote), while the ``base`` only refers to a secondary asset
              for a trade. This means, if you call
              :func:`africunia.market.Market.sell` or
              :func:`africunia.market.Market.buy`, you will sell/buy **only
              quote** and obtain/pay **only base**.
    """

    async def __init__(self, *args, **kwargs):
        base = kwargs.get("base", None)
        quote = kwargs.get("quote", None)

        if len(args) == 1 and isinstance(args[0], str):
            quote_symbol, base_symbol = assets_from_string(args[0])
            quote = await Asset(quote_symbol, blockchain_instance=self.blockchain)
            base = await Asset(base_symbol, blockchain_instance=self.blockchain)
            dict.__init__(self, {"base": base, "quote": quote})
        elif len(args) == 0 and base and quote:
            dict.__init__(self, {"base": base, "quote": quote})
        elif len(args) == 2 and not base and not quote:
            dict.__init__(self, {"base": args[1], "quote": args[0]})
        else:
            raise ValueError("Unknown Market Format: %s" % str(args))

    async def ticker(self):
        """
        Returns the ticker for all markets.

        Output Parameters:

        * ``last``: Price of the order last filled
        * ``lowestAsk``: Price of the lowest ask
        * ``highestBid``: Price of the highest bid
        * ``baseVolume``: Volume of the base asset
        * ``quoteVolume``: Volume of the quote asset
        * ``percentChange``: 24h change percentage (in %)
        * ``settlement_price``: Settlement Price for borrow/settlement
        * ``core_exchange_rate``: Core exchange rate for payment of fee in non-AFCASH asset
        * ``price24h``: the price 24h ago

        Sample Output:

        .. code-block:: js

            {
                {
                    "quoteVolume": 48328.73333,
                    "quoteSettlement_price": 332.3344827586207,
                    "lowestAsk": 340.0,
                    "baseVolume": 144.1862,
                    "percentChange": -1.9607843231354893,
                    "highestBid": 334.20000000000005,
                    "latest": 333.33333330133934,
                }
            }
        """
        data = {}
        # Core Exchange rate
        if self["quote"]["id"] == "1.3.0":
            cer = self["base"]["options"]["core_exchange_rate"]
        else:
            cer = self["quote"]["options"]["core_exchange_rate"]
        data["core_exchange_rate"] = await Price(
            cer, blockchain_instance=self.blockchain
        )
        if cer["base"]["asset_id"] == self["quote"]["id"]:
            data["core_exchange_rate"] = await data["core_exchange_rate"].invert()

        # smartcoin stuff
        if "bitasset_data_id" in self["quote"]:
            bitasset = await self.blockchain.rpc.get_object(
                self["quote"]["bitasset_data_id"]
            )
            backing_asset_id = bitasset["options"]["short_backing_asset"]
            if backing_asset_id == self["base"]["id"]:
                sp = bitasset["current_feed"]["settlement_price"]
                data["quoteSettlement_price"] = await Price(
                    sp, blockchain_instance=self.blockchain
                )
                if sp["base"]["asset_id"] == self["quote"]["id"]:
                    data["quoteSettlement_price"] = await data[
                        "quoteSettlement_price"
                    ].invert()

        elif "bitasset_data_id" in self["base"]:
            bitasset = await self.blockchain.rpc.get_object(
                self["base"]["bitasset_data_id"]
            )
            backing_asset_id = bitasset["options"]["short_backing_asset"]
            if backing_asset_id == self["quote"]["id"]:
                data["baseSettlement_price"] = await Price(
                    bitasset["current_feed"]["settlement_price"],
                    blockchain_instance=self.blockchain,
                )

        ticker = await self.blockchain.rpc.get_ticker(
            self["base"]["id"], self["quote"]["id"]
        )
        data["baseVolume"] = await Amount(
            ticker["base_volume"] or 0.0,
            self["base"],
            blockchain_instance=self.blockchain,
        )
        data["quoteVolume"] = await Amount(
            ticker["quote_volume"] or 0.0,
            self["quote"],
            blockchain_instance=self.blockchain,
        )
        data["lowestAsk"] = await Price(
            ticker["lowest_ask"] or 0.0,
            base=self["base"],
            quote=self["quote"],
            blockchain_instance=self.blockchain,
        )
        data["highestBid"] = await Price(
            ticker["highest_bid"] or 0.0,
            base=self["base"],
            quote=self["quote"],
            blockchain_instance=self.blockchain,
        )
        data["latest"] = await Price(
            ticker["latest"] or 0.0,
            quote=self["quote"],
            base=self["base"],
            blockchain_instance=self.blockchain,
        )
        data["percentChange"] = float(ticker.get("percent_change", 0.0) or 0.0)

        return data

    async def volume24h(self):
        """
        Returns the 24-hour volume for all markets, plus totals for primary currencies.

        Sample output:

        .. code-block:: js

            {
                "AFCASH": 361666.63617,
                "USD": 1087.0
            }
        """
        volume = await self.blockchain.rpc.get_24_volume(
            self["base"]["id"], self["quote"]["id"]
        )
        return {
            self["base"]["symbol"]: await Amount(
                volume["base_volume"], self["base"], blockchain_instance=self.blockchain
            ),
            self["quote"]["symbol"]: await Amount(
                volume["quote_volume"],
                self["quote"],
                blockchain_instance=self.blockchain,
            ),
        }

    async def orderbook(self, limit=25):
        """
        Returns the order book for a given market. You may also specify "all" to get the
        orderbooks of all markets.

        :param int limit: Limit the amount of orders (default: 25)

        Sample output:

        .. code-block:: js

            {'bids': [0.003679 USD/AFCASH (1.9103 USD|519.29602 AFCASH),
            0.003676 USD/AFCASH (299.9997 USD|81606.16394 AFCASH),
            0.003665 USD/AFCASH (288.4618 USD|78706.21881 AFCASH),
            0.003665 USD/AFCASH (3.5285 USD|962.74409 AFCASH),
            0.003665 USD/AFCASH (72.5474 USD|19794.41299 AFCASH)],
            'asks': [0.003738 USD/AFCASH (36.4715 USD|9756.17339 AFCASH),
            0.003738 USD/AFCASH (18.6915 USD|5000.00000 AFCASH),
            0.003742 USD/AFCASH (182.6881 USD|48820.22081 AFCASH),
            0.003772 USD/AFCASH (4.5200 USD|1198.14798 AFCASH),
            0.003799 USD/AFCASH (148.4975 USD|39086.59741 AFCASH)]}


        .. note:: Each bid is an instance of
            class:`africunia.price.Order` and thus carries the keys
            ``base``, ``quote`` and ``price``. From those you can
            obtain the actual amounts for sale

        .. note:: This method does order consolidation and hides some
            details of individual orders!
        """
        orders = await self.blockchain.rpc.get_order_book(
            self["base"]["id"], self["quote"]["id"], limit
        )
        asks = [
            await Order(
                float(x["price"]),
                quote=await Amount(
                    x["quote"], self["quote"], blockchain_instance=self.blockchain
                ),
                base=await Amount(
                    x["base"], self["base"], blockchain_instance=self.blockchain
                ),
                blockchain_instance=self.blockchain,
            )
            for x in orders["asks"]
        ]
        bids = [
            await Order(
                float(x["price"]),
                quote=await Amount(
                    x["quote"], self["quote"], blockchain_instance=self.blockchain
                ),
                base=await Amount(
                    x["base"], self["base"], blockchain_instance=self.blockchain
                ),
                blockchain_instance=self.blockchain,
            )
            for x in orders["bids"]
        ]
        data = {"asks": asks, "bids": bids}
        return data

    async def get_limit_orders(self, limit=25):
        """
        Returns the list of limit orders for a given market.

        :param int limit: Limit the amount of orders (default: 25)

        Sample output:

        .. code-block:: js

            [0.003679 USD/AFCASH (1.9103 USD|519.29602 AFCASH),
            0.003676 USD/AFCASH (299.9997 USD|81606.16394 AFCASH),
            0.003665 USD/AFCASH (288.4618 USD|78706.21881 AFCASH),
            0.003665 USD/AFCASH (3.5285 USD|962.74409 AFCASH),
            0.003665 USD/AFCASH (72.5474 USD|19794.41299 AFCASH)],

        .. note:: Each bid is an instance of
            class:`africunia.price.Order` and thus carries the keys
            ``base``, ``quote`` and ``price``. From those you can
            obtain the actual amounts for sale
        """
        orders = await self.blockchain.rpc.get_limit_orders(
            self["base"]["id"], self["quote"]["id"], limit
        )
        return [await Order(x, blockchain_instance=self.blockchain) for x in orders]

    async def trades(self, limit=25, start=None, stop=None):
        """
        Returns your trade history for a given market.

        :param int limit: Limit the amount of orders (default: 25)
        :param datetime start: start time
        :param datetime stop: stop time
        """
        # FIXME, this call should also return whether it was a buy or
        # sell
        if not stop:
            stop = datetime.now()
        if not start:
            start = stop - timedelta(hours=24)
        """
            vector<market_trade> get_trade_history( const string& base, const string& quote, fc::time_point_sec start, fc::time_point_sec stop, unsigned limit = 100 )const;
        """
        sequence = None

        cnt = 0
        while True:
            first_run = min(limit, 100)
            if not sequence:
                # Obtain first set of orders
                orders = await self.blockchain.rpc.get_trade_history(
                    self["base"]["symbol"],
                    self["quote"]["symbol"],
                    formatTime(stop),
                    formatTime(start),
                    first_run,
                )
            else:
                # obtain subsequent set of orders
                continuous_limit = min(limit - cnt, 100)
                orders = await self.blockchain.rpc.get_trade_history_by_sequence(
                    self["base"]["symbol"],
                    self["quote"]["symbol"],
                    sequence,
                    formatTime(start),
                    continuous_limit,
                )

            if len(orders) == 0:
                return
            for order in orders:
                cnt += 1
                yield await FilledOrder(
                    order,
                    quote=await Amount(
                        order["amount"],
                        self["quote"],
                        blockchain_instance=self.blockchain,
                    ),
                    base=await Amount(
                        float(order["amount"]) * float(order["price"]),
                        self["base"],
                        blockchain_instance=self.blockchain,
                    ),
                    blockchain_instance=self.blockchain,
                )
                if cnt >= limit:
                    return
                sequence = order.get("sequence")

    async def accounttrades(self, account=None, limit=25):
        """
        Returns your trade history for a given market, specified by the "currencyPair"
        parameter. You may also specify "all" to get the orderbooks of all markets.

        :param str currencyPair: Return results for a particular market only (default: "all")
        :param int limit: Limit the amount of orders (default: 25)

        Output Parameters:

            - `type`: sell or buy
            - `rate`: price for `quote` denoted in `base` per `quote`
            - `amount`: amount of quote
            - `total`: amount of base at asked price (amount/price)

        .. note:: This call goes through the trade history and
                  searches for your account, if there are no orders
                  within ``limit`` trades, this call will return an
                  empty array.
        """
        if not account:
            if "default_account" in self.blockchain.config:
                account = self.blockchain.config["default_account"]
        if not account:
            raise ValueError("You need to provide an account")
        account = await Account(account, blockchain_instance=self.blockchain)

        filled = await self.blockchain.rpc.get_fill_order_history(
            self["base"]["id"], self["quote"]["id"], 2 * limit, api="history"
        )
        trades = []
        for f in filled:
            if f["op"]["account_id"] == account["id"]:
                trades.append(
                    await FilledOrder(
                        f,
                        base=self["base"],
                        quote=self["quote"],
                        blockchain_instance=self.blockchain,
                    )
                )
        return trades

    async def accountopenorders(self, account=None):
        """
        Returns open Orders.

        :param africunia.account.Account account: Account name or instance of Account to show orders for in this market
        """
        if not account:
            if "default_account" in self.blockchain.config:
                account = self.blockchain.config["default_account"]
        if not account:
            raise ValueError("You need to provide an account")
        account = await Account(account, full=True, blockchain_instance=self.blockchain)

        r = []
        await account.refresh()
        orders = account["limit_orders"]
        for o in orders:
            if (
                o["sell_price"]["base"]["asset_id"] == self["base"]["id"]
                and o["sell_price"]["quote"]["asset_id"] == self["quote"]["id"]
            ) or (
                o["sell_price"]["base"]["asset_id"] == self["quote"]["id"]
                and o["sell_price"]["quote"]["asset_id"] == self["base"]["id"]
            ):
                r.append(await Order(o, blockchain_instance=self.blockchain))
        return r

    async def buy(
        self,
        price,
        amount,
        expiration=None,
        killfill=False,
        account=None,
        returnOrderId=False,
        **kwargs
    ):
        """
        Places a buy order in a given market.

        :param float price: price denoted in ``base``/``quote``
        :param number amount: Amount of ``quote`` to buy
        :param number expiration: (optional) expiration time of the order in seconds (defaults to 7 days)
        :param bool killfill: flag that indicates if the order shall be killed if it is not filled (defaults to False)
        :param string account: Account name that executes that order
        :param string returnOrderId: If set to "head" or "irreversible" the call will wait for the tx to appear in
                                    the head/irreversible block and add the key "orderid" to the tx output

        Prices/Rates are denoted in 'base', i.e. the USD_BTS market
        is priced in AFCASH per USD.

        **Example:** in the USD_BTS market, a price of 300 means
        a USD is worth 300 AFCASH

        .. note::

            All prices returned are in the **reversed** orientation as the
            market. I.e. in the BTC/AFCASH market, prices are AFCASH per BTC.
            That way you can multiply prices with `1.05` to get a +5%.

        .. warning::

            Since buy orders are placed as
            limit-sell orders for the base asset,
            you may end up obtaining more of the
            buy asset than you placed the order
            for. Example:

                * You place and order to buy 10 USD for 100 AFCASH/USD
                * This means that you actually place a sell order for 1000 AFCASH in order to obtain **at least** 10 USD
                * If an order on the market exists that sells USD for cheaper, you will end up with more than 10 USD
        """
        if not expiration:
            expiration = (
                self.blockchain.config["order-expiration"] or 60 * 60 * 24 * 365
            )
        if not account:
            if "default_account" in self.blockchain.config:
                account = self.blockchain.config["default_account"]
        if not account:
            raise ValueError("You need to provide an account")
        account = await Account(account, blockchain_instance=self.blockchain)

        if isinstance(price, Price):
            price = await price.as_base(self["base"]["symbol"])

        if isinstance(amount, Amount):
            amount = await Amount(amount, blockchain_instance=self.blockchain)
            assert (
                amount["asset"]["symbol"] == self["quote"]["symbol"]
            ), "Price: {} does not match amount: {}".format(str(price), str(amount))
        else:
            amount = await Amount(
                amount, self["quote"]["symbol"], blockchain_instance=self.blockchain
            )

        order = operations.Limit_order_create(
            **{
                "fee": {"amount": 0, "asset_id": "1.3.0"},
                "seller": account["id"],
                "amount_to_sell": {
                    "amount": int(
                        round(
                            float(amount)
                            * float(price)
                            * 10 ** self["base"]["precision"]
                        )
                    ),
                    "asset_id": self["base"]["id"],
                },
                "min_to_receive": {
                    "amount": int(
                        round(float(amount) * 10 ** self["quote"]["precision"])
                    ),
                    "asset_id": self["quote"]["id"],
                },
                "expiration": formatTimeFromNow(expiration),
                "fill_or_kill": killfill,
            }
        )

        if returnOrderId:
            # Make blocking broadcasts
            prevblocking = self.blockchain.blocking
            self.blockchain.blocking = returnOrderId

        tx = await self.blockchain.finalizeOp(
            order, account["name"], "active", **kwargs
        )

        if returnOrderId and tx.get("operation_results"):
            tx["orderid"] = tx["operation_results"][0][1]
            self.blockchain.blocking = prevblocking

        return tx

    async def sell(
        self,
        price,
        amount,
        expiration=None,
        killfill=False,
        account=None,
        returnOrderId=False,
        **kwargs
    ):
        """
        Places a sell order in a given market.

        :param float price: price denoted in ``base``/``quote``
        :param number amount: Amount of ``quote`` to sell
        :param number expiration: (optional) expiration time of the order in seconds (defaults to 7 days)
        :param bool killfill: flag that indicates if the order shall be killed if it is not filled (defaults to False)
        :param string account: Account name that executes that order
        :param string returnOrderId: If set to "head" or "irreversible" the call will wait for the tx to appear in
                                    the head/irreversible block and add the key "orderid" to the tx output

        Prices/Rates are denoted in 'base', i.e. the USD_BTS market
        is priced in AFCASH per USD.

        **Example:** in the USD_BTS market, a price of 300 means
        a USD is worth 300 AFCASH

        .. note::

            All prices returned are in the **reversed** orientation as the
            market. I.e. in the BTC/AFCASH market, prices are AFCASH per BTC.
            That way you can multiply prices with `1.05` to get a +5%.
        """
        if not expiration:
            expiration = self.blockchain.config["order-expiration"]
        if not account:
            if "default_account" in self.blockchain.config:
                account = self.blockchain.config["default_account"]
        if not account:
            raise ValueError("You need to provide an account")
        account = await Account(account, blockchain_instance=self.blockchain)
        if isinstance(price, Price):
            price = await price.as_base(self["base"]["symbol"])

        if isinstance(amount, Amount):
            amount = await Amount(amount, blockchain_instance=self.blockchain)
            assert (
                amount["asset"]["symbol"] == self["quote"]["symbol"]
            ), "Price: {} does not match amount: {}".format(str(price), str(amount))
        else:
            amount = await Amount(
                amount, self["quote"]["symbol"], blockchain_instance=self.blockchain
            )

        order = operations.Limit_order_create(
            **{
                "fee": {"amount": 0, "asset_id": "1.3.0"},
                "seller": account["id"],
                "amount_to_sell": {
                    "amount": int(
                        round(float(amount) * 10 ** self["quote"]["precision"])
                    ),
                    "asset_id": self["quote"]["id"],
                },
                "min_to_receive": {
                    "amount": int(
                        round(
                            float(amount)
                            * float(price)
                            * 10 ** self["base"]["precision"]
                        )
                    ),
                    "asset_id": self["base"]["id"],
                },
                "expiration": formatTimeFromNow(expiration),
                "fill_or_kill": killfill,
            }
        )
        if returnOrderId:
            # Make blocking broadcasts
            prevblocking = self.blockchain.blocking
            self.blockchain.blocking = returnOrderId

        tx = await self.blockchain.finalizeOp(
            order, account["name"], "active", **kwargs
        )

        if returnOrderId:
            tx["orderid"] = tx["operation_results"][0][1]
            self.blockchain.blocking = prevblocking

        return tx

    async def cancel(self, orderNumber, account=None, **kwargs):
        """
        Cancels an order you have placed in a given market. Requires only the
        "orderNumber". An order number takes the form ``1.7.xxx``.

        :param str orderNumber: The Order Object ide of the form ``1.7.xxxx``
        """
        return await self.blockchain.cancel(orderNumber, account=account, **kwargs)

    async def core_quote_market(self):
        """
        This returns an instance of the market that has the core market of the quote
        asset.

        It means that quote needs to be a market pegged asset and returns a market to
        it's collateral asset.
        """
        if not self["quote"].is_bitasset:
            raise ValueError("Quote (%s) is not a bitasset!" % self["quote"]["symbol"])
        self["quote"].full = True
        await self["quote"].refresh()
        collateral = await Asset(
            self["quote"]["bitasset_data"]["options"]["short_backing_asset"],
            blockchain_instance=self.blockchain,
        )
        return await Market(quote=self["quote"], base=collateral)

    async def core_base_market(self):
        """
        This returns an instance of the market that has the core market of the base
        asset.

        It means that base needs to be a market pegged asset and returns a market to
        it's collateral asset.
        """
        if not self["base"].is_bitasset:
            raise ValueError("base (%s) is not a bitasset!" % self["base"]["symbol"])
        self["base"].full = True
        await self["base"].refresh()
        collateral = await Asset(
            self["base"]["bitasset_data"]["options"]["short_backing_asset"],
            blockchain_instance=self.blockchain,
        )
        return await Market(quote=self["base"], base=collateral)
