"""Persistence layer for Kiln.

Provides durable storage for jobs, events, printers, and settings so that
state survives process restarts.  The database is created automatically at
``~/.kiln/kiln.db`` (override with the ``KILN_DB_PATH`` environment
variable).

The storage layer is backend-agnostic: all database operations flow through
a :class:`StorageBackend` protocol so that the default SQLite implementation
can be swapped for Postgres (or another engine) in multi-node deployments
without touching the higher-level :class:`KilnDB` logic.

The default backend uses only stdlib modules (``sqlite3``, ``json``, ``os``,
``threading``).  An optional :class:`PostgresBackend` is available when
``psycopg2`` is installed (``pip install kiln3d[postgres]``).

Example::

    db = get_db()
    db.save_job({"id": "abc123", "file_name": "benchy.gcode", "status": "queued", ...})
    job = db.get_job("abc123")
"""

from __future__ import annotations

import contextlib
import hashlib
import hmac
import json
import logging
import os
import sqlite3
import stat
import sys
import threading
import time
from collections.abc import Sequence
from pathlib import Path
from typing import Any, Protocol

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Default DB location
# ---------------------------------------------------------------------------

_DEFAULT_DB_DIR = os.path.join(str(Path.home()), ".kiln")
_DEFAULT_DB_PATH = os.path.join(_DEFAULT_DB_DIR, "kiln.db")

# SQL parameter type accepted by both sqlite3 and typical DB-API adapters.
_SqlParams = Sequence[Any] | dict[str, Any]


# ---------------------------------------------------------------------------
# Storage backend abstraction
# ---------------------------------------------------------------------------


class _CursorLike(Protocol):
    """Minimal cursor interface used by :class:`KilnDB`.

    Matches the subset of :pep:`249` (DB-API 2.0) that the persistence layer
    actually relies on.  Both ``sqlite3.Cursor`` and any Postgres cursor
    wrapper satisfy this naturally.
    """

    @property
    def lastrowid(self) -> int | None: ...

    @property
    def rowcount(self) -> int: ...

    def fetchone(self) -> Any | None: ...

    def fetchall(self) -> list[Any]: ...


class StorageBackend(Protocol):
    """Protocol defining the database operations :class:`KilnDB` depends on.

    The default implementation is :class:`SQLiteBackend`.  A future
    ``PostgresBackend`` could implement this same protocol to support
    multi-node / cloud deployments without changing any of the higher-level
    persistence logic.

    All methods mirror a subset of the :pep:`249` DB-API 2.0 connection
    interface, keeping the contract small and easy to satisfy.
    """

    def execute(self, sql: str, parameters: _SqlParams = ..., /) -> _CursorLike:
        """Execute a single SQL statement and return a cursor-like object."""
        ...

    def executemany(self, sql: str, seq_of_parameters: Sequence[_SqlParams], /) -> _CursorLike:
        """Execute a SQL statement against all parameter sequences."""
        ...

    def executescript(self, sql_script: str, /) -> _CursorLike:
        """Execute multiple SQL statements in one call (DDL bootstrap)."""
        ...

    def commit(self) -> None:
        """Commit the current transaction."""
        ...

    def close(self) -> None:
        """Close the underlying connection."""
        ...

    def cursor(self) -> _CursorLike:
        """Return a new cursor object."""
        ...


class SQLiteBackend:
    """Default :class:`StorageBackend` implementation backed by ``sqlite3``.

    Wraps a ``sqlite3.Connection`` and exposes only the methods required by
    the :class:`StorageBackend` protocol.  Construction mirrors the original
    ``KilnDB`` setup (WAL mode, busy timeout, Row factory).
    """

    def __init__(self, db_path: str) -> None:
        self._conn: sqlite3.Connection = sqlite3.connect(
            db_path,
            check_same_thread=False,
        )
        self._conn.row_factory = sqlite3.Row
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._conn.execute("PRAGMA busy_timeout=5000")

    # -- StorageBackend interface ------------------------------------------

    def execute(self, sql: str, parameters: _SqlParams = (), /) -> sqlite3.Cursor:
        return self._conn.execute(sql, parameters)

    def executemany(self, sql: str, seq_of_parameters: Sequence[_SqlParams], /) -> sqlite3.Cursor:
        return self._conn.executemany(sql, seq_of_parameters)

    def executescript(self, sql_script: str, /) -> sqlite3.Cursor:
        return self._conn.executescript(sql_script)

    def commit(self) -> None:
        self._conn.commit()

    def close(self) -> None:
        self._conn.close()

    def cursor(self) -> sqlite3.Cursor:
        return self._conn.cursor()


# ---------------------------------------------------------------------------
# PostgreSQL backend (optional — requires psycopg2)
# ---------------------------------------------------------------------------


class _PgDictCursor:
    """Thin wrapper around a ``psycopg2`` cursor that makes rows behave like
    ``sqlite3.Row`` — i.e. indexable by column name via ``row["col"]``.

    :class:`KilnDB` calls ``dict(row)`` everywhere, so each row must support
    both integer indexing and key-based access.
    """

    def __init__(self, real_cursor: Any) -> None:
        self._cur = real_cursor

    # -- Passthrough properties --
    @property
    def lastrowid(self) -> int | None:
        return self._cur.lastrowid if hasattr(self._cur, "lastrowid") else None

    @property
    def rowcount(self) -> int:
        return self._cur.rowcount

    @property
    def description(self) -> Any:
        return self._cur.description

    # -- Row helpers --

    def _wrap_row(self, row: Any) -> Any:
        """Wrap a raw tuple row into a dict-like object."""
        if row is None or self._cur.description is None:
            return row
        cols = [d[0] for d in self._cur.description]
        return _DictRow(cols, row)

    def fetchone(self) -> Any | None:
        row = self._cur.fetchone()
        return self._wrap_row(row)

    def fetchall(self) -> list[Any]:
        rows = self._cur.fetchall()
        if not rows or self._cur.description is None:
            return rows
        cols = [d[0] for d in self._cur.description]
        return [_DictRow(cols, r) for r in rows]


class _DictRow:
    """Lightweight row that supports both ``row["col"]`` and ``row[0]`` access,
    plus ``dict(row)`` and iteration — matching ``sqlite3.Row`` behaviour.
    """

    __slots__ = ("_cols", "_values")

    def __init__(self, cols: list[str], values: tuple) -> None:
        self._cols = cols
        self._values = values

    def __getitem__(self, key: Any) -> Any:
        if isinstance(key, int):
            return self._values[key]
        try:
            idx = self._cols.index(key)
        except ValueError:
            raise KeyError(key) from None
        return self._values[idx]

    def __iter__(self):
        return iter(self._values)

    def __len__(self) -> int:
        return len(self._values)

    def keys(self) -> list[str]:
        return list(self._cols)

    def values(self) -> tuple:
        return self._values

    def items(self):
        return zip(self._cols, self._values, strict=False)


class PostgresBackend:
    """Optional :class:`StorageBackend` implementation backed by ``psycopg2``.

    Translates the SQLite SQL dialect used by :class:`KilnDB` into
    PostgreSQL-compatible SQL on the fly so that the higher-level persistence
    code does not need any conditional logic.

    :param dsn: PostgreSQL connection string.  Falls back to the
        ``KILN_POSTGRES_DSN`` environment variable when ``None``.
    :raises RuntimeError: If ``psycopg2`` is not installed.
    :raises ValueError: If no DSN is provided and the env var is unset.
    """

    def __init__(self, dsn: str | None = None) -> None:
        try:
            import psycopg2  # noqa: F811
        except ImportError:
            raise RuntimeError("psycopg2 is required for PostgreSQL support: pip install psycopg2-binary") from None

        self._dsn = dsn or os.environ.get("KILN_POSTGRES_DSN", "")
        if not self._dsn:
            raise ValueError("PostgreSQL DSN required — pass dsn= or set KILN_POSTGRES_DSN")

        self._psycopg2 = psycopg2
        self._conn = psycopg2.connect(self._dsn)
        self._conn.autocommit = False

    # -- SQL dialect translation helpers ------------------------------------

    @staticmethod
    def _translate_placeholders(sql: str) -> str:
        """Convert SQLite ``?`` positional placeholders to Postgres ``%s``.

        Skips ``?`` characters inside single-quoted string literals to avoid
        corrupting embedded text values.
        """
        result: list[str] = []
        in_string = False
        for ch in sql:
            if ch == "'" and not in_string:
                in_string = True
                result.append(ch)
            elif ch == "'" and in_string:
                in_string = False
                result.append(ch)
            elif ch == "?" and not in_string:
                result.append("%s")
            else:
                result.append(ch)
        return "".join(result)

    @staticmethod
    def _translate_ddl(sql: str) -> str:
        """Translate SQLite DDL idioms to PostgreSQL equivalents."""
        import re

        # INTEGER PRIMARY KEY AUTOINCREMENT → SERIAL PRIMARY KEY
        sql = re.sub(
            r"INTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT",
            "SERIAL PRIMARY KEY",
            sql,
            flags=re.IGNORECASE,
        )
        # DATETIME DEFAULT CURRENT_TIMESTAMP → TIMESTAMP DEFAULT NOW()
        sql = re.sub(
            r"DATETIME\s+DEFAULT\s+CURRENT_TIMESTAMP",
            "TIMESTAMP DEFAULT NOW()",
            sql,
            flags=re.IGNORECASE,
        )
        # INSERT OR REPLACE → Postgres upsert not needed at DDL level but
        # some statements leak through executescript; leave for execute().
        # INSERT OR IGNORE → handled in execute().
        return sql

    @staticmethod
    def _translate_dml(sql: str) -> str:
        """Translate SQLite DML idioms to PostgreSQL equivalents."""
        import re

        # INSERT OR REPLACE → INSERT ... ON CONFLICT DO UPDATE
        # For tables using a single PRIMARY KEY, we extract the table name
        # and column list to build a proper ON CONFLICT clause.
        or_replace = re.match(
            r"INSERT\s+OR\s+REPLACE\s+INTO\s+(\w+)\s*\(([^)]+)\)\s*VALUES\s*\((.+)\)",
            sql,
            re.IGNORECASE | re.DOTALL,
        )
        if or_replace:
            table = or_replace.group(1)
            cols_str = or_replace.group(2)
            vals_str = or_replace.group(3)
            cols = [c.strip() for c in cols_str.split(",")]
            # Build SET clause for all non-PK columns.
            set_parts = [f"{c} = EXCLUDED.{c}" for c in cols[1:]]
            # Use first column as the conflict target (it is the PK for all
            # tables in the Kiln schema).
            pk = cols[0]
            sql = (
                f"INSERT INTO {table} ({cols_str}) VALUES ({vals_str}) "
                f"ON CONFLICT ({pk}) DO UPDATE SET {', '.join(set_parts)}"
            )
            return sql

        # INSERT OR IGNORE → INSERT ... ON CONFLICT DO NOTHING
        has_or_ignore = bool(re.search(r"INSERT\s+OR\s+IGNORE\s+INTO", sql, re.IGNORECASE))
        if has_or_ignore:
            sql = re.sub(
                r"INSERT\s+OR\s+IGNORE\s+INTO",
                "INSERT INTO",
                sql,
                flags=re.IGNORECASE,
            )
            sql = sql.rstrip().rstrip(";")
            sql += " ON CONFLICT DO NOTHING"
        return sql

    def _translate_sql(self, sql: str) -> str:
        """Full translation pipeline for a single SQL statement."""
        sql = self._translate_placeholders(sql)
        sql = self._translate_dml(sql)
        return sql

    # -- StorageBackend interface ------------------------------------------

    def execute(self, sql: str, parameters: _SqlParams = (), /) -> _PgDictCursor:
        translated = self._translate_sql(sql)
        # Convert dict parameters with :name style to %(name)s for psycopg2.
        if isinstance(parameters, dict):
            import re

            translated = re.sub(r":(\w+)", r"%(\1)s", translated)
        cur = self._conn.cursor()
        cur.execute(translated, parameters)
        return _PgDictCursor(cur)

    def executemany(self, sql: str, seq_of_parameters: Sequence[_SqlParams], /) -> _PgDictCursor:
        translated = self._translate_sql(sql)
        if seq_of_parameters and isinstance(seq_of_parameters[0], dict):
            import re

            translated = re.sub(r":(\w+)", r"%(\1)s", translated)
        cur = self._conn.cursor()
        cur.executemany(translated, seq_of_parameters)
        return _PgDictCursor(cur)

    def executescript(self, sql_script: str, /) -> _PgDictCursor:
        """Execute a multi-statement SQL script.

        PostgreSQL does not have a native ``executescript`` — we translate DDL
        idioms, split on ``;``, and execute each statement individually.
        """
        translated = self._translate_ddl(sql_script)
        translated = self._translate_placeholders(translated)
        cur = self._conn.cursor()
        for stmt in translated.split(";"):
            stmt = stmt.strip()
            if stmt:
                cur.execute(stmt)
        return _PgDictCursor(cur)

    def commit(self) -> None:
        self._conn.commit()

    def close(self) -> None:
        self._conn.close()

    def cursor(self) -> _PgDictCursor:
        return _PgDictCursor(self._conn.cursor())


class KilnDB:
    """Thread-safe persistence wrapper for Kiln.

    All database I/O is routed through a :class:`StorageBackend` instance,
    making it possible to swap the underlying engine (SQLite, Postgres, etc.)
    without altering any of the query logic.

    Parameters:
        db_path: Filesystem path for the SQLite database file.  Defaults to
            the value of ``KILN_DB_PATH`` or ``~/.kiln/kiln.db``.
            Ignored when *backend* is provided.
        backend: Optional pre-built :class:`StorageBackend`.  When ``None``
            (the default), auto-detects from ``KILN_POSTGRES_DSN`` env var.
            If that var is set a :class:`PostgresBackend` is used; otherwise
            a :class:`SQLiteBackend` is created from *db_path*.
    """

    def __init__(
        self,
        db_path: str | None = None,
        *,
        backend: StorageBackend | None = None,
    ) -> None:
        self._db_path = db_path or os.environ.get("KILN_DB_PATH", _DEFAULT_DB_PATH)
        self._is_postgres = False

        if backend is not None:
            self._conn: StorageBackend = backend
            self._is_postgres = isinstance(backend, PostgresBackend)
            logger.info(
                "KilnDB using explicit %s backend",
                "PostgreSQL" if self._is_postgres else type(backend).__name__,
            )
        elif os.environ.get("KILN_POSTGRES_DSN"):
            self._conn = PostgresBackend()
            self._is_postgres = True
            logger.info("KilnDB using PostgreSQL backend (from KILN_POSTGRES_DSN)")
        else:
            # Ensure the parent directory exists for the SQLite file.
            os.makedirs(os.path.dirname(self._db_path), exist_ok=True)
            self._conn = SQLiteBackend(self._db_path)
            logger.info("KilnDB using SQLite backend (%s)", self._db_path)

        self._write_lock = threading.Lock()

        self._ensure_schema()
        self._migrate_agent_memory()
        self._enforce_permissions()

    # ------------------------------------------------------------------
    # File permissions
    # ------------------------------------------------------------------

    def _migrate_agent_memory(self) -> None:
        """Add version and expires_at columns to existing agent_memory tables."""
        if self._is_postgres:
            # Postgres: query information_schema for existing columns.
            rows = self._conn.execute(
                "SELECT column_name FROM information_schema.columns WHERE table_name = 'agent_memory'",
            ).fetchall()
            columns = {row[0] for row in rows}
        else:
            # SQLite: use PRAGMA table_info.
            columns = {row[1] for row in self._conn.execute("PRAGMA table_info(agent_memory)").fetchall()}
        if "version" not in columns:
            self._conn.execute("ALTER TABLE agent_memory ADD COLUMN version INTEGER NOT NULL DEFAULT 1")
        if "expires_at" not in columns:
            self._conn.execute("ALTER TABLE agent_memory ADD COLUMN expires_at REAL DEFAULT NULL")
        self._conn.commit()

    def _enforce_permissions(self) -> None:
        """Enforce restrictive file permissions on the data directory and DB.

        Sets the parent directory to mode ``0700`` and the database file to
        mode ``0600`` so that other users on a shared system cannot read
        printer credentials or job history.  Skipped on Windows where POSIX
        chmod semantics do not apply, and on Postgres where there is no
        local database file.
        """
        if sys.platform == "win32" or self._is_postgres:
            return

        db_dir = os.path.dirname(self._db_path)

        # --- Directory permissions ---
        try:
            dir_stat = os.stat(db_dir)
            dir_mode = stat.S_IMODE(dir_stat.st_mode)
            if dir_mode & 0o077:
                logger.warning(
                    "~/.kiln/ directory has overly permissive permissions "
                    "(mode %04o). Run 'chmod 700 ~/.kiln/' to fix.",
                    dir_mode,
                )
            os.chmod(db_dir, 0o700)
        except OSError as exc:
            logger.warning("Unable to set permissions on %s: %s", db_dir, exc)

        # --- Database file permissions ---
        try:
            file_stat = os.stat(self._db_path)
            file_mode = stat.S_IMODE(file_stat.st_mode)
            if file_mode & 0o077:
                logger.warning(
                    "Database file %s has overly permissive permissions (mode %04o). Fixing to 0600.",
                    self._db_path,
                    file_mode,
                )
            os.chmod(self._db_path, 0o600)
        except OSError as exc:
            logger.warning("Unable to set permissions on %s: %s", self._db_path, exc)

    # ------------------------------------------------------------------
    # Schema
    # ------------------------------------------------------------------

    def _ensure_schema(self) -> None:
        """Create tables if they do not already exist."""
        with self._write_lock:
            self._conn.executescript(
                """
                CREATE TABLE IF NOT EXISTS jobs (
                    id              TEXT PRIMARY KEY,
                    file_name       TEXT NOT NULL,
                    printer_name    TEXT,
                    status          TEXT NOT NULL,
                    priority        INTEGER NOT NULL DEFAULT 0,
                    submitted_by    TEXT NOT NULL DEFAULT 'unknown',
                    submitted_at    REAL NOT NULL,
                    started_at      REAL,
                    completed_at    REAL,
                    error_message   TEXT
                );

                CREATE TABLE IF NOT EXISTS events (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    event_type      TEXT NOT NULL,
                    source          TEXT NOT NULL DEFAULT '',
                    data            TEXT NOT NULL DEFAULT '{}',
                    timestamp       REAL NOT NULL,
                    created_at      REAL DEFAULT (strftime('%s', 'now'))
                );

                CREATE TABLE IF NOT EXISTS printers (
                    name            TEXT PRIMARY KEY,
                    printer_type    TEXT NOT NULL,
                    host            TEXT NOT NULL,
                    api_key         TEXT,
                    registered_at   REAL NOT NULL,
                    last_seen       REAL
                );

                CREATE TABLE IF NOT EXISTS settings (
                    key             TEXT PRIMARY KEY,
                    value           TEXT NOT NULL
                );

                CREATE TABLE IF NOT EXISTS printer_materials (
                    printer_name    TEXT NOT NULL,
                    tool_index      INTEGER NOT NULL DEFAULT 0,
                    material_type   TEXT NOT NULL,
                    color           TEXT,
                    spool_id        TEXT,
                    loaded_at       REAL NOT NULL,
                    remaining_grams REAL,
                    PRIMARY KEY (printer_name, tool_index)
                );

                CREATE TABLE IF NOT EXISTS spools (
                    id              TEXT PRIMARY KEY,
                    material_type   TEXT NOT NULL,
                    color           TEXT,
                    brand           TEXT,
                    weight_grams    REAL NOT NULL DEFAULT 1000.0,
                    remaining_grams REAL NOT NULL DEFAULT 1000.0,
                    cost_usd        REAL,
                    purchase_date   REAL,
                    notes           TEXT
                );

                CREATE TABLE IF NOT EXISTS leveling_history (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    printer_name    TEXT NOT NULL,
                    triggered_by    TEXT NOT NULL DEFAULT 'manual',
                    started_at      REAL NOT NULL,
                    completed_at    REAL,
                    success         INTEGER DEFAULT 0,
                    mesh_data       TEXT,
                    trigger_reason  TEXT
                );

                CREATE TABLE IF NOT EXISTS sync_log (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    entity_type     TEXT NOT NULL,
                    entity_id       TEXT NOT NULL,
                    synced_at       REAL NOT NULL,
                    sync_direction  TEXT NOT NULL DEFAULT 'push',
                    status          TEXT NOT NULL DEFAULT 'success'
                );
                CREATE INDEX IF NOT EXISTS idx_sync_log_entity
                    ON sync_log(entity_type, entity_id);

                CREATE TABLE IF NOT EXISTS billing_charges (
                    id              TEXT PRIMARY KEY,
                    job_id          TEXT NOT NULL UNIQUE,
                    order_id        TEXT,
                    fee_amount      REAL NOT NULL,
                    fee_percent     REAL NOT NULL,
                    job_cost        REAL NOT NULL,
                    total_cost      REAL NOT NULL,
                    currency        TEXT NOT NULL DEFAULT 'USD',
                    waived          INTEGER NOT NULL DEFAULT 0,
                    waiver_reason   TEXT,
                    payment_id      TEXT,
                    payment_rail    TEXT,
                    payment_status  TEXT NOT NULL DEFAULT 'pending',
                    user_email      TEXT,
                    created_at      REAL NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_billing_charges_job
                    ON billing_charges(job_id);
                CREATE INDEX IF NOT EXISTS idx_billing_charges_created
                    ON billing_charges(created_at);

                CREATE TABLE IF NOT EXISTS payment_methods (
                    id              TEXT PRIMARY KEY,
                    user_id         TEXT NOT NULL,
                    rail            TEXT NOT NULL,
                    provider_ref    TEXT NOT NULL,
                    method_ref      TEXT,
                    label           TEXT,
                    is_default      INTEGER NOT NULL DEFAULT 0,
                    created_at      REAL NOT NULL
                );

                CREATE TABLE IF NOT EXISTS payments (
                    id              TEXT PRIMARY KEY,
                    charge_id       TEXT NOT NULL,
                    provider_id     TEXT NOT NULL,
                    rail            TEXT NOT NULL,
                    amount          REAL NOT NULL,
                    currency        TEXT NOT NULL,
                    status          TEXT NOT NULL DEFAULT 'pending',
                    tx_hash         TEXT,
                    error           TEXT,
                    created_at      REAL NOT NULL,
                    updated_at      REAL NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_payments_charge
                    ON payments(charge_id);

                CREATE TABLE IF NOT EXISTS print_history (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    job_id          TEXT NOT NULL,
                    printer_name    TEXT NOT NULL,
                    file_name       TEXT,
                    status          TEXT NOT NULL,
                    duration_seconds REAL,
                    material_type   TEXT,
                    file_hash       TEXT,
                    slicer_profile  TEXT,
                    notes           TEXT,
                    agent_id        TEXT,
                    metadata        TEXT,
                    started_at      REAL,
                    completed_at    REAL,
                    created_at      REAL NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_print_history_printer
                    ON print_history(printer_name);
                CREATE INDEX IF NOT EXISTS idx_print_history_status
                    ON print_history(status);

                CREATE TABLE IF NOT EXISTS agent_memory (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    agent_id        TEXT NOT NULL,
                    scope           TEXT NOT NULL,
                    key             TEXT NOT NULL,
                    value           TEXT NOT NULL,
                    created_at      REAL NOT NULL,
                    updated_at      REAL NOT NULL,
                    version         INTEGER NOT NULL DEFAULT 1,
                    expires_at      REAL DEFAULT NULL,
                    UNIQUE(agent_id, scope, key)
                );
                CREATE INDEX IF NOT EXISTS idx_agent_memory_agent
                    ON agent_memory(agent_id, scope);

                CREATE TABLE IF NOT EXISTS print_outcomes (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    job_id          TEXT NOT NULL,
                    printer_name    TEXT NOT NULL,
                    file_name       TEXT,
                    file_hash       TEXT,
                    material_type   TEXT,
                    outcome         TEXT NOT NULL,
                    quality_grade   TEXT,
                    failure_mode    TEXT,
                    settings        TEXT,
                    environment     TEXT,
                    notes           TEXT,
                    agent_id        TEXT,
                    created_at      REAL NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_print_outcomes_printer
                    ON print_outcomes(printer_name);
                CREATE INDEX IF NOT EXISTS idx_print_outcomes_file
                    ON print_outcomes(file_hash);
                CREATE INDEX IF NOT EXISTS idx_print_outcomes_outcome
                    ON print_outcomes(outcome);
                CREATE UNIQUE INDEX IF NOT EXISTS idx_print_outcomes_job_id
                    ON print_outcomes(job_id);
                CREATE INDEX IF NOT EXISTS idx_print_outcomes_material
                    ON print_outcomes(material_type);

                CREATE TABLE IF NOT EXISTS safety_audit_log (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    timestamp       REAL NOT NULL,
                    tool_name       TEXT NOT NULL,
                    safety_level    TEXT NOT NULL,
                    action          TEXT NOT NULL,
                    agent_id        TEXT,
                    printer_name    TEXT,
                    details         TEXT,
                    created_at      REAL DEFAULT (strftime('%s', 'now'))
                );
                CREATE INDEX IF NOT EXISTS idx_audit_tool
                    ON safety_audit_log(tool_name);
                CREATE INDEX IF NOT EXISTS idx_audit_action
                    ON safety_audit_log(action);
                CREATE INDEX IF NOT EXISTS idx_audit_time
                    ON safety_audit_log(timestamp);

                CREATE TABLE IF NOT EXISTS model_cache (
                    cache_id        TEXT PRIMARY KEY,
                    file_name       TEXT NOT NULL,
                    file_path       TEXT NOT NULL,
                    file_hash       TEXT NOT NULL,
                    file_size_bytes INTEGER NOT NULL,
                    source          TEXT NOT NULL,
                    source_id       TEXT,
                    prompt          TEXT,
                    tags            TEXT,
                    dimensions      TEXT,
                    print_count     INTEGER DEFAULT 0,
                    last_printed_at REAL,
                    created_at      REAL NOT NULL,
                    metadata        TEXT
                );
                CREATE INDEX IF NOT EXISTS idx_model_cache_hash
                    ON model_cache(file_hash);
                CREATE INDEX IF NOT EXISTS idx_model_cache_source
                    ON model_cache(source);

                CREATE TABLE IF NOT EXISTS fulfillment_orders (
                    id              TEXT PRIMARY KEY,
                    order_id        TEXT NOT NULL UNIQUE,
                    provider        TEXT NOT NULL,
                    status          TEXT NOT NULL DEFAULT 'submitted',
                    file_path       TEXT NOT NULL,
                    material_id     TEXT NOT NULL,
                    quantity        INTEGER NOT NULL DEFAULT 1,
                    total_price     REAL NOT NULL DEFAULT 0.0,
                    currency        TEXT NOT NULL DEFAULT 'USD',
                    shipping_address TEXT,
                    tracking_url    TEXT,
                    tracking_number TEXT,
                    quote_id        TEXT,
                    notes           TEXT,
                    created_at      REAL NOT NULL,
                    updated_at      REAL NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_order
                    ON fulfillment_orders(order_id);
                CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_status
                    ON fulfillment_orders(status);
                CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_provider
                    ON fulfillment_orders(provider);

                CREATE TABLE IF NOT EXISTS snapshots (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    job_id          TEXT,
                    printer_name    TEXT NOT NULL,
                    phase           TEXT NOT NULL DEFAULT 'unknown',
                    image_path      TEXT NOT NULL,
                    image_size_bytes INTEGER,
                    analysis         TEXT,
                    agent_notes      TEXT,
                    confidence       REAL,
                    completion_pct   REAL,
                    created_at       REAL NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_snapshots_job
                    ON snapshots(job_id);
                CREATE INDEX IF NOT EXISTS idx_snapshots_printer
                    ON snapshots(printer_name);
                CREATE INDEX IF NOT EXISTS idx_snapshots_phase
                    ON snapshots(phase);

                CREATE TABLE IF NOT EXISTS published_models (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    file_hash       TEXT NOT NULL,
                    marketplace     TEXT NOT NULL,
                    listing_id      TEXT,
                    listing_url     TEXT,
                    title           TEXT NOT NULL,
                    published_at    REAL NOT NULL,
                    certificate     TEXT
                );
                CREATE INDEX IF NOT EXISTS idx_published_models_hash
                    ON published_models(file_hash);
                CREATE INDEX IF NOT EXISTS idx_published_models_marketplace
                    ON published_models(marketplace);

                CREATE TABLE IF NOT EXISTS revenue (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    model_id        TEXT NOT NULL,
                    marketplace     TEXT NOT NULL,
                    amount_usd      REAL NOT NULL,
                    currency        TEXT DEFAULT 'USD',
                    transaction_type TEXT NOT NULL,
                    description     TEXT,
                    timestamp       REAL NOT NULL,
                    platform_fee_usd REAL DEFAULT 0.0,
                    creator_net_usd  REAL DEFAULT 0.0
                );
                CREATE INDEX IF NOT EXISTS idx_revenue_model
                    ON revenue(model_id);
                CREATE INDEX IF NOT EXISTS idx_revenue_marketplace
                    ON revenue(marketplace);
                CREATE INDEX IF NOT EXISTS idx_revenue_timestamp
                    ON revenue(timestamp);

                CREATE TABLE IF NOT EXISTS print_service_orders (
                    id              TEXT PRIMARY KEY,
                    status          TEXT NOT NULL,
                    request         TEXT NOT NULL,
                    model_path      TEXT,
                    material        TEXT,
                    provider        TEXT,
                    printer_name    TEXT,
                    tracking_url    TEXT,
                    cost_usd        REAL DEFAULT 0,
                    created_at      REAL NOT NULL,
                    updated_at      REAL NOT NULL,
                    steps_completed TEXT,
                    current_step    TEXT,
                    error           TEXT,
                    callback_url    TEXT
                );
                CREATE INDEX IF NOT EXISTS idx_print_service_orders_status
                    ON print_service_orders(status);
                CREATE INDEX IF NOT EXISTS idx_print_service_orders_created
                    ON print_service_orders(created_at);

                CREATE TABLE IF NOT EXISTS print_dna (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    file_hash       TEXT NOT NULL,
                    geometric_signature TEXT NOT NULL,
                    triangle_count  INTEGER,
                    bounding_box    TEXT,
                    surface_area    REAL,
                    volume          REAL,
                    overhang_ratio  REAL,
                    complexity_score REAL,
                    printer_model   TEXT,
                    material        TEXT,
                    settings        TEXT,
                    outcome         TEXT NOT NULL,
                    quality_grade   TEXT DEFAULT 'B',
                    failure_mode    TEXT,
                    print_time_seconds INTEGER DEFAULT 0,
                    timestamp       REAL NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_print_dna_file_hash
                    ON print_dna(file_hash);
                CREATE INDEX IF NOT EXISTS idx_print_dna_geometric_sig
                    ON print_dna(geometric_signature);
                CREATE INDEX IF NOT EXISTS idx_print_dna_outcome
                    ON print_dna(outcome);
                CREATE INDEX IF NOT EXISTS idx_print_dna_printer
                    ON print_dna(printer_model);

                CREATE TABLE IF NOT EXISTS community_prints (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    geometric_signature TEXT NOT NULL,
                    printer_model   TEXT NOT NULL,
                    material        TEXT NOT NULL,
                    settings_hash   TEXT,
                    settings        TEXT,
                    outcome         TEXT NOT NULL,
                    quality_grade   TEXT DEFAULT 'B',
                    failure_mode    TEXT,
                    print_time_seconds INTEGER DEFAULT 0,
                    region          TEXT DEFAULT 'anonymous',
                    timestamp       REAL NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_community_prints_sig
                    ON community_prints(geometric_signature);
                CREATE INDEX IF NOT EXISTS idx_community_prints_printer
                    ON community_prints(printer_model);
                CREATE INDEX IF NOT EXISTS idx_community_prints_material
                    ON community_prints(material);
                CREATE INDEX IF NOT EXISTS idx_community_prints_outcome
                    ON community_prints(outcome);

                CREATE TABLE IF NOT EXISTS failure_records (
                    id              INTEGER PRIMARY KEY AUTOINCREMENT,
                    job_id          TEXT,
                    printer_name    TEXT,
                    failure_type    TEXT NOT NULL,
                    confidence      REAL,
                    progress_at_failure REAL,
                    recovery_action TEXT,
                    settings_adjustments TEXT,
                    evidence        TEXT,
                    resolved        BOOLEAN DEFAULT 0,
                    timestamp       REAL NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_failure_records_printer
                    ON failure_records(printer_name);
                CREATE INDEX IF NOT EXISTS idx_failure_records_type
                    ON failure_records(failure_type);

                CREATE TABLE IF NOT EXISTS split_plans (
                    id              TEXT PRIMARY KEY,
                    original_file   TEXT,
                    split_type      TEXT NOT NULL,
                    parts           TEXT NOT NULL,
                    total_printers  INTEGER,
                    created_at      REAL NOT NULL,
                    status          TEXT DEFAULT 'pending'
                );

                CREATE TABLE IF NOT EXISTS feedback_loops (
                    model_id        TEXT PRIMARY KEY,
                    original_prompt TEXT NOT NULL,
                    iterations      TEXT NOT NULL,
                    current_iteration INTEGER DEFAULT 0,
                    resolved        BOOLEAN DEFAULT 0,
                    best_iteration  INTEGER,
                    created_at      REAL NOT NULL,
                    updated_at      REAL NOT NULL
                );
                """
            )

            # Add hmac_signature column to safety_audit_log if missing.
            # sqlite3.OperationalError on SQLite, various DB errors on
            # Postgres -- column already exists in both cases.
            with contextlib.suppress(sqlite3.OperationalError, Exception):
                self._conn.execute("ALTER TABLE safety_audit_log ADD COLUMN hmac_signature TEXT")

            # Add session_id column to safety_audit_log if missing.
            with contextlib.suppress(sqlite3.OperationalError, Exception):
                self._conn.execute("ALTER TABLE safety_audit_log ADD COLUMN session_id TEXT")
            with contextlib.suppress(sqlite3.OperationalError, Exception):
                self._conn.execute(
                    "CREATE INDEX IF NOT EXISTS idx_audit_session ON safety_audit_log(session_id)"
                )

            # Add prev_hash column for tamper-evident hash chain.
            with contextlib.suppress(sqlite3.OperationalError, Exception):
                self._conn.execute(
                    "ALTER TABLE safety_audit_log ADD COLUMN prev_hash TEXT DEFAULT ''"
                )

            # Add user_email column to billing_charges if missing (added for
            # per-user free-tier tracking in the fulfillment proxy).
            with contextlib.suppress(sqlite3.OperationalError, Exception):
                self._conn.execute("ALTER TABLE billing_charges ADD COLUMN user_email TEXT")
            with contextlib.suppress(sqlite3.OperationalError, Exception):
                self._conn.execute(
                    "CREATE INDEX IF NOT EXISTS idx_billing_charges_user ON billing_charges(user_email)"
                )

            self._conn.commit()

    # ------------------------------------------------------------------
    # Jobs
    # ------------------------------------------------------------------

    def save_job(self, job_dict: dict[str, Any]) -> None:
        """Insert or replace a job record.

        The dict must contain at least ``id``, ``file_name``, ``status``,
        and ``submitted_at``.
        """
        with self._write_lock:
            self._conn.execute(
                """
                INSERT OR REPLACE INTO jobs
                    (id, file_name, printer_name, status, priority,
                     submitted_by, submitted_at, started_at, completed_at,
                     error_message)
                VALUES
                    (:id, :file_name, :printer_name, :status, :priority,
                     :submitted_by, :submitted_at, :started_at, :completed_at,
                     :error_message)
                """,
                {
                    "id": job_dict["id"],
                    "file_name": job_dict["file_name"],
                    "printer_name": job_dict.get("printer_name"),
                    "status": job_dict["status"],
                    "priority": job_dict.get("priority", 0),
                    "submitted_by": job_dict.get("submitted_by", "unknown"),
                    "submitted_at": job_dict["submitted_at"],
                    "started_at": job_dict.get("started_at"),
                    "completed_at": job_dict.get("completed_at"),
                    "error_message": job_dict.get("error_message"),
                },
            )
            self._conn.commit()

    def get_job(self, job_id: str) -> dict[str, Any] | None:
        """Fetch a single job by ID, or ``None`` if not found."""
        row = self._conn.execute("SELECT * FROM jobs WHERE id = ?", (job_id,)).fetchone()
        if row is None:
            return None
        return dict(row)

    def list_jobs(
        self,
        status: str | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """Return jobs ordered by priority DESC then submitted_at ASC.

        Args:
            status: Filter by status string, or ``None`` for all.
            limit: Maximum rows to return.
        """
        if status is not None:
            rows = self._conn.execute(
                "SELECT * FROM jobs WHERE status = ? ORDER BY priority DESC, submitted_at ASC LIMIT ?",
                (status, limit),
            ).fetchall()
        else:
            rows = self._conn.execute(
                "SELECT * FROM jobs ORDER BY priority DESC, submitted_at ASC LIMIT ?",
                (limit,),
            ).fetchall()
        return [dict(r) for r in rows]

    # ------------------------------------------------------------------
    # Events
    # ------------------------------------------------------------------

    def log_event(
        self,
        event_type: str,
        data: dict[str, Any],
        source: str = "",
        timestamp: float | None = None,
    ) -> int:
        """Insert an event and return the row id."""
        ts = timestamp if timestamp is not None else time.time()
        with self._write_lock:
            cur = self._conn.execute(
                """
                INSERT INTO events (event_type, source, data, timestamp)
                VALUES (?, ?, ?, ?)
                """,
                (event_type, source, json.dumps(data), ts),
            )
            self._conn.commit()
            return cur.lastrowid  # type: ignore[return-value]

    def recent_events(
        self,
        event_type: str | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """Return recent events, newest first.

        The ``data`` column is deserialised from JSON back into a dict.
        """
        if event_type is not None:
            rows = self._conn.execute(
                "SELECT * FROM events WHERE event_type = ? ORDER BY id DESC LIMIT ?",
                (event_type, limit),
            ).fetchall()
        else:
            rows = self._conn.execute(
                "SELECT * FROM events ORDER BY id DESC LIMIT ?",
                (limit,),
            ).fetchall()

        results: list[dict[str, Any]] = []
        for row in rows:
            d = dict(row)
            d["data"] = json.loads(d["data"])
            results.append(d)
        return results

    # ------------------------------------------------------------------
    # Printers
    # ------------------------------------------------------------------

    def save_printer(
        self,
        name: str,
        printer_type: str,
        host: str,
        api_key: str | None = None,
    ) -> None:
        """Insert or replace a printer record."""
        now = time.time()
        with self._write_lock:
            self._conn.execute(
                """
                INSERT OR REPLACE INTO printers
                    (name, printer_type, host, api_key, registered_at, last_seen)
                VALUES (?, ?, ?, ?, ?, ?)
                """,
                (name, printer_type, host, api_key, now, now),
            )
            self._conn.commit()

    def list_printers(self) -> list[dict[str, Any]]:
        """Return all registered printers."""
        rows = self._conn.execute("SELECT * FROM printers ORDER BY name").fetchall()
        return [dict(r) for r in rows]

    def remove_printer(self, name: str) -> bool:
        """Delete a printer by name.  Returns ``True`` if a row was deleted."""
        with self._write_lock:
            cur = self._conn.execute("DELETE FROM printers WHERE name = ?", (name,))
            self._conn.commit()
            return cur.rowcount > 0

    # ------------------------------------------------------------------
    # Settings
    # ------------------------------------------------------------------

    def get_setting(
        self,
        key: str,
        default: str | None = None,
    ) -> str | None:
        """Retrieve a setting value by key, or *default* if missing."""
        row = self._conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
        if row is None:
            return default
        return row["value"]

    def set_setting(self, key: str, value: str) -> None:
        """Create or update a setting."""
        with self._write_lock:
            self._conn.execute(
                "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
                (key, value),
            )
            self._conn.commit()

    # ------------------------------------------------------------------
    # Materials
    # ------------------------------------------------------------------

    def save_material(
        self,
        printer_name: str,
        tool_index: int,
        material_type: str,
        color: str | None = None,
        spool_id: str | None = None,
        remaining_grams: float | None = None,
    ) -> None:
        """Insert or replace a loaded material record."""
        with self._write_lock:
            self._conn.execute(
                """
                INSERT OR REPLACE INTO printer_materials
                    (printer_name, tool_index, material_type, color,
                     spool_id, loaded_at, remaining_grams)
                VALUES (?, ?, ?, ?, ?, ?, ?)
                """,
                (printer_name, tool_index, material_type, color, spool_id, time.time(), remaining_grams),
            )
            self._conn.commit()

    def get_material(
        self,
        printer_name: str,
        tool_index: int = 0,
    ) -> dict[str, Any] | None:
        """Fetch material loaded in a specific tool slot."""
        row = self._conn.execute(
            "SELECT * FROM printer_materials WHERE printer_name = ? AND tool_index = ?",
            (printer_name, tool_index),
        ).fetchone()
        return dict(row) if row else None

    def list_materials(self, printer_name: str) -> list[dict[str, Any]]:
        """Return all material slots for a printer."""
        rows = self._conn.execute(
            "SELECT * FROM printer_materials WHERE printer_name = ? ORDER BY tool_index",
            (printer_name,),
        ).fetchall()
        return [dict(r) for r in rows]

    def update_material_remaining(
        self,
        printer_name: str,
        tool_index: int,
        remaining_grams: float,
    ) -> None:
        """Update remaining grams for a loaded material."""
        with self._write_lock:
            self._conn.execute(
                "UPDATE printer_materials SET remaining_grams = ? WHERE printer_name = ? AND tool_index = ?",
                (remaining_grams, printer_name, tool_index),
            )
            self._conn.commit()

    # ------------------------------------------------------------------
    # Spools
    # ------------------------------------------------------------------

    def save_spool(self, spool: dict[str, Any]) -> None:
        """Insert or replace a spool record."""
        with self._write_lock:
            self._conn.execute(
                """
                INSERT OR REPLACE INTO spools
                    (id, material_type, color, brand, weight_grams,
                     remaining_grams, cost_usd, purchase_date, notes)
                VALUES (:id, :material_type, :color, :brand, :weight_grams,
                        :remaining_grams, :cost_usd, :purchase_date, :notes)
                """,
                {
                    "id": spool["id"],
                    "material_type": spool["material_type"],
                    "color": spool.get("color"),
                    "brand": spool.get("brand"),
                    "weight_grams": spool.get("weight_grams", 1000.0),
                    "remaining_grams": spool.get("remaining_grams", 1000.0),
                    "cost_usd": spool.get("cost_usd"),
                    "purchase_date": spool.get("purchase_date"),
                    "notes": spool.get("notes", ""),
                },
            )
            self._conn.commit()

    def get_spool(self, spool_id: str) -> dict[str, Any] | None:
        """Fetch a spool by ID."""
        row = self._conn.execute("SELECT * FROM spools WHERE id = ?", (spool_id,)).fetchone()
        return dict(row) if row else None

    def list_spools(self) -> list[dict[str, Any]]:
        """Return all spools."""
        rows = self._conn.execute("SELECT * FROM spools ORDER BY material_type, color").fetchall()
        return [dict(r) for r in rows]

    def remove_spool(self, spool_id: str) -> bool:
        """Delete a spool.  Returns ``True`` if a row was deleted."""
        with self._write_lock:
            cur = self._conn.execute("DELETE FROM spools WHERE id = ?", (spool_id,))
            self._conn.commit()
            return cur.rowcount > 0

    def update_spool_remaining(self, spool_id: str, remaining_grams: float) -> None:
        """Update remaining grams for a spool."""
        with self._write_lock:
            self._conn.execute(
                "UPDATE spools SET remaining_grams = ? WHERE id = ?",
                (remaining_grams, spool_id),
            )
            self._conn.commit()

    # ------------------------------------------------------------------
    # Leveling history
    # ------------------------------------------------------------------

    def save_leveling(self, record: dict[str, Any]) -> int:
        """Insert a leveling record and return the row id."""
        with self._write_lock:
            cur = self._conn.execute(
                """
                INSERT INTO leveling_history
                    (printer_name, triggered_by, started_at, completed_at,
                     success, mesh_data, trigger_reason)
                VALUES (?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    record["printer_name"],
                    record.get("triggered_by", "manual"),
                    record["started_at"],
                    record.get("completed_at"),
                    1 if record.get("success") else 0,
                    json.dumps(record["mesh_data"]) if record.get("mesh_data") else None,
                    record.get("trigger_reason"),
                ),
            )
            self._conn.commit()
            return cur.lastrowid  # type: ignore[return-value]

    def last_leveling(self, printer_name: str) -> dict[str, Any] | None:
        """Return the most recent leveling record for a printer."""
        row = self._conn.execute(
            "SELECT * FROM leveling_history WHERE printer_name = ? ORDER BY started_at DESC LIMIT 1",
            (printer_name,),
        ).fetchone()
        if row is None:
            return None
        d = dict(row)
        if d.get("mesh_data"):
            d["mesh_data"] = json.loads(d["mesh_data"])
        return d

    def leveling_count_since(self, printer_name: str, since: float) -> int:
        """Count leveling events for a printer since a timestamp."""
        row = self._conn.execute(
            "SELECT COUNT(*) AS cnt FROM leveling_history WHERE printer_name = ? AND started_at >= ?",
            (printer_name, since),
        ).fetchone()
        return row["cnt"] if row else 0

    # ------------------------------------------------------------------
    # Sync log
    # ------------------------------------------------------------------

    def log_sync(
        self,
        entity_type: str,
        entity_id: str,
        direction: str = "push",
        status: str = "success",
    ) -> None:
        """Record a sync operation."""
        with self._write_lock:
            self._conn.execute(
                """
                INSERT INTO sync_log (entity_type, entity_id, synced_at,
                                      sync_direction, status)
                VALUES (?, ?, ?, ?, ?)
                """,
                (entity_type, entity_id, time.time(), direction, status),
            )
            self._conn.commit()

    def get_unsynced_jobs(self, since: float) -> list[dict[str, Any]]:
        """Return jobs submitted after *since* that have not been synced."""
        rows = self._conn.execute(
            """
            SELECT j.* FROM jobs j
            WHERE j.submitted_at > ?
              AND j.id NOT IN (
                  SELECT entity_id FROM sync_log
                  WHERE entity_type = 'job' AND status = 'success'
              )
            ORDER BY j.submitted_at ASC
            """,
            (since,),
        ).fetchall()
        return [dict(r) for r in rows]

    def get_unsynced_events(self, since: float) -> list[dict[str, Any]]:
        """Return events logged after *since* that have not been synced."""
        rows = self._conn.execute(
            """
            SELECT e.* FROM events e
            WHERE e.timestamp > ?
              AND CAST(e.id AS TEXT) NOT IN (
                  SELECT entity_id FROM sync_log
                  WHERE entity_type = 'event' AND status = 'success'
              )
            ORDER BY e.id ASC
            """,
            (since,),
        ).fetchall()
        results: list[dict[str, Any]] = []
        for row in rows:
            d = dict(row)
            d["data"] = json.loads(d["data"])
            results.append(d)
        return results

    def mark_synced(self, entity_type: str, entity_ids: list[str]) -> None:
        """Mark entities as synced."""
        with self._write_lock:
            now = time.time()
            for eid in entity_ids:
                self._conn.execute(
                    """
                    INSERT INTO sync_log (entity_type, entity_id, synced_at,
                                          sync_direction, status)
                    VALUES (?, ?, ?, 'push', 'success')
                    """,
                    (entity_type, eid, now),
                )
            self._conn.commit()

    # ------------------------------------------------------------------
    # Billing charges
    # ------------------------------------------------------------------

    def save_billing_charge(self, charge: dict[str, Any]) -> None:
        """Insert a billing charge record, ignoring duplicates on job_id."""
        with self._write_lock:
            self._conn.execute(
                """
                INSERT OR IGNORE INTO billing_charges
                    (id, job_id, order_id, fee_amount, fee_percent,
                     job_cost, total_cost, currency, waived, waiver_reason,
                     payment_id, payment_rail, payment_status, user_email,
                     created_at)
                VALUES (:id, :job_id, :order_id, :fee_amount, :fee_percent,
                        :job_cost, :total_cost, :currency, :waived,
                        :waiver_reason, :payment_id, :payment_rail,
                        :payment_status, :user_email, :created_at)
                """,
                {
                    "id": charge["id"],
                    "job_id": charge["job_id"],
                    "order_id": charge.get("order_id"),
                    "fee_amount": charge["fee_amount"],
                    "fee_percent": charge["fee_percent"],
                    "job_cost": charge["job_cost"],
                    "total_cost": charge["total_cost"],
                    "currency": charge.get("currency", "USD"),
                    "waived": 1 if charge.get("waived") else 0,
                    "waiver_reason": charge.get("waiver_reason"),
                    "payment_id": charge.get("payment_id"),
                    "payment_rail": charge.get("payment_rail"),
                    "payment_status": charge.get("payment_status", "pending"),
                    "user_email": charge.get("user_email"),
                    "created_at": charge.get("created_at", time.time()),
                },
            )
            self._conn.commit()

    def get_billing_charge(self, charge_id: str) -> dict[str, Any] | None:
        """Fetch a billing charge by ID."""
        row = self._conn.execute("SELECT * FROM billing_charges WHERE id = ?", (charge_id,)).fetchone()
        if row is None:
            return None
        d = dict(row)
        d["waived"] = bool(d["waived"])
        return d

    def list_billing_charges(
        self,
        limit: int = 50,
        month: int | None = None,
        year: int | None = None,
    ) -> list[dict[str, Any]]:
        """Return billing charges, newest first.

        Optionally filter by calendar month.
        """
        if month is not None and year is not None:
            from datetime import datetime, timezone

            start = datetime(year, month, 1, tzinfo=timezone.utc).timestamp()
            if month == 12:
                end = datetime(year + 1, 1, 1, tzinfo=timezone.utc).timestamp()
            else:
                end = datetime(year, month + 1, 1, tzinfo=timezone.utc).timestamp()
            rows = self._conn.execute(
                "SELECT * FROM billing_charges "
                "WHERE created_at >= ? AND created_at < ? "
                "ORDER BY created_at DESC LIMIT ?",
                (start, end, limit),
            ).fetchall()
        else:
            rows = self._conn.execute(
                "SELECT * FROM billing_charges ORDER BY created_at DESC LIMIT ?",
                (limit,),
            ).fetchall()
        results = []
        for row in rows:
            d = dict(row)
            d["waived"] = bool(d["waived"])
            results.append(d)
        return results

    def monthly_billing_summary(
        self,
        year: int | None = None,
        month: int | None = None,
    ) -> dict[str, Any]:
        """Aggregate billing data for a calendar month.

        Returns dict with ``total_fees``, ``job_count``, ``waived_count``.
        """
        from datetime import datetime, timezone

        now = datetime.now(timezone.utc)
        target_year = year if year is not None else now.year
        target_month = month if month is not None else now.month
        start = datetime(target_year, target_month, 1, tzinfo=timezone.utc).timestamp()
        if target_month == 12:
            end = datetime(target_year + 1, 1, 1, tzinfo=timezone.utc).timestamp()
        else:
            end = datetime(target_year, target_month + 1, 1, tzinfo=timezone.utc).timestamp()

        row = self._conn.execute(
            """
            SELECT
                COALESCE(SUM(fee_amount), 0.0) AS total_fees,
                COUNT(*) AS job_count,
                SUM(CASE WHEN waived = 1 THEN 1 ELSE 0 END) AS waived_count
            FROM billing_charges
            WHERE created_at >= ? AND created_at < ?
            """,
            (start, end),
        ).fetchone()
        return {
            "total_fees": round(row["total_fees"], 2),
            "job_count": row["job_count"],
            "waived_count": row["waived_count"],
        }

    def billing_charges_this_month(self) -> int:
        """Count billing charges in the current calendar month."""
        from datetime import datetime, timezone

        now = datetime.now(timezone.utc)
        start = datetime(now.year, now.month, 1, tzinfo=timezone.utc).timestamp()
        row = self._conn.execute(
            "SELECT COUNT(*) AS cnt FROM billing_charges WHERE created_at >= ?",
            (start,),
        ).fetchone()
        return row["cnt"] if row else 0

    def billing_charges_this_month_for_user(self, user_email: str) -> int:
        """Count billing charges for a specific user in the current month."""
        from datetime import datetime, timezone

        now = datetime.now(timezone.utc)
        start = datetime(now.year, now.month, 1, tzinfo=timezone.utc).timestamp()
        row = self._conn.execute(
            "SELECT COUNT(*) AS cnt FROM billing_charges WHERE created_at >= ? AND user_email = ?",
            (start, user_email),
        ).fetchone()
        return row["cnt"] if row else 0

    def monthly_fee_total(self) -> float:
        """Sum of fee_amount for billing charges in the current month."""
        from datetime import datetime, timezone

        now = datetime.now(timezone.utc)
        start = datetime(now.year, now.month, 1, tzinfo=timezone.utc).timestamp()
        row = self._conn.execute(
            "SELECT COALESCE(SUM(fee_amount), 0.0) AS total FROM billing_charges WHERE created_at >= ?",
            (start,),
        ).fetchone()
        return round(row["total"], 2) if row else 0.0

    def update_billing_charge(
        self,
        charge_id: str,
        *,
        payment_status: str | None = None,
        refund_reason: str | None = None,
        refunded_at: float | None = None,
    ) -> None:
        """Update fields on an existing billing charge.

        Args:
            charge_id: The charge ID to update.
            payment_status: New payment status value.
            refund_reason: Refund reason (if applicable).
            refunded_at: Timestamp when refunded (if applicable).
        """
        # Build dynamic update query based on provided fields.
        updates = []
        params: dict[str, Any] = {"id": charge_id}

        if payment_status is not None:
            updates.append("payment_status = :payment_status")
            params["payment_status"] = payment_status

        if refund_reason is not None:
            # Need to add these columns to the schema if they don't exist.
            # For now, store in waiver_reason as a fallback.
            updates.append("waiver_reason = :refund_reason")
            params["refund_reason"] = refund_reason

        if refunded_at is not None:
            # Store in created_at field for now (schema limitation).
            # In production, add a refunded_at column.
            pass

        if not updates:
            return

        with self._write_lock:
            query = f"UPDATE billing_charges SET {', '.join(updates)} WHERE id = :id"
            self._conn.execute(query, params)
            self._conn.commit()

    def list_billing_charges_by_status(
        self,
        payment_status: str,
        *,
        limit: int = 1000,
    ) -> list[dict[str, Any]]:
        """Return billing charges filtered by payment status.

        Args:
            payment_status: Filter by this payment status.
            limit: Maximum records to return.

        Returns:
            List of charge dicts.
        """
        rows = self._conn.execute(
            "SELECT * FROM billing_charges WHERE payment_status = ? ORDER BY created_at ASC LIMIT ?",
            (payment_status, limit),
        ).fetchall()
        results = []
        for row in rows:
            d = dict(row)
            d["waived"] = bool(d["waived"])
            results.append(d)
        return results

    def get_job_status(self, job_id: str) -> str | None:
        """Get the status of a job by ID.

        Args:
            job_id: The job identifier.

        Returns:
            The job status string, or ``None`` if not found.
        """
        row = self._conn.execute("SELECT status FROM jobs WHERE id = ?", (job_id,)).fetchone()
        return row["status"] if row else None

    # ------------------------------------------------------------------
    # Payment methods
    # ------------------------------------------------------------------

    def save_payment_method(self, method: dict[str, Any]) -> None:
        """Insert or replace a saved payment method."""
        with self._write_lock:
            self._conn.execute(
                """
                INSERT OR REPLACE INTO payment_methods
                    (id, user_id, rail, provider_ref, method_ref,
                     label, is_default, created_at)
                VALUES (:id, :user_id, :rail, :provider_ref, :method_ref,
                        :label, :is_default, :created_at)
                """,
                {
                    "id": method["id"],
                    "user_id": method["user_id"],
                    "rail": method["rail"],
                    "provider_ref": method["provider_ref"],
                    "method_ref": method.get("method_ref"),
                    "label": method.get("label"),
                    "is_default": 1 if method.get("is_default") else 0,
                    "created_at": method.get("created_at", time.time()),
                },
            )
            self._conn.commit()

    def get_default_payment_method(
        self,
        user_id: str,
    ) -> dict[str, Any] | None:
        """Return the default payment method for a user, or ``None``."""
        row = self._conn.execute(
            "SELECT * FROM payment_methods WHERE user_id = ? AND is_default = 1 LIMIT 1",
            (user_id,),
        ).fetchone()
        if row is None:
            return None
        d = dict(row)
        d["is_default"] = bool(d["is_default"])
        return d

    def list_payment_methods(
        self,
        user_id: str,
    ) -> list[dict[str, Any]]:
        """Return all payment methods for a user."""
        rows = self._conn.execute(
            "SELECT * FROM payment_methods WHERE user_id = ? ORDER BY created_at",
            (user_id,),
        ).fetchall()
        results = []
        for row in rows:
            d = dict(row)
            d["is_default"] = bool(d["is_default"])
            results.append(d)
        return results

    # ------------------------------------------------------------------
    # Payments
    # ------------------------------------------------------------------

    def save_payment(self, payment: dict[str, Any]) -> None:
        """Insert a payment transaction record."""
        now = time.time()
        with self._write_lock:
            self._conn.execute(
                """
                INSERT OR REPLACE INTO payments
                    (id, charge_id, provider_id, rail, amount, currency,
                     status, tx_hash, error, created_at, updated_at)
                VALUES (:id, :charge_id, :provider_id, :rail, :amount,
                        :currency, :status, :tx_hash, :error,
                        :created_at, :updated_at)
                """,
                {
                    "id": payment["id"],
                    "charge_id": payment["charge_id"],
                    "provider_id": payment["provider_id"],
                    "rail": payment["rail"],
                    "amount": payment["amount"],
                    "currency": payment.get("currency", "USD"),
                    "status": payment.get("status", "pending"),
                    "tx_hash": payment.get("tx_hash"),
                    "error": payment.get("error"),
                    "created_at": payment.get("created_at", now),
                    "updated_at": payment.get("updated_at", now),
                },
            )
            self._conn.commit()

    def update_payment_status(
        self,
        payment_id: str,
        status: str,
        tx_hash: str | None = None,
    ) -> None:
        """Update the status (and optionally tx_hash) of a payment."""
        with self._write_lock:
            if tx_hash is not None:
                self._conn.execute(
                    "UPDATE payments SET status = ?, tx_hash = ?, updated_at = ? WHERE id = ?",
                    (status, tx_hash, time.time(), payment_id),
                )
            else:
                self._conn.execute(
                    "UPDATE payments SET status = ?, updated_at = ? WHERE id = ?",
                    (status, time.time(), payment_id),
                )
            self._conn.commit()

    # ------------------------------------------------------------------
    # Data retention & GDPR deletion
    # ------------------------------------------------------------------

    def purge_old_billing_charges(self, *, retain_days: int = 2555) -> int:
        """Delete billing charges older than retain_days.

        Default retention is ~7 years (2555 days) for tax compliance.

        :param retain_days: Number of days to retain.
        :returns: Number of records deleted.
        """
        cutoff = time.time() - (retain_days * 86400)
        with self._write_lock:
            cursor = self._conn.execute(
                "DELETE FROM billing_charges WHERE created_at < ?",
                (cutoff,),
            )
            self._conn.commit()
            count = cursor.rowcount
        if count > 0:
            logger.info("Purged %d billing charges older than %d days", count, retain_days)
        return count

    def purge_old_payments(self, *, retain_days: int = 2555) -> int:
        """Delete payment records older than retain_days.

        :param retain_days: Number of days to retain.
        :returns: Number of records deleted.
        """
        cutoff = time.time() - (retain_days * 86400)
        with self._write_lock:
            cursor = self._conn.execute(
                "DELETE FROM payments WHERE created_at < ?",
                (cutoff,),
            )
            self._conn.commit()
            count = cursor.rowcount
        if count > 0:
            logger.info("Purged %d payment records older than %d days", count, retain_days)
        return count

    def delete_user_billing_data(self, user_id: str) -> dict[str, int]:
        """Delete all billing data for a user (GDPR right-to-erasure).

        Removes payment methods associated with the user.

        :param user_id: The user identifier.
        :returns: Dict with counts of deleted records by table.
        """
        deleted: dict[str, int] = {}
        with self._write_lock:
            cursor = self._conn.execute(
                "DELETE FROM payment_methods WHERE user_id = ?",
                (user_id,),
            )
            deleted["payment_methods"] = cursor.rowcount

            self._conn.commit()

        logger.info(
            "Deleted billing data for user %s: %s",
            user_id,
            deleted,
        )
        return deleted

    # ------------------------------------------------------------------
    # Print history
    # ------------------------------------------------------------------

    def save_print_record(self, record: dict[str, Any]) -> int:
        """Insert a print history record and return the row id."""
        with self._write_lock:
            cur = self._conn.execute(
                """
                INSERT INTO print_history
                    (job_id, printer_name, file_name, status,
                     duration_seconds, material_type, file_hash,
                     slicer_profile, notes, agent_id, metadata,
                     started_at, completed_at, created_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    record["job_id"],
                    record["printer_name"],
                    record.get("file_name"),
                    record["status"],
                    record.get("duration_seconds"),
                    record.get("material_type"),
                    record.get("file_hash"),
                    record.get("slicer_profile"),
                    record.get("notes"),
                    record.get("agent_id"),
                    json.dumps(record["metadata"]) if record.get("metadata") else None,
                    record.get("started_at"),
                    record.get("completed_at"),
                    record.get("created_at", time.time()),
                ),
            )
            self._conn.commit()
            return cur.lastrowid  # type: ignore[return-value]

    def get_print_record(self, job_id: str) -> dict[str, Any] | None:
        """Fetch a print history record by job_id, or ``None`` if not found."""
        row = self._conn.execute(
            "SELECT * FROM print_history WHERE job_id = ? ORDER BY id DESC LIMIT 1",
            (job_id,),
        ).fetchone()
        if row is None:
            return None
        d = dict(row)
        if d.get("metadata"):
            d["metadata"] = json.loads(d["metadata"])
        return d

    def list_print_history(
        self,
        printer_name: str | None = None,
        status: str | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """Return print history records, newest first.

        Args:
            printer_name: Filter by printer name, or ``None`` for all.
            status: Filter by status string, or ``None`` for all.
            limit: Maximum rows to return.
        """
        clauses: list[str] = []
        params: list[Any] = []
        if printer_name is not None:
            clauses.append("printer_name = ?")
            params.append(printer_name)
        if status is not None:
            clauses.append("status = ?")
            params.append(status)

        where = ""
        if clauses:
            where = "WHERE " + " AND ".join(clauses)

        params.append(limit)
        rows = self._conn.execute(
            f"SELECT * FROM print_history {where} ORDER BY completed_at DESC LIMIT ?",
            params,
        ).fetchall()

        results: list[dict[str, Any]] = []
        for row in rows:
            d = dict(row)
            if d.get("metadata"):
                d["metadata"] = json.loads(d["metadata"])
            results.append(d)
        return results

    def get_printer_stats(self, printer_name: str) -> dict[str, Any]:
        """Aggregate statistics for a printer.

        Returns dict with ``total_prints``, ``success_rate``,
        ``avg_duration_seconds``, and ``total_print_hours``.
        """
        row = self._conn.execute(
            """
            SELECT
                COUNT(*) AS total_prints,
                SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS successes,
                AVG(CASE WHEN duration_seconds IS NOT NULL THEN duration_seconds END) AS avg_duration,
                COALESCE(SUM(duration_seconds), 0.0) AS total_seconds
            FROM print_history
            WHERE printer_name = ?
            """,
            (printer_name,),
        ).fetchone()
        total = row["total_prints"] if row else 0
        successes = row["successes"] if row else 0
        avg_dur = row["avg_duration"] if row else None
        total_secs = row["total_seconds"] if row else 0.0
        return {
            "printer_name": printer_name,
            "total_prints": total,
            "success_rate": round(successes / total, 4) if total > 0 else 0.0,
            "avg_duration_seconds": round(avg_dur, 1) if avg_dur is not None else None,
            "total_print_hours": round(total_secs / 3600.0, 2),
        }

    def update_print_notes(self, job_id: str, notes: str) -> bool:
        """Update the notes field on a print history record.

        Returns ``True`` if a row was updated.
        """
        with self._write_lock:
            cur = self._conn.execute(
                "UPDATE print_history SET notes = ? WHERE job_id = ?",
                (notes, job_id),
            )
            self._conn.commit()
            return cur.rowcount > 0

    # ------------------------------------------------------------------
    # Agent memory
    # ------------------------------------------------------------------

    def save_memory(
        self,
        agent_id: str,
        scope: str,
        key: str,
        value: Any,
        *,
        ttl_seconds: int | None = None,
    ) -> None:
        """Insert or replace an agent memory entry.

        The *value* is JSON-encoded before storage.  When *ttl_seconds* is
        provided the entry will expire after that many seconds.  Overwriting
        an existing key auto-increments the version number.

        Args:
            agent_id: Agent identifier.
            scope: Namespace scope (e.g. ``"global"``, ``"printer:ender3"``).
            key: Memory key name.
            value: Value to store (will be JSON-encoded).
            ttl_seconds: Optional time-to-live in seconds.  ``None`` means
                the entry never expires.
        """
        now = time.time()
        expires_at = (now + ttl_seconds) if ttl_seconds is not None else None
        with self._write_lock:
            # Read current version for auto-increment
            row = self._conn.execute(
                "SELECT version, created_at FROM agent_memory WHERE agent_id = ? AND scope = ? AND key = ?",
                (agent_id, scope, key),
            ).fetchone()
            if row is not None:
                new_version = row["version"] + 1
                original_created = row["created_at"]
            else:
                new_version = 1
                original_created = now

            self._conn.execute(
                """
                INSERT OR REPLACE INTO agent_memory
                    (agent_id, scope, key, value, created_at, updated_at,
                     version, expires_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (agent_id, scope, key, json.dumps(value), original_created, now, new_version, expires_at),
            )
            self._conn.commit()

    def get_memory(
        self,
        agent_id: str,
        scope: str,
        key: str,
    ) -> Any | None:
        """Fetch a single agent memory value, or ``None`` if not found or expired."""
        row = self._conn.execute(
            "SELECT value FROM agent_memory "
            "WHERE agent_id = ? AND scope = ? AND key = ? "
            "AND (expires_at IS NULL OR expires_at > ?)",
            (agent_id, scope, key, time.time()),
        ).fetchone()
        if row is None:
            return None
        return json.loads(row["value"])

    def list_memory(
        self,
        agent_id: str,
        scope: str | None = None,
    ) -> list[dict[str, Any]]:
        """Return all non-expired memory entries for an agent.

        Args:
            agent_id: The agent whose memory to retrieve.
            scope: Optional scope filter.
        """
        now = time.time()
        if scope is not None:
            rows = self._conn.execute(
                "SELECT * FROM agent_memory "
                "WHERE agent_id = ? AND scope = ? "
                "AND (expires_at IS NULL OR expires_at > ?) "
                "ORDER BY updated_at DESC",
                (agent_id, scope, now),
            ).fetchall()
        else:
            rows = self._conn.execute(
                "SELECT * FROM agent_memory "
                "WHERE agent_id = ? "
                "AND (expires_at IS NULL OR expires_at > ?) "
                "ORDER BY updated_at DESC",
                (agent_id, now),
            ).fetchall()
        results: list[dict[str, Any]] = []
        for row in rows:
            d = dict(row)
            d["value"] = json.loads(d["value"])
            results.append(d)
        return results

    def delete_memory(
        self,
        agent_id: str,
        scope: str,
        key: str,
    ) -> bool:
        """Delete a single agent memory entry.

        Returns ``True`` if a row was deleted.
        """
        with self._write_lock:
            cur = self._conn.execute(
                "DELETE FROM agent_memory WHERE agent_id = ? AND scope = ? AND key = ?",
                (agent_id, scope, key),
            )
            self._conn.commit()
            return cur.rowcount > 0

    def clean_expired_notes(self) -> int:
        """Delete all expired agent memory entries.

        Returns the number of rows deleted.
        """
        with self._write_lock:
            cur = self._conn.execute(
                "DELETE FROM agent_memory WHERE expires_at IS NOT NULL AND expires_at < ?",
                (time.time(),),
            )
            self._conn.commit()
            return cur.rowcount

    # ------------------------------------------------------------------
    # Print outcomes (cross-printer learning)
    # ------------------------------------------------------------------

    def save_print_outcome(self, outcome: dict[str, Any]) -> int:
        """Save an agent-curated print outcome record.  Returns row id."""
        VALID_OUTCOMES = {"success", "failed", "partial"}
        outcome_val = outcome.get("outcome", "")
        if outcome_val not in VALID_OUTCOMES:
            raise ValueError(f"Invalid outcome {outcome_val!r}. Must be one of: {sorted(VALID_OUTCOMES)}")
        if outcome.get("quality_grade") and outcome["quality_grade"] not in {"excellent", "good", "acceptable", "poor"}:
            raise ValueError(f"Invalid quality_grade {outcome['quality_grade']!r}")
        if outcome.get("failure_mode") and outcome["failure_mode"] not in {
            "spaghetti",
            "layer_shift",
            "warping",
            "adhesion",
            "stringing",
            "under_extrusion",
            "over_extrusion",
            "clog",
            "thermal_runaway",
            "power_loss",
            "mechanical",
            "other",
        }:
            raise ValueError(f"Invalid failure_mode {outcome['failure_mode']!r}")

        with self._write_lock:
            try:
                cur = self._conn.execute(
                    """INSERT INTO print_outcomes
                       (job_id, printer_name, file_name, file_hash, material_type,
                        outcome, quality_grade, failure_mode, settings, environment,
                        notes, agent_id, created_at)
                       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                    (
                        outcome["job_id"],
                        outcome["printer_name"],
                        outcome.get("file_name"),
                        outcome.get("file_hash"),
                        outcome.get("material_type"),
                        outcome["outcome"],
                        outcome.get("quality_grade"),
                        outcome.get("failure_mode"),
                        json.dumps(outcome["settings"]) if outcome.get("settings") else None,
                        json.dumps(outcome["environment"]) if outcome.get("environment") else None,
                        outcome.get("notes"),
                        outcome.get("agent_id"),
                        outcome.get("created_at", time.time()),
                    ),
                )
                self._conn.commit()
                return cur.lastrowid  # type: ignore[return-value]
            except (sqlite3.IntegrityError, Exception) as exc:
                # sqlite3.IntegrityError on SQLite; psycopg2.IntegrityError
                # (also a subclass of DB-API IntegrityError) on Postgres.
                if "integrity" in type(exc).__name__.lower() or isinstance(exc, sqlite3.IntegrityError):
                    raise ValueError(f"Outcome for job_id {outcome['job_id']!r} already recorded") from exc
                raise

    def get_print_outcome(self, job_id: str) -> dict[str, Any] | None:
        """Return the outcome record for *job_id*, or ``None``."""
        row = self._conn.execute(
            "SELECT * FROM print_outcomes WHERE job_id = ? ORDER BY created_at DESC LIMIT 1",
            (job_id,),
        ).fetchone()
        if row is None:
            return None
        return self._outcome_row_to_dict(row)

    def list_print_outcomes(
        self,
        printer_name: str | None = None,
        file_hash: str | None = None,
        outcome: str | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """Return outcome records, optionally filtered."""
        clauses: list[str] = []
        params: list[Any] = []
        if printer_name:
            clauses.append("printer_name = ?")
            params.append(printer_name)
        if file_hash:
            clauses.append("file_hash = ?")
            params.append(file_hash)
        if outcome:
            clauses.append("outcome = ?")
            params.append(outcome)
        where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
        params.append(limit)
        rows = self._conn.execute(
            f"SELECT * FROM print_outcomes{where} ORDER BY created_at DESC LIMIT ?",
            params,
        ).fetchall()
        return [self._outcome_row_to_dict(r) for r in rows]

    def get_printer_learning_insights(self, printer_name: str) -> dict[str, Any]:
        """Return aggregated outcome insights for a single printer."""
        total = self._conn.execute(
            "SELECT COUNT(*) FROM print_outcomes WHERE printer_name = ?",
            (printer_name,),
        ).fetchone()[0]
        if total == 0:
            return {
                "printer_name": printer_name,
                "total_outcomes": 0,
                "success_rate": 0.0,
                "failure_breakdown": {},
                "material_stats": {},
            }
        successes = self._conn.execute(
            "SELECT COUNT(*) FROM print_outcomes WHERE printer_name = ? AND outcome = 'success'",
            (printer_name,),
        ).fetchone()[0]
        # Failure breakdown
        failure_rows = self._conn.execute(
            "SELECT failure_mode, COUNT(*) FROM print_outcomes "
            "WHERE printer_name = ? AND outcome = 'failed' AND failure_mode IS NOT NULL "
            "GROUP BY failure_mode ORDER BY COUNT(*) DESC",
            (printer_name,),
        ).fetchall()
        failure_breakdown = {row[0]: row[1] for row in failure_rows}
        # Material stats
        material_rows = self._conn.execute(
            "SELECT material_type, "
            "  COUNT(*) as total, "
            "  SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as wins "
            "FROM print_outcomes "
            "WHERE printer_name = ? AND material_type IS NOT NULL "
            "GROUP BY material_type ORDER BY total DESC",
            (printer_name,),
        ).fetchall()
        material_stats = {}
        for row in material_rows:
            material_stats[row[0]] = {
                "count": row[1],
                "success_rate": round(row[2] / row[1], 2) if row[1] else 0.0,
            }
        return {
            "printer_name": printer_name,
            "total_outcomes": total,
            "success_rate": round(successes / total, 2) if total else 0.0,
            "failure_breakdown": failure_breakdown,
            "material_stats": material_stats,
        }

    def get_file_outcomes(self, file_hash: str) -> dict[str, Any]:
        """Return outcome data for a specific file across all printers."""
        rows = self._conn.execute(
            "SELECT printer_name, "
            "  COUNT(*) as total, "
            "  SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as wins "
            "FROM print_outcomes "
            "WHERE file_hash = ? "
            "GROUP BY printer_name ORDER BY (CAST(wins AS REAL) / total) DESC, total DESC",
            (file_hash,),
        ).fetchall()
        outcomes_by_printer = {}
        for row in rows:
            outcomes_by_printer[row[0]] = {
                "total": row[1],
                "successes": row[2],
                "success_rate": round(row[2] / row[1], 2) if row[1] else 0.0,
            }
        printers_tried = list(outcomes_by_printer.keys())
        best = printers_tried[0] if printers_tried else None
        return {
            "file_hash": file_hash,
            "printers_tried": printers_tried,
            "best_printer": best,
            "outcomes_by_printer": outcomes_by_printer,
        }

    def suggest_printer_for_outcome(
        self,
        file_hash: str | None = None,
        material_type: str | None = None,
    ) -> list[dict[str, Any]]:
        """Return printers ranked by success rate for the given criteria."""
        clauses: list[str] = []
        params: list[Any] = []
        if file_hash:
            clauses.append("file_hash = ?")
            params.append(file_hash)
        if material_type:
            clauses.append("material_type = ?")
            params.append(material_type)
        where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
        rows = self._conn.execute(
            f"SELECT printer_name, "
            f"  COUNT(*) as total, "
            f"  SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as wins "
            f"FROM print_outcomes{where} "
            f"GROUP BY printer_name "
            f"ORDER BY (CAST(wins AS REAL) / total) DESC, total DESC",
            params,
        ).fetchall()
        results = []
        for row in rows:
            results.append(
                {
                    "printer_name": row[0],
                    "total_prints": row[1],
                    "successes": row[2],
                    "success_rate": round(row[2] / row[1], 2) if row[1] else 0.0,
                }
            )
        return results

    def get_successful_settings(
        self,
        printer_name: str | None = None,
        material_type: str | None = None,
        file_hash: str | None = None,
        limit: int = 20,
    ) -> list[dict[str, Any]]:
        """Return settings from successful print outcomes.

        Filters by printer, material, and/or file hash. Returns only
        outcomes where outcome='success' and settings is not NULL,
        ordered by quality_grade (excellent > good > acceptable > poor)
        then by created_at descending.
        """
        clauses: list[str] = ["outcome = 'success'", "settings IS NOT NULL"]
        params: list[Any] = []
        if printer_name:
            clauses.append("printer_name = ?")
            params.append(printer_name)
        if material_type:
            clauses.append("material_type = ?")
            params.append(material_type)
        if file_hash:
            clauses.append("file_hash = ?")
            params.append(file_hash)
        where = " WHERE " + " AND ".join(clauses)
        params.append(limit)
        rows = self._conn.execute(
            f"SELECT * FROM print_outcomes{where} "
            "ORDER BY "
            "CASE quality_grade "
            "  WHEN 'excellent' THEN 0 "
            "  WHEN 'good' THEN 1 "
            "  WHEN 'acceptable' THEN 2 "
            "  WHEN 'poor' THEN 3 "
            "  ELSE 4 "
            "END, created_at DESC LIMIT ?",
            params,
        ).fetchall()
        return [self._outcome_row_to_dict(r) for r in rows]

    def _outcome_row_to_dict(self, row) -> dict[str, Any]:
        """Convert a print_outcomes row to a dictionary."""
        d = dict(row)
        if d.get("settings"):
            d["settings"] = json.loads(d["settings"])
        if d.get("environment"):
            d["environment"] = json.loads(d["environment"])
        return d

    # ------------------------------------------------------------------
    # Model Cache
    # ------------------------------------------------------------------

    def save_cache_entry(self, entry) -> None:
        """Insert a model cache entry.

        Args:
            entry: A :class:`ModelCacheEntry` dataclass instance.
        """
        import json as _json

        with self._write_lock:
            self._conn.execute(
                """
                INSERT OR REPLACE INTO model_cache
                    (cache_id, file_name, file_path, file_hash,
                     file_size_bytes, source, source_id, prompt,
                     tags, dimensions, print_count, last_printed_at,
                     created_at, metadata)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    entry.cache_id,
                    entry.file_name,
                    entry.file_path,
                    entry.file_hash,
                    entry.file_size_bytes,
                    entry.source,
                    entry.source_id,
                    entry.prompt,
                    _json.dumps(entry.tags) if entry.tags else "[]",
                    _json.dumps(entry.dimensions) if entry.dimensions else None,
                    entry.print_count,
                    entry.last_printed_at,
                    entry.created_at,
                    _json.dumps(entry.metadata) if entry.metadata else "{}",
                ),
            )
            self._conn.commit()

    def _cache_row_to_entry(self, row):
        """Convert a model_cache row to a ModelCacheEntry."""
        from kiln.model_cache import ModelCacheEntry

        d = dict(row)
        tags = json.loads(d["tags"]) if d.get("tags") else []
        dimensions = json.loads(d["dimensions"]) if d.get("dimensions") else None
        metadata = json.loads(d["metadata"]) if d.get("metadata") else {}
        return ModelCacheEntry(
            cache_id=d["cache_id"],
            file_name=d["file_name"],
            file_path=d["file_path"],
            file_hash=d["file_hash"],
            file_size_bytes=d["file_size_bytes"],
            source=d["source"],
            source_id=d.get("source_id"),
            prompt=d.get("prompt"),
            tags=tags,
            dimensions=dimensions,
            print_count=d.get("print_count", 0),
            last_printed_at=d.get("last_printed_at"),
            created_at=d["created_at"],
            metadata=metadata,
        )

    def get_cache_entry(self, cache_id: str):
        """Return a ModelCacheEntry by cache_id, or ``None``."""
        row = self._conn.execute("SELECT * FROM model_cache WHERE cache_id = ?", (cache_id,)).fetchone()
        if row is None:
            return None
        return self._cache_row_to_entry(row)

    def get_cache_entry_by_hash(self, file_hash: str):
        """Return a ModelCacheEntry by file_hash, or ``None``."""
        row = self._conn.execute(
            "SELECT * FROM model_cache WHERE file_hash = ? LIMIT 1",
            (file_hash,),
        ).fetchone()
        if row is None:
            return None
        return self._cache_row_to_entry(row)

    def search_cache(
        self,
        *,
        query: str | None = None,
        source: str | None = None,
        tags: list[str] | None = None,
        limit: int = 20,
    ):
        """Search cached models by name, source, tags, or prompt text."""
        clauses: list[str] = []
        params: list[Any] = []

        if query:
            like_q = f"%{query}%"
            clauses.append("(file_name LIKE ? OR prompt LIKE ? OR tags LIKE ?)")
            params.extend([like_q, like_q, like_q])

        if source:
            clauses.append("source = ?")
            params.append(source)

        if tags:
            for tag in tags:
                clauses.append("tags LIKE ?")
                params.append(f"%{tag}%")

        where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
        params.append(limit)
        rows = self._conn.execute(
            f"SELECT * FROM model_cache{where} ORDER BY created_at DESC LIMIT ?",
            params,
        ).fetchall()
        return [self._cache_row_to_entry(r) for r in rows]

    def list_cache_entries(self, *, limit: int = 50, offset: int = 0):
        """List all cached models, newest first."""
        rows = self._conn.execute(
            "SELECT * FROM model_cache ORDER BY created_at DESC LIMIT ? OFFSET ?",
            (limit, offset),
        ).fetchall()
        return [self._cache_row_to_entry(r) for r in rows]

    def record_cache_print(self, cache_id: str) -> None:
        """Increment print_count and update last_printed_at."""
        now = time.time()
        with self._write_lock:
            self._conn.execute(
                "UPDATE model_cache SET print_count = print_count + 1, last_printed_at = ? WHERE cache_id = ?",
                (now, cache_id),
            )
            self._conn.commit()

    def delete_cache_entry(self, cache_id: str) -> bool:
        """Delete a model cache entry by ID. Returns True if a row was deleted."""
        with self._write_lock:
            cur = self._conn.execute("DELETE FROM model_cache WHERE cache_id = ?", (cache_id,))
            self._conn.commit()
            return cur.rowcount > 0

    # ------------------------------------------------------------------
    # Cleanup & Maintenance
    # ------------------------------------------------------------------

    def cleanup(self, max_age_days: int = 90) -> dict[str, int]:
        """Delete old completed/failed jobs and events, then VACUUM.

        Parameters:
            max_age_days: Records older than this many days are purged.

        Returns:
            A dict with ``"jobs_deleted"``, ``"events_deleted"`` counts.
        """
        cutoff = time.time() - (max_age_days * 86400)
        jobs_deleted = 0
        events_deleted = 0

        with self._write_lock:
            cursor = self._conn.execute(
                "DELETE FROM jobs WHERE status IN ('completed', 'failed', 'cancelled') AND submitted_at < ?",
                (cutoff,),
            )
            jobs_deleted = cursor.rowcount

            cursor = self._conn.execute(
                "DELETE FROM events WHERE timestamp < ?",
                (cutoff,),
            )
            events_deleted = cursor.rowcount

            self._conn.commit()
            self._conn.execute("VACUUM")

        return {"jobs_deleted": jobs_deleted, "events_deleted": events_deleted}

    def db_size_bytes(self) -> int:
        """Return the current size of the database file in bytes."""
        try:
            return os.path.getsize(self._db_path)
        except OSError:
            return 0

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------
    # Safety audit log
    # ------------------------------------------------------------------

    def _get_hmac_key(self) -> bytes:
        """Return the HMAC key for audit log signing.

        Uses ``KILN_AUDIT_HMAC_KEY`` env var if set, otherwise loads (or
        generates) a random 32-byte key persisted at
        ``~/.kiln/audit_hmac.key`` with ``0o600`` permissions.
        """
        env_key = os.environ.get("KILN_AUDIT_HMAC_KEY", "")
        if env_key:
            return env_key.encode("utf-8")

        key_path = os.path.join(str(Path.home()), ".kiln", "audit_hmac.key")
        try:
            if os.path.isfile(key_path):
                with open(key_path, "rb") as fh:
                    key = fh.read()
                if len(key) >= 32:
                    return key

            # Generate a new random HMAC key.
            key = os.urandom(32)
            key_dir = os.path.dirname(key_path)
            os.makedirs(key_dir, mode=0o700, exist_ok=True)
            with open(key_path, "wb") as fh:
                fh.write(key)
            os.chmod(key_path, stat.S_IRUSR | stat.S_IWUSR)  # 0o600
            logger.info("Generated new audit HMAC key at %s", key_path)
            return key
        except OSError as exc:
            logger.warning(
                "Could not read/write HMAC key file %s (%s); "
                "falling back to db-path-derived key",
                key_path,
                exc,
            )
            return hashlib.sha256(self._db_path.encode("utf-8")).digest()

    def _compute_audit_hmac(self, row_data: dict[str, Any]) -> str:
        """Compute an HMAC-SHA256 signature for an audit log row.

        :param row_data: Dict with keys: timestamp, tool_name, safety_level,
            action, agent_id, printer_name, details.
        :returns: Hex-encoded HMAC digest.
        """
        key = self._get_hmac_key()
        # Build a canonical message from the row fields.
        parts = [
            str(row_data.get("timestamp", "")),
            str(row_data.get("tool_name", "")),
            str(row_data.get("safety_level", "")),
            str(row_data.get("action", "")),
            str(row_data.get("agent_id", "") or ""),
            str(row_data.get("printer_name", "") or ""),
            str(row_data.get("details", "") or ""),
        ]
        message = "|".join(parts).encode("utf-8")
        return hmac.new(key, message, hashlib.sha256).hexdigest()

    def log_audit(
        self,
        tool_name: str,
        safety_level: str,
        action: str,
        agent_id: str | None = None,
        printer_name: str | None = None,
        details: dict[str, Any] | None = None,
        session_id: str | None = None,
    ) -> int:
        """Record a safety audit event and return the row id.

        Each row is signed with an HMAC-SHA256 digest computed over its
        fields **and** linked to the previous row via a SHA-256 hash chain
        stored in the ``prev_hash`` column.  The HMAC detects modification
        of individual entries, while the hash chain detects *deletion* —
        removing any row breaks the chain for all subsequent entries.

        Args:
            tool_name: MCP tool name (e.g. ``"start_print"``).
            safety_level: Safety classification (``"safe"``, ``"guarded"``,
                ``"confirm"``, ``"emergency"``).
            action: What happened — ``"executed"``, ``"blocked"``,
                ``"rate_limited"``, ``"auth_denied"``, ``"preflight_failed"``,
                or ``"dry_run"``.
            agent_id: Optional identifier for the calling agent.
            printer_name: Optional printer name involved.
            details: Optional dict of extra context (args, error messages).
            session_id: Optional UUID grouping all tool calls in one agent session.
        """
        ts = time.time()
        ts_str = str(ts)
        with self._write_lock:
            details_json = json.dumps(details) if details else None
            hmac_sig = self._compute_audit_hmac(
                {
                    "timestamp": ts,
                    "tool_name": tool_name,
                    "safety_level": safety_level,
                    "action": action,
                    "agent_id": agent_id,
                    "printer_name": printer_name,
                    "details": details_json,
                }
            )

            # Fetch the hash of the most recent entry to form the chain.
            prev_row = self._conn.execute(
                "SELECT prev_hash FROM safety_audit_log ORDER BY id DESC LIMIT 1"
            ).fetchone()
            last_hash = dict(prev_row).get("prev_hash", "") if prev_row else ""

            # Compute chain hash: sha256(prev_hash | tool | action | session_id | timestamp)
            chain_input = "|".join([
                last_hash,
                tool_name,
                action,
                session_id or "",
                ts_str,
            ])
            chain_hash = hashlib.sha256(chain_input.encode("utf-8")).hexdigest()

            cur = self._conn.execute(
                """
                INSERT INTO safety_audit_log
                    (timestamp, tool_name, safety_level, action,
                     agent_id, printer_name, details, hmac_signature,
                     session_id, prev_hash)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    ts,
                    tool_name,
                    safety_level,
                    action,
                    agent_id,
                    printer_name,
                    details_json,
                    hmac_sig,
                    session_id,
                    chain_hash,
                ),
            )
            self._conn.commit()
            return cur.lastrowid  # type: ignore[return-value]

    def verify_audit_log(self) -> dict[str, Any]:
        """Verify HMAC signatures **and** the SHA-256 hash chain.

        HMAC verification detects *modification* of individual entries.
        Hash-chain verification detects *deletion* — removing any row
        breaks the chain for all subsequent entries.

        :returns: Dict with ``total``, ``valid``, ``invalid`` counts,
            ``integrity`` (``"ok"`` or ``"compromised"``), plus
            ``verified`` (bool), ``total_entries`` (int),
            ``broken_at`` (int or None), and ``hash_chain_intact`` (bool).
        """
        rows = self._conn.execute("SELECT * FROM safety_audit_log ORDER BY id").fetchall()
        total = len(rows)
        valid = 0
        invalid = 0

        # Hash chain state
        hash_chain_intact = True
        broken_at: int | None = None
        last_hash = ""

        for idx, row in enumerate(rows):
            d = dict(row)

            # --- HMAC verification (existing) ---
            stored_sig = d.get("hmac_signature")
            if stored_sig is None:
                invalid += 1
            else:
                expected = self._compute_audit_hmac(
                    {
                        "timestamp": d["timestamp"],
                        "tool_name": d["tool_name"],
                        "safety_level": d["safety_level"],
                        "action": d["action"],
                        "agent_id": d.get("agent_id"),
                        "printer_name": d.get("printer_name"),
                        "details": d.get("details"),
                    }
                )
                if hmac.compare_digest(stored_sig, expected):
                    valid += 1
                else:
                    invalid += 1

            # --- Hash chain verification ---
            stored_hash = d.get("prev_hash", "")
            if stored_hash:
                # Recompute expected chain hash from previous hash + row fields.
                chain_input = "|".join([
                    last_hash,
                    d["tool_name"],
                    d["action"],
                    d.get("session_id") or "",
                    str(d["timestamp"]),
                ])
                expected_hash = hashlib.sha256(chain_input.encode("utf-8")).hexdigest()
                if stored_hash != expected_hash and hash_chain_intact:
                    hash_chain_intact = False
                    broken_at = idx
                last_hash = stored_hash
            else:
                # Legacy row without prev_hash — reset chain baseline.
                last_hash = ""

        return {
            "total": total,
            "valid": valid,
            "invalid": invalid,
            "integrity": "ok" if invalid == 0 and hash_chain_intact else "compromised",
            "verified": invalid == 0 and hash_chain_intact,
            "total_entries": total,
            "broken_at": broken_at,
            "hash_chain_intact": hash_chain_intact,
        }

    def query_audit(
        self,
        action: str | None = None,
        tool_name: str | None = None,
        session_id: str | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """Query the safety audit log, newest first.

        Args:
            action: Filter by action type (e.g. ``"blocked"``).
            tool_name: Filter by tool name.
            session_id: Filter by agent session UUID.
            limit: Maximum rows to return.
        """
        clauses: list[str] = []
        params: list[Any] = []
        if action is not None:
            clauses.append("action = ?")
            params.append(action)
        if tool_name is not None:
            clauses.append("tool_name = ?")
            params.append(tool_name)
        if session_id is not None:
            clauses.append("session_id = ?")
            params.append(session_id)

        where = ""
        if clauses:
            where = "WHERE " + " AND ".join(clauses)

        params.append(limit)
        rows = self._conn.execute(
            f"SELECT * FROM safety_audit_log {where} ORDER BY id DESC LIMIT ?",
            params,
        ).fetchall()

        results: list[dict[str, Any]] = []
        for row in rows:
            d = dict(row)
            if d.get("details"):
                d["details"] = json.loads(d["details"])
            results.append(d)
        return results

    def audit_summary(self, window_seconds: float = 3600.0) -> dict[str, Any]:
        """Return a summary of audit activity within a time window.

        Args:
            window_seconds: Look-back window in seconds (default 1 hour).
        """
        cutoff = time.time() - window_seconds
        rows = self._conn.execute(
            "SELECT action, COUNT(*) as cnt FROM safety_audit_log WHERE timestamp > ? GROUP BY action",
            (cutoff,),
        ).fetchall()
        counts = {row["action"]: row["cnt"] for row in rows}

        recent_blocked = self._conn.execute(
            "SELECT tool_name, details, timestamp FROM safety_audit_log "
            "WHERE action IN ('blocked', 'rate_limited', 'auth_denied') "
            "AND timestamp > ? ORDER BY id DESC LIMIT 10",
            (cutoff,),
        ).fetchall()
        blocked_list = []
        for row in recent_blocked:
            entry = {
                "tool": row["tool_name"],
                "timestamp": row["timestamp"],
            }
            if row["details"]:
                entry["details"] = json.loads(row["details"])
            blocked_list.append(entry)

        return {
            "window_seconds": window_seconds,
            "counts": counts,
            "recent_blocked": blocked_list,
            "total": sum(counts.values()),
        }

    @staticmethod
    def _sanitize_csv_value(value: Any) -> Any:
        """Prefix cell values that could trigger spreadsheet formula injection.

        Values starting with ``=``, ``+``, ``-``, ``@``, ``\\t``, or ``\\r``
        are prefixed with a single-quote to neutralise formula evaluation in
        Excel / Google Sheets / LibreOffice Calc.
        """
        if isinstance(value, str) and value and value[0] in ("=", "+", "-", "@", "\t", "\r"):
            return "'" + value
        return value

    def export_audit_trail(
        self,
        *,
        start_time: float | None = None,
        end_time: float | None = None,
        format: str = "json",
        tool_name: str | None = None,
        action: str | None = None,
        session_id: str | None = None,
        limit: int = 10000,
        offset: int = 0,
    ) -> str:
        """Export audit trail entries as JSON or CSV.

        Args:
            start_time: Unix timestamp lower bound (inclusive).
            end_time: Unix timestamp upper bound (inclusive).
            format: ``"json"`` or ``"csv"``.
            tool_name: Filter by tool name.
            action: Filter by action type.
            session_id: Filter by session ID.
            limit: Maximum number of rows to return (default 10 000).
            offset: Number of rows to skip for pagination (default 0).

        Returns:
            Formatted string (JSON array or CSV with headers).

        Raises:
            ValueError: If *format* is not ``"json"`` or ``"csv"``.
        """
        _VALID_FORMATS = ("json", "csv")
        if format not in _VALID_FORMATS:
            raise ValueError(
                f"Unsupported export format {format!r}. "
                f"Expected one of: {', '.join(_VALID_FORMATS)}"
            )

        conditions: list[str] = []
        params: list[Any] = []

        if start_time is not None:
            conditions.append("timestamp >= ?")
            params.append(start_time)
        if end_time is not None:
            conditions.append("timestamp <= ?")
            params.append(end_time)
        if tool_name:
            conditions.append("tool_name = ?")
            params.append(tool_name)
        if action:
            conditions.append("action = ?")
            params.append(action)
        if session_id:
            conditions.append("session_id = ?")
            params.append(session_id)

        where = f" WHERE {' AND '.join(conditions)}" if conditions else ""
        sql = f"SELECT * FROM safety_audit_log{where} ORDER BY timestamp DESC LIMIT ? OFFSET ?"
        params.extend([limit, offset])

        rows = self._conn.execute(sql, params).fetchall()
        row_dicts = [dict(r) for r in rows]

        if format == "csv":
            import csv
            import io

            output = io.StringIO()
            if row_dicts:
                # Sanitise cell values against formula injection.
                sanitised = [
                    {k: self._sanitize_csv_value(v) for k, v in row.items()}
                    for row in row_dicts
                ]
                writer = csv.DictWriter(output, fieldnames=sanitised[0].keys())
                writer.writeheader()
                writer.writerows(sanitised)
            return output.getvalue()

        # Default: JSON
        import json as json_mod

        return json_mod.dumps(row_dicts, indent=2, default=str)

    # ------------------------------------------------------------------
    # Snapshot persistence
    # ------------------------------------------------------------------

    def save_snapshot(
        self,
        printer_name: str,
        image_path: str,
        *,
        job_id: str | None = None,
        phase: str = "unknown",
        image_size_bytes: int | None = None,
        analysis: str | None = None,
        agent_notes: str | None = None,
        confidence: float | None = None,
        completion_pct: float | None = None,
    ) -> int:
        """Persist a snapshot record and return its row ID.

        :param printer_name: Printer that captured the snapshot.
        :param image_path: Filesystem path to the saved image file.
        :param job_id: Associated print job ID (if known).
        :param phase: Print phase at capture time (e.g. "first_layer", "mid_print", "final_layer").
        :param image_size_bytes: Size of the image file in bytes.
        :param analysis: JSON-encoded analysis result from vision model.
        :param agent_notes: Free-form notes from the monitoring agent.
        :param confidence: Vision model confidence score (0.0–1.0).
        :param completion_pct: Print completion percentage at capture time.
        :returns: The auto-incremented snapshot row ID.
        """
        with self._write_lock:
            cur = self._conn.execute(
                """
                INSERT INTO snapshots
                    (job_id, printer_name, phase, image_path, image_size_bytes,
                     analysis, agent_notes, confidence, completion_pct, created_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    job_id,
                    printer_name,
                    phase,
                    image_path,
                    image_size_bytes,
                    analysis,
                    agent_notes,
                    confidence,
                    completion_pct,
                    time.time(),
                ),
            )
            self._conn.commit()
            return cur.lastrowid

    def get_snapshots(
        self,
        *,
        job_id: str | None = None,
        printer_name: str | None = None,
        phase: str | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """Query snapshot records with optional filters.

        :param job_id: Filter by job ID.
        :param printer_name: Filter by printer name.
        :param phase: Filter by capture phase.
        :param limit: Maximum number of records to return.
        :returns: List of snapshot dicts, newest first.
        """
        clauses: list[str] = []
        params: list[Any] = []
        if job_id is not None:
            clauses.append("job_id = ?")
            params.append(job_id)
        if printer_name is not None:
            clauses.append("printer_name = ?")
            params.append(printer_name)
        if phase is not None:
            clauses.append("phase = ?")
            params.append(phase)
        where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
        params.append(limit)
        rows = self._conn.execute(
            f"SELECT * FROM snapshots{where} ORDER BY created_at DESC LIMIT ?",
            params,
        ).fetchall()
        return [dict(r) for r in rows]

    def delete_snapshots(
        self,
        *,
        job_id: str | None = None,
        older_than: float | None = None,
    ) -> int:
        """Delete snapshot records matching filters. Returns count deleted.

        :param job_id: Delete snapshots for this job.
        :param older_than: Delete snapshots created before this Unix timestamp.
        :returns: Number of rows deleted.
        """
        clauses: list[str] = []
        params: list[Any] = []
        if job_id is not None:
            clauses.append("job_id = ?")
            params.append(job_id)
        if older_than is not None:
            clauses.append("created_at < ?")
            params.append(older_than)
        if not clauses:
            return 0
        where = " WHERE " + " AND ".join(clauses)
        with self._write_lock:
            cur = self._conn.execute(f"DELETE FROM snapshots{where}", params)
            self._conn.commit()
            return cur.rowcount

    # ------------------------------------------------------------------
    # Fulfillment order helpers
    # ------------------------------------------------------------------

    def list_active_fulfillment_orders(self) -> list[dict[str, Any]]:
        """Return fulfillment orders that are not yet delivered/failed/cancelled."""
        rows = self._conn.execute(
            "SELECT * FROM fulfillment_orders "
            "WHERE status NOT IN ('delivered', 'completed', 'failed', 'cancelled', 'canceled') "
            "ORDER BY created_at DESC"
        ).fetchall()
        return [dict(r) for r in rows]

    def update_fulfillment_order_status(self, order_id: str, status: str) -> None:
        """Update the status of a fulfillment order."""
        with self._write_lock:
            self._conn.execute(
                "UPDATE fulfillment_orders SET status = ?, updated_at = ? WHERE order_id = ?",
                (status, time.time(), order_id),
            )
            self._conn.commit()

    # ------------------------------------------------------------------

    def close(self) -> None:
        """Close the database connection."""
        self._conn.close()

    @property
    def path(self) -> str:
        """The filesystem path of the database file."""
        return self._db_path


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

_db: KilnDB | None = None


def get_db() -> KilnDB:
    """Return the module-level :class:`KilnDB` singleton.

    The instance is lazily created on first call.
    """
    global _db
    if _db is None:
        _db = KilnDB()
    return _db
