# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Familiar v1.4.0 - State Persistence Module

Provides persistent state storage with multiple backends:
- InMemoryStateBackend: Fast, non-persistent (default, backwards compatible)
- SQLiteStateBackend: Durable single-node persistence

Features:
- TTL-based expiration for session state
- State checkpointing for workflow recovery
- JSON serialization for complex values
- Thread-safe operations
- Namespace support for isolation
"""

import abc
import json
import logging
import sqlite3
import threading
import time
import uuid
from contextlib import contextmanager
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Union

logger = logging.getLogger(__name__)


def _utcnow() -> datetime:
    """Get current UTC time as timezone-aware datetime."""
    return datetime.now(timezone.utc)


# ============================================================
# SERIALIZATION
# ============================================================


class StateSerializer:
    """
    Handles serialization/deserialization of state values.

    Supports JSON-serializable types plus common Python types.
    """

    @staticmethod
    def serialize(value: Any) -> str:
        """Serialize a value to JSON string."""
        try:
            return json.dumps(value, default=StateSerializer._default_encoder)
        except (TypeError, ValueError) as e:
            logger.warning(f"Serialization fallback for {type(value)}: {e}")
            return json.dumps({"__pickle__": True, "__repr__": repr(value)})

    @staticmethod
    def deserialize(data: str) -> Any:
        """Deserialize a JSON string to value."""
        try:
            return json.loads(data)
        except json.JSONDecodeError:
            return data  # Return as-is if not valid JSON

    @staticmethod
    def _default_encoder(obj: Any) -> Any:
        """Custom encoder for non-standard types."""
        if isinstance(obj, datetime):
            return {"__datetime__": obj.isoformat()}
        if isinstance(obj, set):
            return {"__set__": list(obj)}
        if isinstance(obj, bytes):
            return {"__bytes__": obj.hex()}
        if hasattr(obj, "__dict__"):
            return {"__class__": obj.__class__.__name__, "__dict__": obj.__dict__}
        raise TypeError(f"Object of type {type(obj)} is not JSON serializable")


# ============================================================
# STATE ENTRY
# ============================================================


@dataclass
class StateEntry:
    """
    A state entry with metadata.

    Attributes:
        key: The state key
        value: The stored value
        namespace: Optional namespace for isolation
        created_at: When the entry was created
        updated_at: When the entry was last updated
        expires_at: When the entry expires (None = never)
        version: Version number for optimistic concurrency
        metadata: Additional metadata
    """

    key: str
    value: Any
    namespace: str = "default"
    created_at: datetime = field(default_factory=_utcnow)
    updated_at: datetime = field(default_factory=_utcnow)
    expires_at: Optional[datetime] = None
    version: int = 1
    metadata: Dict[str, Any] = field(default_factory=dict)

    @property
    def is_expired(self) -> bool:
        """Check if entry has expired."""
        if self.expires_at is None:
            return False
        now = _utcnow()
        # Handle timezone-naive datetimes by assuming UTC
        expires = self.expires_at
        if expires.tzinfo is None:
            expires = expires.replace(tzinfo=timezone.utc)
        return now > expires

    @property
    def ttl_seconds(self) -> Optional[float]:
        """Get remaining TTL in seconds."""
        if self.expires_at is None:
            return None
        now = _utcnow()
        expires = self.expires_at
        if expires.tzinfo is None:
            expires = expires.replace(tzinfo=timezone.utc)
        remaining = (expires - now).total_seconds()
        return max(0, remaining)

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "key": self.key,
            "value": self.value,
            "namespace": self.namespace,
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
            "expires_at": self.expires_at.isoformat() if self.expires_at else None,
            "version": self.version,
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "StateEntry":
        """Create from dictionary."""

        def parse_dt(s):
            if not s:
                return _utcnow()
            dt = datetime.fromisoformat(s)
            if dt.tzinfo is None:
                dt = dt.replace(tzinfo=timezone.utc)
            return dt

        return cls(
            key=data["key"],
            value=data["value"],
            namespace=data.get("namespace", "default"),
            created_at=parse_dt(data.get("created_at")),
            updated_at=parse_dt(data.get("updated_at")),
            expires_at=parse_dt(data.get("expires_at")) if data.get("expires_at") else None,
            version=data.get("version", 1),
            metadata=data.get("metadata", {}),
        )


# ============================================================
# STATE BACKEND INTERFACE
# ============================================================


class StateBackend(abc.ABC):
    """
    Abstract base class for state storage backends.

    All backends must implement these methods for basic CRUD operations.
    """

    @abc.abstractmethod
    def get(self, key: str, namespace: str = "default") -> Optional[StateEntry]:
        """Get a state entry by key."""
        pass

    @abc.abstractmethod
    def set(
        self,
        key: str,
        value: Any,
        namespace: str = "default",
        ttl_seconds: Optional[float] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> StateEntry:
        """Set a state entry."""
        pass

    @abc.abstractmethod
    def delete(self, key: str, namespace: str = "default") -> bool:
        """Delete a state entry. Returns True if deleted."""
        pass

    @abc.abstractmethod
    def exists(self, key: str, namespace: str = "default") -> bool:
        """Check if a key exists (and is not expired)."""
        pass

    @abc.abstractmethod
    def keys(self, namespace: str = "default", pattern: str = "*") -> List[str]:
        """List keys in namespace, optionally filtered by pattern."""
        pass

    @abc.abstractmethod
    def clear(self, namespace: str = "default") -> int:
        """Clear all entries in namespace. Returns count deleted."""
        pass

    @abc.abstractmethod
    def cleanup_expired(self) -> int:
        """Remove expired entries. Returns count removed."""
        pass

    # Optional: Batch operations
    def get_many(self, keys: List[str], namespace: str = "default") -> Dict[str, StateEntry]:
        """Get multiple entries at once."""
        result = {}
        for key in keys:
            entry = self.get(key, namespace)
            if entry:
                result[key] = entry
        return result

    def set_many(
        self, items: Dict[str, Any], namespace: str = "default", ttl_seconds: Optional[float] = None
    ) -> Dict[str, StateEntry]:
        """Set multiple entries at once."""
        result = {}
        for key, value in items.items():
            result[key] = self.set(key, value, namespace, ttl_seconds)
        return result

    def delete_many(self, keys: List[str], namespace: str = "default") -> int:
        """Delete multiple entries. Returns count deleted."""
        count = 0
        for key in keys:
            if self.delete(key, namespace):
                count += 1
        return count


# ============================================================
# IN-MEMORY BACKEND
# ============================================================


class InMemoryStateBackend(StateBackend):
    """
    Fast in-memory state backend.

    Non-persistent - all data lost on restart.
    Best for testing and session-scoped state.
    """

    def __init__(self):
        self._store: Dict[str, Dict[str, StateEntry]] = {}
        self._lock = threading.RLock()

    def get(self, key: str, namespace: str = "default") -> Optional[StateEntry]:
        with self._lock:
            ns_store = self._store.get(namespace, {})
            entry = ns_store.get(key)

            if entry is None:
                return None

            if entry.is_expired:
                del ns_store[key]
                return None

            return entry

    def set(
        self,
        key: str,
        value: Any,
        namespace: str = "default",
        ttl_seconds: Optional[float] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> StateEntry:
        with self._lock:
            if namespace not in self._store:
                self._store[namespace] = {}

            now = _utcnow()
            existing = self._store[namespace].get(key)

            entry = StateEntry(
                key=key,
                value=value,
                namespace=namespace,
                created_at=existing.created_at if existing else now,
                updated_at=now,
                expires_at=now + timedelta(seconds=ttl_seconds) if ttl_seconds else None,
                version=(existing.version + 1) if existing else 1,
                metadata=metadata or {},
            )

            self._store[namespace][key] = entry
            return entry

    def delete(self, key: str, namespace: str = "default") -> bool:
        with self._lock:
            ns_store = self._store.get(namespace, {})
            if key in ns_store:
                del ns_store[key]
                return True
            return False

    def exists(self, key: str, namespace: str = "default") -> bool:
        entry = self.get(key, namespace)
        return entry is not None

    def keys(self, namespace: str = "default", pattern: str = "*") -> List[str]:
        with self._lock:
            ns_store = self._store.get(namespace, {})

            # Simple wildcard matching
            if pattern == "*":
                keys = list(ns_store.keys())
            elif pattern.endswith("*"):
                prefix = pattern[:-1]
                keys = [k for k in ns_store.keys() if k.startswith(prefix)]
            elif pattern.startswith("*"):
                suffix = pattern[1:]
                keys = [k for k in ns_store.keys() if k.endswith(suffix)]
            else:
                keys = [k for k in ns_store.keys() if k == pattern]

            # Filter out expired
            valid_keys = []
            for key in keys:
                entry = ns_store.get(key)
                if entry and not entry.is_expired:
                    valid_keys.append(key)

            return valid_keys

    def clear(self, namespace: str = "default") -> int:
        with self._lock:
            ns_store = self._store.get(namespace, {})
            count = len(ns_store)
            self._store[namespace] = {}
            return count

    def cleanup_expired(self) -> int:
        with self._lock:
            count = 0
            for namespace in list(self._store.keys()):
                ns_store = self._store[namespace]
                expired_keys = [k for k, v in ns_store.items() if v.is_expired]
                for key in expired_keys:
                    del ns_store[key]
                    count += 1
            return count

    def get_stats(self) -> dict:
        """Get statistics about the store."""
        with self._lock:
            total_entries = sum(len(ns) for ns in self._store.values())
            return {
                "backend": "memory",
                "namespaces": len(self._store),
                "total_entries": total_entries,
                "entries_by_namespace": {ns: len(entries) for ns, entries in self._store.items()},
            }


# ============================================================
# SQLITE BACKEND
# ============================================================


class SQLiteStateBackend(StateBackend):
    """
    SQLite-based persistent state backend.

    Features:
    - Durable storage survives restarts
    - TTL-based expiration
    - Namespace isolation
    - Version tracking for optimistic concurrency
    - Automatic schema migration

    Args:
        db_path: Path to SQLite database file
        auto_cleanup_interval: Seconds between automatic cleanup (0 = disabled)
        wal_mode: Enable WAL mode for better concurrency
    """

    SCHEMA_VERSION = 1

    def __init__(
        self,
        db_path: Union[str, Path] = "familiar_state.db",
        auto_cleanup_interval: float = 300.0,  # 5 minutes
        wal_mode: bool = True,
    ):
        self.db_path = Path(db_path)
        self.wal_mode = wal_mode
        self._local = threading.local()
        self._cleanup_interval = auto_cleanup_interval
        self._last_cleanup = 0.0
        self._lock = threading.RLock()

        # Initialize database
        self._init_database()

        logger.info(f"SQLiteStateBackend initialized at {self.db_path}")

    def _get_connection(self) -> sqlite3.Connection:
        """Get thread-local database connection."""
        if not hasattr(self._local, "connection") or self._local.connection is None:
            self._local.connection = sqlite3.connect(str(self.db_path), timeout=30.0)
            self._local.connection.row_factory = sqlite3.Row

            if self.wal_mode:
                self._local.connection.execute("PRAGMA journal_mode=WAL")

            self._local.connection.execute("PRAGMA foreign_keys=ON")

        return self._local.connection

    @contextmanager
    def _transaction(self):
        """Context manager for transactions."""
        conn = self._get_connection()
        try:
            yield conn
            conn.commit()
        except Exception:
            conn.rollback()
            raise

    def _init_database(self):
        """Initialize database schema."""
        with self._transaction() as conn:
            # Main state table
            conn.execute("""
                CREATE TABLE IF NOT EXISTS state_entries (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    key TEXT NOT NULL,
                    namespace TEXT NOT NULL DEFAULT 'default',
                    value TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL,
                    expires_at TEXT,
                    version INTEGER NOT NULL DEFAULT 1,
                    metadata TEXT,
                    UNIQUE(namespace, key)
                )
            """)

            # Checkpoints table for workflow recovery
            conn.execute("""
                CREATE TABLE IF NOT EXISTS state_checkpoints (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    checkpoint_id TEXT NOT NULL UNIQUE,
                    namespace TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    description TEXT,
                    state_snapshot TEXT NOT NULL
                )
            """)

            # Schema version tracking
            conn.execute("""
                CREATE TABLE IF NOT EXISTS schema_version (
                    version INTEGER PRIMARY KEY
                )
            """)

            # Indexes for performance
            conn.execute("""
                CREATE INDEX IF NOT EXISTS idx_state_namespace
                ON state_entries(namespace)
            """)
            conn.execute("""
                CREATE INDEX IF NOT EXISTS idx_state_expires
                ON state_entries(expires_at)
                WHERE expires_at IS NOT NULL
            """)
            conn.execute("""
                CREATE INDEX IF NOT EXISTS idx_checkpoint_namespace
                ON state_checkpoints(namespace)
            """)

            # Set schema version
            conn.execute(
                """
                INSERT OR REPLACE INTO schema_version (version) VALUES (?)
            """,
                (self.SCHEMA_VERSION,),
            )

    def _maybe_cleanup(self):
        """Run cleanup if interval has elapsed."""
        if self._cleanup_interval <= 0:
            return

        now = time.time()
        if now - self._last_cleanup > self._cleanup_interval:
            self.cleanup_expired()
            self._last_cleanup = now

    def get(self, key: str, namespace: str = "default") -> Optional[StateEntry]:
        self._maybe_cleanup()

        with self._transaction() as conn:
            row = conn.execute(
                """
                SELECT key, namespace, value, created_at, updated_at,
                       expires_at, version, metadata
                FROM state_entries
                WHERE namespace = ? AND key = ?
            """,
                (namespace, key),
            ).fetchone()

            if row is None:
                return None

            # Check expiration
            if row["expires_at"]:
                expires_at = datetime.fromisoformat(row["expires_at"])
                if _utcnow() > expires_at:
                    conn.execute(
                        """
                        DELETE FROM state_entries
                        WHERE namespace = ? AND key = ?
                    """,
                        (namespace, key),
                    )
                    return None

            return StateEntry(
                key=row["key"],
                value=StateSerializer.deserialize(row["value"]),
                namespace=row["namespace"],
                created_at=datetime.fromisoformat(row["created_at"]),
                updated_at=datetime.fromisoformat(row["updated_at"]),
                expires_at=datetime.fromisoformat(row["expires_at"]) if row["expires_at"] else None,
                version=row["version"],
                metadata=json.loads(row["metadata"]) if row["metadata"] else {},
            )

    def set(
        self,
        key: str,
        value: Any,
        namespace: str = "default",
        ttl_seconds: Optional[float] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> StateEntry:
        now = _utcnow()
        expires_at = now + timedelta(seconds=ttl_seconds) if ttl_seconds else None

        with self._transaction() as conn:
            # Check for existing entry
            existing = conn.execute(
                """
                SELECT version, created_at FROM state_entries
                WHERE namespace = ? AND key = ?
            """,
                (namespace, key),
            ).fetchone()

            version = (existing["version"] + 1) if existing else 1
            created_at = datetime.fromisoformat(existing["created_at"]) if existing else now

            # Upsert
            conn.execute(
                """
                INSERT INTO state_entries
                    (key, namespace, value, created_at, updated_at, expires_at, version, metadata)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                ON CONFLICT(namespace, key) DO UPDATE SET
                    value = excluded.value,
                    updated_at = excluded.updated_at,
                    expires_at = excluded.expires_at,
                    version = excluded.version,
                    metadata = excluded.metadata
            """,
                (
                    key,
                    namespace,
                    StateSerializer.serialize(value),
                    created_at.isoformat(),
                    now.isoformat(),
                    expires_at.isoformat() if expires_at else None,
                    version,
                    json.dumps(metadata) if metadata else None,
                ),
            )

            return StateEntry(
                key=key,
                value=value,
                namespace=namespace,
                created_at=created_at,
                updated_at=now,
                expires_at=expires_at,
                version=version,
                metadata=metadata or {},
            )

    def delete(self, key: str, namespace: str = "default") -> bool:
        with self._transaction() as conn:
            cursor = conn.execute(
                """
                DELETE FROM state_entries
                WHERE namespace = ? AND key = ?
            """,
                (namespace, key),
            )
            return cursor.rowcount > 0

    def exists(self, key: str, namespace: str = "default") -> bool:
        entry = self.get(key, namespace)
        return entry is not None

    def keys(self, namespace: str = "default", pattern: str = "*") -> List[str]:
        self._maybe_cleanup()

        with self._transaction() as conn:
            now = _utcnow().isoformat()

            if pattern == "*":
                rows = conn.execute(
                    """
                    SELECT key FROM state_entries
                    WHERE namespace = ?
                    AND (expires_at IS NULL OR expires_at > ?)
                """,
                    (namespace, now),
                ).fetchall()
            elif pattern.endswith("*"):
                prefix = pattern[:-1]
                rows = conn.execute(
                    """
                    SELECT key FROM state_entries
                    WHERE namespace = ?
                    AND key LIKE ?
                    AND (expires_at IS NULL OR expires_at > ?)
                """,
                    (namespace, f"{prefix}%", now),
                ).fetchall()
            elif pattern.startswith("*"):
                suffix = pattern[1:]
                rows = conn.execute(
                    """
                    SELECT key FROM state_entries
                    WHERE namespace = ?
                    AND key LIKE ?
                    AND (expires_at IS NULL OR expires_at > ?)
                """,
                    (namespace, f"%{suffix}", now),
                ).fetchall()
            else:
                rows = conn.execute(
                    """
                    SELECT key FROM state_entries
                    WHERE namespace = ?
                    AND key = ?
                    AND (expires_at IS NULL OR expires_at > ?)
                """,
                    (namespace, pattern, now),
                ).fetchall()

            return [row["key"] for row in rows]

    def clear(self, namespace: str = "default") -> int:
        with self._transaction() as conn:
            cursor = conn.execute(
                """
                DELETE FROM state_entries WHERE namespace = ?
            """,
                (namespace,),
            )
            return cursor.rowcount

    def cleanup_expired(self) -> int:
        with self._lock:
            now = _utcnow().isoformat()
            with self._transaction() as conn:
                cursor = conn.execute(
                    """
                    DELETE FROM state_entries
                    WHERE expires_at IS NOT NULL AND expires_at < ?
                """,
                    (now,),
                )
                count = cursor.rowcount
                if count > 0:
                    logger.debug(f"Cleaned up {count} expired state entries")
                return count

    # ============================================================
    # CHECKPOINTING
    # ============================================================

    def create_checkpoint(self, namespace: str = "default", description: str = "") -> str:
        """
        Create a checkpoint of the current state.

        Returns checkpoint_id for later restoration.
        """
        checkpoint_id = str(uuid.uuid4())
        now = _utcnow()

        with self._transaction() as conn:
            # Get all non-expired entries in namespace
            entries = conn.execute(
                """
                SELECT key, value, created_at, updated_at, expires_at, version, metadata
                FROM state_entries
                WHERE namespace = ?
                AND (expires_at IS NULL OR expires_at > ?)
            """,
                (namespace, now.isoformat()),
            ).fetchall()

            # Serialize snapshot
            snapshot = {
                row["key"]: {
                    "value": row["value"],
                    "created_at": row["created_at"],
                    "updated_at": row["updated_at"],
                    "expires_at": row["expires_at"],
                    "version": row["version"],
                    "metadata": row["metadata"],
                }
                for row in entries
            }

            conn.execute(
                """
                INSERT INTO state_checkpoints
                    (checkpoint_id, namespace, created_at, description, state_snapshot)
                VALUES (?, ?, ?, ?, ?)
            """,
                (checkpoint_id, namespace, now.isoformat(), description, json.dumps(snapshot)),
            )

            logger.info(f"Created checkpoint {checkpoint_id} with {len(snapshot)} entries")
            return checkpoint_id

    def restore_checkpoint(self, checkpoint_id: str) -> bool:
        """
        Restore state from a checkpoint.

        Returns True if successful, False if checkpoint not found.
        """
        with self._transaction() as conn:
            row = conn.execute(
                """
                SELECT namespace, state_snapshot FROM state_checkpoints
                WHERE checkpoint_id = ?
            """,
                (checkpoint_id,),
            ).fetchone()

            if row is None:
                logger.warning(f"Checkpoint {checkpoint_id} not found")
                return False

            namespace = row["namespace"]
            snapshot = json.loads(row["state_snapshot"])

            # Clear current state
            conn.execute(
                """
                DELETE FROM state_entries WHERE namespace = ?
            """,
                (namespace,),
            )

            # Restore entries
            for key, data in snapshot.items():
                conn.execute(
                    """
                    INSERT INTO state_entries
                        (key, namespace, value, created_at, updated_at,
                         expires_at, version, metadata)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """,
                    (
                        key,
                        namespace,
                        data["value"],
                        data["created_at"],
                        data["updated_at"],
                        data["expires_at"],
                        data["version"],
                        data["metadata"],
                    ),
                )

            logger.info(f"Restored checkpoint {checkpoint_id} with {len(snapshot)} entries")
            return True

    def list_checkpoints(self, namespace: str = "default", limit: int = 100) -> List[dict]:
        """List checkpoints for a namespace."""
        with self._transaction() as conn:
            rows = conn.execute(
                """
                SELECT checkpoint_id, created_at, description,
                       json_array_length(state_snapshot) as entry_count
                FROM state_checkpoints
                WHERE namespace = ?
                ORDER BY created_at DESC
                LIMIT ?
            """,
                (namespace, limit),
            ).fetchall()

            return [
                {
                    "checkpoint_id": row["checkpoint_id"],
                    "created_at": row["created_at"],
                    "description": row["description"],
                    "entry_count": row["entry_count"],
                }
                for row in rows
            ]

    def delete_checkpoint(self, checkpoint_id: str) -> bool:
        """Delete a checkpoint."""
        with self._transaction() as conn:
            cursor = conn.execute(
                """
                DELETE FROM state_checkpoints WHERE checkpoint_id = ?
            """,
                (checkpoint_id,),
            )
            return cursor.rowcount > 0

    def cleanup_old_checkpoints(self, namespace: str = "default", keep_count: int = 10) -> int:
        """Keep only the most recent N checkpoints."""
        with self._transaction() as conn:
            # Get checkpoint IDs to keep
            keep_rows = conn.execute(
                """
                SELECT checkpoint_id FROM state_checkpoints
                WHERE namespace = ?
                ORDER BY created_at DESC
                LIMIT ?
            """,
                (namespace, keep_count),
            ).fetchall()

            keep_ids = [row["checkpoint_id"] for row in keep_rows]

            if not keep_ids:
                return 0

            # Delete older checkpoints
            placeholders = ",".join("?" * len(keep_ids))
            cursor = conn.execute(
                f"""
                DELETE FROM state_checkpoints
                WHERE namespace = ?
                AND checkpoint_id NOT IN ({placeholders})
            """,
                [namespace] + keep_ids,
            )

            return cursor.rowcount

    # ============================================================
    # UTILITIES
    # ============================================================

    def get_stats(self) -> dict:
        """Get statistics about the database."""
        with self._transaction() as conn:
            # Entry counts
            entry_count = conn.execute("""
                SELECT COUNT(*) as count FROM state_entries
            """).fetchone()["count"]

            namespace_stats = conn.execute("""
                SELECT namespace, COUNT(*) as count
                FROM state_entries
                GROUP BY namespace
            """).fetchall()

            checkpoint_count = conn.execute("""
                SELECT COUNT(*) as count FROM state_checkpoints
            """).fetchone()["count"]

            # Database size
            page_count = conn.execute("PRAGMA page_count").fetchone()[0]
            page_size = conn.execute("PRAGMA page_size").fetchone()[0]
            db_size_bytes = page_count * page_size

            return {
                "backend": "sqlite",
                "db_path": str(self.db_path),
                "db_size_bytes": db_size_bytes,
                "db_size_mb": round(db_size_bytes / (1024 * 1024), 2),
                "total_entries": entry_count,
                "entries_by_namespace": {row["namespace"]: row["count"] for row in namespace_stats},
                "checkpoint_count": checkpoint_count,
                "schema_version": self.SCHEMA_VERSION,
            }

    def vacuum(self):
        """Reclaim unused space in the database."""
        conn = self._get_connection()
        conn.execute("VACUUM")
        logger.info("Database vacuumed")

    def close(self):
        """Close the database connection."""
        if hasattr(self._local, "connection") and self._local.connection:
            self._local.connection.close()
            self._local.connection = None


# ============================================================
# PERSISTENT SHARED STATE
# ============================================================


class PersistentSharedState:
    """
    Thread-safe shared state with pluggable backend.

    Drop-in replacement for the original SharedState class
    with added persistence and TTL support.

    Args:
        backend: Storage backend (InMemoryStateBackend or SQLiteStateBackend)
        namespace: Default namespace for operations
    """

    def __init__(self, backend: Optional[StateBackend] = None, namespace: str = "default"):
        self._backend = backend or InMemoryStateBackend()
        self._namespace = namespace
        self._lock = threading.RLock()
        self._watchers: Dict[str, List[Callable]] = {}

    @property
    def backend(self) -> StateBackend:
        """Get the storage backend."""
        return self._backend

    @property
    def namespace(self) -> str:
        """Get the default namespace."""
        return self._namespace

    def get(self, key: str, default: Any = None) -> Any:
        """Get a value from shared state."""
        entry = self._backend.get(key, self._namespace)
        if entry is None:
            return default
        return entry.value

    def get_entry(self, key: str) -> Optional[StateEntry]:
        """Get full entry with metadata."""
        return self._backend.get(key, self._namespace)

    def set(
        self,
        key: str,
        value: Any,
        notify: bool = True,
        ttl_seconds: Optional[float] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ):
        """Set a value in shared state."""
        with self._lock:
            old_entry = self._backend.get(key, self._namespace)
            old_value = old_entry.value if old_entry else None

            self._backend.set(key, value, self._namespace, ttl_seconds, metadata)

            if notify and key in self._watchers:
                for watcher in self._watchers[key]:
                    try:
                        watcher(key, old_value, value)
                    except Exception as e:
                        logger.error(f"State watcher error: {e}")

    def delete(self, key: str):
        """Delete a value from shared state."""
        self._backend.delete(key, self._namespace)

    def exists(self, key: str) -> bool:
        """Check if a key exists."""
        return self._backend.exists(key, self._namespace)

    def watch(self, key: str, callback: Callable):
        """Watch for changes to a key."""
        if key not in self._watchers:
            self._watchers[key] = []
        self._watchers[key].append(callback)

    def unwatch(self, key: str, callback: Callable):
        """Stop watching a key."""
        if key in self._watchers and callback in self._watchers[key]:
            self._watchers[key].remove(callback)

    def keys(self, pattern: str = "*") -> List[str]:
        """Get all keys, optionally filtered by pattern."""
        return self._backend.keys(self._namespace, pattern)

    def clear(self) -> int:
        """Clear all entries in namespace."""
        return self._backend.clear(self._namespace)

    def to_dict(self) -> dict:
        """Export state as dict (for debugging)."""
        result = {}
        for key in self.keys():
            entry = self._backend.get(key, self._namespace)
            if entry:
                result[key] = entry.value
        return result

    # TTL operations
    def set_with_ttl(self, key: str, value: Any, ttl_seconds: float):
        """Set a value with TTL."""
        self.set(key, value, ttl_seconds=ttl_seconds)

    def get_ttl(self, key: str) -> Optional[float]:
        """Get remaining TTL for a key."""
        entry = self._backend.get(key, self._namespace)
        if entry:
            return entry.ttl_seconds
        return None

    # Checkpointing (only available with SQLite backend)
    def create_checkpoint(self, description: str = "") -> Optional[str]:
        """Create a checkpoint of current state."""
        if hasattr(self._backend, "create_checkpoint"):
            return self._backend.create_checkpoint(self._namespace, description)
        logger.warning("Checkpointing not supported by current backend")
        return None

    def restore_checkpoint(self, checkpoint_id: str) -> bool:
        """Restore state from a checkpoint."""
        if hasattr(self._backend, "restore_checkpoint"):
            return self._backend.restore_checkpoint(checkpoint_id)
        logger.warning("Checkpointing not supported by current backend")
        return False

    def list_checkpoints(self, limit: int = 100) -> List[dict]:
        """List available checkpoints."""
        if hasattr(self._backend, "list_checkpoints"):
            return self._backend.list_checkpoints(self._namespace, limit)
        return []


# ============================================================
# FACTORY FUNCTIONS
# ============================================================

_default_backend: Optional[StateBackend] = None


def get_state_backend() -> StateBackend:
    """Get the default state backend."""
    global _default_backend
    if _default_backend is None:
        _default_backend = InMemoryStateBackend()
    return _default_backend


def set_state_backend(backend: StateBackend):
    """Set the default state backend."""
    global _default_backend
    _default_backend = backend


def create_sqlite_backend(
    db_path: Union[str, Path] = "familiar_state.db",
    auto_cleanup_interval: float = 300.0,
    wal_mode: bool = True,
) -> SQLiteStateBackend:
    """Create and optionally set as default a SQLite backend."""
    backend = SQLiteStateBackend(
        db_path=db_path, auto_cleanup_interval=auto_cleanup_interval, wal_mode=wal_mode
    )
    return backend


def create_persistent_state(
    db_path: Optional[Union[str, Path]] = None, namespace: str = "default"
) -> PersistentSharedState:
    """
    Create a PersistentSharedState with appropriate backend.

    If db_path is provided, uses SQLite. Otherwise uses in-memory.
    """
    if db_path:
        backend = SQLiteStateBackend(db_path=db_path)
    else:
        backend = InMemoryStateBackend()

    return PersistentSharedState(backend=backend, namespace=namespace)


# ============================================================
# EXPORTS
# ============================================================

__all__ = [
    # Core classes
    "StateEntry",
    "StateBackend",
    "StateSerializer",
    # Backends
    "InMemoryStateBackend",
    "SQLiteStateBackend",
    # Shared state
    "PersistentSharedState",
    # Factory functions
    "get_state_backend",
    "set_state_backend",
    "create_sqlite_backend",
    "create_persistent_state",
]
