# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Entropy Markets](https://entropy.trade/) support is available at:
#   [Docs](https://docs.entropy.trade/)
#   [Discord](https://discord.gg/67jySBhxrg)
#   [Twitter](https://twitter.com/entropymarkets)
#   [Github](https://github.com/blockworks-foundation)
#   [Email](mailto:hello@blockworks.foundation)


import entropy
import typing

from datetime import timedelta
from decimal import Decimal

from ..modelstate import ModelState
from .orderreconciler import OrderReconciler
from .reconciledorders import ReconciledOrders


# # 🥭 ToleranceOrderReconciler class
#
# Has a level of 'tolerance' around whether a desired order matches an existing order.
#
# There are two tolerance levels:
# * A tolerance for price matching
# * A tolderance for quantity matching
#
# Tolerances are expressed as a ratio. To match the existing value must be within +/- the tolderance
# of the desired value.
#
# Note:
# * A BUY only matches with a BUY, a SELL only matches with a SELL.
# * ID and Client ID are ignored when matching.
# * ModelState is ignored when matching.
#
class ToleranceOrderReconciler(OrderReconciler):
    def __init__(
        self,
        price_tolerance: Decimal,
        quantity_tolerance: Decimal,
        time_in_force_tolerance: timedelta,
    ) -> None:
        super().__init__()
        self.price_tolerance: Decimal = price_tolerance
        self.quantity_tolerance: Decimal = quantity_tolerance
        self.time_in_force_tolerance: timedelta = time_in_force_tolerance

    @staticmethod
    def zero_tolerance_order_reconciler() -> "ToleranceOrderReconciler":
        return ToleranceOrderReconciler(Decimal(0), Decimal(0), timedelta(seconds=0))

    def reconcile(
        self,
        _: ModelState,
        existing_orders: typing.Sequence[entropy.Order],
        desired_orders: typing.Sequence[entropy.Order],
    ) -> ReconciledOrders:
        remaining_existing_orders: typing.List[entropy.Order] = list(existing_orders)
        outcomes: ReconciledOrders = ReconciledOrders()
        for desired in desired_orders:
            acceptable = self.find_acceptable_order(desired, remaining_existing_orders)
            if acceptable is None:
                outcomes.to_place += [desired]
            else:
                outcomes.to_keep += [acceptable]
                outcomes.to_ignore += [desired]
                remaining_existing_orders.remove(acceptable)

        # By this point we have removed all acceptable existing orders, so those that remain
        # should be cancelled.
        outcomes.to_cancel = remaining_existing_orders

        in_count = len(existing_orders) + len(desired_orders)
        out_count = (
            len(outcomes.to_place)
            + len(outcomes.to_cancel)
            + len(outcomes.to_keep)
            + len(outcomes.to_ignore)
        )
        if in_count != out_count:
            raise Exception(
                f"Failure processing all desired orders. Count of orders in: {in_count}. Count of orders out: {out_count}."
            )

        return outcomes

    def find_acceptable_order(
        self, desired: entropy.Order, existing_orders: typing.Sequence[entropy.Order]
    ) -> typing.Optional[entropy.Order]:
        for existing in existing_orders:
            if self.is_within_tolderance(existing, desired):
                return existing
        return None

    def is_within_tolderance(
        self, existing: entropy.Order, desired: entropy.Order
    ) -> bool:
        if existing.side != desired.side:
            return False

        price_tolerance: Decimal = existing.price * self.price_tolerance
        if desired.price > (existing.price + price_tolerance):
            return False

        if desired.price < (existing.price - price_tolerance):
            return False

        quantity_tolerance: Decimal = existing.quantity * self.quantity_tolerance
        if desired.quantity > (existing.quantity + quantity_tolerance):
            return False

        if desired.quantity < (existing.quantity - quantity_tolerance):
            return False

        if desired.expiration > (existing.expiration + self.time_in_force_tolerance):
            return False

        if desired.expiration < (existing.expiration - self.time_in_force_tolerance):
            return False

        return True

    def __str__(self) -> str:
        return f"« ToleranceOrderReconciler [price tolerance: {self.price_tolerance}, quantity tolerance: {self.quantity_tolerance}, time-in-force tolerance: {self.time_in_force_tolerance}] »"
