"""Continuous evaluation runner for production-derived evaluation suites."""
from __future__ import annotations

import asyncio
import json
import os
import sqlite3
from dataclasses import asdict
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import TYPE_CHECKING, Any
from uuid import uuid4

if TYPE_CHECKING:
    from logos_adk.agent import Agent
    from logos_adk.quality_history import QualityExecutionManager, QualityRunRecord
    from logos_adk.testing_framework import LLMJudge

from logos_adk.persistence import (
    get_postgresql_schema_version,
    get_sqlite_schema_version,
    set_postgresql_schema_version,
    set_sqlite_schema_version,
)
from logos_adk.quality_history import QualityRunRecord

from .engine import EvaluationLoopEngine
from .models import (
    CONTINUOUS_EVAL_SCHEMA_KIND,
    CONTINUOUS_EVAL_SCHEMA_VERSION,
    ContinuousEvalIncident,
    ContinuousEvalIncidentSnapshot,
    ContinuousEvalJob,
    ContinuousEvalTrendDeltaReport,
    ScoreWindow,
)
from .utils import (
    _decode_json_field,
    _execute,
    _fetchall,
    _fetchone,
    _mean,
    _parse_ts,
    _row_value,
    utcnow,
)


class ContinuousEvalRunner:
    """Periodic runner for production-derived evaluation suites."""

    def __init__(
        self,
        engine: EvaluationLoopEngine,
        quality_manager: QualityExecutionManager,
        *,
        path: str | None = None,
        dsn: str | None = None,
        agent: Agent | None = None,
        agent_name: str | None = None,
        prompt_registry: Any | None = None,
    ) -> None:
        """Initialize continuous evaluation runner.
        
        Args:
            engine: EvaluationLoopEngine instance
            quality_manager: QualityExecutionManager instance
            path: SQLite database path
            dsn: PostgreSQL DSN
            agent: Default agent for evaluation
            agent_name: Default agent name
            prompt_registry: Optional PromptRegistry for prompt management
        """
        self.engine = engine
        self.quality_manager = quality_manager
        self.path = path or os.getenv("LOGOS_ADK_CONTINUOUS_EVAL_DB_PATH", ".logos_adk/continuous_eval.db")
        self.dsn = self._resolve_database_url(
            dsn or os.getenv("LOGOS_ADK_CONTINUOUS_EVAL_DSN"),
        )
        self.default_agent = agent
        self.default_agent_name = agent_name or getattr(agent, "name", None)
        self.prompt_registry = prompt_registry
        self.jobs: dict[str, ContinuousEvalJob] = {}
        self._stop = asyncio.Event()
        self._restored = False

    def _resolve_database_url(self, dsn: str | None) -> str | None:
        """Resolve database URL from environment."""
        from logos_adk.postgres_utils import resolve_database_url
        return resolve_database_url(
            dsn,
            env_vars=("LOGOS_ADK_CONTINUOUS_EVAL_DSN", "LOGOS_ADK_DATABASE_URL", "DATABASE_URL"),
        )

    def _connect(self):
        """Create database connection."""
        from logos_adk.postgres_utils import import_psycopg
        
        if self.dsn:
            connection = import_psycopg().connect(self.dsn)
            return 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) -> None:
        """Ensure database schema is up to date."""
        if self.dsn:
            self._ensure_postgresql_schema(connection)
        else:
            self._ensure_sqlite_schema(connection)
        connection.commit()

    def _ensure_postgresql_schema(self, connection) -> None:
        """Ensure PostgreSQL schema."""
        with connection.cursor() as cursor:
            cursor.execute(
                """
                CREATE TABLE IF NOT EXISTS continuous_eval_jobs (
                    name TEXT PRIMARY KEY,
                    kind TEXT NOT NULL,
                    agent_name TEXT NOT NULL,
                    workspace_id TEXT,
                    agent_filter TEXT,
                    interval_seconds DOUBLE PRECISION NOT NULL,
                    limit_count INTEGER NOT NULL,
                    include_unreviewed INTEGER NOT NULL DEFAULT 0,
                    metadata_json JSONB,
                    last_run_id TEXT,
                    last_started_at TEXT,
                    last_finished_at TEXT
                )
                """
            )
            cursor.execute(
                """
                CREATE TABLE IF NOT EXISTS continuous_eval_incidents (
                    id TEXT PRIMARY KEY,
                    job_name TEXT NOT NULL,
                    kind TEXT NOT NULL,
                    severity TEXT NOT NULL,
                    message TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    run_id TEXT,
                    workspace_id TEXT,
                    agent_name TEXT,
                    prompt_name TEXT,
                    experiment_id TEXT,
                    status TEXT NOT NULL DEFAULT 'open',
                    acknowledged_at TEXT,
                    acknowledged_by TEXT,
                    resolved_at TEXT,
                    resolved_by TEXT,
                    metadata_json JSONB
                )
                """
            )
            cursor.execute(
                "CREATE INDEX IF NOT EXISTS idx_continuous_eval_jobs_agent_name "
                "ON continuous_eval_jobs (agent_name, kind, workspace_id)"
            )
            cursor.execute(
                "CREATE INDEX IF NOT EXISTS idx_continuous_eval_incidents_created "
                "ON continuous_eval_incidents (created_at DESC)"
            )
            cursor.execute(
                "CREATE INDEX IF NOT EXISTS idx_continuous_eval_incidents_job_name "
                "ON continuous_eval_incidents (job_name, created_at DESC)"
            )
        recorded = get_postgresql_schema_version(connection, CONTINUOUS_EVAL_SCHEMA_KIND) or 0
        if recorded != CONTINUOUS_EVAL_SCHEMA_VERSION:
            set_postgresql_schema_version(connection, CONTINUOUS_EVAL_SCHEMA_KIND, CONTINUOUS_EVAL_SCHEMA_VERSION)

    def _ensure_sqlite_schema(self, connection) -> None:
        """Ensure SQLite schema."""
        connection.execute(
            """
            CREATE TABLE IF NOT EXISTS continuous_eval_jobs (
                name TEXT PRIMARY KEY,
                kind TEXT NOT NULL,
                agent_name TEXT NOT NULL,
                workspace_id TEXT,
                agent_filter TEXT,
                interval_seconds REAL NOT NULL,
                limit_count INTEGER NOT NULL,
                include_unreviewed INTEGER NOT NULL DEFAULT 0,
                metadata_json TEXT,
                last_run_id TEXT,
                last_started_at TEXT,
                last_finished_at TEXT
            )
            """
        )
        connection.execute(
            """
            CREATE TABLE IF NOT EXISTS continuous_eval_incidents (
                id TEXT PRIMARY KEY,
                job_name TEXT NOT NULL,
                kind TEXT NOT NULL,
                severity TEXT NOT NULL,
                message TEXT NOT NULL,
                created_at TEXT NOT NULL,
                run_id TEXT,
                workspace_id TEXT,
                agent_name TEXT,
                prompt_name TEXT,
                experiment_id TEXT,
                status TEXT NOT NULL DEFAULT 'open',
                acknowledged_at TEXT,
                acknowledged_by TEXT,
                resolved_at TEXT,
                resolved_by TEXT,
                metadata_json TEXT
            )
            """
        )
        connection.execute(
            "CREATE INDEX IF NOT EXISTS idx_continuous_eval_jobs_agent_name "
            "ON continuous_eval_jobs (agent_name, kind, workspace_id)"
        )
        connection.execute(
            "CREATE INDEX IF NOT EXISTS idx_continuous_eval_incidents_created "
            "ON continuous_eval_incidents (created_at DESC)"
        )
        connection.execute(
            "CREATE INDEX IF NOT EXISTS idx_continuous_eval_incidents_job_name "
            "ON continuous_eval_incidents (job_name, created_at DESC)"
        )
        recorded = get_sqlite_schema_version(connection, CONTINUOUS_EVAL_SCHEMA_KIND) or 0
        if recorded < 2:
            columns = {
                row[1]
                for row in connection.execute("PRAGMA table_info(continuous_eval_incidents)").fetchall()
            }
            if "status" not in columns:
                connection.execute(
                    "ALTER TABLE continuous_eval_incidents ADD COLUMN status TEXT NOT NULL DEFAULT 'open'"
                )
            if "acknowledged_at" not in columns:
                connection.execute("ALTER TABLE continuous_eval_incidents ADD COLUMN acknowledged_at TEXT")
            if "acknowledged_by" not in columns:
                connection.execute("ALTER TABLE continuous_eval_incidents ADD COLUMN acknowledged_by TEXT")
            if "resolved_at" not in columns:
                connection.execute("ALTER TABLE continuous_eval_incidents ADD COLUMN resolved_at TEXT")
            if "resolved_by" not in columns:
                connection.execute("ALTER TABLE continuous_eval_incidents ADD COLUMN resolved_by TEXT")
        if recorded != CONTINUOUS_EVAL_SCHEMA_VERSION:
            set_sqlite_schema_version(connection, CONTINUOUS_EVAL_SCHEMA_KIND, CONTINUOUS_EVAL_SCHEMA_VERSION)

    def _persist_job(self, job: ContinuousEvalJob) -> None:
        """Persist job to database."""
        metadata_placeholder = "%s::jsonb" if self.dsn else "?"
        with self._connect() as connection:
            self._ensure_schema(connection)
            _execute(
                connection,
                """
                INSERT INTO continuous_eval_jobs
                (name, kind, agent_name, workspace_id, agent_filter, interval_seconds, limit_count,
                 include_unreviewed, metadata_json, last_run_id, last_started_at, last_finished_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, """
                + metadata_placeholder
                + """, ?, ?, ?)
                ON CONFLICT(name) DO UPDATE SET
                    kind = excluded.kind,
                    agent_name = excluded.agent_name,
                    workspace_id = excluded.workspace_id,
                    agent_filter = excluded.agent_filter,
                    interval_seconds = excluded.interval_seconds,
                    limit_count = excluded.limit_count,
                    include_unreviewed = excluded.include_unreviewed,
                    metadata_json = excluded.metadata_json,
                    last_run_id = excluded.last_run_id,
                    last_started_at = excluded.last_started_at,
                    last_finished_at = excluded.last_finished_at
                """,
                (
                    job.name,
                    job.kind,
                    job.agent_name,
                    job.workspace_id,
                    job.agent_filter,
                    job.interval_seconds,
                    job.limit,
                    1 if job.include_unreviewed else 0,
                    json.dumps(job.metadata),
                    job.last_run_id,
                    job.last_started_at,
                    job.last_finished_at,
                ),
                self.dsn,
            )
            connection.commit()

    def _delete_persisted_job(self, name: str) -> None:
        """Delete persisted job from database."""
        with self._connect() as connection:
            self._ensure_schema(connection)
            _execute(connection, "DELETE FROM continuous_eval_jobs WHERE name = ?", (name,), self.dsn)
            connection.commit()

    def _create_incident(
        self,
        *,
        job: ContinuousEvalJob,
        kind: str,
        severity: str,
        message: str,
        run_id: str | None = None,
        prompt_name: str | None = None,
        experiment_id: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> ContinuousEvalIncident:
        """Create incident record."""
        metadata_placeholder = "%s::jsonb" if self.dsn else "?"
        incident = ContinuousEvalIncident(
            id=uuid4().hex,
            job_name=job.name,
            kind=kind,
            severity=severity,
            message=message,
            created_at=utcnow(),
            run_id=run_id,
            workspace_id=job.workspace_id,
            agent_name=job.agent_name,
            prompt_name=prompt_name,
            experiment_id=experiment_id,
            metadata=dict(metadata or {}),
        )
        with self._connect() as connection:
            self._ensure_schema(connection)
            _execute(
                connection,
                """
                INSERT INTO continuous_eval_incidents
                (id, job_name, kind, severity, message, created_at, run_id, workspace_id,
                 agent_name, prompt_name, experiment_id, status, acknowledged_at,
                 acknowledged_by, resolved_at, resolved_by, metadata_json)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, """
                + metadata_placeholder
                + ")",
                (
                    incident.id,
                    incident.job_name,
                    incident.kind,
                    incident.severity,
                    incident.message,
                    incident.created_at,
                    incident.run_id,
                    incident.workspace_id,
                    incident.agent_name,
                    incident.prompt_name,
                    incident.experiment_id,
                    incident.status,
                    incident.acknowledged_at,
                    incident.acknowledged_by,
                    incident.resolved_at,
                    incident.resolved_by,
                    json.dumps(incident.metadata),
                ),
                self.dsn,
            )
            connection.commit()
        return incident

    def get_incident(self, incident_id: str) -> dict[str, Any] | None:
        """Get incident by ID."""
        with self._connect() as connection:
            self._ensure_schema(connection)
            row = _fetchone(
                connection,
                "SELECT * FROM continuous_eval_incidents WHERE id = ?",
                (incident_id,),
                self.dsn,
            )
        return asdict(self._row_to_incident(row)) if row is not None else None

    def acknowledge_incident(self, incident_id: str, *, actor: str | None = None, note: str | None = None) -> dict[str, Any] | None:
        """Acknowledge incident."""
        return self._update_incident_status(
            incident_id,
            status="acknowledged",
            actor=actor,
            note=note,
        )

    def resolve_incident(self, incident_id: str, *, actor: str | None = None, note: str | None = None) -> dict[str, Any] | None:
        """Resolve incident."""
        return self._update_incident_status(
            incident_id,
            status="resolved",
            actor=actor,
            note=note,
        )

    def _update_incident_status(
        self,
        incident_id: str,
        *,
        status: str,
        actor: str | None,
        note: str | None,
    ) -> dict[str, Any] | None:
        """Update incident status."""
        current = self.get_incident(incident_id)
        if current is None:
            return None
        metadata = dict(current.get("metadata") or {})
        notes = list(metadata.get("operator_notes") or [])
        if note:
            notes.append(
                {
                    "status": status,
                    "actor": actor,
                    "note": note,
                    "created_at": utcnow(),
                }
            )
        if notes:
            metadata["operator_notes"] = notes
        assignments = ["status = ?", "metadata_json = ?"]
        values: list[Any] = [status, json.dumps(metadata)]
        now = utcnow()
        if status == "acknowledged":
            assignments.extend(["acknowledged_at = ?", "acknowledged_by = ?"])
            values.extend([now, actor])
        if status == "resolved":
            assignments.extend(["resolved_at = ?", "resolved_by = ?"])
            values.extend([now, actor])
            if current.get("acknowledged_at") is None:
                assignments.extend(["acknowledged_at = ?", "acknowledged_by = ?"])
                values.extend([now, actor])
        values.append(incident_id)
        with self._connect() as connection:
            self._ensure_schema(connection)
            _execute(
                connection,
                f"UPDATE continuous_eval_incidents SET {', '.join(assignments)} WHERE id = ?",
                values,
                self.dsn,
            )
            connection.commit()
        return self.get_incident(incident_id)

    def list_incidents(
        self,
        *,
        limit: int = 50,
        kind: str | None = None,
        job_name: str | None = None,
        workspace_id: str | None = None,
        status: str | None = None,
    ) -> list[dict[str, Any]]:
        """List incidents with filters."""
        clauses: list[str] = []
        params: list[Any] = []
        if kind is not None:
            clauses.append("kind = ?")
            params.append(kind)
        if job_name is not None:
            clauses.append("job_name = ?")
            params.append(job_name)
        if workspace_id is not None:
            clauses.append("workspace_id = ?")
            params.append(workspace_id)
        if status is not None:
            clauses.append("status = ?")
            params.append(status)
        where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
        params.append(limit)
        with self._connect() as connection:
            self._ensure_schema(connection)
            rows = _fetchall(
                connection,
                f"""
                SELECT * FROM continuous_eval_incidents
                {where_sql}
                ORDER BY created_at DESC
                LIMIT ?
                """,
                params,
                self.dsn,
            )
        return [asdict(self._row_to_incident(row)) for row in rows]

    def list_restored_jobs(self) -> list[dict[str, Any]]:
        """List restored jobs."""
        return [snapshot for snapshot in self.list_jobs() if bool(snapshot.get("restored"))]

    def trend_delta_report(
        self,
        *,
        workspace_id: str | None = None,
        job_name: str | None = None,
        window_days: int = 7,
    ) -> ContinuousEvalTrendDeltaReport:
        """Generate trend delta report."""
        anchor = datetime.now(UTC)
        current_start = anchor - timedelta(days=window_days)
        previous_start = current_start - timedelta(days=window_days)
        target_job = self.jobs.get(job_name) if job_name is not None else None
        target_agent_name = target_job.agent_name if target_job is not None else None
        target_kind = target_job.kind if target_job is not None else None
        quality_runs = self.quality_manager.list_runs(
            agent_name=target_agent_name,
            kind=target_kind,
            limit=max(200, window_days * 100),
        )
        current_runs = [
            run
            for run in quality_runs
            if self._in_window(run.created_at, start=current_start, end=anchor)
        ]
        previous_runs = [
            run
            for run in quality_runs
            if self._in_window(run.created_at, start=previous_start, end=current_start)
        ]
        incidents = self.list_incidents(
            limit=max(500, window_days * 200),
            job_name=job_name,
            workspace_id=workspace_id,
        )
        current_incidents = [
            item
            for item in incidents
            if self._in_window(item.get("created_at"), start=current_start, end=anchor)
        ]
        previous_incidents = [
            item
            for item in incidents
            if self._in_window(item.get("created_at"), start=previous_start, end=current_start)
        ]

        def _pass_rate(runs: list[QualityRunRecord]) -> float:
            values: list[float] = []
            for run in runs:
                summary = run.summary or {}
                total = int(summary.get("total", 0) or 0)
                passed = int(summary.get("passed", 0) or 0)
                if total > 0:
                    values.append(passed / total)
            return round(_mean(values, default=0.0), 4)

        def _failed_runs(runs: list[QualityRunRecord]) -> float:
            return float(sum(1 for run in runs if run.status == "failed"))

        def _count_kind(items: list[dict[str, Any]], kind: str) -> float:
            return float(sum(1 for item in items if item.get("kind") == kind))

        def _window(current: float, previous: float) -> ScoreWindow:
            return ScoreWindow(
                current=round(current, 4),
                previous=round(previous, 4),
                delta=round(current - previous, 4),
            )

        return ContinuousEvalTrendDeltaReport(
            workspace_id=workspace_id,
            job_name=job_name,
            window_days=window_days,
            current_window_start=current_start.isoformat(),
            current_window_end=anchor.isoformat(),
            previous_window_start=previous_start.isoformat(),
            previous_window_end=current_start.isoformat(),
            pass_rate=_window(_pass_rate(current_runs), _pass_rate(previous_runs)),
            incidents=_window(float(len(current_incidents)), float(len(previous_incidents))),
            rollbacks=_window(
                _count_kind(current_incidents, "prompt_auto_rollback"),
                _count_kind(previous_incidents, "prompt_auto_rollback"),
            ),
            experiment_disables=_window(
                _count_kind(current_incidents, "experiment_auto_disabled"),
                _count_kind(previous_incidents, "experiment_auto_disabled"),
            ),
            run_failures=_window(_failed_runs(current_runs), _failed_runs(previous_runs)),
            counts={
                "current_runs": len(current_runs),
                "previous_runs": len(previous_runs),
                "current_incidents": len(current_incidents),
                "previous_incidents": len(previous_incidents),
            },
        )

    def incident_snapshot(
        self,
        *,
        workspace_id: str | None = None,
        job_name: str | None = None,
        status: str | None = None,
        limit: int = 20,
    ) -> ContinuousEvalIncidentSnapshot:
        """Generate incident snapshot."""
        incidents = self.list_incidents(
            limit=max(limit, 200),
            job_name=job_name,
            workspace_id=workspace_id,
            status=status,
        )
        latest = incidents[:limit]
        by_kind: dict[str, int] = {}
        by_status: dict[str, int] = {}
        by_severity: dict[str, int] = {}
        prompt_counts: dict[str, int] = {}
        job_counts: dict[str, int] = {}
        for item in incidents:
            by_kind[str(item.get("kind") or "unknown")] = by_kind.get(str(item.get("kind") or "unknown"), 0) + 1
            by_status[str(item.get("status") or "open")] = by_status.get(str(item.get("status") or "open"), 0) + 1
            by_severity[str(item.get("severity") or "unknown")] = by_severity.get(str(item.get("severity") or "unknown"), 0) + 1
            prompt_name = item.get("prompt_name")
            if prompt_name:
                prompt_counts[str(prompt_name)] = prompt_counts.get(str(prompt_name), 0) + 1
            incident_job_name = item.get("job_name")
            if incident_job_name:
                job_counts[str(incident_job_name)] = job_counts.get(str(incident_job_name), 0) + 1
        affected_prompts = [
            {"prompt_name": name, "incident_count": count}
            for name, count in sorted(prompt_counts.items(), key=lambda entry: (-entry[1], entry[0]))
        ][:10]
        affected_jobs = [
            {"job_name": name, "incident_count": count}
            for name, count in sorted(job_counts.items(), key=lambda entry: (-entry[1], entry[0]))
        ][:10]
        restored_jobs = [
            item
            for item in self.list_restored_jobs()
            if (job_name is None or item.get("name") == job_name)
            and (workspace_id is None or item.get("workspace_id") == workspace_id)
        ]
        return ContinuousEvalIncidentSnapshot(
            workspace_id=workspace_id,
            job_name=job_name,
            status_filter=status,
            generated_at=utcnow(),
            summary={
                "total_incidents": len(incidents),
                "open_incidents": by_status.get("open", 0),
                "acknowledged_incidents": by_status.get("acknowledged", 0),
                "resolved_incidents": by_status.get("resolved", 0),
                "by_kind": by_kind,
                "by_status": by_status,
                "by_severity": by_severity,
                "restored_jobs": len(restored_jobs),
            },
            latest_incidents=latest,
            affected_prompts=affected_prompts,
            affected_jobs=affected_jobs,
            restored_jobs=restored_jobs,
        )

    def _in_window(self, value: str | None, *, start: datetime, end: datetime) -> bool:
        """Check if timestamp is in window."""
        timestamp = _parse_ts(value)
        if timestamp is None:
            return False
        return start <= timestamp < end

    def _row_to_incident(self, row) -> ContinuousEvalIncident:
        """Convert database row to incident."""
        return ContinuousEvalIncident(
            id=str(_row_value(row, "id", 0)),
            job_name=str(_row_value(row, "job_name", 1)),
            kind=str(_row_value(row, "kind", 2)),
            severity=str(_row_value(row, "severity", 3)),
            message=str(_row_value(row, "message", 4)),
            created_at=str(_row_value(row, "created_at", 5)),
            run_id=_row_value(row, "run_id", 6),
            workspace_id=_row_value(row, "workspace_id", 7),
            agent_name=_row_value(row, "agent_name", 8),
            prompt_name=_row_value(row, "prompt_name", 9),
            experiment_id=_row_value(row, "experiment_id", 10),
            status=str(_row_value(row, "status", 11) or "open"),
            acknowledged_at=_row_value(row, "acknowledged_at", 12),
            acknowledged_by=_row_value(row, "acknowledged_by", 13),
            resolved_at=_row_value(row, "resolved_at", 14),
            resolved_by=_row_value(row, "resolved_by", 15),
            metadata=_decode_json_field(_row_value(row, "metadata_json", 16), default={}),
        )

    def restore_jobs(self) -> list[dict[str, Any]]:
        """Restore jobs from database."""
        if self._restored:
            return self.list_jobs()
        query = """
                SELECT * FROM continuous_eval_jobs
                ORDER BY name ASC
                """
        params: tuple[Any, ...] = ()
        if self.default_agent_name is not None:
            query = """
                SELECT * FROM continuous_eval_jobs
                WHERE agent_name = ?
                ORDER BY name ASC
                """
            params = (self.default_agent_name,)
        with self._connect() as connection:
            self._ensure_schema(connection)
            rows = _fetchall(
                connection,
                query,
                params,
                self.dsn,
            )
        for row in rows:
            metadata = _decode_json_field(_row_value(row, "metadata_json", 8), default={})
            if _row_value(row, "name", 0) in self.jobs:
                continue
            job = ContinuousEvalJob(
                name=str(_row_value(row, "name", 0)),
                kind=str(_row_value(row, "kind", 1)),
                agent_name=str(_row_value(row, "agent_name", 2)),
                workspace_id=_row_value(row, "workspace_id", 3),
                agent_filter=_row_value(row, "agent_filter", 4),
                interval_seconds=float(_row_value(row, "interval_seconds", 5)),
                limit=int(_row_value(row, "limit_count", 6)),
                include_unreviewed=bool(_row_value(row, "include_unreviewed", 7)),
                judge=self._restore_judge(metadata),
                agent=self.default_agent,
                last_run_id=_row_value(row, "last_run_id", 9),
                last_started_at=_row_value(row, "last_started_at", 10),
                last_finished_at=_row_value(row, "last_finished_at", 11),
                metadata=metadata,
            )
            job.metadata["restored"] = True
            job.metadata["restore_count"] = int(job.metadata.get("restore_count", 0) or 0) + 1
            self.jobs[job.name] = job
            self._persist_job(job)
        self._restored = True
        return self.list_jobs()

    def _restore_judge(self, metadata: dict[str, Any]) -> LLMJudge | None:
        """Restore judge from metadata."""
        if metadata.get("judge_kind") != "mock":
            return None
        agent = self.default_agent
        if agent is None:
            return None
        mock_response = str(metadata.get("mock_response", '{"score": 0.5, "rationale": "no judge configured"}'))
        pass_threshold = float(metadata.get("pass_threshold", 0.7))
        from logos_adk.providers.mock import MockProvider
        from logos_adk.testing_framework import LLMJudge
        return LLMJudge(
            agent.__class__("judge", model=MockProvider(responses=[mock_response])),
            pass_threshold=pass_threshold,
        )

    def get_job(self, name: str) -> ContinuousEvalJob | None:
        """Get job by name."""
        return self.jobs.get(name)

    def list_jobs(self) -> list[dict[str, Any]]:
        """List all jobs."""
        return [self.job_snapshot(job) for job in self.jobs.values()]

    def job_snapshot(self, job: ContinuousEvalJob) -> dict[str, Any]:
        """Create job snapshot."""
        return {
            "name": job.name,
            "kind": job.kind,
            "agent_name": job.agent_name,
            "workspace_id": job.workspace_id,
            "agent_filter": job.agent_filter,
            "interval_seconds": job.interval_seconds,
            "limit": job.limit,
            "include_unreviewed": job.include_unreviewed,
            "last_run_id": job.last_run_id,
            "last_started_at": job.last_started_at,
            "last_finished_at": job.last_finished_at,
            "running": bool(job.task is not None and not job.task.done()),
            "has_judge": job.judge is not None,
            "restored": bool(job.metadata.get("restored")),
            "restore_count": int(job.metadata.get("restore_count", 0) or 0),
            "metadata": dict(job.metadata),
        }

    async def remove_job(self, name: str) -> dict[str, Any] | None:
        """Remove job."""
        job = self.jobs.pop(name, None)
        if job is None:
            return None
        if job.task is not None:
            job.task.cancel()
            await asyncio.gather(job.task, return_exceptions=True)
            job.task = None
        self._delete_persisted_job(name)
        return self.job_snapshot(job)

    def register_regression_job(
        self,
        name: str,
        *,
        agent: Agent,
        workspace_id: str | None = None,
        agent_name: str | None = None,
        interval_seconds: float = 3600.0,
        limit: int = 100,
        metadata: dict[str, Any] | None = None,
    ) -> ContinuousEvalJob:
        """Register regression job."""
        job = ContinuousEvalJob(
            name=name,
            kind="regression",
            agent_name=agent_name or getattr(agent, "name", "agent"),
            workspace_id=workspace_id,
            agent_filter=agent_name or getattr(agent, "name", "agent"),
            interval_seconds=interval_seconds,
            limit=limit,
            agent=agent,
            metadata=dict(metadata or {}),
        )
        self.jobs[name] = job
        self._persist_job(job)
        return job

    def register_judge_job(
        self,
        name: str,
        *,
        agent: Agent,
        judge: LLMJudge,
        workspace_id: str | None = None,
        agent_name: str | None = None,
        interval_seconds: float = 3600.0,
        limit: int = 100,
        metadata: dict[str, Any] | None = None,
    ) -> ContinuousEvalJob:
        """Register judge job."""
        job = ContinuousEvalJob(
            name=name,
            kind="judge-eval",
            agent_name=agent_name or getattr(agent, "name", "agent"),
            workspace_id=workspace_id,
            agent_filter=agent_name or getattr(agent, "name", "agent"),
            interval_seconds=interval_seconds,
            limit=limit,
            judge=judge,
            agent=agent,
            metadata=dict(metadata or {}),
        )
        self.jobs[name] = job
        self._persist_job(job)
        return job

    async def start(self) -> None:
        """Start evaluation runner."""
        self.restore_jobs()
        self._stop.clear()
        for job in self.jobs.values():
            if job.task is None or job.task.done():
                job.task = asyncio.create_task(self._run_job_loop(job))

    async def stop(self) -> None:
        """Stop evaluation runner."""
        self._stop.set()
        tasks = [job.task for job in self.jobs.values() if job.task is not None]
        for task in tasks:
            task.cancel()
        if tasks:
            await asyncio.gather(*tasks, return_exceptions=True)
        for job in self.jobs.values():
            job.task = None

    async def run_job_once(self, name: str) -> QualityRunRecord:
        """Run job once."""
        job = self.jobs[name]
        suite = self._suite_payload(job)
        started_at = utcnow()
        record = QualityRunRecord(
            id=uuid4().hex,
            agent_name=job.agent_name,
            kind=job.kind,
            status="running",
            created_at=started_at,
            started_at=started_at,
            suite=suite,
        )
        self.quality_manager.store.create_run(record)
        job.last_started_at = started_at
        try:
            payload = await self._run_payload(job)
        except Exception as exc:
            finished_at = utcnow()
            self.quality_manager.store.update_run(
                record.id,
                status="failed",
                finished_at=finished_at,
                error=str(exc),
            )
            job.last_run_id = record.id
            job.last_finished_at = finished_at
            self._persist_job(job)
            raise
        finished_at = utcnow()
        self.quality_manager.store.update_run(
            record.id,
            status="completed",
            finished_at=finished_at,
            summary=payload.get("summary"),
            rows=payload.get("rows"),
            reports=payload.get("reports"),
        )
        job.last_run_id = record.id
        job.last_finished_at = finished_at
        self._persist_job(job)
        completed = self.quality_manager.get(record.id)
        if completed is not None:
            self._apply_prompt_policy(job, completed)
        return completed or record

    async def _run_job_loop(self, job: ContinuousEvalJob) -> None:
        """Run job loop."""
        while not self._stop.is_set():
            try:
                await self.run_job_once(job.name)
            except asyncio.CancelledError:
                raise
            except Exception:
                pass
            try:
                await asyncio.wait_for(self._stop.wait(), timeout=max(0.01, job.interval_seconds))
            except TimeoutError:
                continue

    def _suite_payload(self, job: ContinuousEvalJob) -> dict[str, Any]:
        """Create suite payload."""
        if job.kind == "regression":
            return self.engine.build_regression_suite(
                workspace_id=job.workspace_id,
                agent_name=job.agent_filter,
                limit=job.limit,
            )
        return {
            "cases": [
                asdict(case)
                for case in self.engine.build_judge_cases_from_prod(
                    workspace_id=job.workspace_id,
                    agent_name=job.agent_filter,
                    limit=job.limit,
                )
            ],
            "workspace_id": job.workspace_id,
            "agent_name": job.agent_filter,
            "generated_at": utcnow(),
        }

    async def _run_payload(self, job: ContinuousEvalJob) -> dict[str, Any]:
        """Run evaluation payload."""
        if job.agent is None:
            raise ValueError(f"Eval job {job.name} has no bound agent")
        agent = job.agent
        prompt_override = None
        if self.prompt_registry is not None:
            policy = job.metadata.get("self_learning_policy") if isinstance(job.metadata, dict) else None
            task_type = policy.get("task_type") if isinstance(policy, dict) else job.metadata.get("task_type")
            user_segment = policy.get("user_segment") if isinstance(policy, dict) else job.metadata.get("user_segment")
            environment = str(policy.get("environment", "prod")) if isinstance(policy, dict) else "prod"
            prompt_name = str(policy.get("prompt_name") or job.agent_filter or job.agent_name) if isinstance(policy, dict) else (job.agent_filter or job.agent_name)
            target_version = int(policy.get("target_version", 0) or 0) if isinstance(policy, dict) else 0
            if target_version > 0:
                version_record = self.prompt_registry.get_version(prompt_name, target_version)
                prompt_override = version_record.content if version_record is not None else None
            if prompt_override is None:
                resolution = self.prompt_registry.resolve(
                    prompt_name,
                    workspace_id=job.workspace_id,
                    task_type=task_type,
                    user_segment=user_segment,
                    environment=environment,
                )
                prompt_override = resolution.content if resolution is not None else None
        if job.kind == "regression":
            return await self.engine.run_regression_snapshot(
                agent,
                workspace_id=job.workspace_id,
                agent_name=job.agent_filter,
                limit=job.limit,
                system_prompt_override=prompt_override,
            )
        if job.judge is None:
            raise ValueError(f"Judge job {job.name} requires a judge")
        return await self.engine.run_judge_snapshot(
            agent,
            judge=job.judge,
            workspace_id=job.workspace_id,
            agent_name=job.agent_filter,
            limit=job.limit,
            system_prompt_override=prompt_override,
        )

    def _apply_prompt_policy(self, job: ContinuousEvalJob, record: QualityRunRecord) -> None:
        """Apply prompt policy based on evaluation results."""
        if self.prompt_registry is None:
            return
        policy = job.metadata.get("self_learning_policy")
        if not isinstance(policy, dict):
            return
        prompt_name = str(policy.get("prompt_name") or job.agent_filter or job.agent_name)
        workspace_id = policy.get("workspace_id", job.workspace_id)
        task_type = policy.get("task_type")
        user_segment = policy.get("user_segment")
        environment = str(policy.get("environment", "prod"))
        min_total = int(policy.get("min_total_cases", 1) or 1)
        min_pass_rate = float(policy.get("min_pass_rate", 0.95))
        auto_rollback = bool(policy.get("auto_rollback_on_regression", False))
        disable_experiment_after = int(policy.get("disable_experiment_after", 2) or 2)
        experiment_id = str(policy.get("experiment_id")) if policy.get("experiment_id") else None
        target_version = int(policy.get("target_version", 0) or 0) or None
        summary = record.summary or {}
        total = int(summary.get("total", 0) or 0)
        passed = int(summary.get("passed", 0) or 0)
        pass_rate = (passed / total) if total else 1.0
        is_regression = total >= min_total and pass_rate < min_pass_rate
        policy_state = dict(job.metadata.get("policy_state") or {})
        if is_regression:
            policy_state["consecutive_regressions"] = int(policy_state.get("consecutive_regressions", 0) or 0) + 1
        else:
            policy_state["consecutive_regressions"] = 0
        policy_state["last_pass_rate"] = round(pass_rate, 4)
        policy_state["last_total_cases"] = total
        job.metadata["policy_state"] = policy_state
        self._persist_job(job)
        if not is_regression:
            return
        streak = int(policy_state.get("consecutive_regressions", 0) or 0)
        incident = self._create_incident(
            job=job,
            kind="regression_threshold_breach",
            severity="warning",
            message="Continuous eval detected a regression against the configured threshold policy.",
            run_id=record.id,
            prompt_name=prompt_name,
            experiment_id=experiment_id,
            metadata={
                "pass_rate": round(pass_rate, 4),
                "min_pass_rate": min_pass_rate,
                "total_cases": total,
                "min_total_cases": min_total,
                "consecutive_regressions": streak,
            },
        )
        self._emit_policy_telemetry(
            metric_name="adk_continuous_eval_regression_total",
            value=1.0,
            job=job,
            prompt_name=prompt_name,
            extra_attributes={"incident_id": incident.id},
            log_message="Continuous eval regression threshold breached",
        )
        if experiment_id and streak >= disable_experiment_after:
            try:
                experiment = self.prompt_registry.get_experiment(experiment_id)
                if experiment is not None and experiment.status == "active":
                    self.prompt_registry.end_experiment(experiment_id, status="disabled")
                    disable_incident = self._create_incident(
                        job=job,
                        kind="experiment_auto_disabled",
                        severity="critical",
                        message="Prompt experiment auto-disabled after sustained regression.",
                        run_id=record.id,
                        prompt_name=prompt_name,
                        experiment_id=experiment_id,
                        metadata={
                            "consecutive_regressions": streak,
                            "disable_experiment_after": disable_experiment_after,
                        },
                    )
                    self._emit_policy_telemetry(
                        metric_name="adk_self_learning_experiment_disabled_total",
                        value=1.0,
                        job=job,
                        prompt_name=prompt_name,
                        extra_attributes={"incident_id": disable_incident.id, "experiment_id": experiment_id},
                        log_message="Prompt experiment auto-disabled after sustained regression",
                    )
            except Exception:
                pass
        should_rollback = auto_rollback
        if not should_rollback:
            return
        deployments = self.prompt_registry.list_deployments(
            prompt_name,
            environment=environment,
            workspace_id=workspace_id,
            task_type=task_type,
            user_segment=user_segment,
            include_inactive=True,
        )
        if len(deployments) < 2:
            return
        active = deployments[0]
        active_meta = dict(active.metadata or {})
        created_by = active_meta.get("source")
        if target_version is not None and active.version != target_version:
            return
        if created_by != "self_learning_loop":
            return
        rollback = self.prompt_registry.rollback(
            prompt_name,
            environment=environment,
            workspace_id=workspace_id,
            task_type=task_type,
            user_segment=user_segment,
        )
        reports = dict(record.reports or {})
        reports["auto_rollback"] = json.dumps(
            {
                "prompt_name": prompt_name,
                "from_version": active.version,
                "to_version": rollback.version,
                "pass_rate": round(pass_rate, 4),
                "min_pass_rate": min_pass_rate,
                "quality_run_id": record.id,
            },
            sort_keys=True,
        )
        self.quality_manager.store.update_run(record.id, reports=reports)
        rollback_incident = self._create_incident(
            job=job,
            kind="prompt_auto_rollback",
            severity="critical",
            message="Prompt deployment auto-rolled back after continuous eval regression.",
            run_id=record.id,
            prompt_name=prompt_name,
            experiment_id=experiment_id,
            metadata={
                "from_version": active.version,
                "to_version": rollback.version,
                "pass_rate": round(pass_rate, 4),
                "min_pass_rate": min_pass_rate,
                "consecutive_regressions": streak,
            },
        )
        self._emit_policy_telemetry(
            metric_name="adk_self_learning_auto_rollback_total",
            value=1.0,
            job=job,
            prompt_name=prompt_name,
            extra_attributes={"incident_id": rollback_incident.id},
            log_message="Prompt deployment auto-rolled back after regression",
        )

    def _emit_policy_telemetry(
        self,
        *,
        metric_name: str,
        value: float,
        job: ContinuousEvalJob,
        prompt_name: str,
        extra_attributes: dict[str, Any] | None = None,
        log_message: str,
    ) -> None:
        """Emit policy telemetry."""
        try:
            from logos_adk.observe import Metric, get_observability_store

            store = get_observability_store()
            attributes = {
                "job_name": job.name,
                "agent_name": job.agent_name,
                "workspace_id": job.workspace_id or "__global__",
                "prompt_name": prompt_name,
                "kind": job.kind,
            }
            attributes.update(dict(extra_attributes or {}))
            store.record_metric(
                Metric(
                    name=metric_name,
                    value=value,
                    unit="count",
                    attributes=attributes,
                )
            )
            store.log(
                level="warning" if value < 2 else "critical",
                message=log_message,
                source="continuous_eval",
                agent_name=job.agent_name,
                attributes=attributes,
            )
        except Exception:
            return


__all__ = ["ContinuousEvalRunner"]
