"""Local SQLite persistence for provisional server snapshots."""

from __future__ import annotations

import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable, Mapping

from .config import get_storage_path


DEFAULT_GAME_SOURCE = {
    "slug": "current-hll",
    "display_name": "Current Hell Let Loose",
    "provider_kind": "development",
}
SUMMARY_SNAPSHOT_LIMIT = 6


def initialize_storage(*, db_path: Path | None = None) -> Path:
    """Create the local database file and minimal schema when missing."""
    resolved_path = db_path or get_storage_path()
    resolved_path.parent.mkdir(parents=True, exist_ok=True)

    with _connect(resolved_path) as connection:
        connection.executescript(
            """
            CREATE TABLE IF NOT EXISTS game_sources (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                slug TEXT NOT NULL UNIQUE,
                display_name TEXT NOT NULL,
                provider_kind TEXT NOT NULL,
                is_active INTEGER NOT NULL DEFAULT 1,
                created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
            );

            CREATE TABLE IF NOT EXISTS servers (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                game_source_id INTEGER NOT NULL,
                external_server_id TEXT,
                server_name TEXT NOT NULL,
                region TEXT,
                first_seen_at TEXT NOT NULL,
                last_seen_at TEXT NOT NULL,
                created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (game_source_id, external_server_id),
                FOREIGN KEY (game_source_id) REFERENCES game_sources(id)
            );

            CREATE TABLE IF NOT EXISTS server_snapshots (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                server_id INTEGER NOT NULL,
                captured_at TEXT NOT NULL,
                status TEXT NOT NULL,
                players INTEGER,
                max_players INTEGER,
                current_map TEXT,
                source_name TEXT NOT NULL,
                snapshot_origin TEXT,
                source_ref TEXT,
                raw_payload_ref TEXT,
                created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (server_id) REFERENCES servers(id)
            );

            CREATE INDEX IF NOT EXISTS idx_server_snapshots_server_time
            ON server_snapshots(server_id, captured_at);
            """
        )
        _ensure_server_snapshot_columns(connection)

    return resolved_path


def persist_snapshot_batch(
    snapshots: Iterable[Mapping[str, object]],
    *,
    source_name: str,
    captured_at: str,
    game_source: Mapping[str, str] | None = None,
    db_path: Path | None = None,
) -> dict[str, object]:
    """Persist a batch of normalized snapshots into local SQLite storage."""
    resolved_path = initialize_storage(db_path=db_path)
    source_definition = dict(DEFAULT_GAME_SOURCE)
    if game_source is not None:
        source_definition.update(game_source)

    persisted = 0
    with _connect(resolved_path) as connection:
        game_source_id = _upsert_game_source(connection, source_definition)
        for snapshot in snapshots:
            server_id = _upsert_server(
                connection,
                game_source_id=game_source_id,
                snapshot=snapshot,
                captured_at=captured_at,
            )
            connection.execute(
                """
                INSERT INTO server_snapshots (
                    server_id,
                    captured_at,
                    status,
                    players,
                    max_players,
                    current_map,
                    source_name,
                    snapshot_origin,
                    source_ref,
                    raw_payload_ref
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    server_id,
                    captured_at,
                    snapshot.get("status"),
                    snapshot.get("players"),
                    snapshot.get("max_players"),
                    snapshot.get("current_map"),
                    snapshot.get("source_name") or source_name,
                    snapshot.get("snapshot_origin"),
                    snapshot.get("source_ref"),
                    None,
                ),
            )
            persisted += 1

    return {
        "db_path": str(resolved_path),
        "captured_at": captured_at,
        "persisted_snapshots": persisted,
        "game_source_slug": source_definition["slug"],
    }


def list_latest_snapshots(*, db_path: Path | None = None) -> list[dict[str, object]]:
    """Return the latest persisted snapshot for each known server."""
    resolved_path = initialize_storage(db_path=db_path)
    with _connect(resolved_path) as connection:
        rows = connection.execute(
            """
            SELECT
                servers.id AS server_id,
                servers.external_server_id,
                servers.server_name,
                servers.region,
                game_sources.slug AS context,
                server_snapshots.source_name,
                server_snapshots.snapshot_origin,
                server_snapshots.source_ref,
                server_snapshots.captured_at,
                server_snapshots.status,
                server_snapshots.players,
                server_snapshots.max_players,
                server_snapshots.current_map
            FROM servers
            INNER JOIN game_sources
                ON game_sources.id = servers.game_source_id
            INNER JOIN server_snapshots
                ON server_snapshots.server_id = servers.id
            INNER JOIN (
                SELECT server_id, MAX(captured_at) AS latest_captured_at
                FROM server_snapshots
                GROUP BY server_id
            ) AS latest
                ON latest.server_id = server_snapshots.server_id
                AND latest.latest_captured_at = server_snapshots.captured_at
            ORDER BY servers.server_name ASC
            """
        ).fetchall()
        items = [_serialize_snapshot_row(row) for row in rows]
        return _attach_history_summaries(connection, items)


def list_snapshot_history(
    *,
    db_path: Path | None = None,
    limit: int = 20,
) -> list[dict[str, object]]:
    """Return recent persisted snapshots across all servers."""
    resolved_path = initialize_storage(db_path=db_path)
    with _connect(resolved_path) as connection:
        rows = connection.execute(
            """
            SELECT
                servers.id AS server_id,
                servers.external_server_id,
                servers.server_name,
                servers.region,
                game_sources.slug AS context,
                server_snapshots.source_name,
                server_snapshots.snapshot_origin,
                server_snapshots.source_ref,
                server_snapshots.captured_at,
                server_snapshots.status,
                server_snapshots.players,
                server_snapshots.max_players,
                server_snapshots.current_map
            FROM server_snapshots
            INNER JOIN servers
                ON servers.id = server_snapshots.server_id
            INNER JOIN game_sources
                ON game_sources.id = servers.game_source_id
            ORDER BY server_snapshots.captured_at DESC, servers.server_name ASC
            LIMIT ?
            """,
            (limit,),
        ).fetchall()
    return [_serialize_snapshot_row(row) for row in rows]


def list_server_history(
    server_id: str,
    *,
    db_path: Path | None = None,
    limit: int = 20,
) -> list[dict[str, object]]:
    """Return recent history for one server by numeric id or external id."""
    resolved_path = initialize_storage(db_path=db_path)
    server_filter, server_value = _build_server_filter(server_id)
    with _connect(resolved_path) as connection:
        rows = connection.execute(
            f"""
            SELECT
                servers.id AS server_id,
                servers.external_server_id,
                servers.server_name,
                servers.region,
                game_sources.slug AS context,
                server_snapshots.source_name,
                server_snapshots.snapshot_origin,
                server_snapshots.source_ref,
                server_snapshots.captured_at,
                server_snapshots.status,
                server_snapshots.players,
                server_snapshots.max_players,
                server_snapshots.current_map
            FROM server_snapshots
            INNER JOIN servers
                ON servers.id = server_snapshots.server_id
            INNER JOIN game_sources
                ON game_sources.id = servers.game_source_id
            WHERE {server_filter} = ?
            ORDER BY server_snapshots.captured_at DESC
            LIMIT ?
            """,
            (server_value, limit),
        ).fetchall()
    return [_serialize_snapshot_row(row) for row in rows]


def _connect(db_path: Path) -> sqlite3.Connection:
    connection = sqlite3.connect(db_path)
    connection.row_factory = sqlite3.Row
    return connection


def _upsert_game_source(
    connection: sqlite3.Connection,
    game_source: Mapping[str, str],
) -> int:
    connection.execute(
        """
        INSERT INTO game_sources (slug, display_name, provider_kind, is_active)
        VALUES (?, ?, ?, 1)
        ON CONFLICT(slug) DO UPDATE SET
            display_name = excluded.display_name,
            provider_kind = excluded.provider_kind,
            is_active = 1,
            updated_at = CURRENT_TIMESTAMP
        """,
        (
            game_source["slug"],
            game_source["display_name"],
            game_source["provider_kind"],
        ),
    )
    row = connection.execute(
        "SELECT id FROM game_sources WHERE slug = ?",
        (game_source["slug"],),
    ).fetchone()
    if row is None:
        raise RuntimeError("Failed to resolve game source during snapshot persistence.")

    return int(row["id"])


def _upsert_server(
    connection: sqlite3.Connection,
    *,
    game_source_id: int,
    snapshot: Mapping[str, object],
    captured_at: str,
) -> int:
    external_server_id = snapshot.get("external_server_id")
    if not isinstance(external_server_id, str) or not external_server_id.strip():
        external_server_id = _build_fallback_external_id(snapshot)

    server_name = str(snapshot.get("server_name") or "Unknown server")
    region = snapshot.get("region")

    connection.execute(
        """
        INSERT INTO servers (
            game_source_id,
            external_server_id,
            server_name,
            region,
            first_seen_at,
            last_seen_at
        ) VALUES (?, ?, ?, ?, ?, ?)
        ON CONFLICT(game_source_id, external_server_id) DO UPDATE SET
            server_name = excluded.server_name,
            region = excluded.region,
            last_seen_at = excluded.last_seen_at,
            updated_at = CURRENT_TIMESTAMP
        """,
        (
            game_source_id,
            external_server_id,
            server_name,
            region,
            captured_at,
            captured_at,
        ),
    )
    row = connection.execute(
        """
        SELECT id
        FROM servers
        WHERE game_source_id = ? AND external_server_id = ?
        """,
        (game_source_id, external_server_id),
    ).fetchone()
    if row is None:
        raise RuntimeError("Failed to resolve server during snapshot persistence.")

    return int(row["id"])


def _build_fallback_external_id(snapshot: Mapping[str, object]) -> str:
    server_name = str(snapshot.get("server_name") or "unknown-server")
    normalized = "".join(
        character.lower() if character.isalnum() else "-"
        for character in server_name
    )
    compact = "-".join(part for part in normalized.split("-") if part)
    return compact or "unknown-server"


def _ensure_server_snapshot_columns(connection: sqlite3.Connection) -> None:
    columns = {
        str(row["name"])
        for row in connection.execute("PRAGMA table_info(server_snapshots)").fetchall()
    }

    if "snapshot_origin" not in columns:
        connection.execute("ALTER TABLE server_snapshots ADD COLUMN snapshot_origin TEXT")
    if "source_ref" not in columns:
        connection.execute("ALTER TABLE server_snapshots ADD COLUMN source_ref TEXT")

    connection.execute(
        """
        UPDATE server_snapshots
        SET snapshot_origin = CASE
            WHEN source_name = 'controlled-placeholder' THEN 'controlled-fallback'
            WHEN source_name LIKE '%a2s%' THEN 'real-a2s'
            ELSE 'unknown'
        END
        WHERE snapshot_origin IS NULL OR snapshot_origin = ''
        """
    )
    connection.execute(
        """
        UPDATE server_snapshots
        SET source_ref = source_name
        WHERE source_ref IS NULL OR source_ref = ''
        """
    )
    _backfill_registered_a2s_source_refs(connection)


def _backfill_registered_a2s_source_refs(connection: sqlite3.Connection) -> None:
    from .server_targets import load_a2s_targets

    for target in load_a2s_targets():
        if not target.external_server_id:
            continue

        connection.execute(
            """
            UPDATE server_snapshots
            SET source_ref = ?
            WHERE snapshot_origin = 'real-a2s'
              AND source_ref = source_name
              AND server_id IN (
                  SELECT id
                  FROM servers
                  WHERE external_server_id = ?
              )
            """,
            (
                f"a2s://{target.host}:{target.query_port}",
                target.external_server_id,
            ),
        )


def _serialize_snapshot_row(row: sqlite3.Row) -> dict[str, object]:
    return {
        "server_id": row["server_id"],
        "external_server_id": row["external_server_id"],
        "server_name": row["server_name"],
        "region": row["region"],
        "context": row["context"],
        "source_name": row["source_name"],
        "snapshot_origin": row["snapshot_origin"],
        "source_ref": row["source_ref"],
        "captured_at": row["captured_at"],
        "status": row["status"],
        "players": row["players"],
        "max_players": row["max_players"],
        "current_map": row["current_map"],
    }


def _attach_history_summaries(
    connection: sqlite3.Connection,
    items: list[dict[str, object]],
) -> list[dict[str, object]]:
    enriched_items: list[dict[str, object]] = []
    for item in items:
        enriched = dict(item)
        enriched["history_summary"] = _build_history_summary(
            connection,
            int(item["server_id"]),
        )
        enriched_items.append(enriched)

    return enriched_items


def _build_history_summary(
    connection: sqlite3.Connection,
    server_id: int,
) -> dict[str, object]:
    rows = connection.execute(
        """
        SELECT
            captured_at,
            status,
            players
        FROM server_snapshots
        WHERE server_id = ?
        ORDER BY captured_at DESC
        LIMIT ?
        """,
        (server_id, SUMMARY_SNAPSHOT_LIMIT),
    ).fetchall()
    return _summarize_history_rows(rows)


def _summarize_history_rows(rows: list[sqlite3.Row]) -> dict[str, object]:
    capture_count = len(rows)
    player_values = [
        int(row["players"])
        for row in rows
        if row["players"] is not None
    ]
    online_rows = [row for row in rows if row["status"] == "online"]
    latest_captured_at = str(rows[0]["captured_at"]) if rows else None
    last_seen_online_at = str(online_rows[0]["captured_at"]) if online_rows else None

    return {
        "window_size": SUMMARY_SNAPSHOT_LIMIT,
        "recent_capture_count": capture_count,
        "recent_online_count": len(online_rows),
        "recent_average_players": _round_average(player_values),
        "recent_peak_players": max(player_values, default=None),
        "last_seen_online_at": last_seen_online_at,
        "minutes_since_last_capture": _minutes_since_timestamp(latest_captured_at),
    }


def _round_average(values: list[int]) -> float | None:
    if not values:
        return None

    return round(sum(values) / len(values), 1)


def _minutes_since_timestamp(timestamp: str | None) -> int | None:
    if not timestamp:
        return None

    normalized = timestamp.replace("Z", "+00:00")
    captured_at = datetime.fromisoformat(normalized)
    if captured_at.tzinfo is None:
        captured_at = captured_at.replace(tzinfo=timezone.utc)

    delta = datetime.now(timezone.utc) - captured_at.astimezone(timezone.utc)
    return max(0, int(delta.total_seconds() // 60))


def _build_server_filter(server_id: str) -> tuple[str, object]:
    normalized = server_id.strip()
    if normalized.isdigit():
        return "servers.id", int(normalized)

    return "servers.external_server_id", normalized
