"""Lifecycle-only storage harness for service-owned persistent data.

This module owns the *lifecycle* of per-concern SQLite databases: provisioning,
schema migrations, and cascade-delete when a service instance is removed. It
does **not** own data operations — each integration implements its own Store
with bespoke operations (``append``/``window``, ``replace_items``/``query``,
etc.). This keeps the general interface narrow (lifecycle) and the specific
interfaces rich (per-integration operations).

Each storage *concern* is registered with a :class:`StorageConcern` dataclass
declaring its DB filename, ordered migration statements, owned tables, and the
column used for service scoping.
"""

from __future__ import annotations

import logging
import os
import sqlite3
from dataclasses import dataclass, field
from pathlib import Path

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class StorageConcern:
    """A per-integration storage namespace registered with the harness."""

    concern_key: str  # e.g. "qbittorrent", "media_index"
    db_filename: str  # e.g. "qbittorrent.db", "media_index.sqlite"
    migrations: list[str]  # ordered CREATE/ALTER statements (idempotent via IF NOT EXISTS or ALTER-catch)
    tables: list[str] = field(default_factory=list)  # tables owned by this concern (for cascade)
    service_id_column: str = "service_id"


class ServiceDataHarness:
    """Lifecycle-only registry of per-concern storage.

    Owns:
      * DB provisioning (per-concern SQLite files under ``base_dir``).
      * Schema migrations (run on first access via :meth:`run_migrations`).
      * ``service_id`` cascade-delete when a service instance is removed.

    Does **not** own:
      * Data operations — each store keeps bespoke append/window/query/etc.
      * A generic value table or generic CRUD layer.
    """

    def __init__(self, base_dir: Path | str) -> None:
        self._base_dir = Path(base_dir)
        self._concerns: dict[str, StorageConcern] = {}

    @property
    def base_dir(self) -> Path:
        return self._base_dir

    def register(self, concern: StorageConcern) -> None:
        """Register a storage concern. Called at startup / on first access."""
        self._concerns[concern.concern_key] = concern

    def db_path(self, concern_key: str) -> Path:
        """Return the absolute path to a concern's DB file."""
        concern = self._concerns[concern_key]
        return self._base_dir / concern.db_filename

    def connect(self, concern_key: str) -> sqlite3.Connection:
        """Open a WAL-mode connection to a concern's DB."""
        path = self.db_path(concern_key)
        path.parent.mkdir(parents=True, exist_ok=True)
        conn = sqlite3.connect(path, timeout=30)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA busy_timeout=30000")
        return conn

    def run_migrations(self) -> None:
        """Run pending migrations for every registered concern.

        Each migration string is split into individual statements (by ``;``)
        and executed individually. ``ALTER TABLE ... ADD COLUMN`` statements
        that fail with "duplicate column name" are silently skipped, making
        migrations idempotent across re-runs and fresh installs where
        ``init_schema`` may have already created the column.
        """
        for concern in self._concerns.values():
            path = self.db_path(concern.concern_key)
            path.parent.mkdir(parents=True, exist_ok=True)
            conn = sqlite3.connect(path, timeout=30)
            try:
                conn.execute("PRAGMA journal_mode=WAL")
                for migration_sql in concern.migrations:
                    statements = [s.strip() for s in migration_sql.split(";") if s.strip()]
                    for stmt in statements:
                        try:
                            conn.execute(stmt)
                        except sqlite3.OperationalError as exc:
                            lowered = str(exc).lower()
                            if "duplicate column name" in lowered or "no such table" in lowered:
                                logger.debug("Skipping migration (already applied or table absent): %s", stmt[:80])
                            else:
                                raise
                conn.commit()
            finally:
                conn.close()

    def cascade_delete(self, service_id: str) -> None:
        """Delete all rows for a ``service_id`` across every concern's tables.

        Called from :meth:`SettingsStore.delete_service` after the service row
        is removed. Best-effort: callers wrap in try/except so a harness
        failure does not block service deletion.
        """
        for concern in self._concerns.values():
            col = concern.service_id_column
            path = self.db_path(concern.concern_key)
            if not path.exists():
                continue
            with sqlite3.connect(path, timeout=30) as conn:
                for table in concern.tables:
                    cols = {row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
                    if col in cols:
                        conn.execute(f"DELETE FROM {table} WHERE {col} = ?", (service_id,))


# ---------------------------------------------------------------------------
# Module-level singleton
# ---------------------------------------------------------------------------

_HARNESS: ServiceDataHarness | None = None


def get_service_data_harness() -> ServiceDataHarness:
    """Return the process-wide harness singleton, initializing it on first call.

    Lazy registration of built-in concerns happens here (local imports avoid
    circular dependencies). Migrations are run immediately after registration.
    """
    global _HARNESS
    if _HARNESS is None:
        base_dir = Path(os.environ.get("BACKEND_CACHE_DIR", ".cache/media_library_viewer"))
        _HARNESS = ServiceDataHarness(base_dir)
        # Register built-in concerns (lazy import avoids circular dependency).
        from media_library_viewer_api.services.qbittorrent_store import QBITTORRENT_CONCERN

        _HARNESS.register(QBITTORRENT_CONCERN)

        from media_library_viewer_api.services.media_index_impl import MEDIA_INDEX_CONCERN

        _HARNESS.register(MEDIA_INDEX_CONCERN)
        _HARNESS.run_migrations()
    return _HARNESS


def reset_service_data_harness() -> None:
    """Reset the singleton (for testing)."""
    global _HARNESS
    _HARNESS = None
