"""Agent API endpoints — agent discovery, config display, and eval history."""

from __future__ import annotations

import json
import re
import sqlite3
from pathlib import Path
from typing import Any

import yaml
from aiohttp import web

agent_routes = web.RouteTableDef()

AGENT_DB_FILENAME = ".hdx/agent_evals.db"


# ---------------------------------------------------------------------------
# Helpers — agent discovery
# ---------------------------------------------------------------------------

_ENV_VAR_PATTERN = re.compile(r"\$\{([^}]+)}")
_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]*$")


def _validate_agent_name(name: str) -> bool:
    """Validate agent name to prevent path traversal."""
    return bool(_NAME_PATTERN.match(name))


def _find_agents_dir() -> Path:
    return Path.cwd() / "agents"


def _list_agent_dirs(agents_dir: Path) -> list[Path]:
    """List all agent directories that contain agent.yaml."""
    if not agents_dir.is_dir():
        return []
    return sorted(
        d for d in agents_dir.iterdir()
        if d.is_dir() and (d / "agent.yaml").exists()
    )


def _load_agent_yaml_raw(agent_dir: Path) -> dict[str, Any] | None:
    """Load agent.yaml WITHOUT env-var interpolation (safe for dashboard display).

    Returns None if the file doesn't exist or is unparseable.
    """
    yaml_path = agent_dir / "agent.yaml"
    if not yaml_path.exists():
        return None
    try:
        raw = yaml.safe_load(yaml_path.read_text())
        if not isinstance(raw, dict):
            return None
        return raw
    except (yaml.YAMLError, OSError):
        return None


def _extract_env_var_names(value: Any) -> list[str]:
    """Extract ${VAR} references from a string value."""
    if not isinstance(value, str):
        return []
    return _ENV_VAR_PATTERN.findall(value)


def _resolve_system_prompt_preview(
    raw: dict[str, Any], agent_dir: Path
) -> str:
    """Read the system prompt for preview. Returns empty string on failure."""
    prompt_file = raw.get("system_prompt_file")
    if prompt_file:
        prompt_path = agent_dir / prompt_file
        try:
            return prompt_path.read_text().strip()
        except OSError:
            return f"(file not readable: {prompt_file})"

    prompt = raw.get("system_prompt")
    if prompt:
        return str(prompt).strip()

    return ""


def _server_type(server: dict[str, Any]) -> str:
    """Determine connection type for a server entry."""
    if server.get("path"):
        return "path"
    if server.get("url"):
        return "url"
    return "command"


def _build_server_info(
    server: dict[str, Any], tool_filter: dict[str, Any] | None
) -> dict[str, Any]:
    """Build a server info dict for the detail response."""
    name = server.get("name", "unknown")
    s_type = _server_type(server)
    info: dict[str, Any] = {
        "name": name,
        "type": s_type,
        "env_vars": [],
    }

    if s_type == "path":
        info["path"] = server.get("path")
    elif s_type == "url":
        info["url"] = server.get("url")
    else:
        info["command"] = server.get("command")

    # Tool filter for this server
    info["tool_filter"] = None
    if tool_filter and name in tool_filter:
        filt = tool_filter[name]
        if isinstance(filt, dict):
            if filt.get("include"):
                info["tool_filter"] = {
                    "mode": "include",
                    "tools": filt["include"],
                }
            elif filt.get("exclude"):
                info["tool_filter"] = {
                    "mode": "exclude",
                    "tools": filt["exclude"],
                }

    # Env var names (not values — just the keys or ${VAR} references)
    env = server.get("env", {})
    if isinstance(env, dict):
        env_vars: list[str] = []
        for v in env.values():
            env_vars.extend(_extract_env_var_names(v))
        if not env_vars:
            env_vars = list(env.keys())
        info["env_vars"] = sorted(set(env_vars))

    return info


# ---------------------------------------------------------------------------
# Helpers — agent eval database
# ---------------------------------------------------------------------------


def _get_agent_db_conn() -> sqlite3.Connection | None:
    """Return a connection to the agent eval DB, or None."""
    db_path = Path.cwd() / AGENT_DB_FILENAME
    if not db_path.exists():
        return None
    conn = sqlite3.connect(str(db_path), timeout=5)
    conn.row_factory = sqlite3.Row
    return conn


def _get_last_eval_for_agent(agent_name: str) -> dict[str, Any] | None:
    """Get the most recent eval run for an agent."""
    conn = _get_agent_db_conn()
    if conn is None:
        return None

    try:
        row = conn.execute(
            """
            SELECT id, suite_name, completed_at, total_cases, passed, failed
            FROM eval_runs
            WHERE agent_name = ?
            ORDER BY id DESC
            LIMIT 1
            """,
            (agent_name,),
        ).fetchone()

        if row is None:
            return None

        total = row["total_cases"]
        passed = row["passed"] or 0
        return {
            "run_id": row["id"],
            "suite_name": row["suite_name"],
            "passed": row["failed"] == 0 and total > 0,
            "pass_rate": round(passed / total, 4) if total > 0 else 0,
            "completed_at": row["completed_at"],
        }
    finally:
        conn.close()


def _get_agent_eval_stats(agent_name: str) -> dict[str, Any]:
    """Get aggregate eval stats for an agent."""
    conn = _get_agent_db_conn()
    if conn is None:
        return {
            "total_eval_runs": 0,
            "avg_pass_rate": 0,
            "total_cost_usd": 0,
            "avg_latency_ms": 0,
        }

    try:
        row = conn.execute(
            """
            SELECT
                COUNT(*) as total_runs,
                AVG(CASE WHEN total_cases > 0
                    THEN CAST(passed AS REAL) / total_cases
                    ELSE 0 END) as avg_pass_rate,
                SUM(total_cost_usd) as total_cost,
                AVG(c_avg_latency) as avg_latency
            FROM eval_runs r
            LEFT JOIN (
                SELECT run_id, AVG(latency_ms) as c_avg_latency
                FROM eval_cases
                GROUP BY run_id
            ) c ON r.id = c.run_id
            WHERE r.agent_name = ?
            """,
            (agent_name,),
        ).fetchone()

        if row is None or row["total_runs"] == 0:
            return {
                "total_eval_runs": 0,
                "avg_pass_rate": 0,
                "total_cost_usd": 0,
                "avg_latency_ms": 0,
            }

        return {
            "total_eval_runs": row["total_runs"],
            "avg_pass_rate": round(row["avg_pass_rate"] or 0, 4),
            "total_cost_usd": round(row["total_cost"] or 0, 4),
            "avg_latency_ms": round(row["avg_latency"] or 0, 1),
        }
    finally:
        conn.close()


# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------


@agent_routes.get("/api/agents")
async def list_agents(request: web.Request) -> web.Response:
    """List all agents in the workspace."""
    agents_dir = _find_agents_dir()
    agent_dirs = _list_agent_dirs(agents_dir)

    agents: list[dict[str, Any]] = []
    for agent_dir in agent_dirs:
        raw = _load_agent_yaml_raw(agent_dir)
        if raw is None:
            agents.append({
                "name": agent_dir.name,
                "description": "(invalid config)",
                "version": "?",
                "model": {"provider": "?", "model": "?"},
                "server_count": 0,
                "last_eval": None,
            })
            continue

        name = raw.get("name", agent_dir.name)
        model_data = raw.get("model", {})
        servers_data = raw.get("servers") or []

        last_eval = _get_last_eval_for_agent(name)

        agents.append({
            "name": name,
            "description": raw.get("description", ""),
            "version": raw.get("version", "0.1.0"),
            "model": {
                "provider": model_data.get("provider", "?"),
                "model": model_data.get("model", "?"),
            },
            "server_count": len(servers_data) if isinstance(servers_data, list) else 0,
            "last_eval": last_eval,
        })

    return web.json_response({"agents": agents})


@agent_routes.get("/api/agents/{name}")
async def get_agent(request: web.Request) -> web.Response:
    """Get detailed info for a single agent."""
    name = request.match_info["name"]
    if not _validate_agent_name(name):
        return web.json_response(
            {"error": f"Invalid agent name: '{name}'"}, status=400
        )
    agents_dir = _find_agents_dir()
    agent_dir = agents_dir / name

    if not agent_dir.is_dir() or not (agent_dir / "agent.yaml").exists():
        return web.json_response(
            {"error": f"Agent '{name}' not found"}, status=404
        )

    raw = _load_agent_yaml_raw(agent_dir)
    if raw is None:
        return web.json_response(
            {"error": f"Invalid config for agent '{name}'"}, status=500
        )

    agent_name = raw.get("name", name)
    model_data = raw.get("model", {})
    servers_data = raw.get("servers") or []
    settings_data = raw.get("settings") or {}
    tool_filter_data = raw.get("tool_filter")

    # Build server list
    servers: list[dict[str, Any]] = []
    if isinstance(servers_data, list):
        for server in servers_data:
            if isinstance(server, dict):
                servers.append(_build_server_info(server, tool_filter_data))

    # System prompt preview
    system_prompt_preview = _resolve_system_prompt_preview(raw, agent_dir)

    # Eval stats
    stats = _get_agent_eval_stats(agent_name)

    return web.json_response({
        "name": agent_name,
        "description": raw.get("description", ""),
        "version": raw.get("version", "0.1.0"),
        "model": {
            "provider": model_data.get("provider", "?"),
            "model": model_data.get("model", "?"),
            "max_tokens": model_data.get("max_tokens", 4096),
            "temperature": model_data.get("temperature", 0.0),
        },
        "servers": servers,
        "settings": {
            "max_tool_calls": settings_data.get("max_tool_calls", 50),
            "tool_call_timeout": settings_data.get("tool_call_timeout", "30s"),
            "retry_on_error": settings_data.get("retry_on_error", True),
            "max_retries": settings_data.get("max_retries", 1),
            "tool_namespacing": settings_data.get("tool_namespacing", "auto"),
        },
        "system_prompt_preview": system_prompt_preview,
        "stats": stats,
    })


@agent_routes.get("/api/agents/{name}/evals/runs")
async def list_agent_eval_runs(request: web.Request) -> web.Response:
    """List eval runs for a specific agent."""
    name = request.match_info["name"]
    if not _validate_agent_name(name):
        return web.json_response({"error": "Invalid agent name"}, status=400)
    limit = min(int(request.query.get("limit", "50")), 500)
    offset = int(request.query.get("offset", "0"))

    conn = _get_agent_db_conn()
    if conn is None:
        return web.json_response({"runs": [], "total": 0})

    try:
        count_row = conn.execute(
            "SELECT COUNT(*) FROM eval_runs WHERE agent_name = ?",
            (name,),
        ).fetchone()
        total = count_row[0] if count_row else 0

        cursor = conn.execute(
            """
            SELECT
                id, agent_name, suite_name, model, temperature,
                started_at, completed_at,
                total_cases, passed, failed,
                total_cost_usd, total_tokens
            FROM eval_runs
            WHERE agent_name = ?
            ORDER BY id DESC
            LIMIT ? OFFSET ?
            """,
            (name, limit, offset),
        )

        runs: list[dict[str, Any]] = []
        for row in cursor.fetchall():
            runs.append({
                "id": row["id"],
                "agent_name": row["agent_name"],
                "suite_name": row["suite_name"],
                "model": row["model"],
                "started_at": row["started_at"],
                "completed_at": row["completed_at"],
                "total_cases": row["total_cases"],
                "passed": row["passed"] or 0,
                "failed": row["failed"] or 0,
                "total_cost_usd": row["total_cost_usd"] or 0,
                "total_tokens": row["total_tokens"] or 0,
            })

        return web.json_response({"runs": runs, "total": total})
    finally:
        conn.close()


@agent_routes.get("/api/agents/{name}/evals/runs/{run_id}")
async def get_agent_eval_run(request: web.Request) -> web.Response:
    """Get a single eval run with its cases."""
    name = request.match_info["name"]
    if not _validate_agent_name(name):
        return web.json_response({"error": "Invalid agent name"}, status=400)
    run_id = request.match_info["run_id"]

    conn = _get_agent_db_conn()
    if conn is None:
        return web.json_response(
            {"error": "No agent eval database found"}, status=404
        )

    try:
        run_row = conn.execute(
            """
            SELECT
                id, agent_name, suite_name, model, temperature,
                started_at, completed_at,
                total_cases, passed, failed,
                total_cost_usd, total_tokens
            FROM eval_runs
            WHERE id = ? AND agent_name = ?
            """,
            (run_id, name),
        ).fetchone()

        if run_row is None:
            return web.json_response(
                {"error": f"Run {run_id} not found for agent '{name}'"},
                status=404,
            )

        # Fetch cases
        case_cursor = conn.execute(
            """
            SELECT
                id, case_name, input, output, status,
                tool_calls_json, assertion_results_json,
                latency_ms, tokens_used, cost_usd, error_message
            FROM eval_cases
            WHERE run_id = ?
            ORDER BY id
            """,
            (run_id,),
        )

        cases: list[dict[str, Any]] = []
        for row in case_cursor.fetchall():
            cases.append({
                "id": row["id"],
                "case_name": row["case_name"],
                "input": row["input"],
                "output": row["output"],
                "status": row["status"],
                "tool_calls": json.loads(row["tool_calls_json"])
                if row["tool_calls_json"]
                else [],
                "assertion_results": json.loads(row["assertion_results_json"])
                if row["assertion_results_json"]
                else [],
                "latency_ms": row["latency_ms"],
                "tokens_used": row["tokens_used"],
                "cost_usd": row["cost_usd"],
                "error_message": row["error_message"],
            })

        total_cases = run_row["total_cases"]
        passed = run_row["passed"] or 0

        return web.json_response({
            "run_id": run_row["id"],
            "agent_name": run_row["agent_name"],
            "suite_name": run_row["suite_name"],
            "model": run_row["model"],
            "started_at": run_row["started_at"],
            "completed_at": run_row["completed_at"],
            "total_cases": total_cases,
            "passed": passed,
            "failed": run_row["failed"] or 0,
            "pass_rate": round(passed / total_cases, 4) if total_cases > 0 else 0,
            "total_cost_usd": run_row["total_cost_usd"],
            "total_tokens": run_row["total_tokens"],
            "cases": cases,
        })
    finally:
        conn.close()


@agent_routes.get("/api/agents/{name}/evals/trends")
async def get_agent_eval_trends(request: web.Request) -> web.Response:
    """Get eval trends for an agent aggregated by date."""
    name = request.match_info["name"]
    if not _validate_agent_name(name):
        return web.json_response({"error": "Invalid agent name"}, status=400)
    days = min(int(request.query.get("days", "30")), 365)

    conn = _get_agent_db_conn()
    if conn is None:
        return web.json_response({"trends": []})

    try:
        cursor = conn.execute(
            """
            SELECT
                DATE(r.started_at) as date,
                COUNT(*) as run_count,
                AVG(CASE WHEN r.total_cases > 0
                    THEN CAST(r.passed AS REAL) / r.total_cases
                    ELSE 0 END) as avg_pass_rate,
                AVG(c_avg.avg_latency) as avg_latency_ms,
                SUM(r.total_cost_usd) as total_cost_usd
            FROM eval_runs r
            LEFT JOIN (
                SELECT run_id, AVG(latency_ms) as avg_latency
                FROM eval_cases
                GROUP BY run_id
            ) c_avg ON r.id = c_avg.run_id
            WHERE r.agent_name = ?
              AND r.started_at >= datetime('now', ?)
            GROUP BY DATE(r.started_at)
            ORDER BY date
            """,
            (name, f"-{days} days"),
        )

        trends: list[dict[str, Any]] = []
        for row in cursor.fetchall():
            trends.append({
                "date": row["date"],
                "run_count": row["run_count"],
                "pass_rate": round(row["avg_pass_rate"] or 0, 4),
                "avg_latency_ms": round(row["avg_latency_ms"] or 0, 1),
                "total_cost_usd": round(row["total_cost_usd"] or 0, 4),
            })

        return web.json_response({"trends": trends})
    finally:
        conn.close()
