"""Learning CLI groups - learning, retrieval, tool-workflow, model-routing, evaluation, improvement, transfer."""
from __future__ import annotations

import asyncio
import json
from dataclasses import asdict
from pathlib import Path
from typing import TYPE_CHECKING, Any

import click

from ..utils.output import write_result_output

if TYPE_CHECKING:
    pass


def _retrieval_dsn_option(f: Any) -> Any:
    """Add --retrieval-dsn option to command."""
    return click.option(
        "--retrieval-dsn",
        help="Override retrieval learning DSN (PostgreSQL)",
    )(f)


def _build_retrieval_engine(
    db_path: str | None,
    config_path: Path | None,
    dsn: str | None = None,
) -> Any:
    """Build retrieval learning engine."""
    from logos_adk.learning.retrieval_learning import RetrievalLearningEngine
    
    if dsn:
        return RetrievalLearningEngine(dsn=dsn)
    
    if config_path:
        from logos_adk.config_loader import load_agent_config
        config = load_agent_config(str(config_path))
        return RetrievalLearningEngine.from_config(config)
    
    return RetrievalLearningEngine(path=db_path or ".logos_adk/retrieval_learning.db")


def _write_and_echo_retrieval_items(
    *,
    output: Path | None,
    payload: dict[str, Any],
    empty_message: str,
    render: Any,
) -> None:
    """Write payload to file and print a human-readable summary."""
    write_result_output(output, payload)
    items = payload["items"]
    if not items:
        click.echo(empty_message)
        return
    render(items)


def _require_retrieval_request(action: str, request: Any | None, request_id: str) -> Any:
    """Raise a ClickException when a reindex request action cannot be applied."""
    if request is None:
        raise click.ClickException(f"Unknown or unsupported reindex request for {action}: {request_id}")
    return request


def _build_prompt_registry(path: str | None) -> Any | None:
    """Build prompt registry when a path is provided."""
    if not path:
        return None
    from logos_adk.learning import PromptRegistry

    return PromptRegistry(path)


def _build_evaluation_components(
    *,
    learning_db_path: str | None,
    quality_db_path: str | None,
    continuous_eval_db_path: str | None,
    prompt_registry_path: str | None,
    agent: Any | None = None,
) -> tuple[Any, Any, Any]:
    """Build runner, engine, and prompt registry for evaluation CLI commands."""
    from logos_adk.learning import EvaluationLoopEngine

    from ..helpers import (
        _build_continuous_eval_runner,
        _build_learning_store,
        _build_quality_manager,
    )

    learning_store = _build_learning_store(learning_db_path)
    quality_manager = _build_quality_manager(quality_db_path)
    engine = EvaluationLoopEngine(learning_store)
    prompt_registry = _build_prompt_registry(prompt_registry_path)
    runner = _build_continuous_eval_runner(
        learning_store,
        quality_manager,
        engine,
        path=continuous_eval_db_path,
        agent=agent,
        prompt_registry=prompt_registry,
        restore_jobs=True,
        restore_all_jobs=True,
    )
    return runner, engine, prompt_registry


def _load_agent_from_config(agent_config: Path | None) -> Any | None:
    """Load an agent from config when the CLI command needs one."""
    if agent_config is None:
        return None
    from logos_adk.cli import Agent

    return Agent.from_config(str(agent_config))


def _parse_json_option(value: str | None, *, option_name: str) -> dict[str, Any] | None:
    """Parse a JSON-encoded CLI option."""
    if value is None:
        return None
    try:
        parsed = json.loads(value)
    except json.JSONDecodeError as exc:
        raise click.ClickException(f"Invalid JSON for {option_name}: {exc}") from exc
    if parsed is not None and not isinstance(parsed, dict):
        raise click.ClickException(f"{option_name} must be a JSON object")
    return parsed


# =============================================================================
# Storage Group
# =============================================================================

@click.group()
def storage() -> None:
    """Persistence schema and migration commands."""
    pass


@storage.command("upgrade-check")
@click.option("--kind", type=click.Choice(["state", "memory", "cache", "quality"]), required=True)
@click.option("--backend", type=click.Choice(["sqlite", "postgresql"]), default="sqlite")
@click.option("--path", help="SQLite database path")
@click.option("--dsn", help="PostgreSQL DSN")
def storage_upgrade_check(kind: str, backend: str, path: str | None, dsn: str | None) -> None:
    """Check whether a persistence target needs migration."""
    if backend == "sqlite":
        sqlite_path = path or {
            "state": ".logos_adk/state.db",
            "memory": ".logos_adk/memory.db",
            "cache": ".logos_cache.db",
            "quality": ".logos_adk/quality_runs.db",
        }[kind]
        
        from logos_adk.persistence import get_sqlite_schema_version
        import sqlite3
        
        connection = sqlite3.connect(sqlite_path)
        try:
            connection.row_factory = sqlite3.Row
            current = get_sqlite_schema_version(connection, {
                "state": "state_store",
                "memory": "memory_backend",
                "cache": "cache_backend",
                "quality": "quality_history",
            }[kind]) or 0
        finally:
            connection.close()
        
        click.echo(f"{kind}:{backend}")
        click.echo(f"  path: {sqlite_path}")
        click.echo(f"  schema_version: {current}")
        click.echo("  expected_version: 1")
        click.echo(f"  upgrade_required: {'yes' if current < 1 else 'no'}")
        return
    
    from logos_adk.persistence import get_postgresql_schema_version
    from logos_adk.postgres_utils import import_psycopg, resolve_database_url
    
    resolved_dsn = resolve_database_url(dsn)
    if not resolved_dsn:
        raise click.ClickException("PostgreSQL upgrade-check requires a DSN or DATABASE_URL")
    
    psycopg = import_psycopg()
    with psycopg.connect(resolved_dsn) as connection:
        current = get_postgresql_schema_version(connection, {
            "state": "state_store",
            "memory": "memory_backend",
            "cache": "cache_backend",
            "quality": "quality_history",
        }[kind]) or 0
    
    click.echo(f"{kind}:{backend}")
    click.echo(f"  dsn: {resolved_dsn}")
    click.echo(f"  schema_version: {current}")
    click.echo("  expected_version: 1")
    click.echo(f"  upgrade_required: {'yes' if current < 1 else 'no'}")


@storage.command("migrate")
@click.option("--kind", type=click.Choice(["state", "memory", "cache", "quality"]), required=True)
@click.option("--backend", type=click.Choice(["sqlite", "postgresql"]), default="sqlite")
@click.option("--path", help="SQLite database path")
@click.option("--dsn", help="PostgreSQL DSN")
def storage_migrate(kind: str, backend: str, path: str | None, dsn: str | None) -> None:
    """Create/upgrade storage schema metadata and run built-in migrations."""
    if kind == "state" and backend == "sqlite":
        from logos_adk.state_store import SQLiteStateStore
        store = SQLiteStateStore(path or ".logos_adk/state.db")
        with store._connect() as connection:
            store._ensure_schema(connection)
    elif kind == "state" and backend == "postgresql":
        from logos_adk.state_store import PostgreSQLStateStore
        store = PostgreSQLStateStore(dsn or "")
        with store._connect() as connection:
            store._ensure_schema(connection)
    elif kind == "memory" and backend == "sqlite":
        from logos_adk.memory.backends.sqlite import SQLiteBackend
        backend_obj = SQLiteBackend(path or ".logos_adk/memory.db")
        with backend_obj._connect() as connection:
            backend_obj._ensure_schema(connection)
    elif kind == "memory" and backend == "postgresql":
        from logos_adk.memory.backends.postgresql import PostgreSQLBackend
        backend_obj = PostgreSQLBackend(dsn or "")
        with backend_obj._connect() as connection:
            backend_obj._ensure_schema(connection)
    elif kind == "cache" and backend == "sqlite":
        from logos_adk.cache.sqlite import SQLiteCacheBackend
        SQLiteCacheBackend(path or ".logos_cache.db")
    elif kind == "cache" and backend == "postgresql":
        from logos_adk.cache.postgresql import PostgreSQLCacheBackend
        backend_obj = PostgreSQLCacheBackend(dsn=dsn)
        with backend_obj._connect() as connection:
            backend_obj._ensure_schema(connection)
    elif kind == "quality" and backend == "sqlite":
        from logos_adk.quality_history import SQLiteQualityHistoryStore
        store = SQLiteQualityHistoryStore(path=path or ".logos_adk/quality_runs.db")
        with store._connect() as connection:
            store._ensure_schema(connection)
    elif kind == "quality" and backend == "postgresql":
        from logos_adk.quality_history import PostgreSQLQualityHistoryStore
        store = PostgreSQLQualityHistoryStore(dsn or "")
        with store._connect() as connection:
            store._ensure_schema(connection)
    else:
        raise click.ClickException(f"Unsupported storage target: {kind}/{backend}")
    
    click.echo(f"Migration complete for {kind}:{backend}")


# =============================================================================
# Retrieval Group
# =============================================================================

@click.group()
def retrieval() -> None:
    """Inspect retrieval-learning state for operators."""
    pass


@retrieval.command("source-scores")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--output", "-o", type=click.Path(path_type=Path))
def retrieval_source_scores(retrieval_dsn: str | None, db_path: str | None, config_path: Path | None, workspace_id: str | None, output: Path | None) -> None:
    """Print learned source usefulness scores."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    items = [asdict(item) for item in engine.list_source_scores(workspace_id=workspace_id)]
    payload = {"workspace_id": workspace_id, "items": items}
    write_result_output(output, payload)
    
    if not items:
        click.echo("No source scores found.")
        return
    
    click.echo(f"Workspace: {workspace_id or '*'}")
    for item in items:
        click.echo(
            f"{item['source_key']}: score={item['usefulness_score']} "
            f"helpful={item['helpful_count']} harmful={item['harmful_count']} "
            f"accepted={item['accepted_count']} rejected={item['rejected_count']}"
        )
        if item.get("connector"):
            click.echo(f"  connector: {item['connector']}")


@retrieval.command("reindex-triggers")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--output", "-o", type=click.Path(path_type=Path))
def retrieval_reindex_triggers(retrieval_dsn: str | None, db_path: str | None, config_path: Path | None, workspace_id: str | None, output: Path | None) -> None:
    """Print open retrieval reindex triggers."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    items = [asdict(item) for item in engine.list_reindex_triggers(workspace_id=workspace_id)]
    payload = {"workspace_id": workspace_id, "items": items}
    write_result_output(output, payload)
    
    if not items:
        click.echo("No reindex triggers found.")
        return
    
    click.echo(f"Workspace: {workspace_id or '*'}")
    for item in items:
        click.echo(
            f"{item['source_key']}: reason={item['reason']} severity={item['severity']} "
            f"occurrences={item['occurrences']} chunk_size={item['suggested_chunk_size']} "
            f"overlap={item['suggested_overlap']}"
        )


@retrieval.command("profiles")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--task-type", help="Task type segment")
@click.option("--user-segment", help="User segment")
@click.option("--output", "-o", type=click.Path(path_type=Path))
def retrieval_profiles(
    retrieval_dsn: str | None,
    db_path: str | None,
    config_path: Path | None,
    workspace_id: str | None,
    task_type: str | None,
    user_segment: str | None,
    output: Path | None,
) -> None:
    """Print learned retrieval profiles."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    items = [
        asdict(item)
        for item in engine.list_profiles(
            workspace_id=workspace_id,
            task_type=task_type,
            user_segment=user_segment,
        )
    ]
    payload = {
        "workspace_id": workspace_id,
        "task_type": task_type,
        "user_segment": user_segment,
        "items": items,
    }
    write_result_output(output, payload)
    
    if not items:
        click.echo("No retrieval profiles found.")
        return
    
    click.echo(f"Workspace: {workspace_id or '*'} task_type: {task_type or '*'} user_segment: {user_segment or '*'}")
    for item in items:
        click.echo(
            f"limit={item['limit']} score={item['score']} success={item['success_count']} "
            f"failure={item['failure_count']} filter={json.dumps(item['filter'], sort_keys=True)}"
        )


@retrieval.command("ack-trigger")
@click.argument("trigger_id")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--note", help="Operator note")
def retrieval_ack_trigger(trigger_id: str, retrieval_dsn: str | None, db_path: str | None, config_path: Path | None, note: str | None) -> None:
    """Acknowledge a reindex trigger."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    trigger = engine.acknowledge_reindex_trigger(trigger_id, note=note)
    if trigger is None:
        raise click.ClickException(f"Unknown reindex trigger: {trigger_id}")
    click.echo(f"{trigger.id}: status={trigger.status} source={trigger.source_key}")


@retrieval.command("resolve-trigger")
@click.argument("trigger_id")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--note", help="Operator note")
def retrieval_resolve_trigger(trigger_id: str, retrieval_dsn: str | None, db_path: str | None, config_path: Path | None, note: str | None) -> None:
    """Resolve a reindex trigger."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    trigger = engine.resolve_reindex_trigger(trigger_id, note=note)
    if trigger is None:
        raise click.ClickException(f"Unknown reindex trigger: {trigger_id}")
    click.echo(f"{trigger.id}: status={trigger.status} source={trigger.source_key}")


@retrieval.command("close-trigger")
@click.argument("trigger_id")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--note", help="Operator note")
def retrieval_close_trigger(trigger_id: str, retrieval_dsn: str | None, db_path: str | None, config_path: Path | None, note: str | None) -> None:
    """Close a reindex trigger."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    trigger = engine.close_reindex_trigger(trigger_id, note=note)
    if trigger is None:
        raise click.ClickException(f"Unknown reindex trigger: {trigger_id}")
    click.echo(f"{trigger.id}: status={trigger.status} source={trigger.source_key}")


@retrieval.command("source-documents")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--source-key", help="Source identifier")
@click.option("--output", "-o", type=click.Path(path_type=Path))
def retrieval_source_documents(
    retrieval_dsn: str | None,
    db_path: str | None,
    config_path: Path | None,
    workspace_id: str | None,
    source_key: str | None,
    output: Path | None,
) -> None:
    """Print stored source-document snapshots."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    items = []
    for item in engine.list_source_documents(workspace_id=workspace_id, source_key=source_key):
        payload = asdict(item)
        payload["content_length"] = len(item.content)
        items.append(payload)
    _write_and_echo_retrieval_items(
        output=output,
        payload={"workspace_id": workspace_id, "source_key": source_key, "items": items},
        empty_message="No source documents found.",
        render=lambda rows: (
            click.echo(f"Workspace: {workspace_id or '*'} source_key: {source_key or '*'}"),
            [click.echo(
                f"{row['source_key']}:{row['document_id']} length={row['content_length']} updated={row['updated_at']}"
            ) for row in rows],
        ),
    )


@retrieval.command("force-reindex")
@click.argument("source_key")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--reason", default="operator_requested", help="Reason for the request")
@click.option("--requested-by", help="Operator or system requesting reindex")
@click.option("--output", "-o", type=click.Path(path_type=Path))
def retrieval_force_reindex(
    source_key: str,
    retrieval_dsn: str | None,
    db_path: str | None,
    config_path: Path | None,
    workspace_id: str | None,
    reason: str,
    requested_by: str | None,
    output: Path | None,
) -> None:
    """Create a manual reindex request."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    request = engine.create_force_reindex_request(
        workspace_id=workspace_id,
        source_key=source_key,
        reason=reason,
        requested_by=requested_by,
    )
    payload = {"request": asdict(request)}
    write_result_output(output, payload)
    click.echo(f"{request.id}: status={request.status} source={request.source_key}")


@retrieval.command("export-source-scores")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--format", "output_format", type=click.Choice(["json", "csv"]), default="json")
@click.option("--output", "-o", type=click.Path(path_type=Path))
def retrieval_export_source_scores(
    retrieval_dsn: str | None,
    db_path: str | None,
    config_path: Path | None,
    workspace_id: str | None,
    output_format: str,
    output: Path | None,
) -> None:
    """Export learned source usefulness scores."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    report = engine.export_source_scores_report(workspace_id=workspace_id, format=output_format)
    if output is not None:
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(report)
    click.echo(report)


@retrieval.command("reindex-requests")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--output", "-o", type=click.Path(path_type=Path))
def retrieval_reindex_requests(
    retrieval_dsn: str | None,
    db_path: str | None,
    config_path: Path | None,
    workspace_id: str | None,
    output: Path | None,
) -> None:
    """Print reindex requests."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    items = [asdict(item) for item in engine.list_reindex_requests(workspace_id=workspace_id)]
    _write_and_echo_retrieval_items(
        output=output,
        payload={"workspace_id": workspace_id, "items": items},
        empty_message="No reindex requests found.",
        render=lambda rows: (
            click.echo(f"Workspace: {workspace_id or '*'}"),
            [click.echo(
                f"{row['id']}: status={row['status']} source={row['source_key']} requested_by={row['requested_by']}"
            ) for row in rows],
        ),
    )


@retrieval.command("cancel-request")
@click.argument("request_id")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--note", help="Operator note")
def retrieval_cancel_request(
    request_id: str,
    retrieval_dsn: str | None,
    db_path: str | None,
    config_path: Path | None,
    note: str | None,
) -> None:
    """Cancel a queued or running reindex request."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    request = _require_retrieval_request(
        "cancel",
        engine.cancel_reindex_request(request_id, note=note),
        request_id,
    )
    click.echo(f"{request.id}: status={request.status} source={request.source_key}")


@retrieval.command("resume-request")
@click.argument("request_id")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--note", help="Operator note")
def retrieval_resume_request(
    request_id: str,
    retrieval_dsn: str | None,
    db_path: str | None,
    config_path: Path | None,
    note: str | None,
) -> None:
    """Resume a canceled reindex request."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    request = _require_retrieval_request(
        "resume",
        engine.resume_reindex_request(request_id, note=note),
        request_id,
    )
    click.echo(f"{request.id}: status={request.status} source={request.source_key}")


@retrieval.command("replay-request")
@click.argument("request_id")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--note", help="Operator note")
def retrieval_replay_request(
    request_id: str,
    retrieval_dsn: str | None,
    db_path: str | None,
    config_path: Path | None,
    note: str | None,
) -> None:
    """Replay a canceled reindex request as a fresh request."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    request = _require_retrieval_request(
        "replay",
        engine.replay_reindex_request(request_id, note=note),
        request_id,
    )
    click.echo(f"{request.id}: status={request.status} source={request.source_key}")


@retrieval.command("summary")
@_retrieval_dsn_option
@click.option("--db-path", default=None, help="Retrieval learning SQLite DB path")
@click.option("--config", "config_path", type=click.Path(exists=True, path_type=Path), help="Agent YAML config")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--output", "-o", type=click.Path(path_type=Path))
def retrieval_summary(
    retrieval_dsn: str | None,
    db_path: str | None,
    config_path: Path | None,
    workspace_id: str | None,
    output: Path | None,
) -> None:
    """Print retrieval operations summary."""
    engine = _build_retrieval_engine(db_path, config_path, dsn=retrieval_dsn)
    payload = engine.ops_summary(workspace_id=workspace_id)
    write_result_output(output, payload)
    click.echo(f"Workspace: {workspace_id or '*'}")
    click.echo(f"Requests: {payload['requests']['total']}")
    click.echo(f"Triggers: {payload['triggers']['total']}")
    click.echo(f"Source Documents: {payload['source_documents']['total']}")
    click.echo(f"Coalescing: {payload['coalescing']['coalesced_total']}")


# =============================================================================
# Tool Workflow Group
# =============================================================================

@click.group("tool-workflow")
def tool_workflow() -> None:
    """Inspect tool and workflow learning state for operators."""
    pass


# =============================================================================
# Model Routing Group
# =============================================================================

@click.group("model-routing")
def model_routing() -> None:
    """Inspect model routing learning state for operators."""
    pass


# =============================================================================
# Evaluation Group
# =============================================================================

@click.group()
def evaluation() -> None:
    """Inspect and run evaluation loop workflows."""
    pass


@evaluation.command("scorecard")
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--agent-name", help="Agent scope")
@click.option("--window-days", default=7, type=int, show_default=True)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_scorecard(
    db_path: str | None,
    workspace_id: str | None,
    agent_name: str | None,
    window_days: int,
    output: Path | None,
) -> None:
    """Print an operator scorecard for recent production behavior."""
    from logos_adk.learning import EvaluationLoopEngine

    from ..helpers import _build_learning_store

    engine = EvaluationLoopEngine(_build_learning_store(db_path))
    scorecard = asdict(
        engine.build_scorecard(
            workspace_id=workspace_id,
            agent_name=agent_name,
            window_days=window_days,
        )
    )
    write_result_output(output, {"scorecard": scorecard})
    click.echo(
        "quality_delta={quality:.4f} retrieval_delta={retrieval:.4f} "
        "tool_delta={tool:.4f} latency_delta={latency:.4f}".format(
            quality=float(scorecard["quality"]["delta"]),
            retrieval=float(scorecard["retrieval_relevance"]["delta"]),
            tool=float(scorecard["tool_success"]["delta"]),
            latency=float(scorecard["latency"]["delta"]),
        )
    )


@evaluation.command("regression-dataset")
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--agent-name", help="Agent scope")
@click.option("--limit", default=100, type=int, show_default=True)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_regression_dataset(
    db_path: str | None,
    workspace_id: str | None,
    agent_name: str | None,
    limit: int,
    output: Path | None,
) -> None:
    """Print a regression dataset derived from production feedback."""
    from logos_adk.learning import EvaluationLoopEngine

    from ..helpers import _build_learning_store

    engine = EvaluationLoopEngine(_build_learning_store(db_path))
    dataset = asdict(
        engine.build_regression_dataset(
            workspace_id=workspace_id,
            agent_name=agent_name,
            limit=limit,
        )
    )
    write_result_output(output, {"dataset": dataset})
    click.echo(
        "examples={count} workspace_id={workspace} agent_name={agent}".format(
            count=len(dataset["examples"]),
            workspace=dataset.get("workspace_id") or "*",
            agent=dataset.get("agent_name") or "*",
        )
    )


@evaluation.command("judge-disagreements")
@click.argument("agent_config", type=click.Path(exists=True, path_type=Path))
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--agent-name", help="Agent scope")
@click.option("--limit", default=100, type=int, show_default=True)
@click.option("--threshold", default=0.35, type=float, show_default=True)
@click.option("--pass-threshold", default=0.7, type=float, show_default=True)
@click.option("--mock-response", default='{"score": 0.5, "rationale": "no judge configured"}', show_default=True)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_judge_disagreements(
    agent_config: Path,
    db_path: str | None,
    workspace_id: str | None,
    agent_name: str | None,
    limit: int,
    threshold: float,
    pass_threshold: float,
    mock_response: str,
    output: Path | None,
) -> None:
    """Run a judge snapshot and print human-vs-judge disagreements."""
    from logos_adk.learning import EvaluationLoopEngine
    from logos_adk.providers.mock import MockProvider
    from logos_adk.testing_framework import LLMJudge

    from ..helpers import _build_learning_store

    agent = _load_agent_from_config(agent_config)
    engine = EvaluationLoopEngine(_build_learning_store(db_path))
    judge = LLMJudge(
        agent.__class__("judge", model=MockProvider(responses=[mock_response])),
        pass_threshold=pass_threshold,
    )
    payload = asyncio.run(
        engine.run_judge_snapshot(
            agent,
            judge=judge,
            workspace_id=workspace_id,
            agent_name=agent_name,
            limit=limit,
        )
    )
    result = {
        "summary": payload.get("summary", {}),
        "items": payload.get("disagreements", []),
    }
    write_result_output(output, result)
    click.echo(
        "disagreements={count} judge_disagreements={summary}".format(
            count=len(result["items"]),
            summary=result["summary"].get("judge_disagreements", 0),
        )
    )


@evaluation.command("champion-challenger")
@click.argument("agent_config", type=click.Path(exists=True, path_type=Path))
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--agent-name", help="Agent scope")
@click.option("--champion-prompt", default=None)
@click.option("--challenger-prompt", required=True)
@click.option("--champion-filter-json", default=None)
@click.option("--challenger-filter-json", default=None)
@click.option("--limit", default=100, type=int, show_default=True)
@click.option("--reward-scorer-mode", default="heuristic", show_default=True)
@click.option("--reward-training-limit", default=500, type=int, show_default=True)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_champion_challenger(
    agent_config: Path,
    db_path: str | None,
    workspace_id: str | None,
    agent_name: str | None,
    champion_prompt: str | None,
    challenger_prompt: str,
    champion_filter_json: str | None,
    challenger_filter_json: str | None,
    limit: int,
    reward_scorer_mode: str,
    reward_training_limit: int,
    output: Path | None,
) -> None:
    """Compare champion and challenger prompts against production-derived cases."""
    from logos_adk.learning import EvaluationLoopEngine

    from ..helpers import _build_learning_store

    agent = _load_agent_from_config(agent_config)
    engine = EvaluationLoopEngine(_build_learning_store(db_path))
    report = asdict(
        asyncio.run(
            engine.compare_champion_challenger(
                agent,
                challenger_prompt=challenger_prompt,
                champion_prompt=champion_prompt,
                workspace_id=workspace_id,
                agent_name=agent_name,
                champion_filter=_parse_json_option(
                    champion_filter_json,
                    option_name="--champion-filter-json",
                ),
                challenger_filter=_parse_json_option(
                    challenger_filter_json,
                    option_name="--challenger-filter-json",
                ),
                limit=limit,
                reward_scorer_mode=reward_scorer_mode,
                reward_training_limit=reward_training_limit,
            )
        )
    )
    write_result_output(output, {"report": report})
    click.echo(f"Recommendation: {report['recommendation']}")


@evaluation.command("run-regression-job")
@click.argument("agent_config", type=click.Path(exists=True, path_type=Path))
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--quality-db-path", default=None, help="Quality history SQLite DB path")
@click.option("--continuous-eval-db-path", default=None, help="Continuous eval SQLite DB path")
@click.option("--prompt-registry-path", default=None, help="Prompt registry SQLite DB path")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--agent-name", help="Agent scope")
@click.option("--name", required=True, help="Job name")
@click.option("--interval-seconds", default=3600.0, type=float, show_default=True)
@click.option("--limit", default=100, type=int, show_default=True)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_run_regression_job(
    agent_config: Path,
    db_path: str | None,
    quality_db_path: str | None,
    continuous_eval_db_path: str | None,
    prompt_registry_path: str | None,
    workspace_id: str | None,
    agent_name: str | None,
    name: str,
    interval_seconds: float,
    limit: int,
    output: Path | None,
) -> None:
    """Register and run one regression job immediately."""
    agent = _load_agent_from_config(agent_config)
    runner, _, _ = _build_evaluation_components(
        learning_db_path=db_path,
        quality_db_path=quality_db_path,
        continuous_eval_db_path=continuous_eval_db_path,
        prompt_registry_path=prompt_registry_path,
        agent=agent,
    )
    if runner.get_job(name) is None:
        runner.register_regression_job(
            name,
            agent=agent,
            workspace_id=workspace_id,
            agent_name=agent_name,
            interval_seconds=interval_seconds,
            limit=limit,
        )
    run = asyncio.run(runner.run_job_once(name))
    job = runner.get_job(name)
    payload = {
        "job": runner.job_snapshot(job) if job is not None else {"name": name},
        "run": asdict(run),
    }
    write_result_output(output, payload)
    click.echo(f"{name}: status={payload['run']['status']}")


@evaluation.command("restored-jobs")
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--quality-db-path", default=None, help="Quality history SQLite DB path")
@click.option("--continuous-eval-db-path", default=None, help="Continuous eval SQLite DB path")
@click.option("--prompt-registry-path", default=None, help="Prompt registry SQLite DB path")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_restored_jobs(
    db_path: str | None,
    quality_db_path: str | None,
    continuous_eval_db_path: str | None,
    prompt_registry_path: str | None,
    workspace_id: str | None,
    output: Path | None,
) -> None:
    """List restored continuous evaluation jobs."""
    runner, _, _ = _build_evaluation_components(
        learning_db_path=db_path,
        quality_db_path=quality_db_path,
        continuous_eval_db_path=continuous_eval_db_path,
        prompt_registry_path=prompt_registry_path,
    )
    items = runner.list_restored_jobs()
    if workspace_id is not None:
        items = [item for item in items if item.get("workspace_id") == workspace_id]
    payload = {"items": items}
    write_result_output(output, payload)
    if not items:
        click.echo("No restored jobs found.")
        return
    for item in items:
        click.echo(f"{item['name']}: kind={item['kind']} restored={item['restored']}")


@evaluation.command("trend-deltas")
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--quality-db-path", default=None, help="Quality history SQLite DB path")
@click.option("--continuous-eval-db-path", default=None, help="Continuous eval SQLite DB path")
@click.option("--prompt-registry-path", default=None, help="Prompt registry SQLite DB path")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--job-name", default=None)
@click.option("--window-days", default=7, type=int, show_default=True)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_trend_deltas(
    db_path: str | None,
    quality_db_path: str | None,
    continuous_eval_db_path: str | None,
    prompt_registry_path: str | None,
    workspace_id: str | None,
    job_name: str | None,
    window_days: int,
    output: Path | None,
) -> None:
    """Print trend deltas for recent continuous evaluation activity."""
    runner, _, _ = _build_evaluation_components(
        learning_db_path=db_path,
        quality_db_path=quality_db_path,
        continuous_eval_db_path=continuous_eval_db_path,
        prompt_registry_path=prompt_registry_path,
    )
    report = asdict(
        runner.trend_delta_report(
            workspace_id=workspace_id,
            job_name=job_name,
            window_days=window_days,
        )
    )
    write_result_output(output, {"report": report})
    click.echo(f"incidents_delta={report['incidents']['delta']:.4f}")


@evaluation.command("rollback-incidents")
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--quality-db-path", default=None, help="Quality history SQLite DB path")
@click.option("--continuous-eval-db-path", default=None, help="Continuous eval SQLite DB path")
@click.option("--prompt-registry-path", default=None, help="Prompt registry SQLite DB path")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--job-name", default=None)
@click.option("--status", default=None)
@click.option("--limit", default=50, type=int, show_default=True)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_rollback_incidents(
    db_path: str | None,
    quality_db_path: str | None,
    continuous_eval_db_path: str | None,
    prompt_registry_path: str | None,
    workspace_id: str | None,
    job_name: str | None,
    status: str | None,
    limit: int,
    output: Path | None,
) -> None:
    """List rollback and regression incidents."""
    runner, _, _ = _build_evaluation_components(
        learning_db_path=db_path,
        quality_db_path=quality_db_path,
        continuous_eval_db_path=continuous_eval_db_path,
        prompt_registry_path=prompt_registry_path,
    )
    items = runner.list_incidents(
        limit=limit,
        job_name=job_name,
        workspace_id=workspace_id,
        status=status,
    )
    payload = {"items": items}
    write_result_output(output, payload)
    if not items:
        click.echo("No incidents found.")
        return
    for item in items:
        click.echo(f"{item['id']}: {item['kind']} status={item['status']}")


@evaluation.command("incident-snapshot")
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--quality-db-path", default=None, help="Quality history SQLite DB path")
@click.option("--continuous-eval-db-path", default=None, help="Continuous eval SQLite DB path")
@click.option("--prompt-registry-path", default=None, help="Prompt registry SQLite DB path")
@click.option("--workspace-id", help="Workspace scope")
@click.option("--job-name", default=None)
@click.option("--status", default=None)
@click.option("--limit", default=20, type=int, show_default=True)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_incident_snapshot(
    db_path: str | None,
    quality_db_path: str | None,
    continuous_eval_db_path: str | None,
    prompt_registry_path: str | None,
    workspace_id: str | None,
    job_name: str | None,
    status: str | None,
    limit: int,
    output: Path | None,
) -> None:
    """Print an aggregate snapshot of recent incidents."""
    runner, _, _ = _build_evaluation_components(
        learning_db_path=db_path,
        quality_db_path=quality_db_path,
        continuous_eval_db_path=continuous_eval_db_path,
        prompt_registry_path=prompt_registry_path,
    )
    snapshot = asdict(
        runner.incident_snapshot(
            workspace_id=workspace_id,
            job_name=job_name,
            status=status,
            limit=limit,
        )
    )
    write_result_output(output, {"snapshot": snapshot})
    click.echo(f"total_incidents={snapshot['summary']['total_incidents']}")


@evaluation.command("ack-incident")
@click.argument("incident_id")
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--quality-db-path", default=None, help="Quality history SQLite DB path")
@click.option("--continuous-eval-db-path", default=None, help="Continuous eval SQLite DB path")
@click.option("--prompt-registry-path", default=None, help="Prompt registry SQLite DB path")
@click.option("--actor", default=None)
@click.option("--note", default=None)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_ack_incident(
    incident_id: str,
    db_path: str | None,
    quality_db_path: str | None,
    continuous_eval_db_path: str | None,
    prompt_registry_path: str | None,
    actor: str | None,
    note: str | None,
    output: Path | None,
) -> None:
    """Acknowledge an evaluation incident."""
    runner, _, _ = _build_evaluation_components(
        learning_db_path=db_path,
        quality_db_path=quality_db_path,
        continuous_eval_db_path=continuous_eval_db_path,
        prompt_registry_path=prompt_registry_path,
    )
    incident = runner.acknowledge_incident(incident_id, actor=actor, note=note)
    if incident is None:
        raise click.ClickException(f"Unknown incident: {incident_id}")
    write_result_output(output, {"incident": incident})
    click.echo(f"{incident_id}: status={incident['status']}")


@evaluation.command("resolve-incident")
@click.argument("incident_id")
@click.option("--db-path", default=None, help="Learning store SQLite DB path")
@click.option("--quality-db-path", default=None, help="Quality history SQLite DB path")
@click.option("--continuous-eval-db-path", default=None, help="Continuous eval SQLite DB path")
@click.option("--prompt-registry-path", default=None, help="Prompt registry SQLite DB path")
@click.option("--actor", default=None)
@click.option("--note", default=None)
@click.option("--output", "-o", type=click.Path(path_type=Path))
def evaluation_resolve_incident(
    incident_id: str,
    db_path: str | None,
    quality_db_path: str | None,
    continuous_eval_db_path: str | None,
    prompt_registry_path: str | None,
    actor: str | None,
    note: str | None,
    output: Path | None,
) -> None:
    """Resolve an evaluation incident."""
    runner, _, _ = _build_evaluation_components(
        learning_db_path=db_path,
        quality_db_path=quality_db_path,
        continuous_eval_db_path=continuous_eval_db_path,
        prompt_registry_path=prompt_registry_path,
    )
    incident = runner.resolve_incident(incident_id, actor=actor, note=note)
    if incident is None:
        raise click.ClickException(f"Unknown incident: {incident_id}")
    write_result_output(output, {"incident": incident})
    click.echo(f"{incident_id}: status={incident['status']}")


# =============================================================================
# Improvement Group
# =============================================================================

@click.group()
def improvement() -> None:
    """Inspect true-improvement learning state for operators."""
    pass


# =============================================================================
# Transfer Group
# =============================================================================

@click.group()
def transfer() -> None:
    """Inspect and apply cross-agent learning transfers."""
    pass


__all__ = [
    "storage",
    "storage_upgrade_check",
    "storage_migrate",
    "retrieval",
    "retrieval_source_scores",
    "retrieval_reindex_triggers",
    "retrieval_profiles",
    "retrieval_ack_trigger",
    "retrieval_resolve_trigger",
    "retrieval_close_trigger",
    "retrieval_source_documents",
    "retrieval_force_reindex",
    "retrieval_export_source_scores",
    "retrieval_reindex_requests",
    "retrieval_cancel_request",
    "retrieval_resume_request",
    "retrieval_replay_request",
    "retrieval_summary",
    "tool_workflow",
    "model_routing",
    "evaluation",
    "evaluation_scorecard",
    "evaluation_regression_dataset",
    "evaluation_judge_disagreements",
    "evaluation_champion_challenger",
    "evaluation_run_regression_job",
    "evaluation_restored_jobs",
    "evaluation_trend_deltas",
    "evaluation_rollback_incidents",
    "evaluation_incident_snapshot",
    "evaluation_ack_incident",
    "evaluation_resolve_incident",
    "improvement",
    "transfer",
]
