"""SQLite backend for semantic graph storage — synchronous sqlite3 wrapped as async."""

from __future__ import annotations

import asyncio
import logging
import sqlite3
from functools import partial
from pathlib import Path

logger = logging.getLogger("engram.semantic.sqlite")


class SqliteBackend:
    """SQLite-backed graph storage.

    Sync sqlite3 operations run in the default thread-pool executor via
    asyncio.get_running_loop().run_in_executor() to avoid blocking the event loop.
    Uses check_same_thread=False for safe cross-thread access.
    """

    def __init__(self, db_path: str) -> None:
        # Expand ~ to HOME to avoid creating a literal "~" directory depending on cwd.
        self._db_path = Path(db_path).expanduser()
        self._db_path.parent.mkdir(parents=True, exist_ok=True)
        self._conn: sqlite3.Connection | None = None

    def _connect(self) -> sqlite3.Connection:
        """Return cached connection (check_same_thread=False for executor use)."""
        if self._conn is None:
            self._conn = sqlite3.connect(str(self._db_path), check_same_thread=False)
            self._conn.execute("PRAGMA journal_mode=WAL")
            self._conn.execute("PRAGMA foreign_keys=ON")  # D-H4: enforce FK constraints
        return self._conn

    # --- Sync helpers (called from executor thread pool) ---

    def _sync_initialize(self) -> None:
        conn = self._connect()
        conn.execute(
            "CREATE TABLE IF NOT EXISTS nodes "
            "(key TEXT PRIMARY KEY, type TEXT, name TEXT, attributes TEXT)"
        )
        conn.execute(
            "CREATE TABLE IF NOT EXISTS edges "
            "(key TEXT PRIMARY KEY, from_key TEXT, to_key TEXT, relation TEXT, "
            "weight REAL DEFAULT 1.0, attributes TEXT DEFAULT '{}')"
        )
        conn.execute("CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name)")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_nodes_key ON nodes(key)")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation)")
        conn.commit()
        # Migrate existing tables: add weight + attributes columns if missing
        try:
            conn.execute("ALTER TABLE edges ADD COLUMN weight REAL DEFAULT 1.0")
            conn.commit()
        except Exception as exc:
            logger.debug("sqlite_backend: weight column migration skipped (already exists): %s", exc)
        try:
            conn.execute("ALTER TABLE edges ADD COLUMN attributes TEXT DEFAULT '{}'")
            conn.commit()
        except Exception as exc:
            logger.debug("sqlite_backend: attributes column migration skipped (already exists): %s", exc)

    def _sync_load_nodes(self) -> list[tuple[str, str, str, str]]:
        conn = self._connect()
        return list(conn.execute("SELECT key, type, name, attributes FROM nodes"))

    def _sync_load_edges(self) -> list[tuple[str, str, str, str, float, str]]:
        conn = self._connect()
        return list(conn.execute(
            "SELECT key, from_key, to_key, relation, "
            "COALESCE(weight, 1.0), COALESCE(attributes, '{}') FROM edges"
        ))

    def _sync_save_node(self, key: str, type: str, name: str, attrs_json: str) -> None:
        conn = self._connect()
        conn.execute(
            "INSERT OR REPLACE INTO nodes (key, type, name, attributes) VALUES (?, ?, ?, ?)",
            (key, type, name, attrs_json),
        )
        conn.commit()

    def _sync_save_edge(self, key: str, from_key: str, to_key: str, relation: str,
                        weight: float = 1.0, attrs_json: str = "{}") -> None:
        conn = self._connect()
        conn.execute(
            "INSERT OR REPLACE INTO edges (key, from_key, to_key, relation, weight, attributes) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            (key, from_key, to_key, relation, weight, attrs_json),
        )
        conn.commit()

    def _sync_save_nodes_batch(self, rows: list[tuple[str, str, str, str]]) -> None:
        if not rows:
            return
        conn = self._connect()
        with conn:
            conn.executemany(
                "INSERT OR REPLACE INTO nodes (key, type, name, attributes) VALUES (?, ?, ?, ?)",
                rows,
            )

    def _sync_save_edges_batch(self, rows: list[tuple]) -> None:
        if not rows:
            return
        conn = self._connect()
        with conn:
            conn.executemany(
                "INSERT OR REPLACE INTO edges (key, from_key, to_key, relation, weight, attributes) "
                "VALUES (?, ?, ?, ?, ?, ?)",
                rows,
            )

    def _sync_delete_node(self, key: str) -> None:
        """Delete node and its edges atomically (D-C2: single transaction)."""
        conn = self._connect()
        with conn:
            conn.execute("DELETE FROM edges WHERE from_key=? OR to_key=?", (key, key))
            conn.execute("DELETE FROM nodes WHERE key=?", (key,))

    def _sync_delete_edges_for_node(self, key: str) -> None:
        """Delete all edges for a node. Kept for interface compatibility."""
        conn = self._connect()
        conn.execute("DELETE FROM edges WHERE from_key=? OR to_key=?", (key, key))
        conn.commit()

    def _sync_delete_edge(self, key: str) -> None:
        conn = self._connect()
        conn.execute("DELETE FROM edges WHERE key=?", (key,))
        conn.commit()

    def _sync_query_nodes_by_name(self, pattern: str, limit: int, offset: int) -> list[dict]:
        """SELECT nodes whose key LIKE pattern without loading the full graph."""
        conn = self._connect()
        rows = conn.execute(
            "SELECT key, type, name, attributes FROM nodes WHERE key LIKE ? LIMIT ? OFFSET ?",
            (pattern, limit, offset),
        )
        return [{"key": r[0], "type": r[1], "name": r[2], "attributes": r[3]} for r in rows]

    def _sync_get_node_by_key(self, key: str) -> dict | None:
        """Return a single node row by exact key."""
        conn = self._connect()
        row = conn.execute(
            "SELECT key, type, name, attributes FROM nodes WHERE key = ?", (key,)
        ).fetchone()
        if row is None:
            return None
        return {"key": row[0], "type": row[1], "name": row[2], "attributes": row[3]}

    def _sync_close(self) -> None:
        if self._conn is not None:
            self._conn.close()
            self._conn = None

    # --- Async public interface (runs sync ops in executor) ---

    async def initialize(self) -> None:
        """Create tables and indexes if they don't exist."""
        await asyncio.get_running_loop().run_in_executor(None, self._sync_initialize)

    async def load_nodes(self) -> list[tuple[str, str, str, str]]:
        """Return all nodes as (key, type, name, attrs_json) tuples."""
        return await asyncio.get_running_loop().run_in_executor(None, self._sync_load_nodes)

    async def load_edges(self) -> list[tuple[str, str, str, str, float, str]]:
        """Return all edges as (key, from_key, to_key, relation, weight, attrs_json) tuples."""
        return await asyncio.get_running_loop().run_in_executor(None, self._sync_load_edges)

    async def save_node(self, key: str, type: str, name: str, attrs_json: str) -> None:
        """Upsert a single node."""
        await asyncio.get_running_loop().run_in_executor(
            None, partial(self._sync_save_node, key, type, name, attrs_json)
        )

    async def save_edge(self, key: str, from_key: str, to_key: str, relation: str,
                        weight: float = 1.0, attrs_json: str = "{}") -> None:
        """Upsert a single edge."""
        await asyncio.get_running_loop().run_in_executor(
            None, partial(self._sync_save_edge, key, from_key, to_key, relation, weight, attrs_json)
        )

    async def save_nodes_batch(self, rows: list[tuple[str, str, str, str]]) -> None:
        """Upsert multiple nodes in one transaction."""
        await asyncio.get_running_loop().run_in_executor(
            None, partial(self._sync_save_nodes_batch, rows)
        )

    async def save_edges_batch(self, rows: list[tuple]) -> None:
        """Upsert multiple edges in one transaction."""
        await asyncio.get_running_loop().run_in_executor(
            None, partial(self._sync_save_edges_batch, rows)
        )

    async def delete_node(self, key: str) -> None:
        """Delete a node by key."""
        await asyncio.get_running_loop().run_in_executor(
            None, partial(self._sync_delete_node, key)
        )

    async def delete_edges_for_node(self, key: str) -> None:
        """Delete all edges connected to a node (from or to)."""
        await asyncio.get_running_loop().run_in_executor(
            None, partial(self._sync_delete_edges_for_node, key)
        )

    async def delete_edge(self, key: str) -> None:
        """Delete an edge by key."""
        await asyncio.get_running_loop().run_in_executor(
            None, partial(self._sync_delete_edge, key)
        )

    async def query_nodes_by_name(self, pattern: str, limit: int, offset: int) -> list[dict]:
        """Return nodes whose key LIKE pattern (fast path, no full graph load)."""
        return await asyncio.get_running_loop().run_in_executor(
            None, partial(self._sync_query_nodes_by_name, pattern, limit, offset)
        )

    async def get_node_by_key(self, key: str) -> dict | None:
        """Return a single node dict by exact key, or None."""
        return await asyncio.get_running_loop().run_in_executor(
            None, partial(self._sync_get_node_by_key, key)
        )

    async def get_related_nodes(self, node_key: str, depth: int) -> list[dict]:
        """Stub — SQLite backend defers graph traversal to NetworkX (returns [])."""
        return []

    async def close(self) -> None:
        """Close the SQLite connection."""
        await asyncio.get_running_loop().run_in_executor(None, self._sync_close)
