"""Annual ranking snapshot generator and reader over materialized RCON match stats."""

from __future__ import annotations

import argparse
import json
import sqlite3
from contextlib import closing
from contextlib import contextmanager
from contextlib import nullcontext
from datetime import date, datetime, timezone
from pathlib import Path

from .config import get_storage_path, use_postgres_rcon_storage
from .historical_storage import ALL_SERVERS_SLUG
from .rcon_admin_log_materialization import MATCH_RESULT_SOURCE, initialize_rcon_materialized_storage
from .sqlite_utils import connect_sqlite_readonly, connect_sqlite_writer


SUPPORTED_ANNUAL_RANKING_METRICS = (
    "kills",
    "deaths",
    "teamkills",
    "matches_considered",
    "kd_ratio",
    "kills_per_match",
)


def generate_annual_ranking_snapshot(
    *,
    year: int,
    server_key: str | None = None,
    metric: str = "kills",
    limit: int = 20,
    replace_existing: bool = True,
    db_path: Path | None = None,
) -> dict[str, object]:
    """Generate and persist an annual top-k ranking snapshot for materialized RCON data."""
    normalized_year = _normalize_year(year)
    normalized_server_key = _normalize_server_key(server_key)
    normalized_metric = _normalize_metric(metric)
    normalized_limit = _normalize_limit(limit)
    window_start, window_end = _annual_window(normalized_year)

    resolved_path = initialize_rcon_materialized_storage(db_path=db_path)
    scope_sql, scope_params = _build_scope_sql(normalized_server_key)
    postgres_enabled = use_postgres_rcon_storage(explicit_sqlite_path=db_path)
    if postgres_enabled:
        from .postgres_rcon_storage import connect_postgres_compat

        connection_scope = connect_postgres_compat()
    else:
        connection_scope = closing(connect_sqlite_writer(resolved_path))

    with connection_scope as connection:
        transaction_scope = nullcontext() if postgres_enabled else connection
        with transaction_scope:
            source_matches_count = _count_matches_in_window(
                connection=connection,
                start=window_start,
                end=window_end,
                scope_sql=scope_sql,
                scope_params=scope_params,
            )
            existing_snapshot_id = _find_existing_snapshot(
                connection=connection,
                year=normalized_year,
                server_key=normalized_server_key,
                metric=normalized_metric,
            )
            if existing_snapshot_id is not None and not replace_existing:
                snapshot = _get_snapshot(connection=connection, snapshot_id=existing_snapshot_id)
                items = _list_items(connection=connection, snapshot_id=existing_snapshot_id)
                return {
                    "status": "ok",
                    "snapshot": snapshot,
                    "items": items,
                    "source_matches_count": source_matches_count,
                    "ranked_players": len(items),
                    "skipped_regeneration": True,
                }

            ranking_rows = _fetch_annual_ranking_rows(
                connection=connection,
                start=window_start,
                end=window_end,
                metric=normalized_metric,
                limit=normalized_limit,
                scope_sql=scope_sql,
                scope_params=scope_params,
            )

            _delete_existing_snapshot(
                connection=connection,
                year=normalized_year,
                server_key=normalized_server_key,
                metric=normalized_metric,
            )
            snapshot_id = _insert_snapshot(
                connection=connection,
                year=normalized_year,
                server_key=normalized_server_key,
                metric=normalized_metric,
                limit=normalized_limit,
                source_matches_count=source_matches_count,
                window_start=window_start,
                window_end=window_end,
            )

            _insert_items(
                connection=connection,
                snapshot_id=snapshot_id,
                rows=ranking_rows,
                limit=normalized_limit,
            )

            snapshot = _get_snapshot(connection=connection, snapshot_id=snapshot_id)
            items = _list_items(connection=connection, snapshot_id=snapshot_id)

    return {
        "status": "ok",
        "snapshot": snapshot,
        "items": items,
        "source_matches_count": source_matches_count,
        "ranked_players": len(items),
    }


def get_annual_ranking_snapshot(
    *,
    year: int,
    server_key: str | None = None,
    metric: str = "kills",
    limit: int = 20,
    db_path: Path | None = None,
) -> dict[str, object]:
    """Load one annual ranking snapshot without recalculating the ranking."""
    normalized_year = _normalize_year(year)
    normalized_server_key = _normalize_server_key(server_key)
    normalized_metric = _normalize_metric(metric)
    normalized_limit = _normalize_limit(limit)

    try:
        with _open_annual_snapshot_read_connection(db_path=db_path) as connection:
            snapshot = _find_snapshot(
                connection=connection,
                year=normalized_year,
                server_key=normalized_server_key,
                metric=normalized_metric,
            )
            if snapshot is None:
                return _build_missing_snapshot_result(
                    year=normalized_year,
                    server_key=normalized_server_key,
                    metric=normalized_metric,
                    limit=normalized_limit,
                )

            snapshot_limit = _normalize_limit(snapshot.get("limit_size") or normalized_limit)
            item_count = _count_items(
                connection=connection,
                snapshot_id=int(snapshot["id"]),
            )
            effective_limit = _resolve_effective_limit(
                requested_limit=normalized_limit,
                snapshot_limit=snapshot_limit,
                item_count=item_count,
            )
            items = _list_items(
                connection=connection,
                snapshot_id=int(snapshot["id"]),
                limit=effective_limit if effective_limit > 0 else None,
            )
    except (FileNotFoundError, sqlite3.OperationalError):
        return _build_missing_snapshot_result(
            year=normalized_year,
            server_key=normalized_server_key,
            metric=normalized_metric,
            limit=normalized_limit,
        )

    return {
        "snapshot_status": "ready",
        "year": normalized_year,
        "server_id": normalized_server_key,
        "metric": normalized_metric,
        "limit": effective_limit,
        "requested_limit": normalized_limit,
        "effective_limit": effective_limit,
        "snapshot_limit": snapshot_limit,
        "item_count": item_count,
        "source": "rcon-annual-ranking-snapshot",
        "generated_at": snapshot.get("generated_at"),
        "window_start": snapshot.get("window_start"),
        "window_end": snapshot.get("window_end"),
        "source_matches_count": int(snapshot.get("source_matches_count") or 0),
        "items": items,
    }


@contextmanager
def _open_annual_snapshot_read_connection(*, db_path: Path | None = None):
    if use_postgres_rcon_storage(explicit_sqlite_path=db_path):
        from .postgres_rcon_storage import PostgresCompatConnection, connect_postgres

        with connect_postgres() as connection:
            yield PostgresCompatConnection(connection)
        return

    resolved_path = _resolve_annual_snapshot_sqlite_path(db_path=db_path)
    if not resolved_path.exists():
        raise FileNotFoundError(resolved_path)
    with closing(connect_sqlite_readonly(resolved_path)) as connection:
        yield connection


def _resolve_annual_snapshot_sqlite_path(*, db_path: Path | None = None) -> Path:
    return db_path if db_path is not None else get_storage_path()


def _build_missing_snapshot_result(
    *,
    year: int,
    server_key: str,
    metric: str,
    limit: int,
) -> dict[str, object]:
    return {
        "snapshot_status": "missing",
        "year": year,
        "server_id": server_key,
        "metric": metric,
        "limit": limit,
        "requested_limit": limit,
        "effective_limit": 0,
        "snapshot_limit": None,
        "item_count": 0,
        "source": "rcon-annual-ranking-snapshot",
        "generated_at": None,
        "window_start": None,
        "window_end": None,
        "source_matches_count": 0,
        "items": [],
    }


def _normalize_server_key(server_key: str | None) -> str:
    normalized = str(server_key or "").strip()
    normalized_lower = normalized.lower()
    if not normalized or normalized_lower in {ALL_SERVERS_SLUG, "all"}:
        return ALL_SERVERS_SLUG
    return normalized


def _normalize_metric(metric: str) -> str:
    normalized = str(metric or "kills").strip().lower()
    if normalized not in SUPPORTED_ANNUAL_RANKING_METRICS:
        raise ValueError(
            f"Metric '{normalized}' is not supported for annual ranking snapshots."
        )
    return normalized


def _normalize_year(year: int) -> int:
    normalized_year = int(year)
    if normalized_year < 1 or normalized_year > 9999:
        raise ValueError("year must be between 1 and 9999")
    return normalized_year


def _normalize_limit(limit: object, *, maximum: int = 100) -> int:
    normalized_limit = int(limit or 1)
    if normalized_limit < 1:
        raise ValueError("limit must be greater than zero")
    return min(normalized_limit, maximum)


def _resolve_effective_limit(
    *,
    requested_limit: int,
    snapshot_limit: int,
    item_count: int,
) -> int:
    return max(0, min(requested_limit, snapshot_limit, item_count))


def _annual_window(year: int) -> tuple[str, str]:
    start = datetime(year, 1, 1, 0, 0, 0, tzinfo=timezone.utc).isoformat().replace("+00:00", "Z")
    end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc).isoformat().replace("+00:00", "Z")
    return start, end


def _fetch_annual_ranking_rows(
    *,
    connection: object,
    start: str,
    end: str,
    metric: str,
    limit: int,
    scope_sql: str,
    scope_params: list[object],
) -> list[dict[str, object]]:
    metric_sql, having_sql = _resolve_metric_sql(metric)

    rows = connection.execute(
        f"""
        SELECT
            stats.player_id,
            COALESCE(MAX(stats.player_name), stats.player_id) AS player_name,
            {metric_sql} AS metric_value,
            SUM(COALESCE(stats.kills, 0)) AS kills,
            SUM(COALESCE(stats.deaths, 0)) AS deaths,
            SUM(COALESCE(stats.teamkills, 0)) AS teamkills,
            COUNT(DISTINCT stats.match_key) AS matches_considered
        FROM rcon_match_player_stats AS stats
        INNER JOIN rcon_materialized_matches AS matches
            ON matches.target_key = stats.target_key
           AND matches.match_key = stats.match_key
        WHERE matches.source_basis = ?
          AND COALESCE(CAST(matches.ended_at AS TEXT), CAST(matches.started_at AS TEXT)) >= ?
          AND COALESCE(CAST(matches.ended_at AS TEXT), CAST(matches.started_at AS TEXT)) < ?
          {scope_sql}
          AND TRIM(COALESCE(stats.player_name, '')) != ''
        GROUP BY stats.player_id
        {having_sql}
        ORDER BY metric_value DESC, matches_considered DESC, kills DESC, player_name ASC
        LIMIT ?
        """,
        [MATCH_RESULT_SOURCE, start, end, *scope_params, limit],
    ).fetchall()
    return [dict(row) for row in rows]


def _resolve_metric_sql(metric: str) -> tuple[str, str]:
    metric_sql_by_metric = {
        "kills": "SUM(COALESCE(stats.kills, 0))",
        "deaths": "SUM(COALESCE(stats.deaths, 0))",
        "teamkills": "SUM(COALESCE(stats.teamkills, 0))",
        "matches_considered": "COUNT(DISTINCT stats.match_key)",
        "kd_ratio": (
            "CASE "
            "WHEN SUM(COALESCE(stats.deaths, 0)) > 0 "
            "THEN ROUND(CAST(SUM(COALESCE(stats.kills, 0)) AS NUMERIC) / "
            "CAST(SUM(COALESCE(stats.deaths, 0)) AS NUMERIC), 2) "
            "ELSE CAST(SUM(COALESCE(stats.kills, 0)) AS NUMERIC) "
            "END"
        ),
        "kills_per_match": (
            "CASE "
            "WHEN COUNT(DISTINCT stats.match_key) > 0 "
            "THEN ROUND(CAST(SUM(COALESCE(stats.kills, 0)) AS NUMERIC) / "
            "CAST(COUNT(DISTINCT stats.match_key) AS NUMERIC), 2) "
            "ELSE CAST(0 AS NUMERIC) "
            "END"
        ),
    }
    having_sql_by_metric = {
        "kills": "HAVING SUM(COALESCE(stats.kills, 0)) > 0",
        "deaths": "HAVING SUM(COALESCE(stats.deaths, 0)) > 0",
        "teamkills": "HAVING SUM(COALESCE(stats.teamkills, 0)) > 0",
        "matches_considered": "HAVING COUNT(DISTINCT stats.match_key) > 0",
        "kd_ratio": "HAVING SUM(COALESCE(stats.kills, 0)) > 0",
        "kills_per_match": (
            "HAVING COUNT(DISTINCT stats.match_key) > 0 "
            "AND SUM(COALESCE(stats.kills, 0)) > 0"
        ),
    }
    return metric_sql_by_metric[metric], having_sql_by_metric[metric]


def _count_matches_in_window(
    *,
    connection: object,
    start: str,
    end: str,
    scope_sql: str,
    scope_params: list[object],
) -> int:
    row = connection.execute(
        f"""
        SELECT COUNT(*) AS source_matches_count
        FROM (
            SELECT matches.target_key, matches.match_key
            FROM rcon_materialized_matches AS matches
            WHERE matches.source_basis = ?
              AND COALESCE(CAST(matches.ended_at AS TEXT), CAST(matches.started_at AS TEXT)) >= ?
              AND COALESCE(CAST(matches.ended_at AS TEXT), CAST(matches.started_at AS TEXT)) < ?
              {scope_sql}
            GROUP BY matches.target_key, matches.match_key
        ) AS source_matches
        """,
        [MATCH_RESULT_SOURCE, start, end, *scope_params],
    ).fetchone()
    return int(row["source_matches_count"] or 0) if row else 0


def _find_snapshot(
    *,
    connection: object,
    year: int,
    server_key: str,
    metric: str,
) -> dict[str, object] | None:
    row = connection.execute(
        """
        SELECT
            id,
            year,
            server_key,
            metric,
            limit_size,
            source_basis,
            window_start,
            window_end,
            status,
            source_matches_count,
            generated_at
        FROM rcon_annual_ranking_snapshots
        WHERE year = ? AND server_key = ? AND metric = ?
        LIMIT 1
        """,
        [year, server_key, metric],
    ).fetchone()
    return dict(row) if row else None


def _find_existing_snapshot(
    *,
    connection: object,
    year: int,
    server_key: str,
    metric: str,
) -> int | None:
    row = connection.execute(
        """
        SELECT id
        FROM rcon_annual_ranking_snapshots
        WHERE year = ? AND server_key = ? AND metric = ?
        LIMIT 1
        """,
        [year, server_key, metric],
    ).fetchone()
    return int(row["id"]) if row else None


def _delete_existing_snapshot(
    *,
    connection: object,
    year: int,
    server_key: str,
    metric: str,
) -> int | None:
    existing_id = _find_existing_snapshot(
        connection=connection,
        year=year,
        server_key=server_key,
        metric=metric,
    )
    if existing_id is None:
        return None
    connection.execute(
        "DELETE FROM rcon_annual_ranking_snapshot_items WHERE snapshot_id = ?",
        (existing_id,),
    )
    connection.execute(
        "DELETE FROM rcon_annual_ranking_snapshots WHERE id = ?",
        (existing_id,),
    )
    return existing_id


def _insert_snapshot(
    *,
    connection: object,
    year: int,
    server_key: str,
    metric: str,
    limit: int,
    source_matches_count: int,
    window_start: str,
    window_end: str,
) -> int:
    cursor = connection.execute(
        """
        INSERT INTO rcon_annual_ranking_snapshots (
            year,
            server_key,
            metric,
            limit_size,
            source_basis,
            window_start,
            window_end,
            source_matches_count
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        RETURNING id
        """,
        [
            year,
            server_key,
            metric,
            limit,
            MATCH_RESULT_SOURCE,
            window_start,
            window_end,
            source_matches_count,
        ],
    )
    try:
        row = cursor.fetchone()
    finally:
        cursor.close()
    if row is not None and row["id"] is not None:
        return int(row["id"])

    existing = _find_existing_snapshot(
        connection=connection,
        year=year,
        server_key=server_key,
        metric=metric,
    )
    if existing is None:
        raise RuntimeError("Unable to resolve annual snapshot id after insert.")
    return existing


def _insert_items(
    *,
    connection: object,
    snapshot_id: int,
    rows: list[dict[str, object]],
    limit: int,
) -> None:
    for index, row in enumerate(rows[:limit], start=1):
        kills = int(row.get("kills") or 0)
        deaths = int(row.get("deaths") or 0)
        metric_value = _coerce_metric_value(row.get("metric_value"))
        connection.execute(
            """
            INSERT INTO rcon_annual_ranking_snapshot_items (
                snapshot_id,
                ranking_position,
                player_id,
                player_name,
                metric_value,
                matches_considered,
                kills,
                deaths,
                teamkills,
                kd_ratio
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            [
                snapshot_id,
                index,
                str(row.get("player_id")),
                str(row.get("player_name")),
                metric_value,
                int(row.get("matches_considered") or 0),
                kills,
                deaths,
                int(row.get("teamkills") or 0),
                float(kills / deaths) if deaths else float(kills),
            ],
        )


def _get_snapshot(*, connection: object, snapshot_id: int) -> dict[str, object] | None:
    row = connection.execute(
        """
        SELECT
            id,
            year,
            server_key,
            metric,
            limit_size,
            source_basis,
            window_start,
            window_end,
            status,
            source_matches_count,
            generated_at
        FROM rcon_annual_ranking_snapshots
        WHERE id = ?
        LIMIT 1
        """,
        [snapshot_id],
    ).fetchone()
    if not row:
        return None
    return dict(row)


def _list_items(*, connection: object, snapshot_id: int, limit: int | None = None) -> list[dict[str, object]]:
    params: list[object] = [snapshot_id]
    query = """
        SELECT
            ranking_position,
            player_id,
            player_name,
            metric_value,
            matches_considered,
            kills,
            deaths,
            teamkills,
            kd_ratio
        FROM rcon_annual_ranking_snapshot_items
        WHERE snapshot_id = ?
        ORDER BY ranking_position ASC
    """
    if limit is not None:
        query = f"{query}\n        LIMIT ?"
        params.append(limit)
    rows = connection.execute(query, params).fetchall()
    return [dict(row) for row in rows]


def _coerce_metric_value(value: object) -> int | float:
    try:
        numeric = float(value or 0)
    except (TypeError, ValueError):
        return 0
    if numeric.is_integer():
        return int(numeric)
    return round(numeric, 2)


def _count_items(*, connection: object, snapshot_id: int) -> int:
    row = connection.execute(
        """
        SELECT COUNT(*) AS item_count
        FROM rcon_annual_ranking_snapshot_items
        WHERE snapshot_id = ?
        """,
        [snapshot_id],
    ).fetchone()
    return int(row["item_count"] or 0) if row else 0


def _build_scope_sql(server_key: str, *, table_alias: str = "matches") -> tuple[str, list[object]]:
    if server_key == ALL_SERVERS_SLUG:
        return "", []
    return (
        f"AND ({table_alias}.target_key = ? OR {table_alias}.external_server_id = ?)",
        [server_key, server_key],
    )


def _json_default(value: object) -> str:
    if isinstance(value, datetime):
        return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
    if isinstance(value, date):
        return value.isoformat()
    raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")


def _main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Generate annual ranking snapshots.")
    subparsers = parser.add_subparsers(dest="command")
    generate_parser = subparsers.add_parser("generate")
    generate_parser.add_argument("--year", type=int, required=True)
    generate_parser.add_argument("--server-key", default=None)
    generate_parser.add_argument("--metric", default="kills")
    generate_parser.add_argument("--limit", type=int, default=20)
    generate_parser.add_argument(
        "--sqlite-path",
        type=Path,
        default=None,
        help="explicit local SQLite override; default operational mode uses PostgreSQL when configured",
    )
    generate_parser.add_argument("--replace-existing", action="store_true", default=True)
    parser.set_defaults(command="generate")
    args = parser.parse_args(argv)

    if args.command == "generate":
        payload = generate_annual_ranking_snapshot(
            year=args.year,
            server_key=args.server_key,
            metric=args.metric,
            limit=args.limit,
            replace_existing=args.replace_existing,
            db_path=args.sqlite_path,
        )
        print(
            json.dumps(
                {"status": "ok", "data": payload},
                ensure_ascii=True,
                indent=2,
                default=_json_default,
            )
        )
        return 0

    parser.print_help()
    return 2


if __name__ == "__main__":
    raise SystemExit(_main())
