"""Database lifecycle management for SQLite campaign databases.

Provides functions to open, initialize, and query SQLite databases
used for campaign persistence. Uses the stdlib sqlite3 module with
no ORM layer.
"""

from __future__ import annotations

import logging
import sqlite3
from typing import TYPE_CHECKING

from unfamiliar.db.schema import SCHEMA_SQL, SCHEMA_VERSION

if TYPE_CHECKING:
    from pathlib import Path

logger = logging.getLogger(__name__)


class DatabaseError(Exception):
    """Raised when a database operation fails."""


def open_database(db_path: Path) -> sqlite3.Connection:
    """Open or create a SQLite database with correct pragmas.

    Creates the database file if it doesn't exist. Enables WAL mode
    for concurrent reads during TUI sessions. Sets the row factory
    to sqlite3.Row for dict-like access.

    Args:
        db_path: Path to the SQLite database file.

    Returns:
        A sqlite3.Connection with WAL mode and foreign keys enabled.

    Raises:
        DatabaseError: If the file path is not writable.
    """
    # Ensure parent directory exists
    db_path = db_path.resolve()
    if not db_path.parent.exists():
        try:
            db_path.parent.mkdir(parents=True, mode=0o700)
        except OSError as exc:
            raise DatabaseError(
                f"Cannot create database directory: {db_path.parent}"
            ) from exc

    try:
        conn = sqlite3.connect(str(db_path))
    except sqlite3.Error as exc:
        raise DatabaseError(f"Cannot open database: {db_path}") from exc

    # Configure connection with pragmas — close on failure
    try:
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA foreign_keys=ON")
        conn.row_factory = sqlite3.Row
    except sqlite3.Error as exc:
        conn.close()
        raise DatabaseError(f"Failed to configure database: {exc}") from exc

    return conn


def init_database(conn: sqlite3.Connection) -> None:
    """Initialize database with schema if tables don't exist.

    Executes SCHEMA_SQL to create all tables and indexes. Inserts
    the schema version into the meta table.

    Idempotent - safe to call on an already-initialized database.

    Args:
        conn: A sqlite3.Connection from open_database().

    Raises:
        DatabaseError: If schema execution fails.
    """
    try:
        conn.executescript(SCHEMA_SQL)

        # Insert schema version if not present
        conn.execute(
            """
            INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)
            """,
            ("schema_version", SCHEMA_VERSION),
        )
        conn.commit()
    except sqlite3.Error as exc:
        raise DatabaseError("Failed to initialize database schema") from exc


def get_schema_version(conn: sqlite3.Connection) -> str | None:
    """Read the current schema version from the meta table.

    Returns None for uninitialized databases or if the meta table
    doesn't exist. This allows detecting whether a database needs
    initialization or upgrade.

    Args:
        conn: A sqlite3.Connection from open_database().

    Returns:
        The schema version string (e.g., "1.0") or None.
    """
    try:
        row = conn.execute(
            "SELECT value FROM meta WHERE key = ?",
            ("schema_version",),
        ).fetchone()
        return row["value"] if row else None
    except sqlite3.OperationalError as exc:
        if "no such table" in str(exc):
            return None
        raise
    except sqlite3.Error as exc:
        raise DatabaseError(f"Cannot read schema version: {exc}") from exc
