"""Persistent storage for learning data capture."""
from __future__ import annotations

from dataclasses import asdict
import json
import os
from pathlib import Path
import sqlite3
from typing import Any
from uuid import uuid4

from logos_adk.errors import ConfigError, StateBackendError
from logos_adk.persistence import (
    get_postgresql_schema_version,
    get_sqlite_schema_version,
    set_postgresql_schema_version,
    set_sqlite_schema_version,
)
from logos_adk.postgres_utils import pooled_connect, resolve_database_url, safe_identifier

from .models import FeedbackRecord, OutcomeRecord, RunRecord, validate_failure_types


LEARNING_SCHEMA_KIND = "learning_store"
LEARNING_SCHEMA_VERSION = 1


class LearningStore:
    """Abstract persistent store for captured learning data."""

    def create_run(self, record: RunRecord) -> None:
        raise NotImplementedError

    def get_run(self, run_id: str) -> RunRecord | None:
        raise NotImplementedError

    def get_run_by_trace(self, trace_id: str) -> RunRecord | None:
        raise NotImplementedError

    def list_runs(
        self,
        *,
        workspace_id: str | None = None,
        agent_name: str | None = None,
        limit: int = 100,
    ) -> list[RunRecord]:
        raise NotImplementedError

    def create_feedback(self, record: FeedbackRecord) -> None:
        raise NotImplementedError

    def list_feedback(
        self,
        *,
        run_id: str | None = None,
        trace_id: str | None = None,
        workspace_id: str | None = None,
    ) -> list[FeedbackRecord]:
        raise NotImplementedError

    def create_outcome(self, record: OutcomeRecord) -> None:
        raise NotImplementedError

    def list_outcomes(
        self,
        *,
        run_id: str | None = None,
        trace_id: str | None = None,
        workspace_id: str | None = None,
    ) -> list[OutcomeRecord]:
        raise NotImplementedError


class SQLiteLearningStore(LearningStore):
    """SQLite-backed learning store."""

    def __init__(self, path: str = ".logos_adk/learning.db", table_prefix: str = "learning") -> None:
        self.path = path
        self.table_prefix = safe_identifier(table_prefix)
        self._schema_ready = False

    def _table(self, suffix: str) -> str:
        return f"{self.table_prefix}_{suffix}"

    def _connect(self) -> sqlite3.Connection:
        db_path = Path(self.path)
        if db_path.parent and not db_path.parent.exists():
            db_path.parent.mkdir(parents=True, exist_ok=True)
        connection = sqlite3.connect(self.path)
        connection.row_factory = sqlite3.Row
        return connection

    def _ensure_schema(self, connection: sqlite3.Connection) -> None:
        if self._schema_ready:
            return
        connection.execute(
            f"""
            CREATE TABLE IF NOT EXISTS {self._table('runs')} (
                id TEXT PRIMARY KEY,
                trace_id TEXT NOT NULL,
                request_id TEXT,
                agent_name TEXT NOT NULL,
                session_id TEXT,
                workspace_id TEXT,
                prompt_version TEXT NOT NULL,
                model TEXT,
                config_fingerprint TEXT,
                input_text TEXT NOT NULL,
                output_text TEXT NOT NULL,
                created_at TEXT NOT NULL,
                usage_json TEXT,
                tool_calls_json TEXT,
                metadata_json TEXT
            )
            """
        )
        connection.execute(
            f"CREATE INDEX IF NOT EXISTS idx_{self._table('runs')}_trace ON {self._table('runs')} (trace_id, created_at DESC)"
        )
        connection.execute(
            f"CREATE INDEX IF NOT EXISTS idx_{self._table('runs')}_workspace ON {self._table('runs')} (workspace_id, created_at DESC)"
        )
        connection.execute(
            f"""
            CREATE TABLE IF NOT EXISTS {self._table('feedback')} (
                id TEXT PRIMARY KEY,
                kind TEXT NOT NULL,
                run_id TEXT,
                trace_id TEXT NOT NULL,
                request_id TEXT,
                session_id TEXT,
                workspace_id TEXT,
                agent_name TEXT,
                prompt_version TEXT NOT NULL,
                model TEXT,
                config_fingerprint TEXT,
                rating REAL,
                suggestion_outcome TEXT,
                corrected_output TEXT,
                correction_notes TEXT,
                expected_output TEXT,
                failure_types_json TEXT,
                metadata_json TEXT,
                created_at TEXT NOT NULL
            )
            """
        )
        connection.execute(
            f"CREATE INDEX IF NOT EXISTS idx_{self._table('feedback')}_trace ON {self._table('feedback')} (trace_id, created_at DESC)"
        )
        connection.execute(
            f"CREATE INDEX IF NOT EXISTS idx_{self._table('feedback')}_run ON {self._table('feedback')} (run_id, created_at DESC)"
        )
        connection.execute(
            f"""
            CREATE TABLE IF NOT EXISTS {self._table('outcomes')} (
                id TEXT PRIMARY KEY,
                kind TEXT NOT NULL,
                run_id TEXT,
                trace_id TEXT NOT NULL,
                request_id TEXT,
                session_id TEXT,
                workspace_id TEXT,
                agent_name TEXT,
                prompt_version TEXT NOT NULL,
                model TEXT,
                config_fingerprint TEXT,
                value_json TEXT,
                metadata_json TEXT,
                created_at TEXT NOT NULL
            )
            """
        )
        connection.execute(
            f"CREATE INDEX IF NOT EXISTS idx_{self._table('outcomes')}_trace ON {self._table('outcomes')} (trace_id, created_at DESC)"
        )
        connection.execute(
            f"CREATE INDEX IF NOT EXISTS idx_{self._table('outcomes')}_run ON {self._table('outcomes')} (run_id, created_at DESC)"
        )
        recorded_version = get_sqlite_schema_version(connection, LEARNING_SCHEMA_KIND)
        if recorded_version != LEARNING_SCHEMA_VERSION:
            set_sqlite_schema_version(connection, LEARNING_SCHEMA_KIND, LEARNING_SCHEMA_VERSION)
        connection.commit()
        self._schema_ready = True

    def create_run(self, record: RunRecord) -> None:
        with self._connect() as connection:
            self._ensure_schema(connection)
            connection.execute(
                f"""
                INSERT INTO {self._table('runs')}
                (id, trace_id, request_id, agent_name, session_id, workspace_id,
                 prompt_version, model, config_fingerprint, input_text, output_text,
                 created_at, usage_json, tool_calls_json, metadata_json)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    record.id,
                    record.trace_id,
                    record.request_id,
                    record.agent_name,
                    record.session_id,
                    record.workspace_id,
                    record.prompt_version,
                    record.model,
                    record.config_fingerprint,
                    record.input_text,
                    record.output_text,
                    record.created_at,
                    json.dumps(record.usage) if record.usage is not None else None,
                    json.dumps(record.tool_calls),
                    json.dumps(record.metadata),
                ),
            )
            connection.commit()

    def get_run(self, run_id: str) -> RunRecord | None:
        with self._connect() as connection:
            self._ensure_schema(connection)
            row = connection.execute(
                f"SELECT * FROM {self._table('runs')} WHERE id = ?",
                (run_id,),
            ).fetchone()
        return self._row_to_run(row) if row else None

    def get_run_by_trace(self, trace_id: str) -> RunRecord | None:
        with self._connect() as connection:
            self._ensure_schema(connection)
            row = connection.execute(
                f"SELECT * FROM {self._table('runs')} WHERE trace_id = ? ORDER BY created_at DESC LIMIT 1",
                (trace_id,),
            ).fetchone()
        return self._row_to_run(row) if row else None

    def list_runs(
        self,
        *,
        workspace_id: str | None = None,
        agent_name: str | None = None,
        limit: int = 100,
    ) -> list[RunRecord]:
        clauses: list[str] = []
        params: list[Any] = []
        if workspace_id is not None:
            clauses.append("workspace_id = ?")
            params.append(workspace_id)
        if agent_name is not None:
            clauses.append("agent_name = ?")
            params.append(agent_name)
        where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
        params.append(limit)
        with self._connect() as connection:
            self._ensure_schema(connection)
            rows = connection.execute(
                f"""
                SELECT * FROM {self._table('runs')}
                {where_sql}
                ORDER BY created_at DESC
                LIMIT ?
                """,
                params,
            ).fetchall()
        return [self._row_to_run(row) for row in rows]

    def create_feedback(self, record: FeedbackRecord) -> None:
        validate_failure_types(record.failure_types)
        with self._connect() as connection:
            self._ensure_schema(connection)
            connection.execute(
                f"""
                INSERT INTO {self._table('feedback')}
                (id, kind, run_id, trace_id, request_id, session_id, workspace_id,
                 agent_name, prompt_version, model, config_fingerprint, rating,
                 suggestion_outcome, corrected_output, correction_notes, expected_output,
                 failure_types_json, metadata_json, created_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    record.id,
                    record.kind,
                    record.run_id,
                    record.trace_id,
                    record.request_id,
                    record.session_id,
                    record.workspace_id,
                    record.agent_name,
                    record.prompt_version,
                    record.model,
                    record.config_fingerprint,
                    record.rating,
                    record.suggestion_outcome,
                    record.corrected_output,
                    record.correction_notes,
                    record.expected_output,
                    json.dumps(record.failure_types),
                    json.dumps(record.metadata),
                    record.created_at,
                ),
            )
            connection.commit()

    def list_feedback(
        self,
        *,
        run_id: str | None = None,
        trace_id: str | None = None,
        workspace_id: str | None = None,
    ) -> list[FeedbackRecord]:
        clauses: list[str] = []
        params: list[Any] = []
        if run_id is not None:
            clauses.append("run_id = ?")
            params.append(run_id)
        if trace_id is not None:
            clauses.append("trace_id = ?")
            params.append(trace_id)
        if workspace_id is not None:
            clauses.append("workspace_id = ?")
            params.append(workspace_id)
        where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
        with self._connect() as connection:
            self._ensure_schema(connection)
            rows = connection.execute(
                f"SELECT * FROM {self._table('feedback')} {where_sql} ORDER BY created_at DESC",
                params,
            ).fetchall()
        return [self._row_to_feedback(row) for row in rows]

    def create_outcome(self, record: OutcomeRecord) -> None:
        with self._connect() as connection:
            self._ensure_schema(connection)
            connection.execute(
                f"""
                INSERT INTO {self._table('outcomes')}
                (id, kind, run_id, trace_id, request_id, session_id, workspace_id,
                 agent_name, prompt_version, model, config_fingerprint, value_json,
                 metadata_json, created_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    record.id,
                    record.kind,
                    record.run_id,
                    record.trace_id,
                    record.request_id,
                    record.session_id,
                    record.workspace_id,
                    record.agent_name,
                    record.prompt_version,
                    record.model,
                    record.config_fingerprint,
                    json.dumps(record.value),
                    json.dumps(record.metadata),
                    record.created_at,
                ),
            )
            connection.commit()

    def list_outcomes(
        self,
        *,
        run_id: str | None = None,
        trace_id: str | None = None,
        workspace_id: str | None = None,
    ) -> list[OutcomeRecord]:
        clauses: list[str] = []
        params: list[Any] = []
        if run_id is not None:
            clauses.append("run_id = ?")
            params.append(run_id)
        if trace_id is not None:
            clauses.append("trace_id = ?")
            params.append(trace_id)
        if workspace_id is not None:
            clauses.append("workspace_id = ?")
            params.append(workspace_id)
        where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
        with self._connect() as connection:
            self._ensure_schema(connection)
            rows = connection.execute(
                f"SELECT * FROM {self._table('outcomes')} {where_sql} ORDER BY created_at DESC",
                params,
            ).fetchall()
        return [self._row_to_outcome(row) for row in rows]

    def _row_to_run(self, row: sqlite3.Row) -> RunRecord:
        return RunRecord(
            id=row["id"],
            trace_id=row["trace_id"],
            request_id=row["request_id"],
            agent_name=row["agent_name"],
            session_id=row["session_id"],
            workspace_id=row["workspace_id"],
            prompt_version=row["prompt_version"],
            model=row["model"],
            config_fingerprint=row["config_fingerprint"],
            input_text=row["input_text"],
            output_text=row["output_text"],
            created_at=row["created_at"],
            usage=json.loads(row["usage_json"]) if row["usage_json"] else None,
            tool_calls=json.loads(row["tool_calls_json"]) if row["tool_calls_json"] else [],
            metadata=json.loads(row["metadata_json"]) if row["metadata_json"] else {},
        )

    def _row_to_feedback(self, row: sqlite3.Row) -> FeedbackRecord:
        return FeedbackRecord(
            id=row["id"],
            kind=row["kind"],
            run_id=row["run_id"],
            trace_id=row["trace_id"],
            request_id=row["request_id"],
            session_id=row["session_id"],
            workspace_id=row["workspace_id"],
            agent_name=row["agent_name"],
            prompt_version=row["prompt_version"],
            model=row["model"],
            config_fingerprint=row["config_fingerprint"],
            rating=row["rating"],
            suggestion_outcome=row["suggestion_outcome"],
            corrected_output=row["corrected_output"],
            correction_notes=row["correction_notes"],
            expected_output=row["expected_output"],
            failure_types=json.loads(row["failure_types_json"]) if row["failure_types_json"] else [],
            metadata=json.loads(row["metadata_json"]) if row["metadata_json"] else {},
            created_at=row["created_at"],
        )

    def _row_to_outcome(self, row: sqlite3.Row) -> OutcomeRecord:
        return OutcomeRecord(
            id=row["id"],
            kind=row["kind"],
            run_id=row["run_id"],
            trace_id=row["trace_id"],
            request_id=row["request_id"],
            session_id=row["session_id"],
            workspace_id=row["workspace_id"],
            agent_name=row["agent_name"],
            prompt_version=row["prompt_version"],
            model=row["model"],
            config_fingerprint=row["config_fingerprint"],
            value=json.loads(row["value_json"]) if row["value_json"] else None,
            metadata=json.loads(row["metadata_json"]) if row["metadata_json"] else {},
            created_at=row["created_at"],
        )


class PostgreSQLLearningStore(LearningStore):
    """PostgreSQL-backed learning store."""

    def __init__(self, dsn: str, schema: str = "public", table_prefix: str = "learning") -> None:
        self.dsn = dsn
        self.schema = safe_identifier(schema)
        self.table_prefix = safe_identifier(table_prefix)
        self._schema_ready = False

    def _connect(self) -> Any:
        try:
            return pooled_connect(self.dsn)
        except Exception as exc:
            raise StateBackendError(f"Failed to open PostgreSQL learning store: {exc}") from exc

    def _table(self, suffix: str) -> str:
        return f"{self.schema}.{self.table_prefix}_{suffix}"

    def _ensure_schema(self, connection: Any) -> None:
        if self._schema_ready:
            return
        with connection.cursor() as cursor:
            cursor.execute(f"CREATE SCHEMA IF NOT EXISTS {self.schema}")
            cursor.execute(
                f"""
                CREATE TABLE IF NOT EXISTS {self._table('runs')} (
                    id TEXT PRIMARY KEY,
                    trace_id TEXT NOT NULL,
                    request_id TEXT,
                    agent_name TEXT NOT NULL,
                    session_id TEXT,
                    workspace_id TEXT,
                    prompt_version TEXT NOT NULL,
                    model TEXT,
                    config_fingerprint TEXT,
                    input_text TEXT NOT NULL,
                    output_text TEXT NOT NULL,
                    created_at TIMESTAMPTZ NOT NULL,
                    usage_json JSONB,
                    tool_calls_json JSONB NOT NULL DEFAULT '[]'::jsonb,
                    metadata_json JSONB NOT NULL DEFAULT '{{}}'::jsonb
                )
                """
            )
            cursor.execute(
                f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_runs_trace ON {self._table('runs')} (trace_id, created_at DESC)"
            )
            cursor.execute(
                f"""
                CREATE TABLE IF NOT EXISTS {self._table('feedback')} (
                    id TEXT PRIMARY KEY,
                    kind TEXT NOT NULL,
                    run_id TEXT,
                    trace_id TEXT NOT NULL,
                    request_id TEXT,
                    session_id TEXT,
                    workspace_id TEXT,
                    agent_name TEXT,
                    prompt_version TEXT NOT NULL,
                    model TEXT,
                    config_fingerprint TEXT,
                    rating DOUBLE PRECISION,
                    suggestion_outcome TEXT,
                    corrected_output TEXT,
                    correction_notes TEXT,
                    expected_output TEXT,
                    failure_types_json JSONB NOT NULL DEFAULT '[]'::jsonb,
                    metadata_json JSONB NOT NULL DEFAULT '{{}}'::jsonb,
                    created_at TIMESTAMPTZ NOT NULL
                )
                """
            )
            cursor.execute(
                f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_feedback_trace ON {self._table('feedback')} (trace_id, created_at DESC)"
            )
            cursor.execute(
                f"""
                CREATE TABLE IF NOT EXISTS {self._table('outcomes')} (
                    id TEXT PRIMARY KEY,
                    kind TEXT NOT NULL,
                    run_id TEXT,
                    trace_id TEXT NOT NULL,
                    request_id TEXT,
                    session_id TEXT,
                    workspace_id TEXT,
                    agent_name TEXT,
                    prompt_version TEXT NOT NULL,
                    model TEXT,
                    config_fingerprint TEXT,
                    value_json JSONB,
                    metadata_json JSONB NOT NULL DEFAULT '{{}}'::jsonb,
                    created_at TIMESTAMPTZ NOT NULL
                )
                """
            )
            cursor.execute(
                f"CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_outcomes_trace ON {self._table('outcomes')} (trace_id, created_at DESC)"
            )
        recorded_version = get_postgresql_schema_version(connection, LEARNING_SCHEMA_KIND)
        if recorded_version != LEARNING_SCHEMA_VERSION:
            set_postgresql_schema_version(connection, LEARNING_SCHEMA_KIND, LEARNING_SCHEMA_VERSION)
        connection.commit()
        self._schema_ready = True

    def create_run(self, record: RunRecord) -> None:
        with self._connect() as connection:
            self._ensure_schema(connection)
            with connection.cursor() as cursor:
                cursor.execute(
                    f"""
                    INSERT INTO {self._table('runs')}
                    (id, trace_id, request_id, agent_name, session_id, workspace_id,
                     prompt_version, model, config_fingerprint, input_text, output_text,
                     created_at, usage_json, tool_calls_json, metadata_json)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                    """,
                    (
                        record.id,
                        record.trace_id,
                        record.request_id,
                        record.agent_name,
                        record.session_id,
                        record.workspace_id,
                        record.prompt_version,
                        record.model,
                        record.config_fingerprint,
                        record.input_text,
                        record.output_text,
                        record.created_at,
                        json.dumps(record.usage) if record.usage is not None else None,
                        json.dumps(record.tool_calls),
                        json.dumps(record.metadata),
                    ),
                )
            connection.commit()

    def get_run(self, run_id: str) -> RunRecord | None:
        with self._connect() as connection:
            self._ensure_schema(connection)
            with connection.cursor() as cursor:
                cursor.execute(f"SELECT * FROM {self._table('runs')} WHERE id = %s", (run_id,))
                row = cursor.fetchone()
        return self._record_to_run(row) if row else None

    def get_run_by_trace(self, trace_id: str) -> RunRecord | None:
        with self._connect() as connection:
            self._ensure_schema(connection)
            with connection.cursor() as cursor:
                cursor.execute(
                    f"SELECT * FROM {self._table('runs')} WHERE trace_id = %s ORDER BY created_at DESC LIMIT 1",
                    (trace_id,),
                )
                row = cursor.fetchone()
        return self._record_to_run(row) if row else None

    def list_runs(
        self,
        *,
        workspace_id: str | None = None,
        agent_name: str | None = None,
        limit: int = 100,
    ) -> list[RunRecord]:
        clauses: list[str] = []
        params: list[Any] = []
        if workspace_id is not None:
            clauses.append("workspace_id = %s")
            params.append(workspace_id)
        if agent_name is not None:
            clauses.append("agent_name = %s")
            params.append(agent_name)
        where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
        params.append(limit)
        with self._connect() as connection:
            self._ensure_schema(connection)
            with connection.cursor() as cursor:
                cursor.execute(
                    f"SELECT * FROM {self._table('runs')} {where_sql} ORDER BY created_at DESC LIMIT %s",
                    params,
                )
                rows = cursor.fetchall()
        return [self._record_to_run(row) for row in rows]

    def create_feedback(self, record: FeedbackRecord) -> None:
        validate_failure_types(record.failure_types)
        with self._connect() as connection:
            self._ensure_schema(connection)
            with connection.cursor() as cursor:
                cursor.execute(
                    f"""
                    INSERT INTO {self._table('feedback')}
                    (id, kind, run_id, trace_id, request_id, session_id, workspace_id,
                     agent_name, prompt_version, model, config_fingerprint, rating,
                     suggestion_outcome, corrected_output, correction_notes, expected_output,
                     failure_types_json, metadata_json, created_at)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                    """,
                    (
                        record.id,
                        record.kind,
                        record.run_id,
                        record.trace_id,
                        record.request_id,
                        record.session_id,
                        record.workspace_id,
                        record.agent_name,
                        record.prompt_version,
                        record.model,
                        record.config_fingerprint,
                        record.rating,
                        record.suggestion_outcome,
                        record.corrected_output,
                        record.correction_notes,
                        record.expected_output,
                        json.dumps(record.failure_types),
                        json.dumps(record.metadata),
                        record.created_at,
                    ),
                )
            connection.commit()

    def list_feedback(
        self,
        *,
        run_id: str | None = None,
        trace_id: str | None = None,
        workspace_id: str | None = None,
    ) -> list[FeedbackRecord]:
        clauses: list[str] = []
        params: list[Any] = []
        if run_id is not None:
            clauses.append("run_id = %s")
            params.append(run_id)
        if trace_id is not None:
            clauses.append("trace_id = %s")
            params.append(trace_id)
        if workspace_id is not None:
            clauses.append("workspace_id = %s")
            params.append(workspace_id)
        where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
        with self._connect() as connection:
            self._ensure_schema(connection)
            with connection.cursor() as cursor:
                cursor.execute(
                    f"SELECT * FROM {self._table('feedback')} {where_sql} ORDER BY created_at DESC",
                    params,
                )
                rows = cursor.fetchall()
        return [self._record_to_feedback(row) for row in rows]

    def create_outcome(self, record: OutcomeRecord) -> None:
        with self._connect() as connection:
            self._ensure_schema(connection)
            with connection.cursor() as cursor:
                cursor.execute(
                    f"""
                    INSERT INTO {self._table('outcomes')}
                    (id, kind, run_id, trace_id, request_id, session_id, workspace_id,
                     agent_name, prompt_version, model, config_fingerprint, value_json,
                     metadata_json, created_at)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                    """,
                    (
                        record.id,
                        record.kind,
                        record.run_id,
                        record.trace_id,
                        record.request_id,
                        record.session_id,
                        record.workspace_id,
                        record.agent_name,
                        record.prompt_version,
                        record.model,
                        record.config_fingerprint,
                        json.dumps(record.value),
                        json.dumps(record.metadata),
                        record.created_at,
                    ),
                )
            connection.commit()

    def list_outcomes(
        self,
        *,
        run_id: str | None = None,
        trace_id: str | None = None,
        workspace_id: str | None = None,
    ) -> list[OutcomeRecord]:
        clauses: list[str] = []
        params: list[Any] = []
        if run_id is not None:
            clauses.append("run_id = %s")
            params.append(run_id)
        if trace_id is not None:
            clauses.append("trace_id = %s")
            params.append(trace_id)
        if workspace_id is not None:
            clauses.append("workspace_id = %s")
            params.append(workspace_id)
        where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
        with self._connect() as connection:
            self._ensure_schema(connection)
            with connection.cursor() as cursor:
                cursor.execute(
                    f"SELECT * FROM {self._table('outcomes')} {where_sql} ORDER BY created_at DESC",
                    params,
                )
                rows = cursor.fetchall()
        return [self._record_to_outcome(row) for row in rows]

    def _record_to_run(self, row: Any) -> RunRecord:
        return RunRecord(
            id=row[0],
            trace_id=row[1],
            request_id=row[2],
            agent_name=row[3],
            session_id=row[4],
            workspace_id=row[5],
            prompt_version=row[6],
            model=row[7],
            config_fingerprint=row[8],
            input_text=row[9],
            output_text=row[10],
            created_at=str(row[11]),
            usage=row[12],
            tool_calls=row[13] or [],
            metadata=row[14] or {},
        )

    def _record_to_feedback(self, row: Any) -> FeedbackRecord:
        return FeedbackRecord(
            id=row[0],
            kind=row[1],
            run_id=row[2],
            trace_id=row[3],
            request_id=row[4],
            session_id=row[5],
            workspace_id=row[6],
            agent_name=row[7],
            prompt_version=row[8],
            model=row[9],
            config_fingerprint=row[10],
            rating=row[11],
            suggestion_outcome=row[12],
            corrected_output=row[13],
            correction_notes=row[14],
            expected_output=row[15],
            failure_types=list(row[16] or []),
            metadata=row[17] or {},
            created_at=str(row[18]),
        )

    def _record_to_outcome(self, row: Any) -> OutcomeRecord:
        return OutcomeRecord(
            id=row[0],
            kind=row[1],
            run_id=row[2],
            trace_id=row[3],
            request_id=row[4],
            session_id=row[5],
            workspace_id=row[6],
            agent_name=row[7],
            prompt_version=row[8],
            model=row[9],
            config_fingerprint=row[10],
            value=row[11],
            metadata=row[12] or {},
            created_at=str(row[13]),
        )


def build_learning_store(
    *,
    backend: str | None = None,
    path: str = ".logos_adk/learning.db",
    dsn: str | None = None,
) -> LearningStore:
    """Build a persistent learning store from explicit args or environment."""
    backend_name = (backend or os.getenv("LOGOS_ADK_LEARNING_BACKEND") or "sqlite").strip().lower()
    if backend_name == "sqlite":
        return SQLiteLearningStore(path=os.getenv("LOGOS_ADK_LEARNING_PATH", path))
    if backend_name == "postgresql":
        resolved_dsn = resolve_database_url(dsn or os.getenv("LOGOS_ADK_LEARNING_DSN"))
        if not resolved_dsn:
            raise ConfigError("LOGOS_ADK_LEARNING_DSN or dsn is required for PostgreSQL learning store")
        return PostgreSQLLearningStore(resolved_dsn)
    raise ConfigError(f"Unsupported learning store backend: {backend_name}")


_learning_store: LearningStore | None = None


def get_learning_store() -> LearningStore:
    """Return the process-global learning store."""
    global _learning_store
    if _learning_store is None:
        _learning_store = build_learning_store()
    return _learning_store


def reset_learning_store(store: LearningStore | None = None) -> LearningStore:
    """Reset the process-global learning store."""
    global _learning_store
    _learning_store = store or build_learning_store()
    return _learning_store


def make_run_id() -> str:
    """Create a stable random run identifier."""
    return uuid4().hex


def record_to_dict(record: Any) -> dict[str, Any]:
    """Normalize a dataclass record to a JSON-serializable dict."""
    return asdict(record)
