"""
Options data for the Tradier API.

This module provides classes for retrieving option chain data including
expirations and strikes for underlying symbols.
"""

from __future__ import annotations

from datetime import date
from decimal import Decimal
from enum import Enum
from typing import TYPE_CHECKING

from loguru import logger
from pydantic import Field

from optrabot.broker.tradier.utils import TradierData

if TYPE_CHECKING:
    from optrabot.broker.tradier.session import Session


# ========== Enums ==========


class ExpirationType(str, Enum):
    """Type of option expiration."""

    STANDARD = 'standard'
    WEEKLYS = 'weeklys'
    QUARTERLY = 'quarterly'
    QUARTERLYS = 'quarterlys'  # Alternative API spelling
    EOM = 'eom'  # End of Month
    EOMW = 'eomw'  # End of Month Weekly


class OptionType(str, Enum):
    """Type of option (call or put)."""

    CALL = 'call'
    PUT = 'put'


class OptionRight(str, Enum):
    """Option right identifier (C for Call, P for Put)."""

    CALL = 'C'
    PUT = 'P'


# ========== Models ==========


class Strike(TradierData):
    """
    A strike price with call and put option symbols.

    Represents a specific strike in an options chain, containing
    the OCC symbols for both call and put options at that strike.

    The symbols can be used with the Market Data Streamer or for
    placing orders.

    Attributes:
        strike_price: The strike price.
        call_symbol: OCC symbol for the call option (e.g., "SPY250117C00500000").
        put_symbol: OCC symbol for the put option (e.g., "SPY250117P00500000").
        expiration_date: The expiration date for this strike.

    Example::

        strike = Strike(
            strike_price=Decimal("500"),
            call_symbol="SPY250117C00500000",
            put_symbol="SPY250117P00500000",
            expiration_date=date(2025, 1, 17)
        )

        # Use with streamer
        streamer.subscribe([strike.call_symbol, strike.put_symbol])
    """

    strike_price: Decimal
    call_symbol: str | None = None
    put_symbol: str | None = None
    expiration_date: date | None = None

    def __repr__(self) -> str:
        return f'Strike({self.strike_price}, call={self.call_symbol!r}, put={self.put_symbol!r})'

    @property
    def symbols(self) -> list[str]:
        """Get both call and put symbols as a list (for streaming)."""
        result = []
        if self.call_symbol:
            result.append(self.call_symbol)
        if self.put_symbol:
            result.append(self.put_symbol)
        return result


class Expiration(TradierData):
    """
    An option expiration date with available strikes.

    Represents a single expiration date for options on an underlying,
    including all available strike prices.

    Attributes:
        expiration_date: The expiration date.
        strikes: List of available strike prices for this expiration.
    """

    expiration_date: date = Field(..., alias='date')
    strikes: list[Decimal] = Field(default_factory=list)

    def __repr__(self) -> str:
        return f'Expiration({self.expiration_date}, strikes={len(self.strikes)})'


class OptionChain(TradierData):
    """
    Option chain for a specific underlying and expiration type.

    Contains all expirations of a specific type (standard, weeklys, etc.)
    for an underlying symbol, along with available strikes for each expiration.

    Use the static `get()` method to retrieve option chains for a symbol.

    Example::

        from optrabot.broker.tradier import Session, OptionChain

        session = Session("your-token")
        chains = OptionChain.get(session, "SPY")

        for chain in chains:
            print(f"{chain.symbol} - {chain.expiration_type}: {len(chain.expirations)} expirations")
            for exp in chain.expirations:
                print(f"  {exp.date}: {len(exp.strikes)} strikes")

    Attributes:
        symbol: The underlying symbol.
        expiration_type: The type of expirations (standard, weeklys, etc.).
        expirations: List of expiration dates with their strikes.
    """

    symbol: str
    expiration_type: ExpirationType
    expirations: list[Expiration] = Field(default_factory=list)

    def __repr__(self) -> str:
        return (
            f'OptionChain({self.symbol!r}, type={self.expiration_type!r}, '
            f'expirations={len(self.expirations)})'
        )

    @staticmethod
    def get(session: Session, symbol: str) -> list[OptionChain]:
        """
        Get option chains for an underlying symbol.

        Retrieves all available expirations and strikes, grouped by
        expiration type (standard, weeklys, quarterly, etc.).

        :param session: An authenticated Tradier Session.
        :param symbol: The underlying symbol (e.g., "SPY", "AAPL").
        :returns: A list of OptionChain objects, one per expiration type.

        Example::

            chains = OptionChain.get(session, "SPY")
            
            # Find standard monthly options
            standard = next(c for c in chains if c.expiration_type == ExpirationType.STANDARD)
            print(f"Standard expirations: {len(standard.expirations)}")
        """
        data = session._get(
            '/markets/options/expirations',
            params={
                'symbol': symbol,
                'includeAllRoots': 'true',
                'strikes': 'true',
                'expirationType': 'true',
            },
        )

        return _parse_expirations_response(symbol, data)

    @staticmethod
    async def a_get(session: Session, symbol: str) -> list[OptionChain]:
        """
        Get option chains for an underlying symbol (async).

        :param session: An authenticated Tradier Session.
        :param symbol: The underlying symbol (e.g., "SPY", "AAPL").
        :returns: A list of OptionChain objects, one per expiration type.
        """
        data = await session._a_get(
            '/markets/options/expirations',
            params={
                'symbol': symbol,
                'includeAllRoots': 'true',
                'strikes': 'true',
                'expirationType': 'true',
            },
        )

        return _parse_expirations_response(symbol, data)

    def get_expiration(self, expiration_date: date) -> Expiration | None:
        """
        Get a specific expiration by date.

        :param expiration_date: The expiration date to find.
        :returns: The Expiration object or None if not found.
        """
        for exp in self.expirations:
            if exp.expiration_date == expiration_date:
                return exp
        return None

    @property
    def all_strikes(self) -> set[Decimal]:
        """
        Get all unique strikes across all expirations.

        :returns: A set of all available strike prices.
        """
        strikes: set[Decimal] = set()
        for exp in self.expirations:
            strikes.update(exp.strikes)
        return strikes

    @property
    def next_expiration(self) -> Expiration | None:
        """
        Get the next (nearest) expiration.

        :returns: The next expiration or None if no expirations exist.
        """
        if not self.expirations:
            return None
        today = date.today()
        future = [e for e in self.expirations if e.expiration_date >= today]
        if not future:
            return None
        return min(future, key=lambda e: e.expiration_date)


# ========== Parsing Helpers ==========


def _parse_expirations_response(
    symbol: str, data: dict
) -> list[OptionChain]:
    """
    Parse the expirations API response into OptionChain objects.

    The response structure varies based on whether expirationType and strikes
    are requested. With both enabled:

    {
        "expirations": {
            "expiration": [
                {
                    "date": "2025-01-17",
                    "expiration_type": "standard",
                    "strikes": {
                        "strike": [400.0, 405.0, ...]
                    }
                },
                ...
            ]
        }
    }
    """
    expirations_data = data.get('expirations', {})

    # Handle null/empty response
    if not expirations_data:
        logger.warning('No expirations data for {}', symbol)
        return []

    # Get the expiration list
    expiration_list = expirations_data.get('expiration', [])

    # Handle single expiration (not wrapped in list)
    if isinstance(expiration_list, dict):
        expiration_list = [expiration_list]

    # Group expirations by type
    chains_by_type: dict[ExpirationType, list[Expiration]] = {}

    for exp_data in expiration_list:
        # Parse expiration type
        exp_type_str = exp_data.get('expiration_type', 'standard')
        try:
            exp_type = ExpirationType(exp_type_str)
        except ValueError:
            logger.warning('Unknown expiration type: {}', exp_type_str)
            exp_type = ExpirationType.STANDARD

        # Parse date
        date_str = exp_data.get('date', '')
        try:
            exp_date = date.fromisoformat(date_str)
        except ValueError:
            logger.warning('Invalid date format: {}', date_str)
            continue

        # Parse strikes
        strikes_data = exp_data.get('strikes', {})
        strike_list = strikes_data.get('strike', []) if strikes_data else []

        # Handle single strike (not wrapped in list)
        if isinstance(strike_list, (int, float)):
            strike_list = [strike_list]

        strikes = [Decimal(str(s)) for s in strike_list]

        # Create Expiration object
        expiration = Expiration(expiration_date=exp_date, strikes=strikes)

        # Group by type
        if exp_type not in chains_by_type:
            chains_by_type[exp_type] = []
        chains_by_type[exp_type].append(expiration)

    # Sort expirations by date within each chain
    for expirations in chains_by_type.values():
        expirations.sort(key=lambda e: e.expiration_date)

    # Create OptionChain objects
    chains = [
        OptionChain(
            symbol=symbol,
            expiration_type=exp_type,
            expirations=expirations,
        )
        for exp_type, expirations in sorted(chains_by_type.items(), key=lambda x: x[0].value)
    ]

    logger.debug(
        'Parsed {} option chains for {}: {}',
        len(chains),
        symbol,
        [f'{c.expiration_type}({len(c.expirations)})' for c in chains],
    )

    return chains


# ========== Option Symbol Functions ==========


async def get_option_symbols(
    session: Session,
    symbol: str,
    expiration: date,
    *,
    strikes: list[Decimal] | None = None,
    option_type: OptionType | None = None,
) -> dict[Decimal, Strike]:
    """
    Get option symbols (OCC format) for a specific expiration.

    Retrieves the actual option contract symbols that can be used for
    streaming market data or placing orders. Returns a dictionary
    keyed by strike price for easy access.

    :param session: An authenticated Tradier Session.
    :param symbol: The underlying symbol (e.g., "SPY", "AAPL", "SPX").
    :param expiration: The expiration date.
    :param strikes: Optional list of specific strike prices to retrieve.
                   If None, retrieves all available strikes.
    :param option_type: Optional filter for call or put only.
    :returns: A dict mapping strike price to Strike objects.

    Example::

        from decimal import Decimal
        from optrabot.broker.tradier import Session
        from optrabot.broker.tradier.options import get_option_symbols

        session = Session("your-token")

        # Get all strikes for an expiration
        strikes = await get_option_symbols(session, "SPY", date(2025, 1, 17))

        # Access a specific strike directly
        strike_500 = strikes[Decimal("500")]
        print(f"Call: {strike_500.call_symbol}")
        print(f"Put: {strike_500.put_symbol}")

        # Use symbols with streamer
        all_symbols = []
        for strike in strikes.values():
            all_symbols.extend(strike.symbols)
        await streamer.subscribe(all_symbols)
    """
    params: dict[str, str] = {
        'symbol': symbol,
        'expiration': expiration.isoformat(),
    }

    if option_type:
        params['option_type'] = option_type.value

    data = await session._a_get('/markets/options/chains', params=params)

    return _parse_option_chains_response(data, expiration, strikes)


def get_option_symbols_sync(
    session: Session,
    symbol: str,
    expiration: date,
    *,
    strikes: list[Decimal] | None = None,
    option_type: OptionType | None = None,
) -> dict[Decimal, Strike]:
    """
    Get option symbols (OCC format) for a specific expiration (sync).

    :param session: An authenticated Tradier Session.
    :param symbol: The underlying symbol (e.g., "SPY", "AAPL", "SPX").
    :param expiration: The expiration date.
    :param strikes: Optional list of specific strike prices to retrieve.
    :param option_type: Optional filter for call or put only.
    :returns: A dict mapping strike price to Strike objects.
    """
    params: dict[str, str] = {
        'symbol': symbol,
        'expiration': expiration.isoformat(),
    }

    if option_type:
        params['option_type'] = option_type.value

    data = session._get('/markets/options/chains', params=params)

    return _parse_option_chains_response(data, expiration, strikes)


def _parse_option_chains_response(
    data: dict,
    expiration: date,
    filter_strikes: list[Decimal] | None = None,
) -> dict[Decimal, Strike]:
    """
    Parse the options chains API response into Strike objects.

    Groups calls and puts by strike price.
    Returns a dict keyed by strike price.
    """
    options_data = data.get('options', {})

    if not options_data:
        logger.warning('No options data in response')
        return {}

    option_list = options_data.get('option', [])

    # Handle single option (not wrapped in list)
    if isinstance(option_list, dict):
        option_list = [option_list]

    # Group by strike price
    strikes_map: dict[Decimal, Strike] = {}

    for opt in option_list:
        strike_price = Decimal(str(opt.get('strike', 0)))

        # Filter if specific strikes requested
        if filter_strikes and strike_price not in filter_strikes:
            continue

        opt_symbol = opt.get('symbol', '')
        opt_type = opt.get('option_type', '')

        if strike_price not in strikes_map:
            strikes_map[strike_price] = Strike(
                strike_price=strike_price,
                expiration_date=expiration,
            )

        strike = strikes_map[strike_price]
        if opt_type == 'call':
            strike.call_symbol = opt_symbol
        elif opt_type == 'put':
            strike.put_symbol = opt_symbol

    logger.debug(
        'Parsed {} strikes for expiration {}',
        len(strikes_map),
        expiration,
    )

    return strikes_map


async def lookup_option_symbols(
    session: Session,
    symbol: str,
) -> list[str]:
    """
    Get all option symbols for an underlying.

    Returns all available option contract symbols (OCC format) including
    additional option roots if applicable (e.g., SPXW for SPX weeklys).

    :param session: An authenticated Tradier Session.
    :param symbol: The underlying symbol.
    :returns: A list of OCC option symbols.

    Example::

        symbols = await lookup_option_symbols(session, "SPX")
        # Returns: ["SPX250117C04000000", "SPX250117P04000000", ...]
    """
    data = await session._a_get(
        '/markets/options/lookup',
        params={'underlying': symbol},
    )

    symbols_data = data.get('symbols', [])
    if isinstance(symbols_data, dict):
        symbols_data = symbols_data.get('options', [])

    # Handle single symbol (not wrapped in list)
    if isinstance(symbols_data, str):
        symbols_data = [symbols_data]

    logger.debug('Found {} option symbols for {}', len(symbols_data), symbol)
    return symbols_data


def lookup_option_symbols_sync(
    session: Session,
    symbol: str,
) -> list[str]:
    """
    Get all option symbols for an underlying (sync).

    :param session: An authenticated Tradier Session.
    :param symbol: The underlying symbol.
    :returns: A list of OCC option symbols.
    """
    data = session._get(
        '/markets/options/lookup',
        params={'underlying': symbol},
    )

    symbols_data = data.get('symbols', [])
    if isinstance(symbols_data, dict):
        symbols_data = symbols_data.get('options', [])

    # Handle single symbol (not wrapped in list)
    if isinstance(symbols_data, str):
        symbols_data = [symbols_data]

    logger.debug('Found {} option symbols for {}', len(symbols_data), symbol)
    return symbols_data
