"""Yahoo Finance data source implementation."""

import asyncio
from datetime import datetime, timedelta
from typing import Any

import pandas as pd
import yfinance as yf

from openclaw.data.interface import (
    DataNotAvailableError,
    DataSource,
    DataSourceError,
    Interval,
    RealtimeQuote,
)


class YahooFinanceDataSource(DataSource):
    """Yahoo Finance data source implementation.

    Uses the yfinance library to fetch stock data from Yahoo Finance.
    Implements caching to reduce API calls.
    """

    # Mapping of Interval to yfinance period strings
    _INTERVAL_MAP: dict[Interval, str] = {
        Interval.MINUTE_1: "1m",
        Interval.MINUTE_5: "5m",
        Interval.MINUTE_15: "15m",
        Interval.MINUTE_30: "30m",
        Interval.HOUR_1: "1h",
        Interval.HOUR_4: "4h",
        Interval.DAY_1: "1d",
        Interval.WEEK_1: "1wk",
        Interval.MONTH_1: "1mo",
    }

    def __init__(self, cache_ttl: int = 60) -> None:
        """Initialize Yahoo Finance data source.

        Args:
            cache_ttl: Cache time-to-live in seconds (default: 60)
        """
        super().__init__("yahoo_finance")
        self._cache_ttl = cache_ttl
        self._cache: dict[str, tuple[pd.DataFrame, datetime]] = {}
        self._last_check: datetime | None = None

    def _get_cache_key(
        self,
        symbol: str,
        interval: Interval,
        start: datetime | None,
        end: datetime | None,
    ) -> str:
        """Generate cache key for a data request."""
        start_str = start.isoformat() if start else "None"
        end_str = end.isoformat() if end else "None"
        return f"{symbol}:{interval.value}:{start_str}:{end_str}"

    def _is_cache_valid(self, cache_time: datetime) -> bool:
        """Check if cached data is still valid."""
        return datetime.now() - cache_time < timedelta(seconds=self._cache_ttl)

    def _get_yfinance_interval(self, interval: Interval) -> str:
        """Convert Interval enum to yfinance interval string."""
        if interval not in self._INTERVAL_MAP:
            raise DataSourceError(f"Unsupported interval: {interval}")
        return self._INTERVAL_MAP[interval]

    def _clear_expired_cache(self) -> None:
        """Remove expired entries from cache."""
        expired_keys = [
            key
            for key, (_, cache_time) in self._cache.items()
            if not self._is_cache_valid(cache_time)
        ]
        for key in expired_keys:
            del self._cache[key]

    async def fetch_ohlcv(
        self,
        symbol: str,
        interval: Interval = Interval.DAY_1,
        start: datetime | None = None,
        end: datetime | None = None,
        limit: int = 100,
    ) -> pd.DataFrame:
        """Fetch OHLCV data from Yahoo Finance.

        Args:
            symbol: Stock symbol (e.g., 'AAPL', 'MSFT')
            interval: Time interval for data points
            start: Start date/time (optional)
            end: End date/time (optional)
            limit: Maximum number of data points to return

        Returns:
            DataFrame with columns: timestamp, open, high, low, close, volume

        Raises:
            DataNotAvailableError: If data cannot be fetched
            DataSourceError: For other data source errors
        """
        self._clear_expired_cache()

        cache_key = self._get_cache_key(symbol, interval, start, end)

        # Check cache
        if cache_key in self._cache:
            df, cache_time = self._cache[cache_key]
            if self._is_cache_valid(cache_time):
                return df.head(limit)

        try:
            # Run yfinance in thread pool to not block
            loop = asyncio.get_event_loop()
            df = await loop.run_in_executor(
                None,
                self._fetch_yfinance_data,
                symbol,
                interval,
                start,
                end,
            )

            if df.empty:
                raise DataNotAvailableError(
                    f"No data available for {symbol} with interval {interval.value}"
                )

            # Standardize column names
            df = df.reset_index()
            if "Date" in df.columns:
                df = df.rename(columns={"Date": "timestamp"})
            elif "Datetime" in df.columns:
                df = df.rename(columns={"Datetime": "timestamp"})

            df = df.rename(
                columns={
                    "Open": "open",
                    "High": "high",
                    "Low": "low",
                    "Close": "close",
                    "Volume": "volume",
                }
            )

            # Ensure required columns exist
            required_cols = ["timestamp", "open", "high", "low", "close", "volume"]
            for col in required_cols:
                if col not in df.columns:
                    raise DataSourceError(f"Missing required column: {col}")

            # Cache the result
            self._cache[cache_key] = (df, datetime.now())

            return df.head(limit)

        except DataNotAvailableError:
            raise
        except Exception as e:
            raise DataSourceError(f"Failed to fetch data for {symbol}: {e}") from e

    def _fetch_yfinance_data(
        self,
        symbol: str,
        interval: Interval,
        start: datetime | None,
        end: datetime | None,
    ) -> pd.DataFrame:
        """Fetch data using yfinance (synchronous)."""
        ticker = yf.Ticker(symbol)
        yf_interval = self._get_yfinance_interval(interval)

        # Determine period based on interval and limit
        if start and end:
            df = ticker.history(
                start=start,
                end=end,
                interval=yf_interval,
            )
        else:
            # Use period for recent data
            period = self._get_period_for_interval(interval)
            df = ticker.history(period=period, interval=yf_interval)

        return df

    def _get_period_for_interval(self, interval: Interval) -> str:
        """Get appropriate period string for interval."""
        period_map: dict[Interval, str] = {
            Interval.MINUTE_1: "5d",  # 1m data limited to 7 days
            Interval.MINUTE_5: "1mo",
            Interval.MINUTE_15: "1mo",
            Interval.MINUTE_30: "1mo",
            Interval.HOUR_1: "3mo",
            Interval.HOUR_4: "6mo",
            Interval.DAY_1: "1y",
            Interval.WEEK_1: "5y",
            Interval.MONTH_1: "max",
        }
        return period_map.get(interval, "1y")

    async def fetch_realtime(self, symbol: str) -> RealtimeQuote:
        """Fetch real-time quote from Yahoo Finance.

        Args:
            symbol: Stock symbol (e.g., 'AAPL', 'MSFT')

        Returns:
            RealtimeQuote with current market data

        Raises:
            DataNotAvailableError: If real-time data is not available
            DataSourceError: For other data source errors
        """
        try:
            loop = asyncio.get_event_loop()
            ticker_info = await loop.run_in_executor(
                None,
                self._fetch_ticker_info,
                symbol,
            )

            if not ticker_info:
                raise DataNotAvailableError(
                    f"No real-time data available for {symbol}"
                )

            return RealtimeQuote(
                symbol=symbol,
                price=ticker_info.get("currentPrice", 0.0),
                bid=ticker_info.get("bid", 0.0),
                ask=ticker_info.get("ask", 0.0),
                bid_size=ticker_info.get("bidSize", 0),
                ask_size=ticker_info.get("askSize", 0),
                volume=ticker_info.get("volume", 0.0),
                timestamp=datetime.now(),
            )

        except DataNotAvailableError:
            raise
        except Exception as e:
            raise DataSourceError(
                f"Failed to fetch real-time data for {symbol}: {e}"
            ) from e

    def _fetch_ticker_info(self, symbol: str) -> dict[str, Any]:
        """Fetch ticker info from yfinance (synchronous)."""
        ticker = yf.Ticker(symbol)
        info: dict[str, Any] = ticker.info
        return info

    def is_available(self) -> bool:
        """Check if Yahoo Finance is available.

        Returns:
            True if Yahoo Finance can be reached, False otherwise
        """
        if not self._available:
            return False

        # Only check periodically to avoid excessive network calls
        if self._last_check and datetime.now() - self._last_check < timedelta(
            minutes=5
        ):
            return self._available

        try:
            # Try to fetch info for a well-known stock
            ticker = yf.Ticker("AAPL")
            _ = ticker.info.get("symbol")
            self._available = True
        except Exception:
            self._available = False

        self._last_check = datetime.now()
        return self._available

    def clear_cache(self) -> None:
        """Clear all cached data."""
        self._cache.clear()

    def get_cache_stats(self) -> dict[str, Any]:
        """Get cache statistics.

        Returns:
            Dictionary with cache statistics
        """
        return {
            "size": len(self._cache),
            "ttl_seconds": self._cache_ttl,
            "keys": list(self._cache.keys()),
        }
