from __future__ import annotations

import os
import sqlite3
from abc import ABC, abstractmethod
from typing import Any, Callable, ClassVar, Dict, List, Optional, TYPE_CHECKING

from ..config.persist_config import PersistConfig

if TYPE_CHECKING:
    from .sync_node import SyncNode


class PersistenceBackend(ABC):
    """同步系统持久化契约。"""

    backend_name: ClassVar[str] = "abstract"

    @staticmethod
    def _is_read_only_sql(sql: str) -> bool:
        stmt = (sql or "").strip().lower()
        return stmt.startswith("select") or stmt.startswith("pragma") or stmt.startswith("with")

    @classmethod
    def query_records(
        cls,
        *,
        backend: str,
        db_path: str,
        sql: str,
        limit: int = 200,
    ) -> tuple[List[str], List[Dict[str, Any]]]:
        if backend != "sqlite":
            raise NotImplementedError(f"records query backend not supported yet: {backend}")
        if not cls._is_read_only_sql(sql):
            raise ValueError("Only read-only SQL is allowed")

        path = db_path
        if not os.path.exists(path):
            raise FileNotFoundError(f"DB not found: {path}")

        with sqlite3.connect(path) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.execute(sql)
            rows = cursor.fetchmany(max(1, min(limit, 2000)))
            columns = [item[0] for item in (cursor.description or [])]
            data = [{k: row[k] for k in columns} for row in rows]
            return columns, data

    @classmethod
    def records_summary(
        cls,
        *,
        backend: str,
        db_path: str,
    ) -> Dict[str, Any]:
        summary: Dict[str, Any] = {
            "backend": backend,
            "db_path": db_path,
            "db_exists": os.path.exists(db_path),
            "tables": [],
            "sample_bindings": [],
        }
        if not os.path.exists(db_path):
            return summary
        if backend != "sqlite":
            summary["error"] = f"records summary backend not supported yet: {backend}"
            return summary

        with sqlite3.connect(db_path) as conn:
            conn.row_factory = sqlite3.Row
            tables = conn.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
            ).fetchall()
            table_names = [str(r["name"]) for r in tables]

            for name in table_names:
                count = conn.execute(f"SELECT COUNT(1) AS c FROM {name}").fetchone()["c"]
                summary["tables"].append({"name": name, "count": int(count)})

            if "bindings" in table_names:
                sample = conn.execute(
                    "SELECT node_type, local_id, remote_id FROM bindings ORDER BY last_updated DESC LIMIT 50"
                ).fetchall()
                summary["sample_bindings"] = [
                    {
                        "node_type": row["node_type"],
                        "local_id": row["local_id"],
                        "remote_id": row["remote_id"],
                    }
                    for row in sample
                ]
        return summary

    @abstractmethod
    async def initialize(self) -> None:
        raise NotImplementedError

    @abstractmethod
    async def save_node(self, collection_id: str, scope: str, node: "SyncNode") -> None:
        raise NotImplementedError

    @abstractmethod
    async def save_nodes_bulk(self, collection_id: str, scope: str, nodes: List["SyncNode"]) -> None:
        raise NotImplementedError

    @abstractmethod
    async def delete_node(self, collection_id: str, scope: str, node_id: str) -> None:
        raise NotImplementedError

    @abstractmethod
    async def delete_nodes_bulk(self, collection_id: str, scope: str, node_ids: List[str]) -> None:
        raise NotImplementedError

    @abstractmethod
    async def load_nodes(
        self,
        collection_id: str,
        scope: str,
        *,
        exclude_node_types: Optional[List[str]] = None,
    ) -> List[Dict[str, Any]]:
        raise NotImplementedError

    @abstractmethod
    async def save_binding(self, scope: str, node_type: str, local_id: str, payload_dict: Dict[str, Any]) -> None:
        raise NotImplementedError

    @abstractmethod
    async def save_bindings_bulk(self, scope: str, node_type: str, bindings: Dict[str, Optional[str]]) -> None:
        raise NotImplementedError

    @abstractmethod
    async def delete_binding(self, scope: str, node_type: str, local_id: str) -> None:
        raise NotImplementedError

    @abstractmethod
    async def delete_bindings_bulk(self, scope: str, node_type: str, local_ids: List[str]) -> None:
        raise NotImplementedError

    @abstractmethod
    async def load_bindings(self, scope: str, node_type: str) -> List[Dict[str, Any]]:
        raise NotImplementedError

    @abstractmethod
    async def load_bind_data_id_overrides(self, node_type: str, scope: str) -> Dict[str, Optional[str]]:
        raise NotImplementedError

    @abstractmethod
    async def delete_scope_node_types(self, *, scope: str, node_types: List[str]) -> None:
        raise NotImplementedError

    @abstractmethod
    async def copy_nodes_between_scopes(
        self,
        *,
        collection_id: str,
        source_scope: str,
        target_scope: str,
        node_types: List[str],
    ) -> None:
        raise NotImplementedError

    @abstractmethod
    async def copy_bindings_between_scopes(
        self,
        *,
        source_scope: str,
        target_scope: str,
        node_types: List[str],
    ) -> None:
        raise NotImplementedError

    @abstractmethod
    async def dump_to_runtime(self, filename: str) -> None:
        raise NotImplementedError

    @abstractmethod
    async def close(self) -> None:
        raise NotImplementedError


PersistenceBackendFactory = Callable[[PersistConfig], PersistenceBackend]
_PERSISTENCE_BACKEND_REGISTRY: dict[str, PersistenceBackendFactory] = {}


def register_persistence_backend(name: str, factory: PersistenceBackendFactory, *, override: bool = False) -> None:
    backend_name = name.strip()
    if not backend_name:
        raise ValueError("backend name cannot be empty")
    if not override and backend_name in _PERSISTENCE_BACKEND_REGISTRY:
        raise ValueError(f"persistence backend already registered: {backend_name}")
    _PERSISTENCE_BACKEND_REGISTRY[backend_name] = factory


def get_registered_persistence_backends() -> Dict[str, PersistenceBackendFactory]:
    return dict(_PERSISTENCE_BACKEND_REGISTRY)


def create_persistence_backend(
    persist_config: PersistConfig,
) -> PersistenceBackend:
    backend_name = (persist_config.backend or "").strip()
    if not backend_name:
        raise ValueError("persist.backend cannot be empty")

    if not _PERSISTENCE_BACKEND_REGISTRY:
        register_persistence_backend(
            "sqlite",
            lambda config: SQLitePersistenceBackend(config),
        )

    factory = _PERSISTENCE_BACKEND_REGISTRY.get(backend_name)
    if factory is None:
        available = ", ".join(sorted(_PERSISTENCE_BACKEND_REGISTRY)) or "none"
        raise NotImplementedError(f"Persistence backend not supported yet: {backend_name}. Available: {available}")
    return factory(persist_config)


def _register_builtin_persistence_backends() -> None:
    from .persistence_backends.sqlite_backend import SQLitePersistenceBackend

    register_persistence_backend("sqlite", lambda config: SQLitePersistenceBackend(config), override=True)


_register_builtin_persistence_backends()
