"""CTF leaderboard scraper and rank estimator.

Scrapes current leaderboard standings from jkpfactors.com/ctf and estimates
expected rank for a submitted model's Sharpe ratio.

Usage::

    board = fetch_leaderboard()
    rank = board.estimate_rank(sharpe=1.5)
    print(board)
"""

from __future__ import annotations

from dataclasses import dataclass, field

import pandas as pd

# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


@dataclass
class LeaderboardEntry:
    """Single leaderboard entry."""

    rank: int
    model_name: str
    sharpe: float
    annual_return: float | None = None
    volatility: float | None = None


@dataclass
class LeaderboardResult:
    """Current leaderboard state and rank estimation."""

    entries: list[LeaderboardEntry] = field(default_factory=list)
    source: str = "unknown"  # "scraped" | "manual" | "unavailable"

    @property
    def top_sharpe(self) -> float | None:
        if not self.entries:
            return None
        return max(e.sharpe for e in self.entries)

    def estimate_rank(self, sharpe: float) -> int:
        """Estimate submission rank by Sharpe comparison.

        Parameters
        ----------
        sharpe : float
            The submitted model's annualized Sharpe ratio.

        Returns
        -------
        int
            Estimated rank (1 = best).  Returns len(entries)+1 if below
            all current entries, or 1 if above all.
        """
        if not self.entries:
            return 1

        # Count entries with strictly higher Sharpe
        n_better = sum(1 for e in self.entries if e.sharpe > sharpe)
        return n_better + 1

    def __str__(self) -> str:
        lines = [
            "=" * 60,
            f"CTF LEADERBOARD  (source: {self.source})",
            "=" * 60,
            f"  {'Rank':>4}  {'Sharpe':>8}  {'Model':<30}",
            "  " + "-" * 50,
        ]
        for e in self.entries[:20]:  # Show top 20
            lines.append(f"  {e.rank:>4}  {e.sharpe:>8.3f}  {e.model_name:<30}")
        if len(self.entries) > 20:
            lines.append(f"  ... ({len(self.entries) - 20} more entries)")
        lines.append("=" * 60)
        return "\n".join(lines)


# ---------------------------------------------------------------------------
# Scraper
# ---------------------------------------------------------------------------


def fetch_leaderboard(timeout: int = 10) -> LeaderboardResult:
    """Scrape the current CTF leaderboard from jkpfactors.com/ctf.

    Attempts to fetch and parse the leaderboard table.  Falls back to a
    stub ``LeaderboardResult`` with ``source="unavailable"`` on failure.

    Parameters
    ----------
    timeout : int
        HTTP request timeout in seconds.

    Returns
    -------
    LeaderboardResult
    """
    try:
        return _scrape_leaderboard(timeout=timeout)
    except Exception as exc:
        print(
            f"[WARNING] Could not fetch leaderboard: {type(exc).__name__}: {exc}\n"
            f"  Use LeaderboardResult with manual entries or call "
            f"fetch_leaderboard() again when network is available."
        )
        return LeaderboardResult(entries=[], source="unavailable")


def _scrape_leaderboard(timeout: int = 10) -> LeaderboardResult:
    """Internal: fetch and parse leaderboard HTML."""
    try:
        import requests
    except ImportError:
        raise ImportError(
            "requests is required to scrape the leaderboard. "
            "Install it: pip install requests"
        )

    url = "https://jkpfactors.com/ctf"
    resp = requests.get(url, timeout=timeout, headers={"User-Agent": "eap-ctf/1.0"})
    resp.raise_for_status()

    # Try pandas HTML parsing (requires lxml or html5lib)
    try:
        tables = pd.read_html(resp.text)
    except Exception as exc:
        raise ValueError(f"Could not parse leaderboard HTML tables: {exc}") from exc

    if not tables:
        raise ValueError("No HTML tables found on leaderboard page.")

    # Find the table most likely to be the leaderboard (contains Sharpe column)
    lb_table: pd.DataFrame | None = None
    for tbl in tables:
        cols_lower = [str(c).lower() for c in tbl.columns]
        if any("sharpe" in c for c in cols_lower):
            lb_table = tbl
            break

    if lb_table is None:
        raise ValueError(
            "Could not identify leaderboard table (no 'Sharpe' column found). "
            "The page structure may have changed."
        )

    entries: list[LeaderboardEntry] = []
    cols_lower = [str(c).lower() for c in lb_table.columns]

    # Map column indices
    def _find_col(keywords: list[str]) -> str | None:
        for kw in keywords:
            for orig, low in zip(lb_table.columns, cols_lower):
                if kw in low:
                    return orig
        return None

    sharpe_col = _find_col(["sharpe"])
    name_col = _find_col(["model", "name", "method", "algorithm"])
    ret_col = _find_col(["return", "ret"])
    vol_col = _find_col(["vol", "volatility"])

    if sharpe_col is None:
        raise ValueError("Sharpe column not found in leaderboard table.")

    for rank_idx, (_, row) in enumerate(lb_table.iterrows(), start=1):
        try:
            sharpe = float(row[sharpe_col])
        except (ValueError, TypeError):
            continue

        model_name = str(row[name_col]) if name_col else f"Model #{rank_idx}"
        ann_ret = None
        if ret_col is not None:
            try:
                ann_ret = float(row[ret_col])
            except (ValueError, TypeError):
                pass
        vol = None
        if vol_col is not None:
            try:
                vol = float(row[vol_col])
            except (ValueError, TypeError):
                pass

        entries.append(LeaderboardEntry(
            rank=rank_idx,
            model_name=model_name,
            sharpe=sharpe,
            annual_return=ann_ret,
            volatility=vol,
        ))

    # Sort by Sharpe descending, re-assign ranks
    entries.sort(key=lambda e: e.sharpe, reverse=True)
    for i, e in enumerate(entries, start=1):
        e.rank = i

    return LeaderboardResult(entries=entries, source="scraped")
