"""Data access layer built on yfinance (free Yahoo Finance data).

Everything the strategies need is normalised into a single `StockData`
object so the scoring code never touches yfinance directly. yfinance is
flaky about which fields exist per ticker, so every getter is defensive.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Optional

import numpy as np
import pandas as pd
import yfinance as yf


def _num(value: Any) -> Optional[float]:
    """Coerce a yfinance value to float, or None if missing/garbage."""
    if value is None:
        return None
    try:
        f = float(value)
    except (TypeError, ValueError):
        return None
    if np.isnan(f) or np.isinf(f):
        return None
    return f


@dataclass
class StockData:
    ticker: str
    info: dict = field(default_factory=dict)
    history: pd.DataFrame = field(default_factory=pd.DataFrame)
    ok: bool = False
    error: str = ""

    # ---- convenience getters over the messy info dict ----
    def get(self, key: str) -> Optional[float]:
        return _num(self.info.get(key))

    @property
    def name(self) -> str:
        return self.info.get("shortName") or self.info.get("longName") or self.ticker

    @property
    def sector(self) -> str:
        return self.info.get("sector") or "Unknown"

    @property
    def price(self) -> Optional[float]:
        p = self.get("currentPrice") or self.get("regularMarketPrice")
        if p is None and not self.history.empty:
            p = _num(self.history["Close"].iloc[-1])
        return p

    @property
    def market_cap(self) -> Optional[float]:
        return self.get("marketCap")

    def returns(self, days: int) -> Optional[float]:
        """Total return over the trailing `days` trading days, as a fraction."""
        if self.history.empty or len(self.history) <= days:
            return None
        close = self.history["Close"]
        past = _num(close.iloc[-days - 1])
        now = _num(close.iloc[-1])
        if not past or past == 0 or now is None:
            return None
        return now / past - 1.0


def fetch(ticker: str, period: str = "1y") -> StockData:
    """Fetch info + price history for a single ticker. Never raises."""
    sd = StockData(ticker=ticker)
    try:
        tk = yf.Ticker(ticker)
        # .info can be slow/partial; guard it.
        try:
            sd.info = dict(tk.info or {})
        except Exception as exc:  # noqa: BLE001
            sd.info = {}
            sd.error = f"info: {exc}"
        try:
            sd.history = tk.history(period=period, auto_adjust=True)
        except Exception as exc:  # noqa: BLE001
            sd.history = pd.DataFrame()
            sd.error = (sd.error + f" hist: {exc}").strip()
        sd.ok = bool(sd.info) or not sd.history.empty
    except Exception as exc:  # noqa: BLE001
        sd.error = str(exc)
        sd.ok = False
    return sd
