import sys
from abc import ABC, abstractmethod
from copy import deepcopy
from enum import Enum
from typing import Any, Dict, Optional

from abides_core import NanosecondTime
from abides_core.utils import fmt_ts

from .utils import dollarize


class Side(Enum):
    BID = "BID"
    ASK = "ASK"

    def is_bid(self) -> bool:
        return self == Side.BID

    def is_ask(self) -> bool:
        return self == Side.ASK


class Order(ABC):
    """A basic Order type used by an Exchange to conduct trades or maintain an order book.

    This should not be confused with order Messages agents send to request an Order.
    Specific order types will inherit from this (like LimitOrder).
    """

    _order_id_counter: int = 0

    @abstractmethod
    def __init__(
        self,
        agent_id: int,
        time_placed: NanosecondTime,
        symbol: str,
        quantity: int,
        side: Side,
        order_id: Optional[int] = None,
        tag: Any = None,
    ) -> None:
        """
        Arguments:
            agent_id: The ID of the agent that created this order.
            time_placed: Time at which the order was created by the agent.
            symbol: Equity symbol for the order.
            quantity: Number of equity units affected by the order.
            side: Indicates if an order is on the BID or ASK side of the market.
            order_id: Either self generated or assigned. Should only be self
            generated by the OrderBook class.
            tag: A free-form user-defined field that can contain any information
                relevant to the entity placing the order.  Recommend keeping it
                alphanumeric rather than shoving in objects, as it will be there
                taking memory for the lifetime of the order and in all logging
                mechanisms.  Intent: for strategy agents to set tags to help keep
                track of the intent of particular orders, to simplify their code.
        """

        self.agent_id: int = agent_id

        self.time_placed: NanosecondTime = time_placed

        self.symbol: str = symbol

        self.quantity: int = quantity

        self.side: Side = side

        if order_id is None:
            order_id = Order._order_id_counter
            Order._order_id_counter += 1

        self.order_id: int = order_id

        # Create placeholder fields that don't get filled in until certain events happen.
        self.fill_price: Optional[int] = None

        self.tag: Optional[Any] = tag

    def to_dict(self) -> Dict[str, Any]:
        as_dict = deepcopy(self).__dict__
        as_dict["time_placed"] = fmt_ts(self.time_placed)
        return as_dict

    def __eq__(self, other):
        return type(other) is type(self) and self.__dict__ == other.__dict__

    def __deepcopy__(self, memodict={}):
        raise NotImplementedError


class LimitOrder(Order):
    """
    LimitOrder class that inherits from Order class and adds a limit price and a
    hidden order flag.

    These are the Orders that typically go in an Exchange's OrderBook.
    """

    def __init__(
        self,
        agent_id: int,
        time_placed: NanosecondTime,
        symbol: str,
        quantity: int,
        side: Side,
        limit_price: int,
        is_hidden: bool = False,
        is_price_to_comply: bool = False,
        insert_by_id: bool = False,
        is_post_only=False,
        order_id: Optional[int] = None,
        tag: Optional[Any] = None,
    ) -> None:
        super().__init__(
            agent_id, time_placed, symbol, quantity, side, order_id, tag=tag
        )

        # The limit price is the minimum price the agent will accept (for a sell order) or
        # the maximum price the agent will pay (for a buy order).
        self.limit_price: int = limit_price
        self.is_hidden: bool = is_hidden
        self.is_price_to_comply: bool = is_price_to_comply
        self.insert_by_id: bool = insert_by_id
        self.is_post_only: bool = is_post_only

    def __str__(self) -> str:
        filled = ""
        if self.fill_price:
            filled = " (filled @ {})".format(dollarize(self.fill_price))

        # Until we make explicit market orders, we make a few assumptions that EXTREME prices on limit
        # orders are trying to represent a market order.  This only affects printing - they still hit
        # the order book like limit orders, which is wrong.
        return "(Agent {} @ {}{}) : {} {} {} @ {}{}".format(
            self.agent_id,
            fmt_ts(self.time_placed),
            f" [{self.tag}]" if self.tag is not None else "",
            self.side.value,
            self.quantity,
            self.symbol,
            dollarize(self.limit_price)
            if abs(self.limit_price) < sys.maxsize
            else "MKT",
            filled,
        )

    def __repr__(self) -> str:
        return self.__str__()

    def __deepcopy__(self, memodict={}) -> "LimitOrder":
        tag = None if self.tag is None else deepcopy(self.tag)

        order = LimitOrder(
            self.agent_id,
            self.time_placed,
            self.symbol,
            self.quantity,
            self.side,
            self.limit_price,
            self.is_hidden,
            self.is_price_to_comply,
            self.insert_by_id,
            order_id=self.order_id,
            is_post_only=self.is_post_only,
            tag=tag,
        )

        order.fill_price = self.fill_price

        return order


class MarketOrder(Order):
    """MarketOrder class, inherits from Order class."""

    def __init__(
        self,
        agent_id: int,
        time_placed: NanosecondTime,
        symbol: str,
        quantity: int,
        side: Side,
        order_id: Optional[int] = None,
        tag: Optional[Any] = None,
    ) -> None:
        super().__init__(
            agent_id, time_placed, symbol, quantity, side, order_id=order_id, tag=tag
        )

    def __str__(self) -> str:
        return "(Agent {} @ {}) : MKT Order {} {} {}".format(
            self.agent_id,
            fmt_ts(self.time_placed),
            self.side.value,
            self.quantity,
            self.symbol,
        )

    def __repr__(self) -> str:
        return self.__str__()

    def __deepcopy__(self, memodict={}) -> "MarketOrder":
        tag = None if self.tag is None else deepcopy(self.tag)

        order = MarketOrder(
            self.agent_id,
            self.time_placed,
            self.symbol,
            self.quantity,
            self.side,
            order_id=self.order_id,
            tag=tag,
        )
        order.fill_price = self.fill_price

        return order
