"""Fetch ETF/futures/BTC prices via yfinance.

Returns pandas DataFrames and structured dicts for journal/weekly/event notes.
"""
from __future__ import annotations

import sys
import time
from datetime import date, datetime, timedelta
from pathlib import Path

import pandas as pd
import yfinance as yf

# Allow `from universe import UNIVERSE` etc.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from universe import UNIVERSE, headline_yf_tickers, yf_tickers  # noqa: E402


def fetch_close_history(tickers: list[str], period: str = "6mo",
                        progress: bool = False) -> pd.DataFrame:
    """Fetch adjusted close for a list of tickers. Returns DataFrame indexed by date,
    columns = tickers. Handles both MultiIndex and flat-column yfinance responses
    (yfinance returns inconsistent shapes when tickers include ^-prefixed indices
    or crypto like BTC-USD)."""
    if not tickers:
        return pd.DataFrame()
    df = yf.download(tickers, period=period, progress=progress,
                     auto_adjust=True, threads=True)
    if df is None or df.empty:
        return pd.DataFrame()
    if isinstance(df.columns, pd.MultiIndex):
        # Normal case: levels are (Price, Ticker) — pick Close
        if "Close" in df.columns.get_level_values(0):
            closes = df["Close"]
        elif "Adj Close" in df.columns.get_level_values(0):
            closes = df["Adj Close"]
        else:
            return pd.DataFrame()
    else:
        # Flat columns: yfinance sometimes returns just Close-level columns
        # when tickers include ^-prefixed or crypto. Two sub-cases:
        #   a) "Close" is a column (single ticker) -> single-column DataFrame
        #   b) tickers are columns directly
        if "Close" in df.columns and len(df.columns) == 1:
            closes = df[["Close"]]
            closes.columns = [tickers[0]]
        else:
            closes = df
    # Final guard: if closes is a Series (single-ticker path), promote to DataFrame
    if isinstance(closes, pd.Series):
        closes = closes.to_frame()
        closes.columns = tickers[:1]
    return closes


def last_close(ticker: str, df: pd.DataFrame | None = None) -> float | None:
    """Last available close for a single ticker."""
    if df is None:
        df = fetch_close_history([ticker], period="5d")
    if df.empty:
        return None
    if isinstance(df.columns, pd.MultiIndex):
        try:
            series = df["Close"][ticker]
        except KeyError:
            return None
    else:
        # Flat columns: either single column "Close" (single-ticker) or ticker columns
        if "Close" in df.columns:
            series = df["Close"]
        elif ticker in df.columns:
            series = df[ticker]
        else:
            return None
    series = series.dropna()
    if series.empty:
        return None
    return float(series.iloc[-1])


def change_metrics(ticker: str, df: pd.DataFrame | None = None) -> dict:
    """Compute 1d, 5d (1w), and 1m percentage changes."""
    if df is None:
        df = fetch_close_history([ticker], period="2mo")
    if df.empty:
        return {"ticker": ticker, "last": None, "chg_1d_pct": None,
                "chg_5d_pct": None, "chg_1m_pct": None, "as_of": None}
    if isinstance(df.columns, pd.MultiIndex):
        try:
            series = df["Close"][ticker].dropna()
        except KeyError:
            return {"ticker": ticker, "last": None, "chg_1d_pct": None,
                    "chg_5d_pct": None, "chg_1m_pct": None, "as_of": None}
    else:
        if "Close" in df.columns:
            series = df["Close"].dropna()
        elif ticker in df.columns:
            series = df[ticker].dropna()
        else:
            return {"ticker": ticker, "last": None, "chg_1d_pct": None,
                    "chg_5d_pct": None, "chg_1m_pct": None, "as_of": None}
    if series.empty:
        return {"ticker": ticker, "last": None, "chg_1d_pct": None,
                "chg_5d_pct": None, "chg_1m_pct": None, "as_of": None}
    last = float(series.iloc[-1])
    as_of = series.index[-1]
    as_of_str = as_of.strftime("%Y-%m-%d") if hasattr(as_of, "strftime") else str(as_of)
    def pct(n_bars: int) -> float | None:
        if len(series) > n_bars:
            prior = float(series.iloc[-1 - n_bars])
            if prior != 0:
                return round((last - prior) / prior * 100, 2)
        return None
    return {
        "ticker": ticker,
        "last": round(last, 4),
        "chg_1d_pct": pct(1),
        "chg_5d_pct": pct(5),
        "chg_1m_pct": pct(21),
        "as_of": as_of_str,
    }


def all_change_metrics(period: str = "2mo") -> list[dict]:
    """Compute change metrics for the entire universe in one yfinance call."""
    tickers = yf_tickers()
    if not tickers:
        return []
    # Pull a wide enough window: 1m and 5d changes
    df = fetch_close_history(tickers, period=period)
    out = []
    for u in UNIVERSE:
        m = change_metrics(u["yf"], df=df)
        m["name"] = u["name"]
        m["display"] = u["display"]
        m["bucket"] = u["bucket"]
        m["kind"] = u["kind"]
        out.append(m)
    return out


def headline_metrics() -> list[dict]:
    """Quick change metrics for the headline set only (faster)."""
    tickers = headline_yf_tickers()
    df = fetch_close_history(tickers, period="2mo")
    out = []
    name_map = {u["yf"]: u["name"] for u in UNIVERSE}
    disp_map = {u["yf"]: u["display"] for u in UNIVERSE}
    for t in tickers:
        m = change_metrics(t, df=df)
        m["name"] = name_map.get(t, t)
        m["display"] = disp_map.get(t, t)
        out.append(m)
    return out


def fetch_event_reaction(event_time_utc: datetime, window_hours: int = 1) -> list[dict]:
    """For a given event time, fetch the price move in the window [-1h, +1h]
    around the event for the headline universe. Returns pct moves."""
    tickers = headline_yf_tickers()
    # Pull last 5d of hourly data
    df = yf.download(tickers, period="5d", interval="1h",
                     progress=False, auto_adjust=True, threads=True)
    if df.empty:
        return []
    if isinstance(df.columns, pd.MultiIndex):
        try:
            closes = df["Close"]
        except KeyError:
            return []
    else:
        closes = df[["Close"]] if "Close" in df.columns else df
    # Event window: [-1h, +1h] around event_time
    # Find the bar closest to event_time
    out = []
    for t in tickers:
        if t not in closes.columns:
            continue
        s = closes[t].dropna()
        if s.empty:
            continue
        # Find the bar at or just before event_time
        prior = s[s.index <= event_time_utc]
        post = s[s.index >= event_time_utc]
        if prior.empty or post.empty:
            continue
        pre_price = float(prior.iloc[-1])
        post_price = float(post.iloc[0])
        if pre_price == 0:
            continue
        chg_pct = round((post_price - pre_price) / pre_price * 100, 2)
        out.append({
            "ticker": t,
            "pre": round(pre_price, 4),
            "post": round(post_price, 4),
            "chg_pct": chg_pct,
            "pre_time": prior.index[-1].strftime("%Y-%m-%d %H:%M %Z") if hasattr(prior.index[-1], 'strftime') else str(prior.index[-1]),
            "post_time": post.index[0].strftime("%Y-%m-%d %H:%M %Z") if hasattr(post.index[0], 'strftime') else str(post.index[0]),
        })
    return out


if __name__ == "__main__":
    # Quick smoke test
    print("Fetching headline universe...")
    t0 = time.time()
    metrics = headline_metrics()
    print(f"  {len(metrics)} tickers in {time.time()-t0:.1f}s")
    print()
    print(f"{'Ticker':<10} {'Last':>10} {'1d%':>8} {'5d%':>8} {'1m%':>8}  As of")
    for m in metrics:
        print(f"{m['ticker']:<10} "
              f"{m['last']!s:>10} "
              f"{m['chg_1d_pct']!s:>8} "
              f"{m['chg_5d_pct']!s:>8} "
              f"{m['chg_1m_pct']!s:>8}  "
              f"{m['as_of']}")
