"""Typisierter Key/Value-Store fuer Laufzeiteinstellungen.

Der Vorgaenger nutzte eine ``settings``-Tabelle als reinen String-Store; jeder
Aufrufer parste selbst und erfand seinen eigenen Default. Hier gibt es eine
Registry: jede Einstellung deklariert Typ, Default, Label und Bereich. Damit
sind Formulare, Import/Export und die Diagnoseseite generisch.
"""

from __future__ import annotations

import json
import sqlite3
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Iterable, Literal, Sequence

from .db import Database

Kind = Literal["str", "int", "bool", "json", "secret"]


@dataclass(frozen=True, slots=True)
class SettingSpec:
    key: str
    default: Any = ""
    kind: Kind = "str"
    label: str = ""
    hint: str = ""
    section: str = "allgemein"
    #: Wertebereich fuer ``int``.
    minimum: int | None = None
    maximum: int | None = None
    #: Auswahlliste fuer ``str``: ``[(wert, label), ...]``.
    choices: tuple[tuple[str, str], ...] = ()
    #: ``secret``-Werte werden in der Oberflaeche nie zurueckgegeben, nur ersetzt.
    export: bool = True

    def parse(self, raw: str | None) -> Any:
        if raw is None:
            return self.default
        if self.kind == "int":
            try:
                value = int(str(raw).strip())
            except (TypeError, ValueError):
                return self.default
            if self.minimum is not None:
                value = max(self.minimum, value)
            if self.maximum is not None:
                value = min(self.maximum, value)
            return value
        if self.kind == "bool":
            return str(raw).strip().lower() in {"1", "true", "yes", "on"}
        if self.kind == "json":
            try:
                return json.loads(raw) if raw else self.default
            except ValueError:
                return self.default
        return str(raw)

    def serialize(self, value: Any) -> str:
        if self.kind == "bool":
            return "1" if value else "0"
        if self.kind == "json":
            return json.dumps(value, separators=(",", ":"), sort_keys=True)
        if self.kind == "int":
            return str(int(value))
        return "" if value is None else str(value)


class SettingsStore:
    """Zugriff auf ``settings``. Alle Methoden nehmen die bestehende Verbindung."""

    def __init__(self, specs: Iterable[SettingSpec]) -> None:
        self._specs: dict[str, SettingSpec] = {spec.key: spec for spec in specs}

    # ------------------------------------------------------------------ #

    def register(self, specs: Iterable[SettingSpec]) -> None:
        for spec in specs:
            self._specs[spec.key] = spec

    def spec(self, key: str) -> SettingSpec:
        return self._specs.get(key) or SettingSpec(key=key)

    def specs(self, section: str | None = None) -> list[SettingSpec]:
        values = list(self._specs.values())
        if section is not None:
            values = [spec for spec in values if spec.section == section]
        return values

    # ------------------------------------------------------------------ #

    def get(self, conn: sqlite3.Connection, key: str, default: Any = None) -> Any:
        spec = self.spec(key)
        raw = Database.value(conn, "SELECT value FROM settings WHERE key=?", (key,))
        if raw is None:
            return spec.default if default is None else default
        return spec.parse(raw)

    def get_many(self, conn: sqlite3.Connection, keys: Sequence[str]) -> dict[str, Any]:
        if not keys:
            return {}
        placeholders = ",".join("?" * len(keys))
        rows = Database.all(
            conn, f"SELECT key, value FROM settings WHERE key IN ({placeholders})", tuple(keys)
        )
        stored = {row["key"]: row["value"] for row in rows}
        return {key: self.spec(key).parse(stored.get(key)) for key in keys}

    def section_values(self, conn: sqlite3.Connection, section: str) -> dict[str, Any]:
        keys = [spec.key for spec in self.specs(section)]
        return self.get_many(conn, keys)

    def set(
        self,
        conn: sqlite3.Connection,
        key: str,
        value: Any,
        *,
        actor: str = "",
    ) -> None:
        """Setzt einen Wert. Nutzt die uebergebene Verbindung -- niemals eine eigene.

        Genau hier lag im Vorgaenger eine Deadlock-Quelle: ``set_setting()``
        oeffnete intern eine zweite Verbindung und lief dadurch im Import-Pfad in
        ``database is locked``.
        """
        spec = self.spec(key)
        conn.execute(
            "INSERT INTO settings (key, value, updated_at, updated_by) VALUES (?,?,?,?) "
            "ON CONFLICT(key) DO UPDATE SET value=excluded.value, "
            "updated_at=excluded.updated_at, updated_by=excluded.updated_by",
            (
                key,
                spec.serialize(value),
                datetime.now(timezone.utc).isoformat(timespec="seconds"),
                actor,
            ),
        )

    def set_many(
        self, conn: sqlite3.Connection, values: dict[str, Any], *, actor: str = ""
    ) -> list[str]:
        """Setzt mehrere Werte, gibt die tatsaechlich geaenderten Schluessel zurueck."""
        changed: list[str] = []
        current = self.get_many(conn, list(values))
        for key, value in values.items():
            spec = self.spec(key)
            if spec.serialize(current.get(key)) == spec.serialize(value):
                continue
            self.set(conn, key, value, actor=actor)
            changed.append(key)
        return changed

    def delete(self, conn: sqlite3.Connection, key: str) -> None:
        conn.execute("DELETE FROM settings WHERE key=?", (key,))

    def all_values(self, conn: sqlite3.Connection) -> dict[str, Any]:
        rows = Database.all(conn, "SELECT key, value FROM settings")
        stored = {row["key"]: row["value"] for row in rows}
        result = {key: spec.parse(stored.get(key)) for key, spec in self._specs.items()}
        # Auch Schluessel ohne Spezifikation ausliefern (App-eigene Ad-hoc-Werte).
        for key, raw in stored.items():
            result.setdefault(key, raw)
        return result
