"""
=========================================
CV6 AI Trading OS — Backtest Data Feed
Fetches REAL historical OHLCV candles.
Primary source: yfinance (NSE .NS suffix)
No future peeking — candles served in order.
=========================================
"""
from __future__ import annotations

import datetime
from dataclasses import dataclass, asdict
from typing import List, Optional
from loguru import logger


# ── Candle dataclass ──────────────────────────────────────────────────────────

@dataclass
class HistoricalCandle:
    timestamp:  str          # ISO datetime string (index from yfinance)
    date:       str          # YYYY-MM-DD
    time:       str          # HH:MM (for intraday)
    open:       float
    high:       float
    low:        float
    close:      float
    volume:     int
    symbol:     str

    def to_dict(self) -> dict:
        return asdict(self)

    @property
    def typical_price(self) -> float:
        return (self.high + self.low + self.close) / 3.0

    @property
    def change_pct(self) -> float:
        return round((self.close - self.open) / self.open * 100, 3) if self.open else 0.0


# ── NSE symbol → yfinance mapping ────────────────────────────────────────────

_INDEX_MAP = {
    "NIFTY": "^NSEI",
    "NIFTY50": "^NSEI",
    "NIFTY 50": "^NSEI",
    "BANKNIFTY": "^NSEBANK",
    "FINNIFTY": "NIFTY_FIN_SERVICE.NS",
    "MIDCAP": "^CNXMDCP",
    "SENSEX": "^BSESN",
}

_SYMBOL_OVERRIDES = {
    "M&M": "M%26M.NS",         # & needs encoding for some APIs
    "LTIMindtree": "LTIM.NS",
}


def _to_yf_symbol(symbol: str) -> str:
    """Convert NSE symbol to yfinance ticker string."""
    s = symbol.upper().strip()
    if s.endswith(".NS") or s.endswith(".BO"):
        return s
    if s in _INDEX_MAP:
        return _INDEX_MAP[s]
    if s in _SYMBOL_OVERRIDES:
        return _SYMBOL_OVERRIDES[s]
    return s + ".NS"


# ── BacktestDataFeed ──────────────────────────────────────────────────────────

class BacktestDataFeed:
    """
    Fetches historical OHLCV candles for a symbol.
    Candles are always returned oldest-first (time-ordered).
    The consumer sees candles one at a time — no future data is exposed.
    """

    VALID_INTERVALS = ("1m", "2m", "5m", "15m", "30m", "60m", "1h", "1d", "1wk", "1mo")

    def fetch_candles(
        self,
        symbol: str,
        from_date: str,         # YYYY-MM-DD
        to_date: str,           # YYYY-MM-DD
        interval: str = "1d",
    ) -> List[HistoricalCandle]:
        """
        Fetch and return candles for symbol between from_date and to_date.
        Raises RuntimeError if yfinance unavailable or no data returned.
        """
        if interval not in self.VALID_INTERVALS:
            raise ValueError(f"Invalid interval {interval}. Valid: {self.VALID_INTERVALS}")

        try:
            import yfinance as yf
        except ImportError:
            raise RuntimeError(
                "yfinance not installed. "
                "Run: pip install yfinance --break-system-packages"
            )

        yf_sym = _to_yf_symbol(symbol)
        # yfinance end date is exclusive — add 1 day
        end_exclusive = (
            datetime.datetime.strptime(to_date, "%Y-%m-%d") + datetime.timedelta(days=1)
        ).strftime("%Y-%m-%d")

        logger.info(f"[DataFeed] Fetching {yf_sym} {from_date}→{to_date} [{interval}]")

        ticker = yf.Ticker(yf_sym)
        df = ticker.history(
            start=from_date,
            end=end_exclusive,
            interval=interval,
            auto_adjust=True,
            prepost=False,
        )

        if df is None or df.empty:
            raise RuntimeError(
                f"No data returned for {symbol} ({yf_sym}) "
                f"from {from_date} to {to_date} at {interval} interval."
            )

        candles: List[HistoricalCandle] = []
        for ts, row in df.iterrows():
            ts_str = str(ts)
            date_str = ts_str[:10]
            time_str = ts_str[11:16] if len(ts_str) > 10 else "09:15"

            candles.append(HistoricalCandle(
                timestamp=ts_str,
                date=date_str,
                time=time_str,
                open=round(float(row["Open"]), 2),
                high=round(float(row["High"]), 2),
                low=round(float(row["Low"]), 2),
                close=round(float(row["Close"]), 2),
                volume=int(row.get("Volume", 0)),
                symbol=symbol,
            ))

        # Always sort oldest → newest (some APIs may return unsorted)
        candles.sort(key=lambda c: c.timestamp)
        logger.info(f"[DataFeed] {symbol}: {len(candles)} candles loaded")
        return candles

    def fetch_multi(
        self,
        symbols: List[str],
        from_date: str,
        to_date: str,
        interval: str = "1d",
    ) -> dict:
        """
        Fetch candles for multiple symbols.
        Returns {symbol: [HistoricalCandle, ...]} dict.
        Logs errors per symbol; does NOT raise.
        """
        result = {}
        for sym in symbols:
            try:
                result[sym] = self.fetch_candles(sym, from_date, to_date, interval)
            except Exception as e:
                logger.warning(f"[DataFeed] {sym} failed: {e}")
                result[sym] = []
        return result


# Singleton
backtest_data_feed = BacktestDataFeed()
