"""SQLite-backed session storage."""

from __future__ import annotations

from contextlib import closing
import json
import re
import sqlite3
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
import threading
from typing import Any


def _utc_now() -> str:
    return datetime.now(timezone.utc).isoformat()


@dataclass(slots=True)
class SessionState:
    """Stored session metadata."""

    session_id: str
    provider_id: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)
    created_at: str | None = None
    updated_at: str | None = None


@dataclass(slots=True)
class SessionCompaction:
    """Stored session compaction summary."""

    id: int
    session_id: str
    start_message_id: int | None
    end_message_id: int
    summary: str
    message_count: int
    estimated_tokens: int | None = None
    provider_id: str | None = None
    model_name: str | None = None
    created_at: str | None = None


@dataclass(slots=True)
class UserRecord:
    """Stored canonical user."""

    user_id: str
    role: str
    metadata: dict[str, Any] = field(default_factory=dict)
    created_at: str | None = None
    updated_at: str | None = None


@dataclass(slots=True)
class UserIdentityBinding:
    """Stored external identity binding."""

    id: int
    channel: str
    external_id: str
    user_id: str
    user_role: str
    metadata: dict[str, Any] = field(default_factory=dict)
    created_at: str | None = None
    updated_at: str | None = None


@dataclass(slots=True)
class MemoryItemRecord:
    """Stored long-term memory item."""

    id: int
    scope_type: str
    scope_id: str
    memory_type: str
    content: str
    conflict_key: str | None = None
    source_session_id: str | None = None
    source_message_id: str | None = None
    confidence: float = 0.0
    metadata: dict[str, Any] = field(default_factory=dict)
    created_at: str | None = None
    updated_at: str | None = None
    last_recalled_at: str | None = None


@dataclass(slots=True)
class SessionStore:
    """SQLite-backed chat history and session metadata."""

    path: Path
    _lock: threading.RLock = field(default_factory=threading.RLock)

    def __post_init__(self) -> None:
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self._initialize()

    def _connect(self) -> sqlite3.Connection:
        connection = sqlite3.connect(self.path, check_same_thread=False)
        connection.row_factory = sqlite3.Row
        return connection

    def _initialize(self) -> None:
        with self._lock:
            with closing(self._connect()) as connection:
                connection.execute("PRAGMA journal_mode=WAL")
                connection.execute(
                    """
                    CREATE TABLE IF NOT EXISTS sessions (
                        session_id TEXT PRIMARY KEY,
                        provider_id TEXT,
                        metadata_json TEXT NOT NULL DEFAULT '{}',
                        created_at TEXT NOT NULL,
                        updated_at TEXT NOT NULL
                    )
                    """
                )
                connection.commit()
                connection.execute(
                    """
                    CREATE TABLE IF NOT EXISTS users (
                        user_id TEXT PRIMARY KEY,
                        role TEXT NOT NULL,
                        metadata_json TEXT NOT NULL DEFAULT '{}',
                        created_at TEXT NOT NULL,
                        updated_at TEXT NOT NULL
                    )
                    """
                )
                connection.execute(
                    """
                    CREATE TABLE IF NOT EXISTS user_identities (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        channel TEXT NOT NULL,
                        external_id TEXT NOT NULL,
                        user_id TEXT NOT NULL,
                        metadata_json TEXT NOT NULL DEFAULT '{}',
                        created_at TEXT NOT NULL,
                        updated_at TEXT NOT NULL,
                        UNIQUE(channel, external_id)
                    )
                    """
                )
                connection.execute(
                    """
                    CREATE TABLE IF NOT EXISTS messages (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        session_id TEXT NOT NULL,
                        role TEXT NOT NULL,
                        content TEXT,
                        payload_json TEXT NOT NULL,
                        created_at TEXT NOT NULL
                    )
                    """
                )
                connection.execute(
                    """
                    CREATE TABLE IF NOT EXISTS session_compactions (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        session_id TEXT NOT NULL,
                        start_message_id INTEGER,
                        end_message_id INTEGER NOT NULL,
                        summary TEXT NOT NULL,
                        message_count INTEGER NOT NULL,
                        estimated_tokens INTEGER,
                        provider_id TEXT,
                        model_name TEXT,
                        created_at TEXT NOT NULL
                    )
                    """
                )
                connection.execute(
                    """
                    CREATE INDEX IF NOT EXISTS idx_user_identities_user_id
                    ON user_identities (user_id, id)
                    """
                )
                connection.execute(
                    """
                    CREATE INDEX IF NOT EXISTS idx_messages_session_id_id
                    ON messages (session_id, id)
                    """
                )
                connection.execute(
                    """
                    CREATE INDEX IF NOT EXISTS idx_session_compactions_session_id_id
                    ON session_compactions (session_id, id)
                    """
                )
                connection.execute(
                    """
                    CREATE TABLE IF NOT EXISTS memory_items (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        scope_type TEXT NOT NULL,
                        scope_id TEXT NOT NULL,
                        memory_type TEXT NOT NULL,
                        content TEXT NOT NULL,
                        conflict_key TEXT,
                        source_session_id TEXT,
                        source_message_id TEXT,
                        confidence REAL NOT NULL DEFAULT 0,
                        metadata_json TEXT NOT NULL DEFAULT '{}',
                        created_at TEXT NOT NULL,
                        updated_at TEXT NOT NULL,
                        last_recalled_at TEXT
                    )
                    """
                )
                connection.execute(
                    """
                    CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_conflict
                    ON memory_items (scope_type, scope_id, memory_type, conflict_key)
                    WHERE conflict_key IS NOT NULL
                    """
                )
                connection.execute(
                    """
                    CREATE INDEX IF NOT EXISTS idx_memory_items_scope
                    ON memory_items (scope_type, scope_id, memory_type, updated_at)
                    """
                )
                try:
                    connection.execute(
                        """
                        CREATE VIRTUAL TABLE IF NOT EXISTS memory_items_fts
                        USING fts5(
                            content,
                            content='memory_items',
                            content_rowid='id',
                            tokenize='unicode61'
                        )
                        """
                    )
                    connection.execute(
                        """
                        CREATE TRIGGER IF NOT EXISTS memory_items_ai
                        AFTER INSERT ON memory_items
                        BEGIN
                            INSERT INTO memory_items_fts(rowid, content)
                            VALUES (new.id, new.content);
                        END
                        """
                    )
                    connection.execute(
                        """
                        CREATE TRIGGER IF NOT EXISTS memory_items_ad
                        AFTER DELETE ON memory_items
                        BEGIN
                            INSERT INTO memory_items_fts(memory_items_fts, rowid, content)
                            VALUES ('delete', old.id, old.content);
                        END
                        """
                    )
                    connection.execute(
                        """
                        CREATE TRIGGER IF NOT EXISTS memory_items_au
                        AFTER UPDATE ON memory_items
                        BEGIN
                            INSERT INTO memory_items_fts(memory_items_fts, rowid, content)
                            VALUES ('delete', old.id, old.content);
                            INSERT INTO memory_items_fts(rowid, content)
                            VALUES (new.id, new.content);
                        END
                        """
                    )
                    connection.execute(
                        """
                        INSERT INTO memory_items_fts(memory_items_fts)
                        VALUES ('rebuild')
                        """
                    )
                except sqlite3.OperationalError:
                    pass
                connection.commit()

    def ensure_session(
        self,
        session_id: str,
        *,
        provider_id: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> SessionState:
        """Ensure a session row exists and return it."""
        now = _utc_now()
        with self._lock:
            with closing(self._connect()) as connection:
                connection.execute(
                    """
                    INSERT INTO sessions (session_id, provider_id, metadata_json, created_at, updated_at)
                    VALUES (?, ?, ?, ?, ?)
                    ON CONFLICT(session_id) DO NOTHING
                    """,
                    (
                        session_id,
                        provider_id,
                        json.dumps(metadata or {}, ensure_ascii=False),
                        now,
                        now,
                    ),
                )
                connection.commit()
        return self.get_session_state(session_id) or SessionState(session_id=session_id)

    def get_session_state(self, session_id: str) -> SessionState | None:
        """Load one session state."""
        with self._lock:
            with closing(self._connect()) as connection:
                row = connection.execute(
                    """
                    SELECT session_id, provider_id, metadata_json, created_at, updated_at
                    FROM sessions
                    WHERE session_id = ?
                    """,
                    (session_id,),
                ).fetchone()
        if row is None:
            return None
        metadata = json.loads(row["metadata_json"] or "{}")
        if not isinstance(metadata, dict):
            metadata = {}
        return SessionState(
            session_id=str(row["session_id"]),
            provider_id=str(row["provider_id"]) if row["provider_id"] else None,
            metadata=metadata,
            created_at=str(row["created_at"]),
            updated_at=str(row["updated_at"]),
        )

    def update_session_state(
        self,
        session_id: str,
        *,
        provider_id: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> SessionState:
        """Merge and persist session metadata."""
        current = self.get_session_state(session_id)
        merged_metadata = dict(current.metadata if current else {})
        if metadata:
            merged_metadata.update(metadata)
        resolved_provider = (
            provider_id
            if provider_id is not None
            else (current.provider_id if current else None)
        )

        now = _utc_now()
        with self._lock:
            with closing(self._connect()) as connection:
                connection.execute(
                    """
                    INSERT INTO sessions (session_id, provider_id, metadata_json, created_at, updated_at)
                    VALUES (?, ?, ?, ?, ?)
                    ON CONFLICT(session_id) DO UPDATE SET
                        provider_id = excluded.provider_id,
                        metadata_json = excluded.metadata_json,
                        updated_at = excluded.updated_at
                    """,
                    (
                        session_id,
                        resolved_provider,
                        json.dumps(merged_metadata, ensure_ascii=False),
                        current.created_at if current and current.created_at else now,
                        now,
                    ),
                )
                connection.commit()
        return self.get_session_state(session_id) or SessionState(session_id=session_id)

    def update_runtime_status(
        self,
        session_id: str,
        *,
        status: str,
        run_id: str | None = None,
        source_mode: str | None = None,
        channel: str | None = None,
    ) -> SessionState:
        """Persist the current runtime status for one session."""
        metadata = {
            "runtime_status": status,
            "runtime_status_updated_at": _utc_now(),
        }
        if run_id:
            metadata["active_run_id"] = run_id
        elif status != "replying":
            metadata["active_run_id"] = None
        if source_mode:
            metadata["source_mode"] = source_mode
        if channel:
            metadata["channel"] = channel
        return self.update_session_state(session_id, metadata=metadata)

    def ensure_user(
        self,
        user_id: str,
        *,
        role: str = "user",
        metadata: dict[str, Any] | None = None,
    ) -> UserRecord:
        """Ensure one canonical user exists."""
        normalized_user_id = user_id.strip()
        if not normalized_user_id:
            raise ValueError("user_id is required")
        normalized_role = role.strip().lower() or "user"
        now = _utc_now()
        current = self.get_user(normalized_user_id)
        merged_metadata = dict(current.metadata if current else {})
        if metadata:
            merged_metadata.update(metadata)
        resolved_role = normalized_role or (current.role if current else "user")
        with self._lock:
            with closing(self._connect()) as connection:
                connection.execute(
                    """
                    INSERT INTO users (user_id, role, metadata_json, created_at, updated_at)
                    VALUES (?, ?, ?, ?, ?)
                    ON CONFLICT(user_id) DO UPDATE SET
                        role = excluded.role,
                        metadata_json = excluded.metadata_json,
                        updated_at = excluded.updated_at
                    """,
                    (
                        normalized_user_id,
                        resolved_role,
                        json.dumps(merged_metadata, ensure_ascii=False),
                        current.created_at if current and current.created_at else now,
                        now,
                    ),
                )
                connection.commit()
        return self.get_user(normalized_user_id) or UserRecord(
            user_id=normalized_user_id,
            role=resolved_role,
            metadata=merged_metadata,
            created_at=now,
            updated_at=now,
        )

    def get_user(self, user_id: str) -> UserRecord | None:
        """Load one canonical user."""
        normalized_user_id = user_id.strip()
        if not normalized_user_id:
            return None
        with self._lock:
            with closing(self._connect()) as connection:
                row = connection.execute(
                    """
                    SELECT user_id, role, metadata_json, created_at, updated_at
                    FROM users
                    WHERE user_id = ?
                    """,
                    (normalized_user_id,),
                ).fetchone()
        if row is None:
            return None
        metadata = json.loads(row["metadata_json"] or "{}")
        if not isinstance(metadata, dict):
            metadata = {}
        return UserRecord(
            user_id=str(row["user_id"]),
            role=str(row["role"] or "user"),
            metadata=metadata,
            created_at=str(row["created_at"]),
            updated_at=str(row["updated_at"]),
        )

    def bind_identity(
        self,
        *,
        channel: str,
        external_id: str,
        user_id: str,
        role: str = "user",
        metadata: dict[str, Any] | None = None,
    ) -> UserIdentityBinding:
        """Bind one external identity to a canonical user."""
        normalized_channel = channel.strip().lower()
        normalized_external_id = external_id.strip()
        normalized_user_id = user_id.strip()
        if not normalized_channel:
            raise ValueError("channel is required")
        if not normalized_external_id:
            raise ValueError("external_id is required")
        if not normalized_user_id:
            raise ValueError("user_id is required")

        user = self.ensure_user(normalized_user_id, role=role)
        now = _utc_now()
        current = self.get_identity_binding(
            channel=normalized_channel,
            external_id=normalized_external_id,
        )
        merged_metadata = dict(current.metadata if current else {})
        if metadata:
            merged_metadata.update(metadata)

        with self._lock:
            with closing(self._connect()) as connection:
                connection.execute(
                    """
                    INSERT INTO user_identities (
                        channel,
                        external_id,
                        user_id,
                        metadata_json,
                        created_at,
                        updated_at
                    )
                    VALUES (?, ?, ?, ?, ?, ?)
                    ON CONFLICT(channel, external_id) DO UPDATE SET
                        user_id = excluded.user_id,
                        metadata_json = excluded.metadata_json,
                        updated_at = excluded.updated_at
                    """,
                    (
                        normalized_channel,
                        normalized_external_id,
                        user.user_id,
                        json.dumps(merged_metadata, ensure_ascii=False),
                        current.created_at if current and current.created_at else now,
                        now,
                    ),
                )
                connection.commit()
        binding = self.get_identity_binding(
            channel=normalized_channel,
            external_id=normalized_external_id,
        )
        if binding is None:
            raise RuntimeError("identity binding was not persisted")
        return binding

    def get_identity_binding(
        self,
        *,
        channel: str,
        external_id: str,
    ) -> UserIdentityBinding | None:
        """Resolve one identity binding."""
        normalized_channel = channel.strip().lower()
        normalized_external_id = external_id.strip()
        if not normalized_channel or not normalized_external_id:
            return None
        with self._lock:
            with closing(self._connect()) as connection:
                row = connection.execute(
                    """
                    SELECT
                        i.id,
                        i.channel,
                        i.external_id,
                        i.user_id,
                        i.metadata_json,
                        i.created_at,
                        i.updated_at,
                        u.role AS user_role
                    FROM user_identities AS i
                    JOIN users AS u ON u.user_id = i.user_id
                    WHERE i.channel = ? AND i.external_id = ?
                    """,
                    (normalized_channel, normalized_external_id),
                ).fetchone()
        if row is None:
            return None
        metadata = json.loads(row["metadata_json"] or "{}")
        if not isinstance(metadata, dict):
            metadata = {}
        return UserIdentityBinding(
            id=int(row["id"]),
            channel=str(row["channel"]),
            external_id=str(row["external_id"]),
            user_id=str(row["user_id"]),
            user_role=str(row["user_role"] or "user"),
            metadata=metadata,
            created_at=str(row["created_at"]),
            updated_at=str(row["updated_at"]),
        )

    def append(self, message: dict[str, Any], *, session_id: str = "main") -> None:
        """Append one raw message payload."""
        self.ensure_session(session_id)
        payload = dict(message)
        content = payload.get("content")
        now = _utc_now()
        with self._lock:
            with closing(self._connect()) as connection:
                connection.execute(
                    """
                    INSERT INTO messages (session_id, role, content, payload_json, created_at)
                    VALUES (?, ?, ?, ?, ?)
                    """,
                    (
                        session_id,
                        str(payload.get("role") or "unknown"),
                        str(content) if content is not None else None,
                        json.dumps(payload, ensure_ascii=False),
                        now,
                    ),
                )
                connection.execute(
                    """
                    UPDATE sessions
                    SET updated_at = ?
                    WHERE session_id = ?
                    """,
                    (now, session_id),
                )
                connection.commit()

    def load_active_history(
        self,
        *,
        session_id: str = "main",
    ) -> list[dict[str, Any]]:
        """Load all uncompacted messages for one session."""
        self.ensure_session(session_id)
        compacted_until_id = self._get_compacted_until_message_id(session_id)
        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(
                    """
                    SELECT payload_json
                    FROM messages
                    WHERE session_id = ?
                      AND (? IS NULL OR id > ?)
                    ORDER BY id ASC
                    """,
                    (session_id, compacted_until_id, compacted_until_id),
                ).fetchall()

        messages: list[dict[str, Any]] = []
        for row in rows:
            payload = json.loads(row["payload_json"])
            message = self._to_llm_message(payload)
            if message is not None:
                messages.append(message)
        return messages

    def list_compaction_candidates(
        self,
        *,
        session_id: str = "main",
    ) -> list[dict[str, Any]]:
        """Return uncompacted stored messages including database ids."""
        self.ensure_session(session_id)
        compacted_until_id = self._get_compacted_until_message_id(session_id)
        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(
                    """
                    SELECT id, payload_json
                    FROM messages
                    WHERE session_id = ?
                      AND (? IS NULL OR id > ?)
                    ORDER BY id ASC
                    """,
                    (session_id, compacted_until_id, compacted_until_id),
                ).fetchall()

        payloads: list[dict[str, Any]] = []
        for row in rows:
            payload = json.loads(row["payload_json"])
            payload["_message_id"] = int(row["id"])
            payloads.append(payload)
        return payloads

    def get_latest_compaction(self, session_id: str) -> SessionCompaction | None:
        """Return the latest compaction summary for one session."""
        with self._lock:
            with closing(self._connect()) as connection:
                row = connection.execute(
                    """
                    SELECT
                        id,
                        session_id,
                        start_message_id,
                        end_message_id,
                        summary,
                        message_count,
                        estimated_tokens,
                        provider_id,
                        model_name,
                        created_at
                    FROM session_compactions
                    WHERE session_id = ?
                    ORDER BY id DESC
                    LIMIT 1
                    """,
                    (session_id,),
                ).fetchone()
        if row is None:
            return None
        return SessionCompaction(
            id=int(row["id"]),
            session_id=str(row["session_id"]),
            start_message_id=(
                int(row["start_message_id"])
                if row["start_message_id"] is not None
                else None
            ),
            end_message_id=int(row["end_message_id"]),
            summary=str(row["summary"]),
            message_count=int(row["message_count"]),
            estimated_tokens=(
                int(row["estimated_tokens"])
                if row["estimated_tokens"] is not None
                else None
            ),
            provider_id=str(row["provider_id"]) if row["provider_id"] else None,
            model_name=str(row["model_name"]) if row["model_name"] else None,
            created_at=str(row["created_at"]),
        )

    def load_compaction_summary(self, session_id: str = "main") -> str | None:
        """Return the latest compaction summary text, if any."""
        latest = self.get_latest_compaction(session_id)
        if latest is None:
            return None
        return latest.summary

    def record_compaction(
        self,
        *,
        session_id: str,
        start_message_id: int | None,
        end_message_id: int,
        summary: str,
        message_count: int,
        estimated_tokens: int | None = None,
        provider_id: str | None = None,
        model_name: str | None = None,
    ) -> SessionCompaction:
        """Persist one compaction snapshot and advance the active cursor."""
        now = _utc_now()
        with self._lock:
            with closing(self._connect()) as connection:
                cursor = connection.execute(
                    """
                    INSERT INTO session_compactions (
                        session_id,
                        start_message_id,
                        end_message_id,
                        summary,
                        message_count,
                        estimated_tokens,
                        provider_id,
                        model_name,
                        created_at
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
                    """,
                    (
                        session_id,
                        start_message_id,
                        end_message_id,
                        summary,
                        message_count,
                        estimated_tokens,
                        provider_id,
                        model_name,
                        now,
                    ),
                )
                connection.commit()
                compaction_id = int(cursor.lastrowid)

        state = self.get_session_state(session_id)
        metadata = dict(state.metadata if state else {})
        metadata.update(
            {
                "compacted_until_message_id": end_message_id,
                "last_compaction_id": compaction_id,
                "last_compaction_at": now,
                "needs_compaction": False,
                "context_compaction_count": int(
                    metadata.get("context_compaction_count") or 0
                )
                + 1,
            }
        )
        self.update_session_state(session_id, metadata=metadata)
        latest = self.get_latest_compaction(session_id)
        if latest is not None:
            return latest
        return SessionCompaction(
            id=compaction_id,
            session_id=session_id,
            start_message_id=start_message_id,
            end_message_id=end_message_id,
            summary=summary,
            message_count=message_count,
            estimated_tokens=estimated_tokens,
            provider_id=provider_id,
            model_name=model_name,
            created_at=now,
        )

    def update_context_metrics(
        self,
        session_id: str,
        *,
        resolved_context_window_tokens: int | None = None,
        max_output_tokens: int | None = None,
        estimated_prompt_tokens: int | None = None,
        actual_prompt_tokens: int | None = None,
        input_budget_tokens: int | None = None,
        needs_compaction: bool | None = None,
    ) -> SessionState:
        """Persist latest context-control metrics."""
        metadata: dict[str, Any] = {}
        if resolved_context_window_tokens is not None:
            metadata["resolved_context_window_tokens"] = resolved_context_window_tokens
        if max_output_tokens is not None:
            metadata["resolved_max_output_tokens"] = max_output_tokens
        if estimated_prompt_tokens is not None:
            metadata["last_estimated_prompt_tokens"] = estimated_prompt_tokens
        if actual_prompt_tokens is not None:
            metadata["last_actual_prompt_tokens"] = actual_prompt_tokens
        if input_budget_tokens is not None:
            metadata["input_budget_tokens"] = input_budget_tokens
        if needs_compaction is not None:
            metadata["needs_compaction"] = needs_compaction
        if not metadata:
            return self.get_session_state(session_id) or SessionState(session_id=session_id)
        metadata["context_metrics_updated_at"] = _utc_now()
        return self.update_session_state(session_id, metadata=metadata)

    def load_history(
        self,
        limit: int = 24,
        *,
        session_id: str = "main",
    ) -> list[dict[str, Any]]:
        """Load chat history for LLM input."""
        self.ensure_session(session_id)
        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(
                    """
                    SELECT payload_json
                    FROM messages
                    WHERE session_id = ?
                    ORDER BY id DESC
                    LIMIT ?
                    """,
                    (session_id, limit),
                ).fetchall()
        parsed_rows = [json.loads(row["payload_json"]) for row in reversed(rows)]
        start_index = 0
        for index, row in enumerate(parsed_rows):
            if row.get("role") == "user":
                start_index = index
                break
        aligned = parsed_rows[start_index:]
        messages: list[dict[str, Any]] = []
        for row in aligned:
            message = self._to_llm_message(row)
            if message is not None:
                messages.append(message)
        return messages

    def list_sessions(self) -> list[dict[str, Any]]:
        """Return all sessions and metadata."""
        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(
                    """
                    SELECT session_id, provider_id, metadata_json, created_at, updated_at
                    FROM sessions
                    ORDER BY updated_at DESC, session_id ASC
                    """
                ).fetchall()
        sessions: list[dict[str, Any]] = []
        for row in rows:
            metadata = json.loads(row["metadata_json"] or "{}")
            sessions.append(
                {
                    "session_id": row["session_id"],
                    "provider_id": row["provider_id"],
                    "metadata": metadata if isinstance(metadata, dict) else {},
                    "created_at": row["created_at"],
                    "updated_at": row["updated_at"],
                }
            )
        return sessions

    def list_users(self) -> list[dict[str, Any]]:
        """Return all canonical users."""
        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(
                    """
                    SELECT user_id, role, metadata_json, created_at, updated_at
                    FROM users
                    ORDER BY user_id ASC
                    """
                ).fetchall()
        users: list[dict[str, Any]] = []
        for row in rows:
            metadata = json.loads(row["metadata_json"] or "{}")
            users.append(
                {
                    "user_id": str(row["user_id"]),
                    "role": str(row["role"] or "user"),
                    "metadata": metadata if isinstance(metadata, dict) else {},
                    "created_at": str(row["created_at"]),
                    "updated_at": str(row["updated_at"]),
                }
            )
        return users

    def list_identities(self) -> list[dict[str, Any]]:
        """Return all stored external identity bindings."""
        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(
                    """
                    SELECT
                        i.id,
                        i.channel,
                        i.external_id,
                        i.user_id,
                        i.metadata_json,
                        i.created_at,
                        i.updated_at,
                        u.role AS user_role
                    FROM user_identities AS i
                    JOIN users AS u ON u.user_id = i.user_id
                    ORDER BY i.channel ASC, i.external_id ASC
                    """
                ).fetchall()
        identities: list[dict[str, Any]] = []
        for row in rows:
            metadata = json.loads(row["metadata_json"] or "{}")
            identities.append(
                {
                    "id": int(row["id"]),
                    "channel": str(row["channel"]),
                    "external_id": str(row["external_id"]),
                    "user_id": str(row["user_id"]),
                    "user_role": str(row["user_role"] or "user"),
                    "metadata": metadata if isinstance(metadata, dict) else {},
                    "created_at": str(row["created_at"]),
                    "updated_at": str(row["updated_at"]),
                }
            )
        return identities

    def list_messages(
        self,
        *,
        session_id: str | None = None,
        limit: int | None = None,
    ) -> list[dict[str, Any]]:
        """Return stored messages."""
        query = """
            SELECT id, session_id, role, content, payload_json, created_at
            FROM messages
        """
        params: list[Any] = []
        clauses: list[str] = []
        if session_id:
            clauses.append("session_id = ?")
            params.append(session_id)
        if clauses:
            query += " WHERE " + " AND ".join(clauses)
        query += " ORDER BY id ASC"
        if limit is not None:
            query += " LIMIT ?"
            params.append(limit)

        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(query, params).fetchall()

        messages: list[dict[str, Any]] = []
        for row in rows:
            payload = json.loads(row["payload_json"])
            messages.append(
                {
                    "id": row["id"],
                    "session_id": row["session_id"],
                    "role": row["role"],
                    "content": row["content"],
                    "payload": payload,
                    "created_at": row["created_at"],
                }
            )
        return messages

    def upsert_memory_item(
        self,
        *,
        existing_id: int | None = None,
        scope_type: str,
        scope_id: str,
        memory_type: str,
        content: str,
        conflict_key: str | None = None,
        source_session_id: str | None = None,
        source_message_id: str | None = None,
        confidence: float = 0.0,
        metadata: dict[str, Any] | None = None,
    ) -> MemoryItemRecord:
        """Insert or update one long-term memory item."""
        normalized_scope_type = scope_type.strip().lower()
        normalized_scope_id = scope_id.strip()
        normalized_memory_type = memory_type.strip().lower()
        normalized_content = content.strip()
        normalized_conflict_key = (
            conflict_key.strip().lower() if conflict_key and conflict_key.strip() else None
        )
        if not normalized_scope_type:
            raise ValueError("scope_type is required")
        if not normalized_scope_id:
            raise ValueError("scope_id is required")
        if not normalized_memory_type:
            raise ValueError("memory_type is required")
        if not normalized_content:
            raise ValueError("content is required")

        now = _utc_now()
        with self._lock:
            with closing(self._connect()) as connection:
                resolved_existing_id = int(existing_id) if existing_id and int(existing_id) > 0 else None
                existing_conflict_key = normalized_conflict_key
                if resolved_existing_id is None and normalized_conflict_key:
                    row = connection.execute(
                        """
                        SELECT id
                        FROM memory_items
                        WHERE scope_type = ?
                          AND scope_id = ?
                          AND memory_type = ?
                          AND conflict_key = ?
                        LIMIT 1
                        """,
                        (
                            normalized_scope_type,
                            normalized_scope_id,
                            normalized_memory_type,
                            normalized_conflict_key,
                        ),
                    ).fetchone()
                    if row is not None:
                        resolved_existing_id = int(row["id"])
                if resolved_existing_id is not None and existing_conflict_key is None:
                    row = connection.execute(
                        """
                        SELECT conflict_key
                        FROM memory_items
                        WHERE id = ?
                        LIMIT 1
                        """,
                        (resolved_existing_id,),
                    ).fetchone()
                    if row is not None and row["conflict_key"]:
                        existing_conflict_key = str(row["conflict_key"])
                if resolved_existing_id is None:
                    cursor = connection.execute(
                        """
                        INSERT INTO memory_items (
                            scope_type,
                            scope_id,
                            memory_type,
                            content,
                            conflict_key,
                            source_session_id,
                            source_message_id,
                            confidence,
                            metadata_json,
                            created_at,
                            updated_at
                        )
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                        """,
                        (
                            normalized_scope_type,
                            normalized_scope_id,
                            normalized_memory_type,
                            normalized_content,
                            existing_conflict_key,
                            source_session_id,
                            source_message_id,
                            float(confidence),
                            json.dumps(metadata or {}, ensure_ascii=False),
                            now,
                            now,
                        ),
                    )
                    record_id = int(cursor.lastrowid)
                else:
                    connection.execute(
                        """
                        UPDATE memory_items
                        SET
                            scope_type = ?,
                            scope_id = ?,
                            memory_type = ?,
                            content = ?,
                            conflict_key = ?,
                            source_session_id = ?,
                            source_message_id = ?,
                            confidence = ?,
                            metadata_json = ?,
                            updated_at = ?
                        WHERE id = ?
                        """,
                        (
                            normalized_scope_type,
                            normalized_scope_id,
                            normalized_memory_type,
                            normalized_content,
                            existing_conflict_key,
                            source_session_id,
                            source_message_id,
                            float(confidence),
                            json.dumps(metadata or {}, ensure_ascii=False),
                            now,
                            resolved_existing_id,
                        ),
                    )
                    record_id = resolved_existing_id
                connection.commit()
        record = self.get_memory_item(record_id)
        if record is None:
            raise RuntimeError("memory item was not persisted")
        return record

    def get_memory_item(self, memory_id: int) -> MemoryItemRecord | None:
        """Load one memory item by id."""
        with self._lock:
            with closing(self._connect()) as connection:
                row = connection.execute(
                    """
                    SELECT
                        id,
                        scope_type,
                        scope_id,
                        memory_type,
                        content,
                        conflict_key,
                        source_session_id,
                        source_message_id,
                        confidence,
                        metadata_json,
                        created_at,
                        updated_at,
                        last_recalled_at
                    FROM memory_items
                    WHERE id = ?
                    """,
                    (memory_id,),
                ).fetchone()
        return self._row_to_memory_item(row)

    def list_memory_items(
        self,
        *,
        scopes: list[tuple[str, str]] | None = None,
        memory_types: list[str] | None = None,
        limit: int | None = 20,
    ) -> list[MemoryItemRecord]:
        """List memory items ordered by freshness."""
        query = """
            SELECT
                id,
                scope_type,
                scope_id,
                memory_type,
                content,
                conflict_key,
                source_session_id,
                source_message_id,
                confidence,
                metadata_json,
                created_at,
                updated_at,
                last_recalled_at
            FROM memory_items
        """
        clauses: list[str] = []
        params: list[Any] = []
        scope_clause, scope_params = self._build_scope_clause(scopes)
        if scope_clause:
            clauses.append(scope_clause)
            params.extend(scope_params)
        if memory_types:
            placeholders = ", ".join("?" for _ in memory_types)
            clauses.append(f"memory_type IN ({placeholders})")
            params.extend(item.strip().lower() for item in memory_types if item.strip())
        if clauses:
            query += " WHERE " + " AND ".join(clauses)
        query += " ORDER BY updated_at DESC, id DESC"
        if limit is not None:
            query += " LIMIT ?"
            params.append(max(1, limit))
        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(query, params).fetchall()
        return [
            item
            for item in (self._row_to_memory_item(row) for row in rows)
            if item is not None
        ]

    def export_memory_items(
        self,
        *,
        scopes: list[tuple[str, str]] | None = None,
        memory_types: list[str] | None = None,
    ) -> dict[str, Any]:
        """Return a JSON-serializable export of all matching memory items."""
        items = self.list_memory_items(
            scopes=scopes,
            memory_types=memory_types,
            limit=None,
        )
        return {
            "database_path": str(self.path),
            "memory_count": len(items),
            "memory_items": [
                {
                    "id": item.id,
                    "scope_type": item.scope_type,
                    "scope_id": item.scope_id,
                    "memory_type": item.memory_type,
                    "content": item.content,
                    "conflict_key": item.conflict_key,
                    "source_session_id": item.source_session_id,
                    "source_message_id": item.source_message_id,
                    "confidence": item.confidence,
                    "metadata": item.metadata,
                    "created_at": item.created_at,
                    "updated_at": item.updated_at,
                    "last_recalled_at": item.last_recalled_at,
                }
                for item in items
            ],
        }

    def search_memory(
        self,
        query: str,
        *,
        scopes: list[tuple[str, str]] | None = None,
        limit: int = 5,
        mark_recalled: bool = True,
    ) -> list[dict[str, Any]]:
        """Search memory items with FTS5 and scope filtering."""
        normalized_query = self._build_memory_match_query(query)
        if not normalized_query:
            return []
        scope_clause, scope_params = self._build_scope_clause(scopes, table_alias="m")
        sql = """
            SELECT
                m.id,
                m.scope_type,
                m.scope_id,
                m.memory_type,
                m.content,
                m.conflict_key,
                m.source_session_id,
                m.source_message_id,
                m.confidence,
                m.metadata_json,
                m.created_at,
                m.updated_at,
                m.last_recalled_at,
                bm25(memory_items_fts) AS rank
            FROM memory_items_fts
            JOIN memory_items AS m ON m.id = memory_items_fts.rowid
            WHERE memory_items_fts MATCH ?
        """
        params: list[Any] = [normalized_query]
        if scope_clause:
            sql += f" AND {scope_clause}"
            params.extend(scope_params)
        sql += " ORDER BY rank ASC, m.updated_at DESC, m.id DESC LIMIT ?"
        params.append(max(1, limit))
        try:
            with self._lock:
                with closing(self._connect()) as connection:
                    rows = connection.execute(sql, params).fetchall()
        except sqlite3.OperationalError:
            return self._search_memory_fallback(
                query,
                scopes=scopes,
                limit=limit,
                mark_recalled=mark_recalled,
            )
        items: list[dict[str, Any]] = []
        ids: list[int] = []
        for row in rows:
            record = self._row_to_memory_item(row)
            if record is None:
                continue
            ids.append(record.id)
            items.append(
                {
                    "id": record.id,
                    "scope_type": record.scope_type,
                    "scope_id": record.scope_id,
                    "memory_type": record.memory_type,
                    "content": record.content,
                    "confidence": record.confidence,
                    "metadata": record.metadata,
                    "updated_at": record.updated_at,
                    "score": float(row["rank"]) * -1.0,
                }
            )
        if ids and mark_recalled:
            self.mark_memory_recalled(ids)
        return items

    def mark_memory_recalled(self, memory_ids: list[int]) -> None:
        """Mark memory items as recently recalled."""
        normalized_ids = sorted({int(item) for item in memory_ids if int(item) > 0})
        if not normalized_ids:
            return
        placeholders = ", ".join("?" for _ in normalized_ids)
        now = _utc_now()
        with self._lock:
            with closing(self._connect()) as connection:
                connection.execute(
                    f"""
                    UPDATE memory_items
                    SET last_recalled_at = ?
                    WHERE id IN ({placeholders})
                    """,
                    [now, *normalized_ids],
                )
                connection.commit()

    def dump_data(
        self,
        *,
        session_id: str | None = None,
        limit: int | None = None,
    ) -> dict[str, Any]:
        """Return a JSON-serializable dump of stored data."""
        sessions = self.list_sessions()
        if session_id is not None:
            sessions = [item for item in sessions if item["session_id"] == session_id]
        return {
            "database_path": str(self.path),
            "sessions": sessions,
            "users": self.list_users(),
            "identities": self.list_identities(),
            "messages": self.list_messages(session_id=session_id, limit=limit),
            "compactions": self.list_compactions(session_id=session_id),
            "memory_items": [
                {
                    "id": item.id,
                    "scope_type": item.scope_type,
                    "scope_id": item.scope_id,
                    "memory_type": item.memory_type,
                    "content": item.content,
                    "conflict_key": item.conflict_key,
                    "source_session_id": item.source_session_id,
                    "source_message_id": item.source_message_id,
                    "confidence": item.confidence,
                    "metadata": item.metadata,
                    "created_at": item.created_at,
                    "updated_at": item.updated_at,
                    "last_recalled_at": item.last_recalled_at,
                }
                for item in self.list_memory_items(limit=limit or 100)
            ],
        }

    def list_compactions(
        self,
        *,
        session_id: str | None = None,
    ) -> list[dict[str, Any]]:
        """Return stored compaction snapshots."""
        query = """
            SELECT
                id,
                session_id,
                start_message_id,
                end_message_id,
                summary,
                message_count,
                estimated_tokens,
                provider_id,
                model_name,
                created_at
            FROM session_compactions
        """
        params: list[Any] = []
        if session_id:
            query += " WHERE session_id = ?"
            params.append(session_id)
        query += " ORDER BY id ASC"
        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(query, params).fetchall()
        return [
            {
                "id": int(row["id"]),
                "session_id": str(row["session_id"]),
                "start_message_id": (
                    int(row["start_message_id"])
                    if row["start_message_id"] is not None
                    else None
                ),
                "end_message_id": int(row["end_message_id"]),
                "summary": str(row["summary"]),
                "message_count": int(row["message_count"]),
                "estimated_tokens": (
                    int(row["estimated_tokens"])
                    if row["estimated_tokens"] is not None
                    else None
                ),
                "provider_id": str(row["provider_id"]) if row["provider_id"] else None,
                "model_name": str(row["model_name"]) if row["model_name"] else None,
                "created_at": str(row["created_at"]),
            }
            for row in rows
        ]

    @staticmethod
    def _to_llm_message(row: dict[str, Any]) -> dict[str, Any] | None:
        role = row.get("role")
        if role not in {"user", "assistant", "tool"}:
            return None

        message: dict[str, Any] = {"role": role, "content": row.get("content")}
        if role == "assistant" and row.get("tool_calls"):
            message["tool_calls"] = row["tool_calls"]
        if role == "tool":
            message["tool_call_id"] = row.get("tool_call_id")
            message["name"] = row.get("name")
        return message

    def _get_compacted_until_message_id(self, session_id: str) -> int | None:
        state = self.get_session_state(session_id)
        if state is None:
            return None
        value = state.metadata.get("compacted_until_message_id")
        if value in (None, ""):
            return None
        try:
            return int(value)
        except (TypeError, ValueError):
            return None

    @staticmethod
    def _build_memory_match_query(query: str) -> str:
        stripped_query = query.strip().lower()
        tokens = [token.strip() for token in re.findall(r"[\w\-/.:]+", stripped_query)]
        unique_tokens: list[str] = []
        seen: set[str] = set()
        for token in tokens:
            if len(token) < 2 or token in seen:
                continue
            seen.add(token)
            unique_tokens.append(token)
        if unique_tokens:
            return " OR ".join(unique_tokens[:8])
        return stripped_query

    @staticmethod
    def _build_scope_clause(
        scopes: list[tuple[str, str]] | None,
        *,
        table_alias: str = "",
    ) -> tuple[str, list[Any]]:
        normalized = [
            (scope_type.strip().lower(), scope_id.strip())
            for scope_type, scope_id in (scopes or [])
            if scope_type.strip() and scope_id.strip()
        ]
        if not normalized:
            return "", []
        prefix = f"{table_alias}." if table_alias else ""
        clauses = [
            f"({prefix}scope_type = ? AND {prefix}scope_id = ?)"
            for _ in normalized
        ]
        params: list[Any] = []
        for scope_type, scope_id in normalized:
            params.extend([scope_type, scope_id])
        return "(" + " OR ".join(clauses) + ")", params

    def _search_memory_fallback(
        self,
        query: str,
        *,
        scopes: list[tuple[str, str]] | None = None,
        limit: int,
        mark_recalled: bool = True,
    ) -> list[dict[str, Any]]:
        like_value = f"%{query.strip()}%"
        sql = """
            SELECT
                id,
                scope_type,
                scope_id,
                memory_type,
                content,
                conflict_key,
                source_session_id,
                source_message_id,
                confidence,
                metadata_json,
                created_at,
                updated_at,
                last_recalled_at
            FROM memory_items
            WHERE content LIKE ?
        """
        params: list[Any] = [like_value]
        scope_clause, scope_params = self._build_scope_clause(scopes)
        if scope_clause:
            sql += f" AND {scope_clause}"
            params.extend(scope_params)
        sql += " ORDER BY updated_at DESC, id DESC LIMIT ?"
        params.append(max(1, limit))
        with self._lock:
            with closing(self._connect()) as connection:
                rows = connection.execute(sql, params).fetchall()
        items: list[dict[str, Any]] = []
        ids: list[int] = []
        for row in rows:
            record = self._row_to_memory_item(row)
            if record is None:
                continue
            ids.append(record.id)
            items.append(
                {
                    "id": record.id,
                    "scope_type": record.scope_type,
                    "scope_id": record.scope_id,
                    "memory_type": record.memory_type,
                    "content": record.content,
                    "confidence": record.confidence,
                    "metadata": record.metadata,
                    "updated_at": record.updated_at,
                    "score": record.confidence,
                }
            )
        if ids and mark_recalled:
            self.mark_memory_recalled(ids)
        return items

    @staticmethod
    def _row_to_memory_item(row: sqlite3.Row | None) -> MemoryItemRecord | None:
        if row is None:
            return None
        metadata = json.loads(row["metadata_json"] or "{}")
        if not isinstance(metadata, dict):
            metadata = {}
        return MemoryItemRecord(
            id=int(row["id"]),
            scope_type=str(row["scope_type"]),
            scope_id=str(row["scope_id"]),
            memory_type=str(row["memory_type"]),
            content=str(row["content"]),
            conflict_key=str(row["conflict_key"]) if row["conflict_key"] else None,
            source_session_id=(
                str(row["source_session_id"]) if row["source_session_id"] else None
            ),
            source_message_id=(
                str(row["source_message_id"]) if row["source_message_id"] else None
            ),
            confidence=float(row["confidence"] or 0.0),
            metadata=metadata,
            created_at=str(row["created_at"]) if row["created_at"] else None,
            updated_at=str(row["updated_at"]) if row["updated_at"] else None,
            last_recalled_at=(
                str(row["last_recalled_at"]) if row["last_recalled_at"] else None
            ),
        )
