"""
weekly_br_elo.py

Window-scoped player & squadron ELO scoring. Python port of the
computePerformanceScore + ratingCtes pipeline from server.js, used by the
Weekly BR Report executor.

The percentile distribution (benchmark) is built from the same in-window
dataset, so scores represent ranking against peers who also played that week.
"""

# Standard Library Imports
import logging
import math
from typing import Any, Dict, List, Optional, Tuple

# Third-Party Library Imports
import aiosqlite

# Local Module Imports
from .utils import SQ_BATTLES_DB_PATH


# Score weights (mirror server.js:631 computePerformanceScore)
_W_KDR = 0.32
_W_HEAVY_RATE = 0.23
_W_KILLS_PER_GAME = 0.14
_W_GAMES_LOG = 0.10
_W_WIN_RATE = 0.06
_W_ASSISTS_PER_GAME = 0.06
_W_CAPTURES_PER_GAME = 0.04
_W_DEATHS_PER_GAME = 0.05  # lower-is-better

_METRIC_KEYS: Tuple[str, ...] = (
    "kdr",
    "kills_per_game",
    "heavy_rate",
    "win_rate",
    "assists_per_game",
    "captures_per_game",
    "deaths_per_game",
    "games_log",
)


def _rating_ctes(entity_expr: str) -> str:
    """Return the WITH-clause CTEs that produce per-session heavy scores.

    Direct port of server.js:647 ratingCtes(). The window predicate uses
    `endtime_unix BETWEEN ? AND ?` -- params order: (start_ts, end_ts).
    """
    return f"""
    WITH player_sessions AS (
        SELECT
            {entity_expr} AS entity_key,
            p.UID,
            p.session_id,
            SUM(p.ground_kills + p.air_kills) AS kills,
            SUM(p.assists) AS assists,
            SUM(p.captures) AS captures,
            SUM(p.deaths) AS deaths,
            MAX(CASE WHEN UPPER(p.victor_bool) = 'WIN' THEN 1 ELSE 0 END) AS win
        FROM player_games_hist p
        WHERE p.UID IS NOT NULL
            AND p.nick NOT LIKE 'coop/%'
            AND p.endtime_unix >= ?
            AND p.endtime_unix <= ?
        GROUP BY entity_key, p.UID, p.session_id
    ),
    leader_stats AS (
        SELECT ps.session_id, MAX(ps.kills) AS max_kills
        FROM player_sessions ps
        GROUP BY ps.session_id
    ),
    leader_counts AS (
        SELECT ps.session_id, COUNT(*) AS top_count
        FROM player_sessions ps
        JOIN leader_stats ls ON ls.session_id = ps.session_id
            AND ls.max_kills = ps.kills
        GROUP BY ps.session_id
    ),
    second_stats AS (
        SELECT ps.session_id, MAX(ps.kills) AS second_kills
        FROM player_sessions ps
        JOIN leader_stats ls ON ls.session_id = ps.session_id
        WHERE ps.kills < ls.max_kills
        GROUP BY ps.session_id
    ),
    scored_sessions AS (
        SELECT
            ps.*,
            CASE
                WHEN ps.kills >= 3
                    AND ps.kills = ls.max_kills
                    AND lc.top_count = 1
                    AND (ps.kills - COALESCE(ss.second_kills, 0)) >= 2
                    THEN MIN(1.0, (ps.kills - 2) / 2.0)
                WHEN ps.kills >= 3
                    AND ps.kills = ls.max_kills
                    AND lc.top_count = 1
                    THEN 0.6 * MIN(1.0, (ps.kills - 2) / 2.0)
                ELSE 0
            END AS heavy_score
        FROM player_sessions ps
        JOIN leader_stats ls ON ls.session_id = ps.session_id
        JOIN leader_counts lc ON lc.session_id = ps.session_id
        LEFT JOIN second_stats ss ON ss.session_id = ps.session_id
    )
    """


async def _fetch_rows(sql: str, params: Tuple[Any, ...]) -> List[Dict[str, Any]]:
    """Run a read-only query against sq_battles.db and return list-of-dicts."""
    async with aiosqlite.connect(
        f"file:{SQ_BATTLES_DB_PATH}?mode=ro", uri=True, timeout=30.0
    ) as db:
        await db.execute("PRAGMA busy_timeout=30000;")
        async with db.execute(sql, params) as cursor:
            cols = [c[0] for c in (cursor.description or [])]
            rows = await cursor.fetchall()
            return [dict(zip(cols, r)) for r in rows]


async def _query_player_aggregates(
    start_ts: int, end_ts: int
) -> List[Dict[str, Any]]:
    sql = _rating_ctes("p.UID") + """
        SELECT
            entity_key AS uid,
            COUNT(*) AS games,
            SUM(kills) AS total_kills,
            SUM(assists) AS total_assists,
            SUM(captures) AS total_captures,
            SUM(deaths) AS total_deaths,
            SUM(win) AS wins,
            SUM(heavy_score) AS heavy_score
        FROM scored_sessions
        GROUP BY entity_key
    """
    return await _fetch_rows(sql, (start_ts, end_ts))


async def _query_squadron_aggregates(
    start_ts: int, end_ts: int
) -> List[Dict[str, Any]]:
    sql = _rating_ctes("p.squadron_name") + """,
        squadron_sessions AS (
            SELECT
                entity_key,
                session_id,
                SUM(kills) AS kills,
                SUM(assists) AS assists,
                SUM(captures) AS captures,
                SUM(deaths) AS deaths,
                MAX(win) AS win,
                SUM(heavy_score) AS heavy_score
            FROM scored_sessions
            WHERE entity_key IS NOT NULL
                AND entity_key != 'UNKNOWN'
                AND entity_key != ''
            GROUP BY entity_key, session_id
        )
        SELECT
            entity_key AS squadron_name,
            COUNT(*) AS games,
            SUM(kills) AS total_kills,
            SUM(assists) AS total_assists,
            SUM(captures) AS total_captures,
            SUM(deaths) AS total_deaths,
            SUM(win) AS wins,
            SUM(heavy_score) AS heavy_score
        FROM squadron_sessions
        GROUP BY entity_key
    """
    return await _fetch_rows(sql, (start_ts, end_ts))


async def _query_uid_to_meta(
    start_ts: int, end_ts: int
) -> Dict[str, Tuple[str, str]]:
    """Map UID -> (most-recent nick, most-recent squadron_name) within window."""
    sql = """
        SELECT
            p.UID AS uid,
            p.nick AS nick,
            p.squadron_name AS squadron_name,
            MAX(p.endtime_unix) AS last_seen
        FROM player_games_hist p
        WHERE p.UID IS NOT NULL
            AND p.nick NOT LIKE 'coop/%'
            AND p.endtime_unix >= ?
            AND p.endtime_unix <= ?
        GROUP BY p.UID
    """
    rows = await _fetch_rows(sql, (start_ts, end_ts))
    out: Dict[str, Tuple[str, str]] = {}
    for r in rows:
        uid = str(r.get("uid") or "")
        if not uid:
            continue
        out[uid] = (str(r.get("nick") or ""), str(r.get("squadron_name") or ""))
    return out


def _normalize_rating_stats(row: Dict[str, Any]) -> Dict[str, float]:
    games = max(0, int(row.get("games") or 0))
    kills = max(0, int(row.get("total_kills") or 0))
    deaths = max(0, int(row.get("total_deaths") or 0))
    assists = max(0, int(row.get("total_assists") or 0))
    captures = max(0, int(row.get("total_captures") or 0))
    wins = max(0, int(row.get("wins") or 0))
    heavy = max(0.0, float(row.get("heavy_score") or 0))
    return {
        "games": float(games),
        "kdr": (kills / deaths) if deaths > 0 else float(kills),
        "kills_per_game": (kills / games) if games > 0 else 0.0,
        "heavy_rate": (heavy / games) if games > 0 else 0.0,
        "win_rate": (wins / games * 100.0) if games > 0 else 0.0,
        "assists_per_game": (assists / games) if games > 0 else 0.0,
        "captures_per_game": (captures / games) if games > 0 else 0.0,
        "deaths_per_game": (deaths / games) if games > 0 else 0.0,
        "games_log": math.log1p(games),
    }


def _build_benchmark(rows: List[Dict[str, Any]]) -> Dict[str, List[float]]:
    """Build sorted percentile distributions for each scoring metric."""
    normalized = [_normalize_rating_stats(r) for r in rows]
    return {m: sorted(float(n[m]) for n in normalized) for m in _METRIC_KEYS}


def _upper_bound(values: List[float], v: float) -> int:
    lo, hi = 0, len(values)
    while lo < hi:
        mid = (lo + hi) // 2
        if values[mid] <= v:
            lo = mid + 1
        else:
            hi = mid
    return lo


def _lower_bound(values: List[float], v: float) -> int:
    lo, hi = 0, len(values)
    while lo < hi:
        mid = (lo + hi) // 2
        if values[mid] < v:
            lo = mid + 1
        else:
            hi = mid
    return lo


def _metric_percentile(
    value: float, values: List[float], lower_is_better: bool = False
) -> float:
    if not values:
        return 0.0
    if lower_is_better:
        return (len(values) - _lower_bound(values, value)) / len(values)
    return _upper_bound(values, value) / len(values)


def _compute_performance_score(
    row: Dict[str, Any], benchmark: Dict[str, List[float]]
) -> float:
    s = _normalize_rating_stats(row)
    weighted = (
        _W_KDR * _metric_percentile(s["kdr"], benchmark["kdr"])
        + _W_HEAVY_RATE * _metric_percentile(s["heavy_rate"], benchmark["heavy_rate"])
        + _W_KILLS_PER_GAME
        * _metric_percentile(s["kills_per_game"], benchmark["kills_per_game"])
        + _W_GAMES_LOG * _metric_percentile(s["games_log"], benchmark["games_log"])
        + _W_WIN_RATE * _metric_percentile(s["win_rate"], benchmark["win_rate"])
        + _W_ASSISTS_PER_GAME
        * _metric_percentile(s["assists_per_game"], benchmark["assists_per_game"])
        + _W_CAPTURES_PER_GAME
        * _metric_percentile(s["captures_per_game"], benchmark["captures_per_game"])
        + _W_DEATHS_PER_GAME
        * _metric_percentile(
            s["deaths_per_game"], benchmark["deaths_per_game"], lower_is_better=True
        )
    )
    sample = max(0.0, min(1.0, math.sqrt(s["games"] / 10.0)))
    return round(max(0.0, min(5.0, 5.0 * weighted * sample)), 2)


async def player_scores(start_ts: int, end_ts: int) -> Dict[str, Dict[str, Any]]:
    """Return {uid: {nick, squadron_name, score, games, kdr, win_rate, kills_per_game}}."""
    rows = await _query_player_aggregates(start_ts, end_ts)
    if not rows:
        return {}
    benchmark = _build_benchmark(rows)
    meta = await _query_uid_to_meta(start_ts, end_ts)
    out: Dict[str, Dict[str, Any]] = {}
    for r in rows:
        uid = str(r.get("uid") or "")
        if not uid:
            continue
        norm = _normalize_rating_stats(r)
        score = _compute_performance_score(r, benchmark)
        nick, squadron = meta.get(uid, ("", ""))
        out[uid] = {
            "uid": uid,
            "nick": nick,
            "squadron_name": squadron,
            "score": score,
            "games": int(norm["games"]),
            "kdr": norm["kdr"],
            "win_rate": norm["win_rate"],
            "kills_per_game": norm["kills_per_game"],
        }
    return out


async def squadron_scores(start_ts: int, end_ts: int) -> Dict[str, Dict[str, Any]]:
    """Return {squadron_name: {score, games, kdr, win_rate, kills_per_game}}."""
    rows = await _query_squadron_aggregates(start_ts, end_ts)
    if not rows:
        return {}
    benchmark = _build_benchmark(rows)
    out: Dict[str, Dict[str, Any]] = {}
    for r in rows:
        sq = str(r.get("squadron_name") or "")
        if not sq:
            continue
        norm = _normalize_rating_stats(r)
        score = _compute_performance_score(r, benchmark)
        out[sq] = {
            "squadron_name": sq,
            "score": score,
            "games": int(norm["games"]),
            "kdr": norm["kdr"],
            "win_rate": norm["win_rate"],
            "kills_per_game": norm["kills_per_game"],
        }
    return out


def top_n_squadrons_with_top_k_players_from_maps(
    sq_map: Dict[str, Dict[str, Any]],
    pl_map: Dict[str, Dict[str, Any]],
    n: int = 20,
    k: int = 5,
) -> List[Dict[str, Any]]:
    """Sync helper: top N squadrons + top K players from already-computed maps.

    Lets a caller pay the cost of `squadron_scores`/`player_scores` once and
    then synthesize multiple report payloads (wildcard + per-squadron) from
    the same data without re-hitting the DB.
    """
    ranked = sorted(
        sq_map.values(),
        key=lambda r: (-float(r["score"]), -int(r["games"]), str(r["squadron_name"])),
    )[:n]

    by_squadron: Dict[str, List[Dict[str, Any]]] = {}
    for p in pl_map.values():
        s = str(p.get("squadron_name") or "")
        if not s:
            continue
        by_squadron.setdefault(s, []).append(p)

    payload: List[Dict[str, Any]] = []
    for r in ranked:
        s = str(r["squadron_name"])
        roster = sorted(
            by_squadron.get(s, []),
            key=lambda p: (-float(p["score"]), -int(p["games"]), str(p.get("nick") or "")),
        )[:k]
        payload.append({**r, "top_players": roster})
    return payload


def squadron_report_for_variants_from_maps(
    sq_map: Dict[str, Dict[str, Any]],
    pl_map: Dict[str, Dict[str, Any]],
    name_variants: List[str],
    k: int = 15,
) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, Any]]]:
    """Sync helper: per-squadron payload from already-computed maps."""
    sq_row: Optional[Dict[str, Any]] = None
    for v in name_variants:
        if not v:
            continue
        if v in sq_map:
            sq_row = sq_map[v]
            break

    variant_set = {v for v in name_variants if v}
    roster = [p for p in pl_map.values() if str(p.get("squadron_name") or "") in variant_set]
    roster.sort(
        key=lambda p: (-float(p["score"]), -int(p["games"]), str(p.get("nick") or ""))
    )
    return sq_row, roster[:k]


async def top_n_squadrons_with_top_k_players(
    start_ts: int, end_ts: int, n: int = 20, k: int = 5
) -> List[Dict[str, Any]]:
    """Wildcard-mode payload: top N squadrons by score, each with their top K players.

    Convenience wrapper that fetches the maps then calls the `_from_maps` helper.
    Prefer the explicit two-step (compute maps once, call from-maps multiple
    times) when building reports for many subscribers.
    """
    sq_map = await squadron_scores(start_ts, end_ts)
    pl_map = await player_scores(start_ts, end_ts)
    return top_n_squadrons_with_top_k_players_from_maps(sq_map, pl_map, n=n, k=k)


async def squadron_report_for_variants(
    start_ts: int,
    end_ts: int,
    name_variants: List[str],
    k: int = 15,
) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, Any]]]:
    """Per-squadron mode payload: best-matching squadron row + its top K players.

    Tries name_variants in order against the squadron name appearing in
    player_games_hist (long_name, tag_name, short_name). Returns (None, [])
    if no variant matched and no players in window carry any of the variants.
    """
    sq_map = await squadron_scores(start_ts, end_ts)
    pl_map = await player_scores(start_ts, end_ts)
    return squadron_report_for_variants_from_maps(sq_map, pl_map, name_variants, k=k)


def br_color_int(max_br: float) -> int:
    """Map a BR value to an embed sidebar color (matches /schedule color bands)."""
    if max_br >= 13.0:
        return 0xE74C3C
    if max_br >= 10.0:
        return 0xE67E22
    if max_br >= 7.0:
        return 0xF1C40F
    if max_br >= 4.0:
        return 0x2ECC71
    return 0x3498DB


__all__ = [
    "player_scores",
    "squadron_scores",
    "top_n_squadrons_with_top_k_players",
    "top_n_squadrons_with_top_k_players_from_maps",
    "squadron_report_for_variants",
    "squadron_report_for_variants_from_maps",
    "br_color_int",
]
