"""
DataFrame & tabular output utilities.

Provides ResultSet (list with .df) and OddsResult (dict with .df)
for instant pandas DataFrame conversion — like Alpaca's bars.df pattern.

    events = client.list_events(status="upcoming")
    print(events)                  # clean table
    df = events.df                 # pandas DataFrame
    df.to_csv("events.csv")       # export

    odds = client.get_moneylines(event_id)
    print(odds)                    # clean table
    df = odds.df                   # pandas DataFrame

Pandas is optional. If not installed, .df raises a helpful ImportError.
All other features (clean printing, iteration, len) work without pandas.
"""

from typing import Any, Dict, List, Optional, Sequence, TypeVar, Generic
import textwrap

T = TypeVar("T")

# Maximum column widths for table display
_MAX_COL_WIDTH = 40
_MAX_ROWS_DISPLAY = 50

# ── DataFrame helper ─────────────────────────────────────────────────────────


def _get_pandas():
    """Import pandas or raise helpful error."""
    try:
        import pandas as pd
        return pd
    except ImportError:
        raise ImportError(
            "pandas required for DataFrame output.\n"
            "Install with: pip install pandas\n"
            "Or: pip install altsportsdata[pandas]"
        )


def as_dataframe(data: Any, columns: Optional[List[str]] = None):
    """
    Convert any ASD response to a pandas DataFrame.

    Handles:
    - List of Pydantic models (EventListing, SportsListing, etc.)
    - List of dicts (get_markets() output)
    - Dict with list values (raw odds responses like {"eventWinner": [...]})
    - ResultSet and OddsResult objects

    Args:
        data: ASD response data
        columns: Optional column filter — only include these columns

    Returns:
        pandas.DataFrame

    Example::

        from altsportsdata.utils import as_dataframe

        events = client.list_events(status="upcoming")
        df = as_dataframe(events, columns=["name", "league", "start_date", "status"])
    """
    pd = _get_pandas()

    if isinstance(data, ResultSet):
        return data.df
    if isinstance(data, OddsResult):
        return data.df

    df = _to_df(data, pd)

    if columns and not df.empty:
        available = [c for c in columns if c in df.columns]
        if available:
            df = df[available]

    return df


def _to_df(data: Any, pd) -> "pd.DataFrame":
    """Internal conversion logic."""
    if isinstance(data, list):
        if not data:
            return pd.DataFrame()
        first = data[0]
        if hasattr(first, "model_dump"):
            rows = [_flatten_model(item) for item in data]
        elif isinstance(first, dict):
            rows = [_flatten_dict(item) for item in data]
        else:
            return pd.DataFrame(data)
        return pd.DataFrame(rows)

    elif isinstance(data, dict):
        # Odds response: {"eventWinner": [...], "headToHead": [...]}
        for key, val in data.items():
            if isinstance(val, list) and val:
                return _to_df(val, pd)
        # Single-level dict
        return pd.DataFrame([data])

    return pd.DataFrame()


def _flatten_model(obj) -> Dict[str, Any]:
    """Flatten a Pydantic model to a dict with snake_case keys, expanding nested athlete."""
    if hasattr(obj, "model_dump"):
        d = obj.model_dump()
    else:
        d = dict(obj) if hasattr(obj, "__iter__") else {"value": obj}
    return _flatten_dict(d)


def _flatten_dict(d: Dict[str, Any]) -> Dict[str, Any]:
    """Flatten nested dicts for tabular output — expands athlete, team, etc."""
    flat = {}
    for k, v in d.items():
        if k == "athlete" and isinstance(v, dict):
            flat["athlete"] = f"{v.get('firstName', '')} {v.get('lastName', '')}".strip()
            if v.get("id"):
                flat["athlete_id"] = v["id"]
            if v.get("nationality"):
                flat["nationality"] = v["nationality"]
        elif k == "eventParticipant1" and isinstance(v, dict):
            a = v.get("athlete", {})
            flat["player1"] = f"{a.get('firstName', '')} {a.get('lastName', '')}".strip()
            flat["player1_odds"] = v.get("odds")
            flat["player1_settlement"] = v.get("settlement")
        elif k == "eventParticipant2" and isinstance(v, dict):
            a = v.get("athlete", {})
            flat["player2"] = f"{a.get('firstName', '')} {a.get('lastName', '')}".strip()
            flat["player2_odds"] = v.get("odds")
            flat["player2_settlement"] = v.get("settlement")
        elif isinstance(v, dict):
            # Skip deeply nested dicts (rounds, teams, etc.)
            continue
        elif isinstance(v, list):
            flat[k] = len(v) if v else 0
        elif isinstance(v, str) and len(v) > _MAX_COL_WIDTH:
            flat[k] = v[:_MAX_COL_WIDTH - 3] + "..."
        else:
            flat[k] = v
    return flat


# ── Clean table formatter (no pandas required) ──────────────────────────────


def _format_table(
    rows: List[Dict[str, Any]],
    columns: Optional[List[str]] = None,
    max_rows: int = _MAX_ROWS_DISPLAY,
    title: Optional[str] = None,
) -> str:
    """Format a list of dicts as a clean ASCII table."""
    if not rows:
        return "(empty)"

    # Determine columns
    if columns:
        cols = [c for c in columns if any(c in r for r in rows)]
    else:
        # Auto-detect: use all keys from first row, prioritize useful ones
        seen = []
        for r in rows[:5]:
            for k in r:
                if k not in seen:
                    seen.append(k)
        cols = seen

    if not cols:
        return "(no displayable columns)"

    # Calculate column widths
    widths = {}
    for c in cols:
        header_w = len(str(c))
        max_val_w = 0
        for r in rows[:max_rows]:
            val = r.get(c, "")
            max_val_w = max(max_val_w, len(_format_cell(val)))
        widths[c] = min(max(header_w, max_val_w), _MAX_COL_WIDTH)

    # Build table
    lines = []
    if title:
        lines.append(title)
        lines.append("")

    # Header
    header = "  ".join(str(c).ljust(widths[c]) for c in cols)
    lines.append(header)
    lines.append("  ".join("─" * widths[c] for c in cols))

    # Rows
    display_rows = rows[:max_rows]
    for r in display_rows:
        cells = []
        for c in cols:
            val = _format_cell(r.get(c, ""))
            if len(val) > widths[c]:
                val = val[: widths[c] - 2] + ".."
            cells.append(val.ljust(widths[c]))
        lines.append("  ".join(cells))

    if len(rows) > max_rows:
        lines.append(f"  ... and {len(rows) - max_rows} more rows")

    lines.append("")
    lines.append(f"  {len(rows)} rows × {len(cols)} columns")

    return "\n".join(lines)


def _format_cell(val: Any) -> str:
    """Format a single cell value for table display."""
    if val is None:
        return ""
    if isinstance(val, float):
        if val == int(val) and val < 1e6:
            return str(int(val))
        if abs(val) < 0.01:
            return f"{val:.6f}"
        return f"{val:.4f}" if abs(val) < 100 else f"{val:.2f}"
    if isinstance(val, list):
        return f"[{len(val)}]"
    return str(val)


# ── ResultSet — list with .df ────────────────────────────────────────────────


# Priority column orderings for different result types
_EVENT_COLUMNS = [
    "sportType", "name", "startDate", "eventStatus",
    "eventLocation", "id",
]
_EVENT_DISPLAY = ["league", "name", "start_date", "status", "location", "id"]

_MARKET_COLUMNS = [
    "league", "event_name", "status", "start_date",
    "market_count", "event_id",
]

_LEAGUE_COLUMNS = [
    "league_key", "name", "market_count", "market_types",
]

_ODDS_DISPLAY = [
    "athlete", "probability", "odds", "settlement", "status",
    "outcome_id",
]

_H2H_DISPLAY = [
    "player1", "player1_odds", "player2", "player2_odds",
    "player1_settlement", "player2_settlement",
]


class ResultSet(list):
    """
    List subclass with DataFrame support and clean table display.

    Behaves exactly like a regular list, but adds:

    - ``.df`` — pandas DataFrame (requires pandas)
    - ``print(results)`` — clean ASCII table
    - ``.to_csv(path)`` — export to CSV
    - ``.to_json()`` — serialize to JSON

    All list operations work normally: indexing, slicing, iteration, len().
    """

    def __init__(self, items=None, _display_columns=None, _title=None):
        super().__init__(items or [])
        self._display_columns = _display_columns
        self._title = _title

    @property
    def df(self):
        """
        Convert to pandas DataFrame.

        Returns a clean, flat DataFrame with snake_case column names.
        Nested objects (athlete, team) are flattened automatically.

        Raises ImportError if pandas is not installed.

        Example::

            events = client.list_events(status="upcoming")
            df = events.df
            print(df[["name", "sportType", "startDate"]])
        """
        pd = _get_pandas()
        if not self:
            return pd.DataFrame()

        first = self[0]
        if hasattr(first, "model_dump"):
            rows = [_flatten_model(item) for item in self]
        elif isinstance(first, dict):
            rows = [_flatten_dict(item) if isinstance(item, dict) else {"value": item} for item in self]
        else:
            return pd.DataFrame(list(self))

        df = pd.DataFrame(rows)

        # Rename to snake_case for common columns
        rename_map = {
            "sportType": "league",
            "startDate": "start_date",
            "endDate": "end_date",
            "eventStatus": "status",
            "eventLocation": "location",
            "eventLocationGroup": "location_group",
            "tourName": "tour",
            "tourYear": "tour_year",
            "eventNumber": "event_number",
            "outcomeId": "outcome_id",
            "updatedAt": "updated_at",
        }
        df = df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns})

        return df

    def to_csv(self, path: str, **kwargs) -> None:
        """Export to CSV file. Requires pandas."""
        self.df.to_csv(path, index=False, **kwargs)

    def to_json(self, **kwargs) -> str:
        """Export to JSON string. Requires pandas."""
        return self.df.to_json(orient="records", **kwargs)

    def __str__(self) -> str:
        if not self:
            return "ResultSet(empty)"

        first = self[0]
        if hasattr(first, "model_dump"):
            rows = [_flatten_model(item) for item in self]
        elif isinstance(first, dict):
            rows = [_flatten_dict(item) if isinstance(item, dict) else {"value": item} for item in self]
        else:
            return f"ResultSet({len(self)} items)"

        return _format_table(
            rows,
            columns=self._display_columns,
            title=self._title,
        )

    def __repr__(self) -> str:
        if not self:
            return "ResultSet(empty)"

        # Summarize
        types = set()
        for item in self[:10]:
            if hasattr(item, "sportType") and item.sportType:
                types.add(item.sportType)
            elif isinstance(item, dict) and "league" in item:
                types.add(item["league"])

        type_str = ", ".join(sorted(types)[:5])
        if len(types) > 5:
            type_str += f" +{len(types)-5} more"

        return f"ResultSet({len(self)} items{f' | {type_str}' if type_str else ''})"


class OddsResult(dict):
    """
    Dict subclass with DataFrame support and clean table display for odds responses.

    Behaves exactly like a regular dict, but adds:

    - ``.df`` — pandas DataFrame of the primary odds data
    - ``print(result)`` — clean ASCII table
    - ``.market_type`` — the primary market type key

    Example::

        odds = client.get_moneylines(event_id)
        print(odds)          # clean table
        df = odds.df         # pandas DataFrame
        df.to_csv("odds.csv")
    """

    @property
    def market_type(self) -> Optional[str]:
        """The primary market type key in this response."""
        for k, v in self.items():
            if isinstance(v, list) and not k.startswith("_"):
                return k
        return None

    @property
    def df(self):
        """
        Convert the primary odds list to a pandas DataFrame.

        Automatically flattens nested athlete objects and renames
        columns to clean snake_case.
        """
        pd = _get_pandas()

        mtype = self.market_type
        if not mtype:
            return pd.DataFrame([_flatten_dict(self)])

        items = self[mtype]
        if not items:
            return pd.DataFrame()

        rows = [_flatten_dict(item) if isinstance(item, dict) else {"value": item}
                for item in items]
        df = pd.DataFrame(rows)

        # Rename common columns
        rename_map = {
            "outcomeId": "outcome_id",
            "updatedAt": "updated_at",
            "winnerParticipantId": "winner_id",
        }
        df = df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns})

        # Sort by odds if available
        if "odds" in df.columns:
            df = df.sort_values("odds", na_position="last").reset_index(drop=True)

        return df

    def to_csv(self, path: str, **kwargs) -> None:
        """Export to CSV file. Requires pandas."""
        self.df.to_csv(path, index=False, **kwargs)

    def __str__(self) -> str:
        mtype = self.market_type
        if not mtype:
            return dict.__repr__(self)

        items = self[mtype]
        if not items:
            return f"OddsResult({mtype}: empty)"

        rows = [_flatten_dict(item) if isinstance(item, dict) else {"value": item}
                for item in items]

        # Detect display columns based on market type
        if mtype == "headToHead":
            display_cols = _H2H_DISPLAY
        else:
            display_cols = _ODDS_DISPLAY

        return _format_table(
            rows,
            columns=display_cols,
            title=f"Market: {mtype}",
        )

    def __repr__(self) -> str:
        mtype = self.market_type
        if mtype:
            items = self[mtype]
            return f"OddsResult({mtype}: {len(items)} outcomes)"
        return f"OddsResult({len(self)} keys)"
