"""SQLite persistence for player accounts and state."""

import hashlib
import hmac
import json
import os
import sqlite3
from pathlib import Path
from typing import TypedDict

from mudlib.player import Player
from mudlib.thing import Thing


class PlayerData(TypedDict):
    """Shape of persisted player data from the database."""

    x: int
    y: int
    pl: float
    stamina: float
    max_stamina: float
    flying: bool
    zone_name: str
    inventory: list[str]
    description: str
    home_zone: str | None


class StatsData(TypedDict):
    """Shape of persisted stats data from the database."""

    kills: int
    deaths: int
    mob_kills: dict[str, int]
    play_time_seconds: float
    unlocked_moves: set[str]


# Module-level database path
_db_path: str | None = None


def init_db(db_path: str | Path) -> None:
    """Initialize the database and create tables if needed.

    Args:
        db_path: Path to the SQLite database file
    """
    global _db_path
    _db_path = str(db_path)

    # Ensure parent directory exists
    Path(_db_path).parent.mkdir(parents=True, exist_ok=True)

    conn = sqlite3.connect(_db_path)
    cursor = conn.cursor()

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS accounts (
            name TEXT PRIMARY KEY COLLATE NOCASE,
            password_hash TEXT NOT NULL,
            salt TEXT NOT NULL,
            x INTEGER NOT NULL DEFAULT 0,
            y INTEGER NOT NULL DEFAULT 0,
            pl REAL NOT NULL DEFAULT 100.0,
            stamina REAL NOT NULL DEFAULT 100.0,
            max_stamina REAL NOT NULL DEFAULT 100.0,
            flying INTEGER NOT NULL DEFAULT 0,
            zone_name TEXT NOT NULL DEFAULT 'overworld',
            inventory TEXT NOT NULL DEFAULT '[]',
            created_at TEXT NOT NULL DEFAULT (datetime('now')),
            last_login TEXT
        )
    """)

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS player_aliases (
            player_name TEXT NOT NULL COLLATE NOCASE,
            alias TEXT NOT NULL COLLATE NOCASE,
            expansion TEXT NOT NULL,
            PRIMARY KEY (player_name, alias)
        )
    """)

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS player_stats (
            player_name TEXT PRIMARY KEY COLLATE NOCASE,
            kills INTEGER NOT NULL DEFAULT 0,
            deaths INTEGER NOT NULL DEFAULT 0,
            mob_kills TEXT NOT NULL DEFAULT '{}',
            play_time_seconds REAL NOT NULL DEFAULT 0.0,
            unlocked_moves TEXT NOT NULL DEFAULT '[]'
        )
    """)

    # Migrations: add columns if they don't exist (old schemas)
    cursor.execute("PRAGMA table_info(accounts)")
    columns = [row[1] for row in cursor.fetchall()]
    if "zone_name" not in columns:
        cursor.execute(
            "ALTER TABLE accounts "
            "ADD COLUMN zone_name TEXT NOT NULL DEFAULT 'overworld'"
        )
    if "inventory" not in columns:
        cursor.execute(
            "ALTER TABLE accounts ADD COLUMN inventory TEXT NOT NULL DEFAULT '[]'"
        )
    if "description" not in columns:
        cursor.execute(
            "ALTER TABLE accounts ADD COLUMN description TEXT NOT NULL DEFAULT ''"
        )
    if "home_zone" not in columns:
        cursor.execute("ALTER TABLE accounts ADD COLUMN home_zone TEXT")

    conn.commit()
    conn.close()


def _get_connection() -> sqlite3.Connection:
    """Get a connection to the database.

    Returns:
        sqlite3.Connection

    Raises:
        RuntimeError: If init_db has not been called
    """
    if _db_path is None:
        raise RuntimeError("Database not initialized. Call init_db() first.")
    return sqlite3.connect(_db_path)


def _hash_password(password: str, salt: bytes) -> str:
    """Hash a password with the given salt.

    Args:
        password: The password to hash
        salt: Salt bytes

    Returns:
        Hex-encoded hash
    """
    return hashlib.pbkdf2_hmac(
        "sha256", password.encode("utf-8"), salt, iterations=100000
    ).hex()


def create_account(name: str, password: str) -> bool:
    """Create a new account with the given name and password.

    Args:
        name: Account name (case-insensitive)
        password: Password to hash and store

    Returns:
        True if account was created, False if name is already taken
    """
    if account_exists(name):
        return False

    # Generate random salt
    salt = os.urandom(32)
    password_hash = _hash_password(password, salt)

    conn = _get_connection()
    cursor = conn.cursor()

    try:
        cursor.execute(
            "INSERT INTO accounts (name, password_hash, salt) VALUES (?, ?, ?)",
            (name, password_hash, salt.hex()),
        )
        conn.commit()
        return True
    except sqlite3.IntegrityError:
        # Race condition: someone created the account between our check and insert
        return False
    finally:
        conn.close()


def account_exists(name: str) -> bool:
    """Check if an account exists.

    Args:
        name: Account name (case-insensitive)

    Returns:
        True if account exists, False otherwise
    """
    conn = _get_connection()
    cursor = conn.cursor()

    cursor.execute("SELECT 1 FROM accounts WHERE name = ? LIMIT 1", (name,))
    result = cursor.fetchone()

    conn.close()
    return result is not None


def authenticate(name: str, password: str) -> bool:
    """Verify a password for the given account.

    Args:
        name: Account name (case-insensitive)
        password: Password to verify

    Returns:
        True if password is correct, False otherwise
    """
    conn = _get_connection()
    cursor = conn.cursor()

    cursor.execute("SELECT password_hash, salt FROM accounts WHERE name = ?", (name,))
    result = cursor.fetchone()
    conn.close()

    if result is None:
        return False

    stored_hash, salt_hex = result
    salt = bytes.fromhex(salt_hex)
    password_hash = _hash_password(password, salt)

    return hmac.compare_digest(password_hash, stored_hash)


def save_player_description(name: str, description: str) -> None:
    """Save a player's description to the database.

    Args:
        name: Account name
        description: Description text
    """
    conn = _get_connection()
    cursor = conn.cursor()
    cursor.execute(
        "UPDATE accounts SET description = ? WHERE name = ?",
        (description, name),
    )
    conn.commit()
    conn.close()


def save_player_home_zone(name: str, home_zone: str | None) -> None:
    """Save a player's home zone to the database.

    Args:
        name: Account name
        home_zone: Home zone name
    """
    conn = _get_connection()
    cursor = conn.cursor()
    cursor.execute(
        "UPDATE accounts SET home_zone = ? WHERE name = ?",
        (home_zone, name),
    )
    conn.commit()
    conn.close()


def save_player(player: Player) -> None:
    """Save player state to the database.

    The account must already exist. This updates game state only.

    Args:
        player: Player instance to save
    """
    # Accumulate play time before saving
    from mudlib.player import accumulate_play_time

    accumulate_play_time(player)

    # Serialize inventory as JSON list of thing names
    inventory_names = [obj.name for obj in player.contents if isinstance(obj, Thing)]
    inventory_json = json.dumps(inventory_names)

    conn = _get_connection()
    cursor = conn.cursor()

    cursor.execute(
        """
        UPDATE accounts
        SET x = ?, y = ?, pl = ?, stamina = ?, max_stamina = ?, flying = ?,
            zone_name = ?, inventory = ?, description = ?, home_zone = ?
        WHERE name = ?
        """,
        (
            player.x,
            player.y,
            player.pl,
            player.stamina,
            player.max_stamina,
            1 if player.flying else 0,
            player.location.name if player.location else "overworld",
            inventory_json,
            player.description,
            player.home_zone,
            player.name,
        ),
    )

    conn.commit()
    conn.close()

    # Save aliases and stats
    save_aliases(player.name, player.aliases)
    save_player_stats(player)


def load_player_data(name: str) -> PlayerData | None:
    """Load player data from the database.

    Args:
        name: Account name (case-insensitive)

    Returns:
        Dictionary of persisted fields, or None if account not found
    """
    conn = _get_connection()
    cursor = conn.cursor()

    # Check which columns exist (for migration compatibility)
    cursor.execute("PRAGMA table_info(accounts)")
    columns = [row[1] for row in cursor.fetchall()]
    has_zone_name = "zone_name" in columns
    has_inventory = "inventory" in columns
    has_description = "description" in columns
    has_home_zone = "home_zone" in columns

    # Build SELECT based on available columns
    select_cols = "x, y, pl, stamina, max_stamina, flying"
    if has_zone_name:
        select_cols += ", zone_name"
    if has_inventory:
        select_cols += ", inventory"
    if has_description:
        select_cols += ", description"
    if has_home_zone:
        select_cols += ", home_zone"

    cursor.execute(
        f"SELECT {select_cols} FROM accounts WHERE name = ?",
        (name,),
    )

    result = cursor.fetchone()
    conn.close()

    if result is None:
        return None

    # Unpack base fields
    x, y, pl, stamina, max_stamina, flying_int = result[:6]
    idx = 6

    zone_name = "overworld"
    if has_zone_name:
        zone_name = result[idx]
        idx += 1

    inventory: list[str] = []
    if has_inventory:
        inventory = json.loads(result[idx])
        idx += 1

    description = ""
    if has_description:
        description = result[idx]
        idx += 1

    home_zone = None
    if has_home_zone:
        home_zone = result[idx]
        idx += 1

    return {
        "x": x,
        "y": y,
        "pl": pl,
        "stamina": stamina,
        "max_stamina": max_stamina,
        "flying": bool(flying_int),
        "zone_name": zone_name,
        "inventory": inventory,
        "description": description,
        "home_zone": home_zone,
    }


def update_last_login(name: str) -> None:
    """Update the last_login timestamp for an account.

    Args:
        name: Account name (case-insensitive)
    """
    conn = _get_connection()
    cursor = conn.cursor()

    cursor.execute(
        "UPDATE accounts SET last_login = datetime('now') WHERE name = ?", (name,)
    )

    conn.commit()
    conn.close()


def save_aliases(
    name: str, aliases: dict[str, str], db_path: str | Path | None = None
) -> None:
    """Save player aliases to the database.

    Replaces all existing aliases for the player.

    Args:
        name: Player name (case-insensitive)
        aliases: Dictionary mapping alias names to expansions
        db_path: Optional explicit database path (for testing)
    """
    conn = sqlite3.connect(str(db_path)) if db_path is not None else _get_connection()

    cursor = conn.cursor()

    # Delete all existing aliases for this player
    cursor.execute("DELETE FROM player_aliases WHERE player_name = ?", (name,))

    # Insert new aliases
    for alias, expansion in aliases.items():
        cursor.execute(
            "INSERT INTO player_aliases (player_name, alias, expansion) "
            "VALUES (?, ?, ?)",
            (name, alias, expansion),
        )

    conn.commit()
    conn.close()


def load_aliases(name: str, db_path: str | Path | None = None) -> dict[str, str]:
    """Load player aliases from the database.

    Args:
        name: Player name (case-insensitive)
        db_path: Optional explicit database path (for testing)

    Returns:
        Dictionary mapping alias names to expansions
    """
    conn = sqlite3.connect(str(db_path)) if db_path is not None else _get_connection()

    cursor = conn.cursor()

    cursor.execute(
        "SELECT alias, expansion FROM player_aliases WHERE player_name = ?",
        (name,),
    )

    result = {alias: expansion for alias, expansion in cursor.fetchall()}

    conn.close()
    return result


def save_player_stats(player: Player, db_path: str | Path | None = None) -> None:
    """Save player stats to the database.

    Args:
        player: Player instance with stats to save
        db_path: Optional explicit database path (for testing)
    """
    conn = sqlite3.connect(str(db_path)) if db_path is not None else _get_connection()

    cursor = conn.cursor()

    # Serialize mob_kills as JSON dict and unlocked_moves as sorted JSON list
    mob_kills_json = json.dumps(player.mob_kills)
    unlocked_moves_json = json.dumps(sorted(player.unlocked_moves))

    cursor.execute(
        """
        INSERT INTO player_stats
            (player_name, kills, deaths, mob_kills, play_time_seconds, unlocked_moves)
        VALUES (?, ?, ?, ?, ?, ?)
        ON CONFLICT(player_name) DO UPDATE SET
            kills = excluded.kills,
            deaths = excluded.deaths,
            mob_kills = excluded.mob_kills,
            play_time_seconds = excluded.play_time_seconds,
            unlocked_moves = excluded.unlocked_moves
        """,
        (
            player.name,
            player.kills,
            player.deaths,
            mob_kills_json,
            player.play_time_seconds,
            unlocked_moves_json,
        ),
    )

    conn.commit()
    conn.close()


def load_player_stats(name: str, db_path: str | Path | None = None) -> StatsData:
    """Load player stats from the database.

    Args:
        name: Player name (case-insensitive)
        db_path: Optional explicit database path (for testing)

    Returns:
        Dictionary with stats fields, defaults if no row exists
    """
    conn = sqlite3.connect(str(db_path)) if db_path is not None else _get_connection()

    cursor = conn.cursor()

    cursor.execute(
        """
        SELECT kills, deaths, mob_kills, play_time_seconds, unlocked_moves
        FROM player_stats
        WHERE player_name = ?
        """,
        (name,),
    )

    result = cursor.fetchone()
    conn.close()

    if result is None:
        # Return defaults if no stats row exists
        return {
            "kills": 0,
            "deaths": 0,
            "mob_kills": {},
            "play_time_seconds": 0.0,
            "unlocked_moves": set(),
        }

    kills, deaths, mob_kills_json, play_time_seconds, unlocked_moves_json = result

    return {
        "kills": kills,
        "deaths": deaths,
        "mob_kills": json.loads(mob_kills_json),
        "play_time_seconds": play_time_seconds,
        "unlocked_moves": set(json.loads(unlocked_moves_json)),
    }
