"""Datenbankzugriff auf SQLite -- eine Verbindung pro Request, Schreibsperren strukturell.

Die schmerzhafteste Fehlerklasse des Vorgaengerprojekts war
``sqlite3.OperationalError: database is locked``: eine Hilfsfunktion oeffnete
still eine *zweite* Verbindung, waehrend die aeussere Funktion noch eine offene
Schreibtransaktion hielt. Das ist hier nicht mehr durch Disziplin, sondern
strukturell verhindert:

* Pro Request existiert genau **eine** Verbindung (in ``flask.g``).
* ``Database.transaction()`` zaehlt die Verschachtelungstiefe. Wer waehrend
  einer offenen Schreibtransaktion ``Database.connect()`` aufruft, bekommt
  sofort einen ``NestedConnectionError`` mit Klartext-Erklaerung -- nicht erst
  nach 15 Sekunden Busy-Timeout einen Deadlock.
* Verschachtelte ``transaction()``-Aufrufe auf derselben Verbindung sind
  erlaubt und werden zu einem einzigen Commit zusammengefasst (Savepoints),
  damit Service-Funktionen sich gefahrlos gegenseitig aufrufen koennen.

Zusaetzlich: WAL-Modus (paralleles Lesen waehrend eines Schreibers),
``foreign_keys=ON`` (im Vorgaenger nie aktiviert -- Fremdschluessel waren
Dekoration) und ``busy_timeout=15000``.
"""

from __future__ import annotations

import os
import sqlite3
import threading
from collections.abc import Iterator, Mapping, Sequence
from contextlib import contextmanager
from pathlib import Path
from typing import Any


class NestedConnectionError(RuntimeError):
    """Zweite Verbindung waehrend einer offenen Schreibtransaktion."""


class Row(sqlite3.Row):
    """``sqlite3.Row`` mit ``dict``-artigem ``get`` und Mapping-Konvertierung."""

    def get(self, key: str, default: Any = None) -> Any:
        try:
            return self[key]
        except (IndexError, KeyError):
            return default

    def as_dict(self) -> dict[str, Any]:
        return {key: self[key] for key in self.keys()}


def row_to_dict(row: sqlite3.Row | None) -> dict[str, Any] | None:
    if row is None:
        return None
    return {key: row[key] for key in row.keys()}


def rows_to_dicts(rows: Sequence[sqlite3.Row]) -> list[dict[str, Any]]:
    return [{key: row[key] for key in row.keys()} for row in rows]


class Database:
    """Zugriffsobjekt fuer eine SQLite-Datei."""

    def __init__(self, path: Path | str, *, timeout: float = 15.0) -> None:
        self.path = Path(path)
        self.timeout = timeout
        self._state = threading.local()

    # ------------------------------------------------------------------ #
    # Verbindungen
    # ------------------------------------------------------------------ #

    def _depth(self) -> int:
        return int(getattr(self._state, "depth", 0))

    def _set_depth(self, value: int) -> None:
        self._state.depth = value

    def connect(self) -> sqlite3.Connection:
        """Oeffnet eine neue Verbindung. Nicht aufrufen, solange eine Transaktion laeuft."""
        if self._depth() > 0:
            raise NestedConnectionError(
                "Es laeuft bereits eine Schreibtransaktion auf dieser Datenbank. "
                "Reiche die bestehende Verbindung durch, statt eine neue zu oeffnen "
                "(sonst blockiert SQLite sich selbst)."
            )
        return self._raw_connect()

    def _raw_connect(self) -> sqlite3.Connection:
        self.path.parent.mkdir(parents=True, exist_ok=True)
        fresh = not self.path.exists()
        conn = sqlite3.connect(
            self.path,
            timeout=self.timeout,
            isolation_level=None,  # Transaktionen steuern wir selbst, explizit.
            check_same_thread=False,
        )
        conn.row_factory = Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA synchronous=NORMAL")
        conn.execute("PRAGMA foreign_keys=ON")
        conn.execute("PRAGMA busy_timeout=15000")
        conn.execute("PRAGMA secure_delete=ON")
        if fresh:
            # Die Datei enthaelt Passwort-Hashes, verschluesselte Zugangsdaten und
            # Sitzungen -- sie gehoert niemandem ausser dem Dienstbenutzer.
            try:
                os.chmod(self.path, 0o600)
            except OSError:  # pragma: no cover - z. B. Windows/NTFS
                pass
        return conn

    @contextmanager
    def session(self) -> Iterator[sqlite3.Connection]:
        """Kurzlebige Verbindung fuer Skripte und Hintergrund-Threads."""
        conn = self.connect()
        try:
            yield conn
        finally:
            conn.close()

    # ------------------------------------------------------------------ #
    # Transaktionen
    # ------------------------------------------------------------------ #

    @contextmanager
    def transaction(self, conn: sqlite3.Connection | None = None) -> Iterator[sqlite3.Connection]:
        """Schreibtransaktion mit ``BEGIN IMMEDIATE``.

        ``BEGIN IMMEDIATE`` nimmt die Schreibsperre sofort statt erst beim ersten
        Schreibzugriff -- damit scheitert ein konkurrierender Schreiber sauber am
        Busy-Timeout, statt mitten in der Transaktion in einen
        ``SQLITE_BUSY_SNAPSHOT`` zu laufen.
        """
        own_connection = conn is None
        if own_connection:
            conn = self._raw_connect()
        assert conn is not None

        depth = self._depth()
        savepoint = f"sp_{depth}"
        self._set_depth(depth + 1)
        try:
            if depth == 0:
                conn.execute("BEGIN IMMEDIATE")
            else:
                conn.execute(f"SAVEPOINT {savepoint}")
            yield conn
        except BaseException:
            try:
                if depth == 0:
                    conn.execute("ROLLBACK")
                else:
                    conn.execute(f"ROLLBACK TO {savepoint}")
            except sqlite3.Error:  # pragma: no cover - Verbindung schon tot
                pass
            raise
        else:
            if depth == 0:
                conn.execute("COMMIT")
            else:
                conn.execute(f"RELEASE {savepoint}")
        finally:
            self._set_depth(depth)
            if own_connection:
                conn.close()

    @property
    def in_transaction(self) -> bool:
        return self._depth() > 0

    # ------------------------------------------------------------------ #
    # Bequeme Abfragehelfer (immer parametrisiert -- kein String-Basteln)
    # ------------------------------------------------------------------ #

    @staticmethod
    def one(
        conn: sqlite3.Connection, sql: str, params: Sequence[Any] | Mapping[str, Any] = ()
    ) -> dict[str, Any] | None:
        return row_to_dict(conn.execute(sql, params).fetchone())

    @staticmethod
    def all(
        conn: sqlite3.Connection, sql: str, params: Sequence[Any] | Mapping[str, Any] = ()
    ) -> list[dict[str, Any]]:
        return rows_to_dicts(conn.execute(sql, params).fetchall())

    @staticmethod
    def value(
        conn: sqlite3.Connection,
        sql: str,
        params: Sequence[Any] | Mapping[str, Any] = (),
        default: Any = None,
    ) -> Any:
        row = conn.execute(sql, params).fetchone()
        if row is None:
            return default
        return row[0]

    @staticmethod
    def column_exists(conn: sqlite3.Connection, table: str, column: str) -> bool:
        rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
        return any(row["name"] == column for row in rows)

    @staticmethod
    def table_exists(conn: sqlite3.Connection, table: str) -> bool:
        return (
            conn.execute(
                "SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name=?",
                (table,),
            ).fetchone()
            is not None
        )
