from collections import OrderedDict
from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import Any, Self, cast
from warnings import deprecated

from pydantic import BaseModel, Field, model_validator

FORMAT = "{str_base}-{str_quote}{str_type}{str_timeframe}{str_expiry}{str_price}{str_price_short}"


class MarketTimeframe(Enum):
    """Enumeration of standard timeframes for financial instruments.

    Defines the standard timeframes used for trading instruments:
    - HOURLY: 1 hour period
    - BI_HOURLY: 2 hour period
    - QUART_HOURLY: 4 hour period
    - DAILY: 1 day period
    - BI_DAILY: 2 day period
    - WEEKLY: 1 week period
    - BI_WEEKLY: 2 week period
    - MONTHLY: 1 month period
    - BI_MONTHLY: 2 month period
    - QUARTERLY: 3 month period
    - BI_QUARTERLY: 6 month period
    - YEARLY: 1 year period
    - UNDEFINED: Undefined timeframe
    """

    HOURLY = "1H"
    BI_HOURLY = "2H"
    QUART_HOURLY = "4H"
    DAILY = "1D"
    BI_DAILY = "2D"
    WEEKLY = "1W"
    BI_WEEKLY = "2W"
    MONTHLY = "1M"
    BI_MONTHLY = "2M"
    QUARTERLY = "1Q"
    BI_QUARTERLY = "2Q"
    YEARLY = "1Y"
    UNDEFINED = "NA"


# Millisec time intervals, those with the months and quarter represent the max time possible
INSTRUMENT_TIMEFRAME_INTERVAL = OrderedDict(
    [
        (3600, MarketTimeframe.HOURLY),
        (7200, MarketTimeframe.BI_HOURLY),
        (14400, MarketTimeframe.QUART_HOURLY),
        (86400, MarketTimeframe.DAILY),
        (172800, MarketTimeframe.BI_DAILY),
        (604800, MarketTimeframe.WEEKLY),
        (1209600, MarketTimeframe.BI_WEEKLY),
        (2678400, MarketTimeframe.MONTHLY),  # Considering 31 days
        (
            5356800,
            MarketTimeframe.BI_MONTHLY,
        ),  # Considering 31 + 31 days (like July + August)
        (
            7948800,
            MarketTimeframe.QUARTERLY,
        ),  # Considering 31 + 31 + 30 days (Q3)
        (15897600, MarketTimeframe.BI_QUARTERLY),  # Considering Q3 + Q4
        (31622400, MarketTimeframe.YEARLY),  # Considering leap year
    ]
)


class MarketType(Enum):
    """Enumeration of financial instrument types.

    Defines the various types of financial instruments supported:
    - SPOT: Regular spot trading
    - FUTURE: Standard futures contracts
    - INVERSE_FUTURE: Inverse futures contracts
    - PERPETUAL: Perpetual futures contracts
    - INVERSE_PERPETUAL: Inverse perpetual futures
    - EQUITY: Equity instruments
    - CALL_OPTION: Standard call options
    - PUT_OPTION: Standard put options
    - INVERSE_CALL_OPTION: Inverse call options
    - INVERSE_PUT_OPTION: Inverse put options
    - CALL_SPREAD: Call option spreads
    - PUT_SPREAD: Put option spreads
    - VOLATILITY: Volatility instruments
    """

    SPOT = "SPOT"
    FUTURE = "FUTURE"
    INVERSE_FUTURE = "INVERSE_FUTURE"
    PERPETUAL = "PERPETUAL"
    INVERSE_PERPETUAL = "INVERSE_PERPETUAL"
    EQUITY = "EQUITY"
    CALL_OPTION = "CALL_OPTION"
    PUT_OPTION = "PUT_OPTION"
    INVERSE_CALL_OPTION = "INVERSE_CALL_OPTION"
    INVERSE_PUT_OPTION = "INVERSE_PUT_OPTION"
    CALL_SPREAD = "CALL_SPREAD"
    PUT_SPREAD = "PUT_SPREAD"
    VOLATILITY = "VOLATILITY"

    @property
    def is_spot(self) -> bool:
        """Check if the instrument is a spot trading instrument.

        Returns:
            bool: True if spot trading instrument, False otherwise
        """
        return self in [MarketType.SPOT]

    @property
    def is_derivative(self) -> bool:
        """Check if the instrument is a derivative.

        Returns:
            bool: True if derivative instrument, False otherwise
        """
        return not self.is_spot

    @property
    def is_perpetual(self) -> bool:
        """Check if the instrument is a perpetual contract.

        Returns:
            bool: True if perpetual contract, False otherwise
        """
        return self in [
            MarketType.PERPETUAL,
            MarketType.INVERSE_PERPETUAL,
            MarketType.EQUITY,
        ]

    @property
    def is_future(self) -> bool:
        """Check if the instrument is a futures contract.

        Returns:
            bool: True if futures contract, False otherwise
        """
        return self in [
            MarketType.FUTURE,
            MarketType.INVERSE_FUTURE,
        ]

    @property
    def is_option(self) -> bool:
        """Check if the instrument is an option contract.

        Returns:
            bool: True if option contract, False otherwise
        """
        return self in [
            MarketType.CALL_OPTION,
            MarketType.PUT_OPTION,
            MarketType.INVERSE_CALL_OPTION,
            MarketType.INVERSE_PUT_OPTION,
            MarketType.CALL_SPREAD,
            MarketType.PUT_SPREAD,
            MarketType.VOLATILITY,
        ]

    @property
    def is_inverse(self) -> bool:
        """Check if the instrument is an inverse contract.

        Returns:
            bool: True if inverse contract, False otherwise
        """
        return self in [
            MarketType.INVERSE_FUTURE,
            MarketType.INVERSE_PERPETUAL,
            MarketType.INVERSE_CALL_OPTION,
            MarketType.INVERSE_PUT_OPTION,
        ]

    @property
    def is_linear(self) -> bool:
        """Check if the instrument is a linear contract.

        Returns:
            bool: True if linear contract, False otherwise
        """
        return self in [
            MarketType.SPOT,
            MarketType.FUTURE,
            MarketType.PERPETUAL,
            MarketType.EQUITY,
            MarketType.CALL_OPTION,
            MarketType.PUT_OPTION,
            MarketType.VOLATILITY,
        ]


class MarketInfo(BaseModel):
    """Information about a trading market/instrument.

    This class contains all the relevant information about a trading instrument,
    including its base and quote currencies, type, timeframe, and other
    specifications.

    Attributes:
        base (str): Base currency or asset
        quote (str): Quote currency or asset
        market_type (InstrumentType): Type of the instrument
        timeframe_type (InstrumentTimeframeType | None): Timeframe for futures/options
        expiry_date (datetime | None): Expiration date for futures/options
        strike_price (Decimal | None): Strike price for options
        metadata (dict[str, Any]): Additional market-specific metadata
    """

    base: str
    quote: str
    market_type: MarketType
    timeframe_type: MarketTimeframe | None = None
    expiry_date: datetime | None = None
    strike_price: Decimal | None = None
    metadata: dict[str, Any] = Field(default_factory=dict)

    @property
    @deprecated("Use market_type instead")
    def instrument_type(self) -> MarketType:
        return self.market_type

    @model_validator(mode="after")
    def validate_fields(self) -> Self:
        """Validate that required fields are present based on instrument type.

        Returns:
            Self: The validated instance

        Raises:
            ValueError: If required fields are missing for the instrument type
        """
        if self.market_type.is_option:
            if self.strike_price is None:
                raise ValueError("Strike price is required for options")
            if self.expiry_date is None:
                raise ValueError("Expiry date is required for options")
        elif self.market_type.is_future and self.expiry_date is None:
            raise ValueError("Expiry date is required for futures")
        return self

    @property
    def is_spot(self) -> bool:
        return self.market_type.is_spot

    @property
    def is_derivative(self) -> bool:
        return self.market_type.is_derivative

    @property
    def is_perpetual(self) -> bool:
        return self.market_type.is_perpetual

    @property
    def is_future(self) -> bool:
        return self.market_type.is_future

    @property
    def is_option(self) -> bool:
        return self.market_type.is_option

    @property
    def is_linear(self) -> bool:
        return self.market_type.is_linear

    @property
    def is_inverse(self) -> bool:
        return self.market_type.is_inverse

    @property
    def client_name(self) -> str:
        if self.is_spot:
            return f"{self.base}-{self.quote}"
        elif self.is_perpetual:
            return f"{self.base}-{self.quote}-{self.market_type.value}"
        elif self.is_option:
            timeframe_type = cast(MarketTimeframe, self.timeframe_type)
            expiry_date = cast(datetime, self.expiry_date)
            return f"{self.base}-{self.quote}-{self.market_type.value}-{timeframe_type.value}-{expiry_date.strftime('%Y%m%d')}-{self.strike_price}"
        elif self.is_future:
            timeframe_type = cast(MarketTimeframe, self.timeframe_type)
            expiry_date = cast(datetime, self.expiry_date)
            return f"{self.base}-{self.quote}-{self.market_type.value}-{timeframe_type.value}-{expiry_date.strftime('%Y%m%d')}"
        else:
            raise ValueError("Invalid instrument type")

    @classmethod
    def get_timeframe_type(
        cls,
        launch_timestamp: int,
        delivery_timestamp: int,
        instrument_timeframe_tolerance: int = 0,
        next_timeframe: bool = False,
    ) -> MarketTimeframe:
        """Determine the timeframe type based on instrument duration.

        Args:
            launch_timestamp (int): Instrument launch timestamp
            delivery_timestamp (int): Instrument delivery/expiry timestamp
            instrument_timeframe_tolerance (int): Allowed tolerance in seconds
            next_timeframe (bool): Whether to get next larger timeframe

        Returns:
            InstrumentTimeframeType: The determined timeframe type
        """
        instrument_duration = delivery_timestamp - launch_timestamp
        if delivery_timestamp < 0:
            return MarketTimeframe.UNDEFINED

        if not next_timeframe:
            for duration, timeframe_type in INSTRUMENT_TIMEFRAME_INTERVAL.items():
                if instrument_duration <= duration + instrument_timeframe_tolerance:
                    return timeframe_type
        else:
            for duration, timeframe_type in reversed(
                INSTRUMENT_TIMEFRAME_INTERVAL.items()
            ):
                if instrument_duration >= duration + instrument_timeframe_tolerance:
                    return timeframe_type

        return MarketTimeframe.UNDEFINED

    @classmethod
    def split_client_instrument_name(cls, name: str) -> Self:
        """Parse a client instrument name into its components.

        Parses a standardized instrument name string into a MarketInfo object.
        The name format should follow:
        {base}-{quote}{type}{timeframe}{expiry}{strike}

        Args:
            name (str): The instrument name to parse

        Returns:
            MarketInfo: Parsed market information

        Example:
            >>> MarketInfo.split_client_instrument_name("BTC-USD-FUTURE-1W-20240101")
        """
        split = name.split("-")

        base = split[0] if len(split) > 0 else ""
        quote = split[1] if len(split) > 1 else ""
        market_type = MarketType(split[2]) if len(split) > 2 else MarketType.SPOT
        timeframe_type = None
        expiry_date = None
        strike_price = None

        if market_type not in [
            MarketType.SPOT,
            MarketType.PERPETUAL,
            MarketType.INVERSE_PERPETUAL,
        ]:
            timeframe_type = (
                MarketTimeframe(split[3])
                if len(split) > 3
                else MarketTimeframe.UNDEFINED
            )
            expiry_date = (
                datetime.strptime(split[4], "%Y%m%d") if len(split) > 4 else None
            )
            if market_type in [
                MarketType.CALL_OPTION,
                MarketType.PUT_OPTION,
                MarketType.INVERSE_CALL_OPTION,
                MarketType.INVERSE_PUT_OPTION,
                MarketType.VOLATILITY,
            ]:
                strike_price = Decimal(split[5]) if len(split) > 5 else None

        return cls(
            base=base,
            quote=quote,
            market_type=market_type,
            timeframe_type=timeframe_type,
            expiry_date=expiry_date,
            strike_price=strike_price,
        )
