"""
SQLite storage backend - the default store.

Stores notes, MemCells, MemScenes, skills, agents, conversations,
and memory edges in a local SQLite database. Zero configuration, works out of the box.

Thread-safety: Each thread gets its own SQLite connection via threading.local().
WAL mode is enabled so readers and writers don't block each other.
"""

import json
import sqlite3
import threading
import time
from typing import List, Optional
from openmemo.storage.base_store import BaseStore


class SQLiteStore(BaseStore):
    def __init__(self, db_path: str = "openmemo.db", check_same_thread: bool = True):
        self.db_path = db_path
        self._local = threading.local()
        self._create_tables()

    @property
    def conn(self) -> sqlite3.Connection:
        """Return a thread-local SQLite connection, creating one if needed."""
        if not hasattr(self._local, "conn") or self._local.conn is None:
            c = sqlite3.connect(self.db_path, check_same_thread=False)
            c.row_factory = sqlite3.Row
            c.execute("PRAGMA journal_mode=WAL")
            c.execute("PRAGMA synchronous=NORMAL")
            c.execute("PRAGMA foreign_keys=ON")
            self._local.conn = c
        return self._local.conn

    def _create_tables(self):
        cursor = self.conn.cursor()
        cursor.executescript("""
            CREATE TABLE IF NOT EXISTS notes (
                id TEXT PRIMARY KEY,
                content TEXT NOT NULL,
                source TEXT DEFAULT 'manual',
                agent_id TEXT DEFAULT '',
                scene TEXT DEFAULT '',
                scope TEXT DEFAULT 'private',
                conversation_id TEXT DEFAULT '',
                timestamp REAL,
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS cells (
                id TEXT PRIMARY KEY,
                note_id TEXT,
                content TEXT NOT NULL,
                cell_type TEXT DEFAULT 'fact',
                facts TEXT DEFAULT '[]',
                stage TEXT DEFAULT 'exploration',
                importance REAL DEFAULT 0.5,
                access_count INTEGER DEFAULT 0,
                last_accessed REAL,
                created_at REAL,
                agent_id TEXT DEFAULT '',
                scene TEXT DEFAULT '',
                scope TEXT DEFAULT 'private',
                conversation_id TEXT DEFAULT '',
                connections TEXT DEFAULT '[]',
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS scenes (
                id TEXT PRIMARY KEY,
                title TEXT,
                summary TEXT,
                cell_ids TEXT DEFAULT '[]',
                theme TEXT,
                agent_id TEXT DEFAULT '',
                created_at REAL,
                updated_at REAL,
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS skills (
                id TEXT PRIMARY KEY,
                name TEXT,
                description TEXT,
                pattern TEXT,
                scene TEXT DEFAULT '',
                trigger TEXT DEFAULT '',
                steps TEXT DEFAULT '[]',
                tools TEXT DEFAULT '[]',
                confidence REAL DEFAULT 0.0,
                usage_count INTEGER DEFAULT 0,
                success_rate REAL DEFAULT 0.0,
                skill_version INTEGER DEFAULT 1,
                status TEXT DEFAULT 'active',
                created_at REAL,
                updated_at REAL,
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS skill_feedback (
                feedback_id TEXT PRIMARY KEY,
                skill_id TEXT,
                result TEXT DEFAULT '',
                success INTEGER DEFAULT 0,
                timestamp REAL
            );

            CREATE TABLE IF NOT EXISTS pyramid (
                id TEXT PRIMARY KEY,
                tier TEXT,
                content TEXT,
                source_ids TEXT DEFAULT '[]',
                created_at REAL,
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS agents (
                agent_id TEXT PRIMARY KEY,
                agent_type TEXT DEFAULT '',
                description TEXT DEFAULT '',
                created_at REAL
            );

            CREATE TABLE IF NOT EXISTS conversations (
                conversation_id TEXT PRIMARY KEY,
                agent_id TEXT DEFAULT '',
                scene TEXT DEFAULT '',
                started_at REAL,
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS memory_edges (
                edge_id TEXT PRIMARY KEY,
                memory_a TEXT NOT NULL,
                memory_b TEXT NOT NULL,
                relation_type TEXT NOT NULL,
                confidence REAL DEFAULT 0.5,
                created_at REAL,
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS outcomes (
                outcome_id TEXT PRIMARY KEY,
                experience_id TEXT DEFAULT '',
                workflow_id TEXT DEFAULT '',
                task_id TEXT DEFAULT '',
                agent_id TEXT DEFAULT '',
                result TEXT DEFAULT 'unknown',
                metrics TEXT DEFAULT '{}',
                agent_feedback TEXT DEFAULT '',
                score REAL DEFAULT -1.0,
                timestamp REAL,
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS experience_versions (
                version_id TEXT PRIMARY KEY,
                experience_id TEXT NOT NULL,
                version INTEGER NOT NULL,
                parent_version INTEGER DEFAULT 0,
                snapshot TEXT DEFAULT '{}',
                reason TEXT DEFAULT '',
                triggered_by TEXT DEFAULT 'system',
                status TEXT DEFAULT 'active',
                created_at REAL
            );

            CREATE TABLE IF NOT EXISTS experiences (
                id TEXT PRIMARY KEY,
                title TEXT DEFAULT '',
                type TEXT DEFAULT 'workflow',
                source_node_ids TEXT DEFAULT '[]',
                steps TEXT DEFAULT '[]',
                outcome TEXT DEFAULT '',
                key_facts TEXT DEFAULT '[]',
                conditions TEXT DEFAULT '{}',
                confidence REAL DEFAULT 0.7,
                scene TEXT DEFAULT '',
                agent_id TEXT DEFAULT '',
                created_at REAL,
                usage_count INTEGER DEFAULT 0,
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS workflow_templates (
                id TEXT PRIMARY KEY,
                name TEXT DEFAULT '',
                trigger TEXT DEFAULT '',
                steps TEXT DEFAULT '[]',
                experience_id TEXT DEFAULT '',
                scene TEXT DEFAULT '',
                agent_id TEXT DEFAULT '',
                success_rate REAL DEFAULT 0.0,
                usage_count INTEGER DEFAULT 0,
                created_at REAL,
                metadata TEXT DEFAULT '{}'
            );

            CREATE TABLE IF NOT EXISTS observations (
                id TEXT PRIMARY KEY,
                type TEXT DEFAULT 'observation',
                agent_id TEXT DEFAULT '',
                task_id TEXT DEFAULT '',
                content TEXT DEFAULT '{}',
                context TEXT DEFAULT '{}',
                timestamp REAL,
                confidence REAL DEFAULT 0.9
            );

            CREATE TABLE IF NOT EXISTS memory_nodes (
                id TEXT PRIMARY KEY,
                type TEXT DEFAULT 'raw',
                content TEXT NOT NULL,
                parent_ids TEXT DEFAULT '[]',
                child_ids TEXT DEFAULT '[]',
                agent_id TEXT DEFAULT '',
                scene TEXT DEFAULT '',
                task_id TEXT DEFAULT '',
                timestamp REAL,
                importance REAL DEFAULT 0.5,
                obs_id TEXT DEFAULT '',
                metadata TEXT DEFAULT '{}'
            );
        """)
        self.conn.commit()
        self._migrate()

    def _migrate(self):
        cursor = self.conn.cursor()
        for table, col, col_def in [
            ("notes", "agent_id", "TEXT DEFAULT ''"),
            ("notes", "scene", "TEXT DEFAULT ''"),
            ("notes", "scope", "TEXT DEFAULT 'private'"),
            ("notes", "conversation_id", "TEXT DEFAULT ''"),
            ("cells", "agent_id", "TEXT DEFAULT ''"),
            ("cells", "scene", "TEXT DEFAULT ''"),
            ("cells", "cell_type", "TEXT DEFAULT 'fact'"),
            ("cells", "scope", "TEXT DEFAULT 'private'"),
            ("cells", "conversation_id", "TEXT DEFAULT ''"),
            ("scenes", "agent_id", "TEXT DEFAULT ''"),
            ("notes", "team_id", "TEXT DEFAULT ''"),
            ("notes", "task_id", "TEXT DEFAULT ''"),
            ("cells", "team_id", "TEXT DEFAULT ''"),
            ("cells", "task_id", "TEXT DEFAULT ''"),
            ("skills", "scene", "TEXT DEFAULT ''"),
            ("skills", "trigger", "TEXT DEFAULT ''"),
            ("skills", "steps", "TEXT DEFAULT '[]'"),
            ("skills", "tools", "TEXT DEFAULT '[]'"),
            ("skills", "confidence", "REAL DEFAULT 0.0"),
            ("skills", "skill_version", "INTEGER DEFAULT 1"),
            ("skills", "status", "TEXT DEFAULT 'active'"),
            ("skills", "updated_at", "REAL"),
        ]:
            try:
                cursor.execute(f"ALTER TABLE {table} ADD COLUMN {col} {col_def}")
            except sqlite3.OperationalError:
                pass

        cursor.executescript("""
            CREATE TABLE IF NOT EXISTS skill_feedback (
                feedback_id TEXT PRIMARY KEY,
                skill_id TEXT,
                result TEXT DEFAULT '',
                success INTEGER DEFAULT 0,
                timestamp REAL
            );
        """)
        self.conn.commit()

    def put_note(self, note: dict) -> str:
        note_id = note.get("id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR REPLACE INTO notes (id, content, source, agent_id, scene, scope, conversation_id, team_id, task_id, timestamp, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (note_id, note.get("content", ""), note.get("source", "manual"),
             note.get("agent_id", ""), note.get("scene", ""),
             note.get("scope", "private"), note.get("conversation_id", ""),
             note.get("team_id", ""), note.get("task_id", ""),
             note.get("timestamp", 0), json.dumps(note.get("metadata", {})))
        )
        self.conn.commit()
        return note_id

    def get_note(self, note_id: str) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM notes WHERE id = ?", (note_id,))
        row = cursor.fetchone()
        if not row:
            return None
        return self._row_to_note(row)

    def list_notes(self, limit: int = 100, offset: int = 0, agent_id: str = None) -> List[dict]:
        cursor = self.conn.cursor()
        if agent_id:
            cursor.execute("SELECT * FROM notes WHERE agent_id = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
                           (agent_id, limit, offset))
        else:
            cursor.execute("SELECT * FROM notes ORDER BY timestamp DESC LIMIT ? OFFSET ?", (limit, offset))
        return [self._row_to_note(row) for row in cursor.fetchall()]

    def delete_note(self, note_id: str) -> bool:
        cursor = self.conn.cursor()
        cursor.execute("DELETE FROM notes WHERE id = ?", (note_id,))
        self.conn.commit()
        return cursor.rowcount > 0

    def put_cell(self, cell: dict) -> str:
        cell_id = cell.get("id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            """INSERT OR REPLACE INTO cells
            (id, note_id, content, cell_type, facts, stage, importance, access_count,
             last_accessed, created_at, agent_id, scene, scope, conversation_id,
             team_id, task_id, connections, metadata)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (cell_id, cell.get("note_id", ""), cell.get("content", ""),
             cell.get("cell_type", "fact"),
             json.dumps(cell.get("facts", [])), cell.get("stage", "exploration"),
             cell.get("importance", 0.5), cell.get("access_count", 0),
             cell.get("last_accessed", 0), cell.get("created_at", 0),
             cell.get("agent_id", ""), cell.get("scene", ""),
             cell.get("scope", "private"), cell.get("conversation_id", ""),
             cell.get("team_id", ""), cell.get("task_id", ""),
             json.dumps(cell.get("connections", [])), json.dumps(cell.get("metadata", {})))
        )
        self.conn.commit()
        return cell_id

    def get_cell(self, cell_id: str) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM cells WHERE id = ?", (cell_id,))
        row = cursor.fetchone()
        if not row:
            return None
        return self._row_to_cell(row)

    def list_cells(self, limit: int = 100, offset: int = 0,
                   agent_id: str = None, scene: str = None) -> List[dict]:
        cursor = self.conn.cursor()
        conditions = []
        params = []
        if agent_id:
            conditions.append("agent_id = ?")
            params.append(agent_id)
        if scene:
            conditions.append("scene = ?")
            params.append(scene)

        where = " WHERE " + " AND ".join(conditions) if conditions else ""
        params.extend([limit, offset])
        cursor.execute(f"SELECT * FROM cells{where} ORDER BY created_at DESC LIMIT ? OFFSET ?", params)
        return [self._row_to_cell(row) for row in cursor.fetchall()]

    def list_cells_scoped(self, agent_id: str = None, conversation_id: str = None,
                          scene: str = None, limit: int = 100,
                          team_id: str = None, task_id: str = None) -> List[dict]:
        cursor = self.conn.cursor()
        scope_conditions = []
        params = []

        if agent_id:
            scope_conditions.append("(agent_id = ? AND scope = 'private')")
            params.append(agent_id)

        if conversation_id:
            scope_conditions.append("(conversation_id = ? AND scope = 'conversation')")
            params.append(conversation_id)

        scope_conditions.append("scope = 'shared'")

        if team_id:
            scope_conditions.append("(team_id = ? AND scope = 'team')")
            params.append(team_id)
        else:
            scope_conditions.append("scope = 'team'")

        scope_clause = " OR ".join(scope_conditions)

        extra_conditions = []
        if scene:
            extra_conditions.append("scene = ?")
            params.append(scene)
        if task_id:
            extra_conditions.append("(task_id = ? OR scope = 'team')")
            params.append(task_id)

        where = f" WHERE ({scope_clause})"
        if extra_conditions:
            where += " AND " + " AND ".join(extra_conditions)

        params.append(limit)
        cursor.execute(f"SELECT * FROM cells{where} ORDER BY created_at DESC LIMIT ?", params)
        return [self._row_to_cell(row) for row in cursor.fetchall()]

    def delete_cell(self, cell_id: str) -> bool:
        cursor = self.conn.cursor()
        cursor.execute("DELETE FROM cells WHERE id = ?", (cell_id,))
        self.conn.commit()
        return cursor.rowcount > 0

    def put_scene(self, scene: dict) -> str:
        scene_id = scene.get("id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            """INSERT OR REPLACE INTO scenes
            (id, title, summary, cell_ids, theme, agent_id, created_at, updated_at, metadata)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (scene_id, scene.get("title", ""), scene.get("summary", ""),
             json.dumps(scene.get("cell_ids", [])), scene.get("theme", ""),
             scene.get("agent_id", ""),
             scene.get("created_at", 0), scene.get("updated_at", 0),
             json.dumps(scene.get("metadata", {})))
        )
        self.conn.commit()
        return scene_id

    def get_scene(self, scene_id: str) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM scenes WHERE id = ?", (scene_id,))
        row = cursor.fetchone()
        if not row:
            return None
        return self._row_to_scene(row)

    def list_scenes(self, limit: int = 100, offset: int = 0, agent_id: str = None) -> List[dict]:
        cursor = self.conn.cursor()
        if agent_id:
            cursor.execute("SELECT * FROM scenes WHERE agent_id = ? ORDER BY updated_at DESC LIMIT ? OFFSET ?",
                           (agent_id, limit, offset))
        else:
            cursor.execute("SELECT * FROM scenes ORDER BY updated_at DESC LIMIT ? OFFSET ?", (limit, offset))
        return [self._row_to_scene(row) for row in cursor.fetchall()]

    def put_skill(self, skill: dict) -> str:
        skill_id = skill.get("id", "")
        cursor = self.conn.cursor()
        steps = skill.get("steps", [])
        tools = skill.get("tools", [])
        cursor.execute(
            """INSERT OR REPLACE INTO skills
            (id, name, description, pattern, scene, trigger, steps, tools,
             confidence, usage_count, success_rate, skill_version, status,
             created_at, updated_at, metadata)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (skill_id, skill.get("name", ""), skill.get("description", ""),
             skill.get("pattern", ""), skill.get("scene", ""),
             skill.get("trigger", ""),
             json.dumps(steps) if isinstance(steps, list) else steps,
             json.dumps(tools) if isinstance(tools, list) else tools,
             skill.get("confidence", 0.0),
             skill.get("usage_count", 0), skill.get("success_rate", 0.0),
             skill.get("skill_version", 1), skill.get("status", "active"),
             skill.get("created_at", 0), skill.get("updated_at", 0),
             json.dumps(skill.get("metadata", {})))
        )
        self.conn.commit()
        return skill_id

    def get_skill(self, skill_id: str) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM skills WHERE id = ?", (skill_id,))
        row = cursor.fetchone()
        if not row:
            return None
        return self._row_to_skill(row)

    def list_skills(self, scene: str = "", status: str = "") -> List[dict]:
        cursor = self.conn.cursor()
        conditions = []
        params = []
        if scene:
            conditions.append("scene = ?")
            params.append(scene)
        if status:
            conditions.append("status = ?")
            params.append(status)

        query = "SELECT * FROM skills"
        if conditions:
            query += " WHERE " + " AND ".join(conditions)
        query += " ORDER BY usage_count DESC"
        cursor.execute(query, params)
        return [self._row_to_skill(row) for row in cursor.fetchall()]

    def delete_skill(self, skill_id: str) -> bool:
        cursor = self.conn.cursor()
        cursor.execute("DELETE FROM skills WHERE id = ?", (skill_id,))
        self.conn.commit()
        return cursor.rowcount > 0

    def put_skill_feedback(self, feedback: dict) -> str:
        fid = feedback.get("feedback_id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            """INSERT OR REPLACE INTO skill_feedback
            (feedback_id, skill_id, result, success, timestamp)
            VALUES (?, ?, ?, ?, ?)""",
            (fid, feedback.get("skill_id", ""), feedback.get("result", ""),
             1 if feedback.get("success") else 0,
             feedback.get("timestamp", 0))
        )
        self.conn.commit()
        return fid

    def list_skill_feedback(self, skill_id: str = "") -> List[dict]:
        cursor = self.conn.cursor()
        if skill_id:
            cursor.execute(
                "SELECT * FROM skill_feedback WHERE skill_id = ? ORDER BY timestamp DESC",
                (skill_id,))
        else:
            cursor.execute("SELECT * FROM skill_feedback ORDER BY timestamp DESC")
        results = []
        for row in cursor.fetchall():
            results.append({
                "feedback_id": row["feedback_id"],
                "skill_id": row["skill_id"],
                "result": row["result"],
                "success": bool(row["success"]),
                "timestamp": row["timestamp"],
            })
        return results

    def put_agent(self, agent: dict) -> str:
        agent_id = agent.get("agent_id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR REPLACE INTO agents (agent_id, agent_type, description, created_at) VALUES (?, ?, ?, ?)",
            (agent_id, agent.get("agent_type", ""), agent.get("description", ""),
             agent.get("created_at", time.time()))
        )
        self.conn.commit()
        return agent_id

    def get_agent(self, agent_id: str) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM agents WHERE agent_id = ?", (agent_id,))
        row = cursor.fetchone()
        if not row:
            return None
        return {
            "agent_id": row["agent_id"],
            "agent_type": row["agent_type"],
            "description": row["description"],
            "created_at": row["created_at"],
        }

    def list_agents(self) -> List[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM agents ORDER BY created_at DESC")
        return [{
            "agent_id": row["agent_id"],
            "agent_type": row["agent_type"],
            "description": row["description"],
            "created_at": row["created_at"],
        } for row in cursor.fetchall()]

    def delete_agent(self, agent_id: str) -> bool:
        cursor = self.conn.cursor()
        cursor.execute("DELETE FROM agents WHERE agent_id = ?", (agent_id,))
        self.conn.commit()
        return cursor.rowcount > 0

    def put_conversation(self, conversation: dict) -> str:
        conv_id = conversation.get("conversation_id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR REPLACE INTO conversations (conversation_id, agent_id, scene, started_at, metadata) VALUES (?, ?, ?, ?, ?)",
            (conv_id, conversation.get("agent_id", ""), conversation.get("scene", ""),
             conversation.get("started_at", time.time()),
             json.dumps(conversation.get("metadata", {})))
        )
        self.conn.commit()
        return conv_id

    def list_conversations(self, agent_id: str = None) -> List[dict]:
        cursor = self.conn.cursor()
        if agent_id:
            cursor.execute("SELECT * FROM conversations WHERE agent_id = ? ORDER BY started_at DESC", (agent_id,))
        else:
            cursor.execute("SELECT * FROM conversations ORDER BY started_at DESC")
        return [{
            "conversation_id": row["conversation_id"],
            "agent_id": row["agent_id"],
            "scene": row["scene"],
            "started_at": row["started_at"],
            "metadata": json.loads(row["metadata"] or "{}"),
        } for row in cursor.fetchall()]

    def put_edge(self, edge: dict) -> str:
        edge_id = edge.get("edge_id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            """INSERT OR REPLACE INTO memory_edges
            (edge_id, memory_a, memory_b, relation_type, confidence, created_at, metadata)
            VALUES (?, ?, ?, ?, ?, ?, ?)""",
            (edge_id, edge.get("memory_a", ""), edge.get("memory_b", ""),
             edge.get("relation_type", "related"), edge.get("confidence", 0.5),
             edge.get("created_at", time.time()),
             json.dumps(edge.get("metadata", {})))
        )
        self.conn.commit()
        return edge_id

    def get_edges(self, memory_id: str) -> List[dict]:
        cursor = self.conn.cursor()
        cursor.execute(
            "SELECT * FROM memory_edges WHERE memory_a = ? OR memory_b = ?",
            (memory_id, memory_id)
        )
        return [self._row_to_edge(row) for row in cursor.fetchall()]

    def delete_edge(self, edge_id: str) -> bool:
        cursor = self.conn.cursor()
        cursor.execute("DELETE FROM memory_edges WHERE edge_id = ?", (edge_id,))
        self.conn.commit()
        return cursor.rowcount > 0

    def list_edges(self, limit: int = 100) -> List[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM memory_edges ORDER BY created_at DESC LIMIT ?", (limit,))
        return [self._row_to_edge(row) for row in cursor.fetchall()]

    def close(self):
        if hasattr(self._local, "conn") and self._local.conn is not None:
            self._local.conn.close()
            self._local.conn = None

    def _row_to_edge(self, row) -> dict:
        return {
            "edge_id": row["edge_id"],
            "memory_a": row["memory_a"],
            "memory_b": row["memory_b"],
            "relation_type": row["relation_type"],
            "confidence": row["confidence"],
            "created_at": row["created_at"],
            "metadata": json.loads(row["metadata"] or "{}"),
        }

    def _row_to_note(self, row) -> dict:
        d = {
            "id": row["id"], "content": row["content"], "source": row["source"],
            "timestamp": row["timestamp"], "metadata": json.loads(row["metadata"] or "{}"),
        }
        try:
            d["agent_id"] = row["agent_id"] or ""
            d["scene"] = row["scene"] or ""
        except (IndexError, KeyError):
            d["agent_id"] = ""
            d["scene"] = ""
        try:
            d["scope"] = row["scope"] or "private"
            d["conversation_id"] = row["conversation_id"] or ""
        except (IndexError, KeyError):
            d["scope"] = "private"
            d["conversation_id"] = ""
        try:
            d["team_id"] = row["team_id"] or ""
            d["task_id"] = row["task_id"] or ""
        except (IndexError, KeyError):
            d["team_id"] = ""
            d["task_id"] = ""
        return d

    def _row_to_cell(self, row) -> dict:
        d = {
            "id": row["id"], "note_id": row["note_id"], "content": row["content"],
            "facts": json.loads(row["facts"] or "[]"), "stage": row["stage"],
            "importance": row["importance"], "access_count": row["access_count"],
            "last_accessed": row["last_accessed"], "created_at": row["created_at"],
            "connections": json.loads(row["connections"] or "[]"),
            "metadata": json.loads(row["metadata"] or "{}"),
        }
        try:
            d["agent_id"] = row["agent_id"] or ""
            d["scene"] = row["scene"] or ""
            d["cell_type"] = row["cell_type"] or "fact"
        except (IndexError, KeyError):
            d["agent_id"] = ""
            d["scene"] = ""
            d["cell_type"] = "fact"
        try:
            d["scope"] = row["scope"] or "private"
            d["conversation_id"] = row["conversation_id"] or ""
        except (IndexError, KeyError):
            d["scope"] = "private"
            d["conversation_id"] = ""
        try:
            d["team_id"] = row["team_id"] or ""
            d["task_id"] = row["task_id"] or ""
        except (IndexError, KeyError):
            d["team_id"] = ""
            d["task_id"] = ""
        return d

    def _row_to_scene(self, row) -> dict:
        d = {
            "id": row["id"], "title": row["title"], "summary": row["summary"],
            "cell_ids": json.loads(row["cell_ids"] or "[]"), "theme": row["theme"],
            "created_at": row["created_at"], "updated_at": row["updated_at"],
            "metadata": json.loads(row["metadata"] or "{}"),
        }
        try:
            d["agent_id"] = row["agent_id"] or ""
        except (IndexError, KeyError):
            d["agent_id"] = ""
        return d

    def _row_to_skill(self, row) -> dict:
        steps_raw = row["steps"] if "steps" in row.keys() else "[]"
        tools_raw = row["tools"] if "tools" in row.keys() else "[]"
        return {
            "id": row["id"], "name": row["name"], "description": row["description"],
            "pattern": row["pattern"],
            "scene": row["scene"] if "scene" in row.keys() else "",
            "trigger": row["trigger"] if "trigger" in row.keys() else "",
            "steps": json.loads(steps_raw) if isinstance(steps_raw, str) else steps_raw,
            "tools": json.loads(tools_raw) if isinstance(tools_raw, str) else tools_raw,
            "confidence": row["confidence"] if "confidence" in row.keys() else 0.0,
            "usage_count": row["usage_count"],
            "success_rate": row["success_rate"],
            "skill_version": row["skill_version"] if "skill_version" in row.keys() else 1,
            "status": row["status"] if "status" in row.keys() else "active",
            "created_at": row["created_at"],
            "updated_at": row["updated_at"] if "updated_at" in row.keys() else 0,
            "metadata": json.loads(row["metadata"] or "{}"),
        }

    # ─── Phase 23.5: Observations ──────────────────────────────────

    def put_observation(self, obs: dict) -> str:
        obs_id = obs.get("id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR REPLACE INTO observations "
            "(id, type, agent_id, task_id, content, context, timestamp, confidence) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
            (
                obs_id,
                obs.get("type", "observation"),
                obs.get("agent_id", ""),
                obs.get("task_id", ""),
                json.dumps(obs.get("content", {})),
                json.dumps(obs.get("context", {})),
                obs.get("timestamp", 0),
                float(obs.get("confidence", 0.9)),
            )
        )
        self.conn.commit()
        return obs_id

    def get_observation(self, obs_id: str) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM observations WHERE id = ?", (obs_id,))
        row = cursor.fetchone()
        if not row:
            return None
        return self._row_to_observation(row)

    def list_observations(self, agent_id: str = "", task_id: str = "",
                          obs_type: str = "", limit: int = 100) -> List[dict]:
        cursor = self.conn.cursor()
        query = "SELECT * FROM observations WHERE 1=1"
        params: list = []
        if agent_id:
            query += " AND agent_id = ?"
            params.append(agent_id)
        if task_id:
            query += " AND task_id = ?"
            params.append(task_id)
        if obs_type:
            query += " AND type = ?"
            params.append(obs_type)
        query += " ORDER BY timestamp DESC LIMIT ?"
        params.append(limit)
        cursor.execute(query, params)
        return [self._row_to_observation(r) for r in cursor.fetchall()]

    def _row_to_observation(self, row) -> dict:
        return {
            "id": row["id"],
            "type": row["type"],
            "agent_id": row["agent_id"] or "",
            "task_id": row["task_id"] or "",
            "content": json.loads(row["content"] or "{}"),
            "context": json.loads(row["context"] or "{}"),
            "timestamp": row["timestamp"],
            "confidence": float(row["confidence"] or 0.9),
        }

    # ─── Phase 23.5: Memory Nodes (DAG) ────────────────────────────

    def put_memory_node(self, node: dict) -> str:
        node_id = node.get("id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR REPLACE INTO memory_nodes "
            "(id, type, content, parent_ids, child_ids, agent_id, scene, "
            "task_id, timestamp, importance, obs_id, metadata) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                node_id,
                node.get("type", "raw"),
                node.get("content", ""),
                json.dumps(node.get("parent_ids", [])),
                json.dumps(node.get("child_ids", [])),
                node.get("agent_id", ""),
                node.get("scene", ""),
                node.get("task_id", ""),
                node.get("timestamp", 0),
                float(node.get("importance", 0.5)),
                node.get("obs_id", ""),
                json.dumps(node.get("metadata", {})),
            )
        )
        self.conn.commit()
        return node_id

    def get_memory_node(self, node_id: str) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM memory_nodes WHERE id = ?", (node_id,))
        row = cursor.fetchone()
        if not row:
            return None
        return self._row_to_memory_node(row)

    def list_memory_nodes(self, node_type: str = "", agent_id: str = "",
                          scene: str = "", task_id: str = "",
                          limit: int = 100) -> List[dict]:
        cursor = self.conn.cursor()
        query = "SELECT * FROM memory_nodes WHERE 1=1"
        params: list = []
        if node_type:
            query += " AND type = ?"
            params.append(node_type)
        if agent_id:
            query += " AND agent_id = ?"
            params.append(agent_id)
        if scene:
            query += " AND scene = ?"
            params.append(scene)
        if task_id:
            query += " AND task_id = ?"
            params.append(task_id)
        query += " ORDER BY timestamp DESC LIMIT ?"
        params.append(limit)
        cursor.execute(query, params)
        return [self._row_to_memory_node(r) for r in cursor.fetchall()]

    def _row_to_memory_node(self, row) -> dict:
        return {
            "id": row["id"],
            "type": row["type"] or "raw",
            "content": row["content"] or "",
            "parent_ids": json.loads(row["parent_ids"] or "[]"),
            "child_ids": json.loads(row["child_ids"] or "[]"),
            "agent_id": row["agent_id"] or "",
            "scene": row["scene"] or "",
            "task_id": row["task_id"] or "",
            "timestamp": row["timestamp"] or 0,
            "importance": float(row["importance"] or 0.5),
            "obs_id": row["obs_id"] or "",
            "metadata": json.loads(row["metadata"] or "{}"),
        }

    # ─── Phase 23.7: Outcomes ──────────────────────────────────────

    def put_outcome(self, outcome: dict) -> str:
        oid = outcome.get("outcome_id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR REPLACE INTO outcomes "
            "(outcome_id, experience_id, workflow_id, task_id, agent_id, "
            "result, metrics, agent_feedback, score, timestamp, metadata) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                oid,
                outcome.get("experience_id", ""),
                outcome.get("workflow_id", ""),
                outcome.get("task_id", ""),
                outcome.get("agent_id", ""),
                outcome.get("result", "unknown"),
                json.dumps(outcome.get("metrics", {})),
                outcome.get("agent_feedback", ""),
                float(outcome.get("score", -1.0)),
                outcome.get("timestamp", 0),
                json.dumps(outcome.get("metadata", {})),
            )
        )
        self.conn.commit()
        return oid

    def list_outcomes(self, experience_id: str = "", workflow_id: str = "",
                      agent_id: str = "", limit: int = 100) -> List[dict]:
        cursor = self.conn.cursor()
        query = "SELECT * FROM outcomes WHERE 1=1"
        params: list = []
        if experience_id:
            query += " AND experience_id = ?"
            params.append(experience_id)
        if workflow_id:
            query += " AND workflow_id = ?"
            params.append(workflow_id)
        if agent_id:
            query += " AND agent_id = ?"
            params.append(agent_id)
        query += " ORDER BY timestamp DESC LIMIT ?"
        params.append(limit)
        cursor.execute(query, params)
        return [self._row_to_outcome(r) for r in cursor.fetchall()]

    def _row_to_outcome(self, row) -> dict:
        return {
            "outcome_id": row["outcome_id"],
            "experience_id": row["experience_id"] or "",
            "workflow_id": row["workflow_id"] or "",
            "task_id": row["task_id"] or "",
            "agent_id": row["agent_id"] or "",
            "result": row["result"] or "unknown",
            "metrics": json.loads(row["metrics"] or "{}"),
            "agent_feedback": row["agent_feedback"] or "",
            "score": float(row["score"] or -1.0),
            "timestamp": row["timestamp"] or 0,
            "metadata": json.loads(row["metadata"] or "{}"),
        }

    # ─── Phase 23.7: Experience Versions ───────────────────────────

    def put_experience_version(self, version: dict) -> str:
        vid = version.get("version_id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR REPLACE INTO experience_versions "
            "(version_id, experience_id, version, parent_version, snapshot, "
            "reason, triggered_by, status, created_at) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                vid,
                version.get("experience_id", ""),
                int(version.get("version", 1)),
                int(version.get("parent_version", 0)),
                json.dumps(version.get("snapshot", {})),
                version.get("reason", ""),
                version.get("triggered_by", "system"),
                version.get("status", "active"),
                version.get("created_at", 0),
            )
        )
        self.conn.commit()
        return vid

    def get_experience_version(self, experience_id: str,
                               version: int) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute(
            "SELECT * FROM experience_versions "
            "WHERE experience_id = ? AND version = ?",
            (experience_id, version)
        )
        row = cursor.fetchone()
        if not row:
            return None
        return self._row_to_exp_version(row)

    def list_experience_versions(self, experience_id: str,
                                 limit: int = 50) -> List[dict]:
        cursor = self.conn.cursor()
        cursor.execute(
            "SELECT * FROM experience_versions WHERE experience_id = ? "
            "ORDER BY version DESC LIMIT ?",
            (experience_id, limit)
        )
        return [self._row_to_exp_version(r) for r in cursor.fetchall()]

    def _row_to_exp_version(self, row) -> dict:
        return {
            "version_id": row["version_id"],
            "experience_id": row["experience_id"],
            "version": int(row["version"]),
            "parent_version": int(row["parent_version"] or 0),
            "snapshot": json.loads(row["snapshot"] or "{}"),
            "reason": row["reason"] or "",
            "triggered_by": row["triggered_by"] or "system",
            "status": row["status"] or "active",
            "created_at": row["created_at"] or 0,
        }

    # ─── Phase 23.6: Experiences ───────────────────────────────────

    def put_experience(self, exp: dict) -> str:
        exp_id = exp.get("id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR REPLACE INTO experiences "
            "(id, title, type, source_node_ids, steps, outcome, key_facts, "
            "conditions, confidence, scene, agent_id, created_at, usage_count, metadata) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                exp_id,
                exp.get("title", ""),
                exp.get("type", "workflow"),
                json.dumps(exp.get("source_node_ids", [])),
                json.dumps(exp.get("steps", [])),
                exp.get("outcome", ""),
                json.dumps(exp.get("key_facts", [])),
                json.dumps(exp.get("conditions", {})),
                float(exp.get("confidence", 0.7)),
                exp.get("scene", ""),
                exp.get("agent_id", ""),
                exp.get("created_at", 0),
                int(exp.get("usage_count", 0)),
                json.dumps(exp.get("metadata", {})),
            )
        )
        self.conn.commit()
        return exp_id

    def get_experience(self, exp_id: str) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM experiences WHERE id = ?", (exp_id,))
        row = cursor.fetchone()
        if not row:
            return None
        return self._row_to_experience(row)

    def list_experiences(self, agent_id: str = "", scene: str = "",
                         exp_type: str = "", limit: int = 100) -> List[dict]:
        cursor = self.conn.cursor()
        query = "SELECT * FROM experiences WHERE 1=1"
        params: list = []
        if agent_id:
            query += " AND agent_id = ?"
            params.append(agent_id)
        if scene:
            query += " AND scene = ?"
            params.append(scene)
        if exp_type:
            query += " AND type = ?"
            params.append(exp_type)
        query += " ORDER BY confidence DESC, created_at DESC LIMIT ?"
        params.append(limit)
        cursor.execute(query, params)
        return [self._row_to_experience(r) for r in cursor.fetchall()]

    def delete_experience(self, exp_id: str) -> bool:
        cursor = self.conn.cursor()
        cursor.execute("DELETE FROM experiences WHERE id = ?", (exp_id,))
        self.conn.commit()
        return cursor.rowcount > 0

    def _row_to_experience(self, row) -> dict:
        return {
            "id": row["id"],
            "title": row["title"] or "",
            "type": row["type"] or "workflow",
            "source_node_ids": json.loads(row["source_node_ids"] or "[]"),
            "steps": json.loads(row["steps"] or "[]"),
            "outcome": row["outcome"] or "",
            "key_facts": json.loads(row["key_facts"] or "[]"),
            "conditions": json.loads(row["conditions"] or "{}"),
            "confidence": float(row["confidence"] or 0.7),
            "scene": row["scene"] or "",
            "agent_id": row["agent_id"] or "",
            "created_at": row["created_at"] or 0,
            "usage_count": int(row["usage_count"] or 0),
            "metadata": json.loads(row["metadata"] or "{}"),
        }

    # ─── Phase 23.6: Workflow Templates ────────────────────────────

    def put_workflow_template(self, wf: dict) -> str:
        wf_id = wf.get("id", "")
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR REPLACE INTO workflow_templates "
            "(id, name, trigger, steps, experience_id, scene, agent_id, "
            "success_rate, usage_count, created_at, metadata) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                wf_id,
                wf.get("name", ""),
                wf.get("trigger", ""),
                json.dumps(wf.get("steps", [])),
                wf.get("experience_id", ""),
                wf.get("scene", ""),
                wf.get("agent_id", ""),
                float(wf.get("success_rate", 0.0)),
                int(wf.get("usage_count", 0)),
                wf.get("created_at", 0),
                json.dumps(wf.get("metadata", {})),
            )
        )
        self.conn.commit()
        return wf_id

    def get_workflow_template(self, wf_id: str) -> Optional[dict]:
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM workflow_templates WHERE id = ?", (wf_id,))
        row = cursor.fetchone()
        if not row:
            return None
        return self._row_to_workflow(row)

    def list_workflow_templates(self, agent_id: str = "", scene: str = "",
                                limit: int = 50) -> List[dict]:
        cursor = self.conn.cursor()
        query = "SELECT * FROM workflow_templates WHERE 1=1"
        params: list = []
        if agent_id:
            query += " AND agent_id = ?"
            params.append(agent_id)
        if scene:
            query += " AND scene = ?"
            params.append(scene)
        query += " ORDER BY success_rate DESC, created_at DESC LIMIT ?"
        params.append(limit)
        cursor.execute(query, params)
        return [self._row_to_workflow(r) for r in cursor.fetchall()]

    def _row_to_workflow(self, row) -> dict:
        return {
            "id": row["id"],
            "name": row["name"] or "",
            "trigger": row["trigger"] or "",
            "steps": json.loads(row["steps"] or "[]"),
            "experience_id": row["experience_id"] or "",
            "scene": row["scene"] or "",
            "agent_id": row["agent_id"] or "",
            "success_rate": float(row["success_rate"] or 0.0),
            "usage_count": int(row["usage_count"] or 0),
            "created_at": row["created_at"] or 0,
            "metadata": json.loads(row["metadata"] or "{}"),
        }
