"""
Market data functions for the Tradier API.

This module provides functions for retrieving market data including
quotes, option chains, historical data, and market status.
"""

from __future__ import annotations

from datetime import date, datetime
from decimal import Decimal
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


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


class Greeks(TradierData):
    """
    Option Greeks data.

    Contains the calculated Greeks for an option contract.
    Only available when requesting quotes with greeks=True.

    Attributes:
        delta: Rate of change of option price relative to underlying price.
        gamma: Rate of change of delta.
        theta: Rate of time decay (per day).
        vega: Sensitivity to implied volatility changes.
        rho: Sensitivity to interest rate changes.
        phi: Sensitivity to dividend yield changes.
        bid_iv: Implied volatility of the bid price.
        mid_iv: Implied volatility of the mid price.
        ask_iv: Implied volatility of the ask price.
        smv_vol: Smoothed volatility.
        updated_at: Timestamp when Greeks were last updated.
    """

    delta: Decimal | None = None
    gamma: Decimal | None = None
    theta: Decimal | None = None
    vega: Decimal | None = None
    rho: Decimal | None = None
    phi: Decimal | None = None
    bid_iv: Decimal | None = None
    mid_iv: Decimal | None = None
    ask_iv: Decimal | None = None
    smv_vol: Decimal | None = None
    updated_at: datetime | None = None

    def __repr__(self) -> str:
        return f'Greeks(delta={self.delta}, gamma={self.gamma}, theta={self.theta}, vega={self.vega})'


class Quote(TradierData):
    """
    A market quote for an equity or option.

    Contains current market data including price, volume, and bid/ask information.

    Attributes:
        symbol: The ticker symbol.
        description: Company or security description.
        exch: Primary exchange code.
        type: Security type (stock, option, etf, index).
        last: Last trade price.
        change: Price change from previous close.
        change_percentage: Percentage change from previous close.
        volume: Current day's trading volume.
        average_volume: Average daily volume.
        last_volume: Size of the last trade.
        trade_date: Timestamp of the last trade (milliseconds).
        open: Opening price for the day.
        high: High price for the day.
        low: Low price for the day.
        close: Previous day's closing price.
        prevclose: Previous day's closing price (alias).
        week_52_high: 52-week high price.
        week_52_low: 52-week low price.
        bid: Current bid price.
        bidsz: Bid size.
        bidexch: Bid exchange code.
        bid_date: Bid timestamp (milliseconds).
        ask: Current ask price.
        asksz: Ask size.
        askexch: Ask exchange code.
        ask_date: Ask timestamp (milliseconds).
        open_interest: Option open interest.
        underlying: Underlying symbol for options.
        strike: Option strike price.
        contract_size: Option contract size.
        expiration_date: Option expiration date.
        expiration_type: Option expiration type.
        option_type: Option type (call or put).
        root_symbol: Option root symbol.
        greeks: Option Greeks (delta, gamma, theta, vega, etc.) - only when requested.
    """

    symbol: str
    description: str | None = None
    exch: str | None = None
    type: str | None = None
    last: Decimal | None = None
    change: Decimal | None = None
    change_percentage: Decimal | None = None
    volume: int | None = None
    average_volume: int | None = None
    last_volume: int | None = None
    trade_date: int | None = None
    open: Decimal | None = None
    high: Decimal | None = None
    low: Decimal | None = None
    close: Decimal | None = None
    prevclose: Decimal | None = None
    week_52_high: Decimal | None = None
    week_52_low: Decimal | None = None
    bid: Decimal | None = None
    bidsz: int | None = None
    bidexch: str | None = None
    bid_date: int | None = None
    ask: Decimal | None = None
    asksz: int | None = None
    askexch: str | None = None
    ask_date: int | None = None
    open_interest: int | None = None
    underlying: str | None = None
    strike: Decimal | None = None
    contract_size: int | None = None
    expiration_date: date | None = None
    expiration_type: str | None = None
    option_type: str | None = None
    root_symbol: str | None = None
    greeks: Greeks | None = None

    @property
    def last_trade_datetime(self) -> datetime | None:
        """Convert last trade timestamp to datetime."""
        if self.trade_date:
            return datetime.fromtimestamp(self.trade_date / 1000)
        return None

    @property
    def bid_datetime(self) -> datetime | None:
        """Convert bid timestamp to datetime."""
        if self.bid_date:
            return datetime.fromtimestamp(self.bid_date / 1000)
        return None

    @property
    def ask_datetime(self) -> datetime | None:
        """Convert ask timestamp to datetime."""
        if self.ask_date:
            return datetime.fromtimestamp(self.ask_date / 1000)
        return None

    @property
    def spread(self) -> Decimal | None:
        """Calculate the bid-ask spread."""
        if self.bid is not None and self.ask is not None:
            return self.ask - self.bid
        return None

    @property
    def mid_price(self) -> Decimal | None:
        """Calculate the mid price between bid and ask."""
        if self.bid is not None and self.ask is not None:
            return (self.bid + self.ask) / 2
        return None

    def __repr__(self) -> str:
        if self.last is not None:
            return f'Quote({self.symbol!r}, last={self.last}, bid={self.bid}, ask={self.ask})'
        return f'Quote({self.symbol!r}, bid={self.bid}, ask={self.ask})'


# ========== Functions ==========


async def get_quotes(
    session: Session,
    symbols: list[str] | str,
    *,
    greeks: bool = False,
) -> list[Quote]:
    """
    Get current market quotes for one or more symbols.

    Retrieves real-time or delayed quotes for equities, options, or indices.
    This is useful for getting on-demand quotes when streaming is not needed
    or not available (e.g., for indices like SPX).

    :param session: An authenticated Tradier Session.
    :param symbols: A symbol or list of symbols to get quotes for.
    :param greeks: Include option Greeks in the response (options only).
    :returns: A list of Quote objects.

    Example::

        from optrabot.broker.tradier import Session
        from optrabot.broker.tradier.markets import get_quotes

        session = Session("your-token")
        
        # Single symbol
        quotes = await get_quotes(session, "SPX")
        print(f"SPX: {quotes[0].last}")
        
        # Multiple symbols
        quotes = await get_quotes(session, ["AAPL", "SPY", "SPX"])
        for quote in quotes:
            print(f"{quote.symbol}: {quote.last}")
    """
    # Handle single symbol or list
    if isinstance(symbols, str):
        symbols = [symbols]

    # Guard against empty symbol list - would cause API error "Invalid Parameter: symbols"
    if not symbols:
        return []

    params = {
        'symbols': ','.join(symbols),
        'greeks': str(greeks).lower(),
    }

    data = await session._a_get('/markets/quotes', params=params)
    quotes_data = data.get('quotes')

    if not quotes_data:
        logger.warning('No quotes data in response for {}', symbols)
        return []

    # Handle single vs multiple quotes
    quote_list = quotes_data.get('quote', [])
    if isinstance(quote_list, dict):
        quote_list = [quote_list]

    quotes = [Quote.model_validate(q) for q in quote_list]
    logger.trace('Retrieved {} quotes', len(quotes))

    return quotes


def get_quotes_sync(
    session: Session,
    symbols: list[str] | str,
    *,
    greeks: bool = False,
) -> list[Quote]:
    """
    Get current market quotes for one or more symbols (synchronous).

    :param session: An authenticated Tradier Session.
    :param symbols: A symbol or list of symbols to get quotes for.
    :param greeks: Include option Greeks in the response (options only).
    :returns: A list of Quote objects.

    Example::

        from optrabot.broker.tradier import Session
        from optrabot.broker.tradier.markets import get_quotes_sync

        session = Session("your-token")
        quotes = get_quotes_sync(session, ["AAPL", "SPY"])
    """
    # Handle single symbol or list
    if isinstance(symbols, str):
        symbols = [symbols]

    params = {
        'symbols': ','.join(symbols),
        'greeks': str(greeks).lower(),
    }

    data = session._get('/markets/quotes', params=params)
    quotes_data = data.get('quotes')

    if not quotes_data:
        logger.warning('No quotes data in response for {}', symbols)
        return []

    # Handle single vs multiple quotes
    quote_list = quotes_data.get('quote', [])
    if isinstance(quote_list, dict):
        quote_list = [quote_list]

    quotes = [Quote.model_validate(q) for q in quote_list]
    logger.trace('Retrieved {} quotes', len(quotes))

    return quotes


# ========== Historical Data Models ==========


class HistoryBar(TradierData):
    """
    A single OHLC bar from historical data.
    
    Attributes:
        date: The date of the bar (daily) or datetime (intraday).
        open: Opening price.
        high: High price.
        low: Low price.
        close: Closing price.
        volume: Trading volume.
    """
    date: date | datetime
    open: Decimal
    high: Decimal
    low: Decimal
    close: Decimal
    volume: int = 0

    def __repr__(self) -> str:
        return f'HistoryBar({self.date}, O={self.open}, H={self.high}, L={self.low}, C={self.close})'


# ========== Historical Data Functions ==========


async def get_history(
    session: Session,
    symbol: str,
    *,
    interval: str = 'daily',
    start: date | None = None,
    end: date | None = None,
) -> list[HistoryBar]:
    """
    Get historical pricing for a security.
    
    This data usually covers the entire lifetime of the security if sending
    reasonable start/end times. You can fetch historical pricing for options
    by passing the OCC option symbol.
    
    :param session: An authenticated Tradier Session.
    :param symbol: The symbol to get history for (e.g., "SPX", "AAPL", or OCC option symbol).
    :param interval: Bar interval - 'daily', 'weekly', or 'monthly'.
    :param start: Start date for the history request (optional).
    :param end: End date for the history request (optional).
    :returns: A list of HistoryBar objects sorted by date ascending.
    
    Example::
    
        from datetime import date
        from optrabot.broker.tradier import Session
        from optrabot.broker.tradier.markets import get_history
        
        session = Session("your-token")
        bars = await get_history(session, "SPX", start=date(2024, 1, 1))
    """
    params: dict[str, str] = {
        'symbol': symbol,
        'interval': interval,
    }
    
    if start:
        params['start'] = start.isoformat()
    if end:
        params['end'] = end.isoformat()
    
    data = await session._a_get('/markets/history', params=params)
    
    return _parse_history_response(data)


def get_history_sync(
    session: Session,
    symbol: str,
    *,
    interval: str = 'daily',
    start: date | None = None,
    end: date | None = None,
) -> list[HistoryBar]:
    """
    Get historical pricing for a security (synchronous).
    
    See get_history for parameter documentation.
    """
    params: dict[str, str] = {
        'symbol': symbol,
        'interval': interval,
    }
    
    if start:
        params['start'] = start.isoformat()
    if end:
        params['end'] = end.isoformat()
    
    data = session._get('/markets/history', params=params)
    
    return _parse_history_response(data)


def _parse_history_response(data: dict) -> list[HistoryBar]:
    """Parse the history API response into HistoryBar objects."""
    history_data = data.get('history')
    
    if not history_data:
        logger.warning('No history data in response')
        return []
    
    day_list = history_data.get('day', [])
    if isinstance(day_list, dict):
        day_list = [day_list]
    
    if not day_list:
        return []
    
    bars = []
    for day in day_list:
        try:
            # Parse the date - could be date string or datetime
            date_str = day.get('date')
            if 'T' in str(date_str):
                bar_date = datetime.fromisoformat(date_str)
            else:
                bar_date = date.fromisoformat(date_str)
            
            bar = HistoryBar(
                date=bar_date,
                open=Decimal(str(day.get('open', 0))),
                high=Decimal(str(day.get('high', 0))),
                low=Decimal(str(day.get('low', 0))),
                close=Decimal(str(day.get('close', 0))),
                volume=int(day.get('volume', 0)),
            )
            bars.append(bar)
        except (ValueError, KeyError) as e:
            logger.warning(f'Error parsing history bar: {e}')
            continue
    
    logger.trace(f'Retrieved {len(bars)} history bars')
    return bars


# ========== Time & Sales (Intraday) Functions ==========


class TimeSalesBar(TradierData):
    """
    A single bar from time and sales (intraday) data.
    
    Attributes:
        time: The timestamp of the bar.
        timestamp: Unix timestamp in milliseconds.
        price: Last trade price in this interval.
        open: Opening price.
        high: High price.
        low: Low price.
        close: Closing price.
        volume: Trading volume.
        vwap: Volume-weighted average price.
    """
    time: datetime | None = None
    timestamp: int | None = None
    price: Decimal | None = None
    open: Decimal | None = None
    high: Decimal | None = None
    low: Decimal | None = None
    close: Decimal | None = None
    volume: int = 0
    vwap: Decimal | None = None

    def __repr__(self) -> str:
        return f'TimeSalesBar({self.time}, O={self.open}, H={self.high}, L={self.low}, C={self.close})'


async def get_timesales(
    session: Session,
    symbol: str,
    *,
    interval: str = '5min',
    start: datetime | None = None,
    end: datetime | None = None,
    session_filter: str = 'all',
) -> list[TimeSalesBar]:
    """
    Get time and sales (intraday) data for a security.
    
    This endpoint is typically used for intraday charting. It captures
    pricing across a time slice at predefined intervals.
    
    :param session: An authenticated Tradier Session.
    :param symbol: The symbol to get data for (e.g., "SPX", "AAPL").
    :param interval: Bar interval - 'tick', '1min', '5min', '15min'.
    :param start: Start datetime (optional). Format: YYYY-MM-DD HH:MM
    :param end: End datetime (optional). Format: YYYY-MM-DD HH:MM
    :param session_filter: Session filter - 'all', 'open' (regular hours only).
    :returns: A list of TimeSalesBar objects sorted by time ascending.
    
    Note: Time and sales data is limited to about 20 trading days.
    
    Example::
    
        from datetime import datetime
        from optrabot.broker.tradier import Session
        from optrabot.broker.tradier.markets import get_timesales
        
        session = Session("your-token")
        bars = await get_timesales(session, "SPX", interval="5min")
    """
    params: dict[str, str] = {
        'symbol': symbol,
        'interval': interval,
        'session_filter': session_filter,
    }
    
    if start:
        # Tradier expects format: YYYY-MM-DD HH:MM
        params['start'] = start.strftime('%Y-%m-%d %H:%M')
    if end:
        params['end'] = end.strftime('%Y-%m-%d %H:%M')
    
    data = await session._a_get('/markets/timesales', params=params)
    
    return _parse_timesales_response(data)


def get_timesales_sync(
    session: Session,
    symbol: str,
    *,
    interval: str = '5min',
    start: datetime | None = None,
    end: datetime | None = None,
    session_filter: str = 'all',
) -> list[TimeSalesBar]:
    """
    Get time and sales (intraday) data for a security (synchronous).
    
    See get_timesales for parameter documentation.
    """
    params: dict[str, str] = {
        'symbol': symbol,
        'interval': interval,
        'session_filter': session_filter,
    }
    
    if start:
        params['start'] = start.strftime('%Y-%m-%d %H:%M')
    if end:
        params['end'] = end.strftime('%Y-%m-%d %H:%M')
    
    data = session._get('/markets/timesales', params=params)
    
    return _parse_timesales_response(data)


def _parse_timesales_response(data: dict) -> list[TimeSalesBar]:
    """Parse the timesales API response into TimeSalesBar objects."""
    series_data = data.get('series')
    
    if not series_data:
        logger.warning('No timesales data in response')
        return []
    
    data_list = series_data.get('data', [])
    if isinstance(data_list, dict):
        data_list = [data_list]
    
    if not data_list:
        return []
    
    bars = []
    for item in data_list:
        try:
            # Parse the time - format is "YYYY-MM-DDTHH:MM:SS"
            time_str = item.get('time')
            bar_time = datetime.fromisoformat(time_str) if time_str else None
            
            bar = TimeSalesBar(
                time=bar_time,
                timestamp=item.get('timestamp'),
                price=Decimal(str(item.get('price', 0))) if item.get('price') else None,
                open=Decimal(str(item.get('open', 0))) if item.get('open') else None,
                high=Decimal(str(item.get('high', 0))) if item.get('high') else None,
                low=Decimal(str(item.get('low', 0))) if item.get('low') else None,
                close=Decimal(str(item.get('close', 0))) if item.get('close') else None,
                volume=int(item.get('volume', 0)),
                vwap=Decimal(str(item.get('vwap', 0))) if item.get('vwap') else None,
            )
            bars.append(bar)
        except (ValueError, KeyError) as e:
            logger.warning(f'Error parsing timesales bar: {e}')
            continue
    
    logger.trace(f'Retrieved {len(bars)} timesales bars')
    return bars