"""Database resource handler for CleverAgents.

Resolves database resources (``postgres``, ``mysql``, ``sqlite``,
``duckdb``) into sandbox-backed :class:`BoundResource` instances
using the ``transaction_rollback`` sandbox strategy.

Each database type defines its own connection arguments (host, port,
database, user, password for networked; path for file-based).
Authentication can be supplied directly in config or via environment
variables.

Connection validation tests connectivity and returns safe error
messages that mask credentials using the shared
:mod:`cleveragents.shared.redaction` module.

Content CRUD operations (issue #827, #1241):

- ``read``  — SQLite: query ``sqlite_master`` schema; remote: connection-info summary
- ``write`` — SQLite: execute SQL statement; remote: not-supported result
- ``delete`` — SQLite: ``DROP TABLE IF EXISTS``; remote: not-supported result
- ``list_children`` — SQLite: list tables/views from ``sqlite_master``
- ``diff``  — compare schemas via content hash

Checkpoint methods (issue #836, #1241):

- ``create_checkpoint`` — SQLite: ``SAVEPOINT``; remote: content hash fallback
- ``rollback_to``  — SQLite: ``ROLLBACK TO SAVEPOINT``; remote: not-supported

Based on:
    - implementation_plan.md group M7.post-resource-db
    - Built-in type definitions for database resources
    - Issue #1241 — DatabaseResourceHandler CRUD and checkpoint methods
"""

from __future__ import annotations

import hashlib
import logging
import os
import sqlite3
from typing import Any

from cleveragents.domain.models.core.resource import Resource, SandboxStrategy
from cleveragents.infrastructure.sandbox.manager import SandboxManager
from cleveragents.resource.handlers._base import EMPTY_CONTENT_HASH, BaseResourceHandler
from cleveragents.resource.handlers.protocol import (
    CheckpointResult,
    Content,
    DeleteResult,
    DiffResult,
    RollbackResult,
    WriteResult,
)
from cleveragents.shared.redaction import mask_database_url, redact_dict

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Built-in database resource type definitions
# ---------------------------------------------------------------------------

POSTGRES_TYPE_DEF: dict[str, Any] = {
    "name": "postgres",
    "description": "PostgreSQL database connection.",
    "resource_kind": "physical",
    "sandbox_strategy": "transaction_rollback",
    "user_addable": True,
    "built_in": True,
    "cli_args": [
        {
            "name": "connection-string",
            "type": "string",
            "required": False,
            "description": (
                "Full connection string "
                "(e.g. postgresql://user:pass@host:5432/db). "
                "Overrides individual args."
            ),
            "default": None,
        },
        {
            "name": "host",
            "type": "string",
            "required": False,
            "description": "Database server hostname.",
            "default": "localhost",
        },
        {
            "name": "port",
            "type": "integer",
            "required": False,
            "description": "Database server port.",
            "default": 5432,
        },
        {
            "name": "dbname",
            "type": "string",
            "required": False,
            "description": "Database name.",
            "default": None,
        },
        {
            "name": "user",
            "type": "string",
            "required": False,
            "description": (
                "Database user. Can also be set via CLEVERAGENTS_DB_USER env var."
            ),
            "default": None,
        },
        {
            "name": "password",
            "type": "string",
            "required": False,
            "description": (
                "Database password. Can also be set via "
                "CLEVERAGENTS_DB_PASSWORD env var."
            ),
            "default": None,
        },
    ],
    "parent_types": [],
    "child_types": [],
    "handler": "cleveragents.resource.handlers.database:DatabaseResourceHandler",
    "capabilities": {
        "read": True,
        "write": True,
        "sandbox": True,
        "checkpoint": False,
    },
}

MYSQL_TYPE_DEF: dict[str, Any] = {
    "name": "mysql",
    "description": "MySQL database connection.",
    "resource_kind": "physical",
    "sandbox_strategy": "transaction_rollback",
    "user_addable": True,
    "built_in": True,
    "cli_args": [
        {
            "name": "connection-string",
            "type": "string",
            "required": False,
            "description": (
                "Full connection string "
                "(e.g. mysql://user:pass@host:3306/db). "
                "Overrides individual args."
            ),
            "default": None,
        },
        {
            "name": "host",
            "type": "string",
            "required": False,
            "description": "Database server hostname.",
            "default": "localhost",
        },
        {
            "name": "port",
            "type": "integer",
            "required": False,
            "description": "Database server port.",
            "default": 3306,
        },
        {
            "name": "dbname",
            "type": "string",
            "required": False,
            "description": "Database name.",
            "default": None,
        },
        {
            "name": "user",
            "type": "string",
            "required": False,
            "description": (
                "Database user. Can also be set via CLEVERAGENTS_DB_USER env var."
            ),
            "default": None,
        },
        {
            "name": "password",
            "type": "string",
            "required": False,
            "description": (
                "Database password. Can also be set via "
                "CLEVERAGENTS_DB_PASSWORD env var."
            ),
            "default": None,
        },
    ],
    "parent_types": [],
    "child_types": [],
    "handler": "cleveragents.resource.handlers.database:DatabaseResourceHandler",
    "capabilities": {
        "read": True,
        "write": True,
        "sandbox": True,
        "checkpoint": False,
    },
}

SQLITE_TYPE_DEF: dict[str, Any] = {
    "name": "sqlite",
    "description": "SQLite database file.",
    "resource_kind": "physical",
    "sandbox_strategy": "transaction_rollback",
    "user_addable": True,
    "built_in": True,
    "cli_args": [
        {
            "name": "path",
            "type": "path",
            "required": True,
            "description": (
                "Path to the SQLite database file (use :memory: for in-memory)."
            ),
        },
    ],
    "parent_types": ["fs-directory", "git-checkout"],
    "child_types": [],
    "handler": "cleveragents.resource.handlers.database:DatabaseResourceHandler",
    "capabilities": {
        "read": True,
        "write": True,
        "sandbox": True,
        "checkpoint": False,
    },
}

DUCKDB_TYPE_DEF: dict[str, Any] = {
    "name": "duckdb",
    "description": "DuckDB database (file-based or in-memory).",
    "resource_kind": "physical",
    "sandbox_strategy": "transaction_rollback",
    "user_addable": True,
    "built_in": True,
    "cli_args": [
        {
            "name": "path",
            "type": "path",
            "required": False,
            "description": (
                "Path to the DuckDB database file. Omit or use :memory: for in-memory."
            ),
            "default": ":memory:",
        },
    ],
    "parent_types": ["fs-directory", "git-checkout"],
    "child_types": [],
    "handler": "cleveragents.resource.handlers.database:DatabaseResourceHandler",
    "capabilities": {
        "read": True,
        "write": True,
        "sandbox": True,
        "checkpoint": False,
    },
}

#: All database type definitions in registration order.
DATABASE_TYPE_DEFS: list[dict[str, Any]] = [
    POSTGRES_TYPE_DEF,
    MYSQL_TYPE_DEF,
    SQLITE_TYPE_DEF,
    DUCKDB_TYPE_DEF,
]

#: Database type names.
DATABASE_TYPE_NAMES: frozenset[str] = frozenset(
    {"postgres", "mysql", "sqlite", "duckdb"}
)


# ---------------------------------------------------------------------------
# Connection argument resolution
# ---------------------------------------------------------------------------


def resolve_connection_args(
    properties: dict[str, str | int | float | bool | None],
    resource_type: str,
) -> dict[str, str | int | None]:
    """Resolve connection arguments from properties and env vars.

    For networked databases (postgres, mysql), credentials can come
    from environment variables ``CLEVERAGENTS_DB_USER`` and
    ``CLEVERAGENTS_DB_PASSWORD`` when not set in properties.

    Args:
        properties: Resource properties dict.
        resource_type: The database resource type name.

    Returns:
        Resolved connection arguments.
    """
    args: dict[str, str | int | None] = {}

    if resource_type in ("postgres", "mysql"):
        conn_str = properties.get("connection-string")
        if conn_str is not None:
            args["connection-string"] = str(conn_str)
            return args

        args["host"] = str(properties.get("host", "localhost") or "localhost")
        default_port = 5432 if resource_type == "postgres" else 3306
        raw_port = properties.get("port", default_port)
        args["port"] = int(raw_port) if raw_port is not None else default_port
        args["dbname"] = str(properties["dbname"]) if properties.get("dbname") else None

        # User from properties or env
        user = properties.get("user")
        if user is None:
            user = os.environ.get("CLEVERAGENTS_DB_USER")
        args["user"] = str(user) if user else None

        # Password from properties or env
        password = properties.get("password")
        if password is None:
            password = os.environ.get("CLEVERAGENTS_DB_PASSWORD")
        args["password"] = str(password) if password else None

    elif resource_type in ("sqlite", "duckdb"):
        path = properties.get("path", ":memory:")
        args["path"] = str(path) if path is not None else ":memory:"

    return args


# ---------------------------------------------------------------------------
# Connection validation
# ---------------------------------------------------------------------------


def validate_connection(
    resource_type: str,
    connection_args: dict[str, str | int | None],
) -> tuple[bool, str]:
    """Test database connectivity and return a safe status message.

    Only SQLite is fully implemented (uses stdlib ``sqlite3``).
    PostgreSQL, MySQL, and DuckDB return stub messages indicating
    the driver is not available.

    Error messages mask credentials using the shared redaction module.

    Args:
        resource_type: Database type name.
        connection_args: Resolved connection arguments.

    Returns:
        Tuple of ``(success, message)``.
    """
    if resource_type == "sqlite":
        return _validate_sqlite(connection_args)
    if resource_type == "duckdb":
        return _validate_duckdb(connection_args)
    if resource_type in ("postgres", "mysql"):
        return _validate_network_db(resource_type, connection_args)

    return (False, f"Unknown database type: {resource_type}")


def _validate_sqlite(
    args: dict[str, str | int | None],
) -> tuple[bool, str]:
    """Validate an SQLite connection."""
    path = str(args.get("path", ":memory:") or ":memory:")
    try:
        conn = sqlite3.connect(path)
        conn.execute("SELECT 1")
        conn.close()
        return (True, f"SQLite connection OK: {path}")
    except Exception as exc:
        return (False, f"SQLite connection failed: {exc}")


def _validate_duckdb(
    args: dict[str, str | int | None],
) -> tuple[bool, str]:
    """Validate a DuckDB connection (stub -- driver not bundled)."""
    path = str(args.get("path", ":memory:") or ":memory:")
    try:
        import importlib

        _duckdb = importlib.import_module("duckdb")
        conn = _duckdb.connect(path)
        conn.execute("SELECT 1")
        conn.close()
        return (True, f"DuckDB connection OK: {path}")
    except (ImportError, ModuleNotFoundError):
        return (
            True,
            f"DuckDB driver not installed; config accepted for: {path}",
        )
    except Exception as exc:
        return (False, f"DuckDB connection failed: {exc}")


def _validate_network_db(
    resource_type: str,
    args: dict[str, str | int | None],
) -> tuple[bool, str]:
    """Validate a networked DB connection (stub -- drivers not bundled).

    Credential values are redacted in the returned message.
    """
    safe_args = redact_dict(
        {k: v for k, v in args.items() if v is not None},
    )
    conn_str = args.get("connection-string")
    if conn_str is not None:
        masked = mask_database_url(str(conn_str))
        return (
            True,
            (f"{resource_type} driver not installed; config accepted for: {masked}"),
        )

    return (
        True,
        (f"{resource_type} driver not installed; config accepted: {safe_args}"),
    )


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _is_sqlite_resource(resource: Resource) -> bool:
    """Return True if the resource is a local SQLite database file."""
    if not resource.location:
        return False
    loc = resource.location
    # In-memory SQLite
    if loc == ":memory:":
        return True
    # File-based SQLite (by extension or by resource type name)
    if resource.resource_type_name == "sqlite":
        return True
    return loc.endswith((".db", ".sqlite", ".sqlite3"))


def _open_sqlite(location: str) -> sqlite3.Connection:
    """Open an SQLite connection to *location* (read-write)."""
    return sqlite3.connect(location)


# ---------------------------------------------------------------------------
# DatabaseResourceHandler
# ---------------------------------------------------------------------------


class DatabaseResourceHandler(BaseResourceHandler):
    """Handler for database resource types.

    Provisions a transaction-rollback sandbox for database resources.
    Supports ``postgres``, ``mysql``, ``sqlite``, and ``duckdb``.

    Content CRUD (issue #1241):

    - ``read``  — SQLite: query ``sqlite_master`` schema; remote: connection-info
      summary
    - ``write`` — SQLite: execute SQL statement; remote: not-supported result
    - ``delete`` — SQLite: ``DROP TABLE IF EXISTS``; remote: not-supported result
    - ``list_children`` — SQLite: list tables/views from ``sqlite_master``
    - ``diff``  — compare schemas via content hash

    Checkpoint methods (issue #1241):

    - ``create_checkpoint`` — SQLite: ``SAVEPOINT``; remote: content hash fallback
    - ``rollback_to``  — SQLite: ``ROLLBACK TO SAVEPOINT``; remote: not-supported
    """

    _default_strategy = SandboxStrategy.TRANSACTION_ROLLBACK
    _type_label = "database"

    def __init__(self) -> None:
        # Maps checkpoint_id -> (connection, savepoint_name) for SQLite SAVEPOINTs.
        # Each checkpoint holds an open connection with an active savepoint.
        self._sqlite_checkpoints: dict[str, tuple[sqlite3.Connection, str]] = {}

    # -- Content CRUD (issue #1241) ----------------------------------------

    def read(self, *, resource: Resource, path: str = "") -> Content:
        """Read content from a database resource.

        For SQLite databases, queries ``sqlite_master`` and returns a
        human-readable schema summary (table names and their DDL).

        For remote databases (postgres, mysql, duckdb), returns a
        connection-info summary using the resource location.

        Args:
            resource: The database resource.
            path: Ignored for database resources (schema is always read).

        Returns:
            A :class:`Content` with the schema or connection-info bytes.
        """
        if not resource.location:
            return Content(
                data=b"",
                encoding="utf-8",
                metadata={"source": "empty"},
            )

        if _is_sqlite_resource(resource):
            return self._read_sqlite(resource)

        # Remote database: return connection-info summary
        return self._read_remote(resource)

    def _read_sqlite(self, resource: Resource) -> Content:
        """Read SQLite schema from sqlite_master."""
        location = resource.location or ":memory:"
        try:
            conn = _open_sqlite(location)
            try:
                cursor = conn.execute(
                    "SELECT type, name, sql FROM sqlite_master "
                    "WHERE type IN ('table', 'view', 'index') "
                    "ORDER BY type, name"
                )
                rows = cursor.fetchall()
                lines: list[str] = []
                for obj_type, name, sql in rows:
                    lines.append(f"-- {obj_type}: {name}")
                    if sql:
                        lines.append(sql)
                    lines.append("")
                schema_text = "\n".join(lines)
                data = schema_text.encode("utf-8")
                content_hash = hashlib.sha256(data).hexdigest()
                return Content(
                    data=data,
                    encoding="utf-8",
                    content_hash=content_hash,
                    metadata={
                        "source": "sqlite_master",
                        "location": location,
                        "object_count": str(len(rows)),
                    },
                )
            finally:
                conn.close()
        except sqlite3.Error as exc:
            logger.warning("SQLite read failed for %s: %s", location, exc)
            error_msg = f"SQLite read error: {exc}"
            return Content(
                data=error_msg.encode("utf-8"),
                encoding="utf-8",
                metadata={"error": str(exc), "location": location},
            )

    def _read_remote(self, resource: Resource) -> Content:
        """Return a connection-info summary for remote databases."""
        location = resource.location or ""
        resource_type = resource.resource_type_name
        # Mask any credentials in the location string
        safe_location = mask_database_url(location) if "://" in location else location
        summary = (
            f"Database resource: {resource_type}\n"
            f"Location: {safe_location}\n"
            f"Resource ID: {resource.resource_id}\n"
        )
        data = summary.encode("utf-8")
        return Content(
            data=data,
            encoding="utf-8",
            metadata={
                "source": "connection_info",
                "resource_type": resource_type,
            },
        )

    def write(self, *, resource: Resource, path: str, data: bytes) -> WriteResult:
        """Write to a database resource.

        For SQLite databases, executes the SQL statement provided in
        *data* (decoded as UTF-8).  The *path* parameter is ignored.

        For remote databases, returns a not-supported result.

        Args:
            resource: The database resource.
            path: Ignored for database resources.
            data: SQL statement bytes to execute (SQLite only).

        Returns:
            A :class:`WriteResult` indicating success or not-supported.
        """
        if not resource.location:
            return WriteResult(
                success=False,
                message="Database resource has no location",
            )

        if _is_sqlite_resource(resource):
            return self._write_sqlite(resource, data)

        # Remote database: not supported
        return WriteResult(
            success=False,
            bytes_written=0,
            message=(
                f"write() is not supported for remote database type "
                f"'{resource.resource_type_name}'"
            ),
        )

    def _write_sqlite(self, resource: Resource, data: bytes) -> WriteResult:
        """Execute a SQL statement on an SQLite database."""
        location = resource.location or ":memory:"
        try:
            sql = data.decode("utf-8")
        except UnicodeDecodeError as exc:
            return WriteResult(
                success=False,
                message=f"SQL data is not valid UTF-8: {exc}",
            )

        try:
            # If there is an open checkpoint connection for this resource,
            # reuse it so that the write is within the SAVEPOINT transaction
            # and can be rolled back by rollback_to().
            checkpoint_conn = self._get_checkpoint_conn(location)
            if checkpoint_conn is not None:
                conn = checkpoint_conn
                should_close = False
            else:
                conn = _open_sqlite(location)
                should_close = True
            try:
                conn.execute(sql)
                if should_close:
                    conn.commit()
                return WriteResult(
                    success=True,
                    bytes_written=len(data),
                    message=f"Executed SQL on SQLite database: {location}",
                )
            finally:
                if should_close:
                    conn.close()
        except sqlite3.Error as exc:
            logger.warning("SQLite write failed for %s: %s", location, exc)
            return WriteResult(
                success=False,
                message=f"SQLite execute error: {exc}",
            )

    def _get_checkpoint_conn(self, location: str) -> sqlite3.Connection | None:
        """Return the open checkpoint connection for a given SQLite location, if any."""
        for conn, _savepoint in self._sqlite_checkpoints.values():
            try:
                # Verify the connection is for the same database file
                if hasattr(conn, "execute"):
                    # Check if this connection is for the same location
                    # by querying the database_list pragma
                    rows = conn.execute("PRAGMA database_list").fetchall()
                    for _seq, _name, db_file in rows:
                        if db_file and (
                            db_file == location
                            or db_file.endswith(location.lstrip("/"))
                        ):
                            return conn
            except Exception:
                pass
        return None

    def delete(self, *, resource: Resource, path: str = "") -> DeleteResult:
        """Delete from a database resource.

        For SQLite databases, executes ``DROP TABLE IF EXISTS <path>``
        where *path* is the table name.  If *path* is empty, returns
        a not-supported result (cannot drop the entire database file
        via this method).

        For remote databases, returns a not-supported result.

        Args:
            resource: The database resource.
            path: Table name to drop (SQLite only).

        Returns:
            A :class:`DeleteResult` indicating success or not-supported.
        """
        if not resource.location:
            return DeleteResult(
                success=False,
                message="Database resource has no location",
            )

        if _is_sqlite_resource(resource):
            return self._delete_sqlite(resource, path)

        # Remote database: not supported
        return DeleteResult(
            success=False,
            message=(
                f"delete() is not supported for remote database type "
                f"'{resource.resource_type_name}'"
            ),
        )

    def _delete_sqlite(self, resource: Resource, path: str) -> DeleteResult:
        """Drop a table from an SQLite database."""
        location = resource.location or ":memory:"

        if not path:
            return DeleteResult(
                success=False,
                message=(
                    "Cannot delete the entire SQLite database via delete(); "
                    "provide a table name as path"
                ),
            )

        # Validate table name: only allow simple identifiers (no SQL injection)
        # Use parameterised quoting via sqlite3.
        try:
            conn = _open_sqlite(location)
            try:
                # Use double-quote escaping for the identifier
                safe_name = path.replace('"', '""')
                conn.execute(f'DROP TABLE IF EXISTS "{safe_name}"')
                conn.commit()
                return DeleteResult(
                    success=True,
                    message=f"Dropped table '{path}' from SQLite database: {location}",
                )
            finally:
                conn.close()
        except sqlite3.Error as exc:
            logger.warning("SQLite delete failed for %s: %s", location, exc)
            return DeleteResult(
                success=False,
                message=f"SQLite DROP TABLE error: {exc}",
            )

    def list_children(self, *, resource: Resource) -> list[str]:
        """List tables and views in a database resource.

        For SQLite databases, queries ``sqlite_master`` and returns
        sorted table and view names.

        For remote databases, returns an empty list.

        Args:
            resource: The database resource.

        Returns:
            Sorted list of table/view names (SQLite) or empty list.
        """
        if not resource.location:
            return []

        if _is_sqlite_resource(resource):
            return self._list_children_sqlite(resource)

        # Remote database: cannot list without a live connection
        return []

    def _list_children_sqlite(self, resource: Resource) -> list[str]:
        """List tables and views from sqlite_master."""
        location = resource.location or ":memory:"
        try:
            conn = _open_sqlite(location)
            try:
                cursor = conn.execute(
                    "SELECT name FROM sqlite_master "
                    "WHERE type IN ('table', 'view') "
                    "ORDER BY name"
                )
                return [row[0] for row in cursor.fetchall()]
            finally:
                conn.close()
        except sqlite3.Error as exc:
            logger.warning("SQLite list_children failed for %s: %s", location, exc)
            return []

    def diff(self, *, resource: Resource, other_location: str) -> DiffResult:
        """Compare a database resource against another location.

        Computes the content hash of both the resource and the
        *other_location* (treated as another database path) and
        returns a :class:`DiffResult` indicating whether they differ.

        For SQLite databases, the hash is based on the schema from
        ``sqlite_master``.  For remote databases, the identity hash
        is used.

        Args:
            resource: The database resource.
            other_location: Path or identifier of the other database.

        Returns:
            A :class:`DiffResult` summarising the differences.
        """
        hash_a = self.content_hash(resource)

        # Build a temporary resource for the other location
        other_resource = Resource(
            resource_id=resource.resource_id,
            name=resource.name,
            resource_type_name=resource.resource_type_name,
            classification=resource.classification,
            description=resource.description,
            location=other_location,
            parents=list(resource.parents),
        )
        hash_b = self.content_hash(other_resource)

        has_changes = hash_a != hash_b
        return DiffResult(
            has_changes=has_changes,
            unified_diff=""
            if not has_changes
            else f"--- {resource.location}\n+++ {other_location}\n(schema differs)",
            files_changed=1 if has_changes else 0,
            insertions=0,
            deletions=0,
        )

    # -- Checkpoint and rollback (issue #1241) -----------------------------

    def create_checkpoint(
        self,
        *,
        resource: Resource,
        plan_id: str,
        sandbox_manager: SandboxManager,
        phase: str = "execution",
    ) -> CheckpointResult:
        """Create a checkpoint of the current database state.

        For SQLite databases, creates a ``SAVEPOINT`` on an open
        connection.  The connection is kept open until
        :meth:`rollback_to` or the checkpoint is discarded.

        For remote databases, falls back to a content-hash-based
        checkpoint (records the hash but cannot restore).

        Args:
            resource: The database resource.
            plan_id: The plan requesting the checkpoint.
            sandbox_manager: The sandbox lifecycle manager.
            phase: Lifecycle phase label (e.g. ``"execution"``).

        Returns:
            A :class:`CheckpointResult` with the checkpoint ID.
        """
        from datetime import UTC, datetime

        timestamp = datetime.now(tz=UTC).strftime("%Y%m%dT%H%M%S")
        checkpoint_id = f"dbckpt-{plan_id}-{timestamp}"

        if resource.location and _is_sqlite_resource(resource):
            return self._create_checkpoint_sqlite(resource, plan_id, checkpoint_id)

        # Remote database: content-hash fallback
        content_hash = self.content_hash(resource)
        return CheckpointResult(
            checkpoint_id=checkpoint_id,
            plan_id=plan_id,
            snapshot_path="",
            message=(
                f"Content-hash checkpoint for remote database "
                f"'{resource.resource_type_name}': {content_hash[:16]}..."
            ),
        )

    def _create_checkpoint_sqlite(
        self,
        resource: Resource,
        plan_id: str,
        checkpoint_id: str,
    ) -> CheckpointResult:
        """Create a SQLite SAVEPOINT checkpoint."""
        location = resource.location or ":memory:"
        # Use a safe savepoint name derived from the checkpoint_id
        savepoint_name = "sp_" + checkpoint_id.replace("-", "_")
        try:
            conn = _open_sqlite(location)
            conn.execute(f"SAVEPOINT {savepoint_name}")
            # Store the open connection so rollback_to can use it
            self._sqlite_checkpoints[checkpoint_id] = (conn, savepoint_name)
            return CheckpointResult(
                checkpoint_id=checkpoint_id,
                plan_id=plan_id,
                snapshot_path=location,
                message=(
                    f"Created SQLite SAVEPOINT '{savepoint_name}' "
                    f"on database: {location}"
                ),
            )
        except sqlite3.Error as exc:
            logger.warning("SQLite SAVEPOINT failed for %s: %s", location, exc)
            raise RuntimeError(f"Failed to create SQLite checkpoint: {exc}") from exc

    def rollback_to(
        self,
        *,
        resource: Resource,
        plan_id: str,
        checkpoint_id: str,
        sandbox_manager: SandboxManager,
    ) -> RollbackResult:
        """Rollback a database resource to a prior checkpoint.

        For SQLite databases, executes ``ROLLBACK TO SAVEPOINT`` on
        the connection stored by :meth:`create_checkpoint`, then
        releases the savepoint and closes the connection.

        For remote databases, returns a not-supported result.

        Args:
            resource: The database resource.
            plan_id: The plan requesting the rollback.
            checkpoint_id: The checkpoint ID returned by
                :meth:`create_checkpoint`.
            sandbox_manager: The sandbox lifecycle manager.

        Returns:
            A :class:`RollbackResult` indicating success.
        """
        if resource.location and _is_sqlite_resource(resource):
            return self._rollback_sqlite(checkpoint_id)

        # Remote database: not supported
        return RollbackResult(
            success=False,
            checkpoint_id=checkpoint_id,
            message=(
                f"rollback_to() is not supported for remote database type "
                f"'{resource.resource_type_name}'"
            ),
        )

    def _rollback_sqlite(self, checkpoint_id: str) -> RollbackResult:
        """Execute ROLLBACK TO SAVEPOINT for a SQLite checkpoint."""
        entry = self._sqlite_checkpoints.get(checkpoint_id)
        if entry is None:
            return RollbackResult(
                success=False,
                checkpoint_id=checkpoint_id,
                message=f"Checkpoint '{checkpoint_id}' not found or already released",
            )

        conn, savepoint_name = entry
        try:
            conn.execute(f"ROLLBACK TO SAVEPOINT {savepoint_name}")
            conn.execute(f"RELEASE SAVEPOINT {savepoint_name}")
            conn.commit()
            conn.close()
            del self._sqlite_checkpoints[checkpoint_id]
            return RollbackResult(
                success=True,
                checkpoint_id=checkpoint_id,
                restored_files=0,
                message=(
                    f"Rolled back SQLite database to SAVEPOINT '{savepoint_name}'"
                ),
            )
        except sqlite3.Error as exc:
            logger.warning(
                "SQLite ROLLBACK TO SAVEPOINT failed for %s: %s",
                checkpoint_id,
                exc,
            )
            # Attempt cleanup
            import contextlib

            with contextlib.suppress(Exception):
                conn.close()
            self._sqlite_checkpoints.pop(checkpoint_id, None)
            return RollbackResult(
                success=False,
                checkpoint_id=checkpoint_id,
                message=f"SQLite rollback error: {exc}",
            )

    # -- Content Hashing ---------------------------------------------------

    def content_hash(
        self,
        resource: Resource,
        *,
        algorithm: str = "sha256",
    ) -> str:
        """Compute content hash for database resources.

        Strategy (in priority order):

        1. **Query result hash** — For SQLite databases, executes
           ``SELECT name, sql FROM sqlite_master ORDER BY name``
           and hashes the schema.  For PostgreSQL/MySQL, attempts a
           schema-level checksum query.
        2. **File hash** — For SQLite with a file path, hashes the
           raw database file as fallback.
        3. **Identity hash** — For remote databases where queries
           fail, hashes the connection string as a stable identity.

        Returns :data:`EMPTY_CONTENT_HASH` if no location is set.
        """
        if not resource.location:
            return EMPTY_CONTENT_HASH

        # Strategy 1: query-based hash
        query_hash = self._try_query_hash(resource, algorithm)
        if query_hash is not None:
            return query_hash

        # Strategy 2: file hash (SQLite)
        if os.path.isfile(resource.location):
            h = hashlib.new(algorithm)
            with open(resource.location, "rb") as f:
                for chunk in iter(lambda: f.read(65536), b""):
                    h.update(chunk)
            return h.hexdigest()

        # Strategy 3: identity hash (remote DB fallback)
        h = hashlib.new(algorithm)
        h.update(resource.resource_type_name.encode("utf-8"))
        h.update(b"\0")
        h.update(resource.location.encode("utf-8"))
        return h.hexdigest()

    def _try_query_hash(
        self,
        resource: Resource,
        algorithm: str,
    ) -> str | None:
        """Attempt to hash database content via schema query.

        For SQLite, queries ``sqlite_master`` for the schema.
        For other databases, returns ``None`` (query not supported
        without an active connection pool).
        """
        if not resource.location:
            return None

        # SQLite: query the schema
        if os.path.isfile(resource.location) and resource.location.endswith(
            (".db", ".sqlite", ".sqlite3")
        ):
            try:
                conn = sqlite3.connect(
                    f"file:{resource.location}?mode=ro",
                    uri=True,
                    timeout=5,
                )
                try:
                    cursor = conn.execute(
                        "SELECT name, sql FROM sqlite_master "
                        "WHERE type IN ('table', 'index', 'view') "
                        "ORDER BY name"
                    )
                    h = hashlib.new(algorithm)
                    for name, sql in cursor:
                        h.update((name or "").encode("utf-8"))
                        h.update(b"\0")
                        h.update((sql or "").encode("utf-8"))
                        h.update(b"\n")
                    return h.hexdigest()
                finally:
                    conn.close()
            except (sqlite3.Error, OSError):
                return None

        return None
