"""DeskClaw Dashboard — FastAPI Backend (§9).

Serves evaluation results, agent metadata, and stress-test matrices
from the ``results/`` directory.  Also serves the React frontend as
static files when built.

Route order (intentional):
  1. API routes (/api/v1/*)        — registered first, highest priority
  2. Static assets mount (/assets) — Vite-built JS/CSS bundles
  3. SPA index route (/)           — serves index.html for the React app

NO catch-all ``/{full_path:path}`` route is used — it would swallow
legitimate 404s and shadow API endpoints.
"""

from __future__ import annotations

import asyncio
import json
import logging
import os
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles

try:
    from starlette.websockets import WebSocket, WebSocketDisconnect
except ImportError:
    WebSocket = None  # type: ignore[assignment,misc]
    WebSocketDisconnect = None  # type: ignore[assignment,misc]

_DESKCLAW_VERSION = "0.5.0"

# ── Logging ──

logger = logging.getLogger("deskclaw.dashboard")
if not logger.handlers:
    _handler = logging.StreamHandler(sys.stderr)
    _handler.setFormatter(
        logging.Formatter("[%(levelname)s] deskclaw.dashboard: %(message)s")
    )
    logger.addHandler(_handler)
    logger.setLevel(logging.DEBUG)

# ── Defaults ──

# Anchor to the project root (4 levels up from this file's directory) so the
# default works regardless of the working directory the process is started from.
_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent

# ── App factory ──


def create_app(results_dir: str | None = None) -> FastAPI:
    """Build and return the FastAPI application.

    Args:
        results_dir: Path to the results directory.  Falls back to the
            ``DESKCLAW_RESULTS_DIR`` environment variable, then
            ``<project_root>/results``.
    """

    logger.info("=" * 60)
    logger.info("DeskClaw Dashboard v%s — starting up", _DESKCLAW_VERSION)
    logger.info("=" * 60)

    logger.info("__file__         = %s", __file__)
    logger.info("_PROJECT_ROOT    = %s", _PROJECT_ROOT)
    logger.info("cwd              = %s", Path.cwd())

    logger.info("--- os.environ (full dump) ---")
    for key in sorted(os.environ):
        # Redact obvious secrets
        if any(
            s in key.upper()
            for s in ("SECRET", "TOKEN", "PASSWORD", "KEY", "CREDENTIAL")
        ):
            logger.info("  %s = <redacted>", key)
        else:
            logger.info("  %s = %s", key, os.environ[key])
    logger.info("--- end os.environ ---")

    # Read the environment variable at startup time, not at import time,
    # so callers can set DESKCLAW_RESULTS_DIR before calling create_app().
    _env_results = os.environ.get("DESKCLAW_RESULTS_DIR")
    _default = _env_results or str(_PROJECT_ROOT / "results")
    results_path = Path(results_dir or _default).resolve()

    logger.info("results_dir arg          = %s", results_dir)
    logger.info("DESKCLAW_RESULTS_DIR env = %s", _env_results)
    logger.info("_default (fallback)      = %s", _default)
    logger.info("results_path (final)     = %s", results_path)
    logger.info("results_path exists      = %s", results_path.is_dir())

    composite_path = results_path / "composite_report.json"
    logger.info(
        "composite_report.json exists = %s (path: %s)",
        composite_path.exists(),
        composite_path,
    )

    if results_path.is_dir():
        logger.info("results_path contents: %s", list(results_path.iterdir()))
    else:
        logger.warning("results_path is NOT a directory — API will return 404s")

    app = FastAPI(
        title="DeskClaw Dashboard",
        version=_DESKCLAW_VERSION,
        description="Web dashboard for DeskClaw Benchmark Platform evaluation results.",
    )

    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # ── Helpers ──

    def _load_json(path: Path) -> Any:
        logger.debug("_load_json called for: %s", path)
        if not path.exists():
            logger.debug("file not found: %s", path)
            return None
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
            logger.debug("loaded OK: %s (%d bytes)", path, path.stat().st_size)
            return data
        except (json.JSONDecodeError, OSError) as exc:
            logger.warning("failed to parse %s: %s", path, exc)
            return None

    def _discover_sessions() -> list[dict[str, Any]]:
        """Walk *results_path* for ``<timestamp>_<agent>/session_config.json``."""
        sessions: list[dict[str, Any]] = []
        if not results_path.is_dir():
            logger.debug("_discover_sessions: results_path is not a dir")
            return sessions
        for child in sorted(results_path.iterdir(), reverse=True):
            if child.is_dir():
                cfg_path = child / "session_config.json"
                cfg = _load_json(cfg_path)
                if cfg is not None:
                    sessions.append(cfg)
        logger.debug("_discover_sessions: found %d session(s)", len(sessions))
        return sessions

    # ──────────────────────────────────────────────────────────────
    #  1. API ROUTES — registered FIRST, highest priority
    # ──────────────────────────────────────────────────────────────

    # ── Health & Debug ──

    @app.get("/api/v1/health")
    def health() -> dict[str, str]:
        logger.debug("GET /api/v1/health")
        return {"status": "ok", "version": _DESKCLAW_VERSION}

    @app.get("/api/v1/debug")
    def debug() -> dict[str, Any]:
        logger.debug("GET /api/v1/debug")
        # Dynamically resolve from env so hot-reloads / env changes are visible.
        _current_env = os.environ.get("DESKCLAW_RESULTS_DIR")
        _dynamic_path = Path(_current_env).resolve() if _current_env else results_path
        return {
            "results_path": str(_dynamic_path),
            "results_dir_exists": _dynamic_path.is_dir(),
            "env_DESKCLAW_RESULTS_DIR": _current_env,
            "version": _DESKCLAW_VERSION,
            "composite_report_exists": (
                _dynamic_path / "composite_report.json"
            ).exists(),
        }

    # ── Agents ──

    @app.get("/api/v1/agents")
    def list_agents() -> dict[str, Any]:
        """Return discovered agents from session configs.

        Response::

            {
                "agents": [
                    {
                        "name": "self",
                        "sessions": [ ... session_config dicts ... ]
                    }
                ]
            }
        """
        logger.debug("GET /api/v1/agents")
        sessions = _discover_sessions()
        agents_map: dict[str, list[dict[str, Any]]] = {}
        for s in sessions:
            agent_name = s.get("agent", "unknown")
            agents_map.setdefault(agent_name, []).append(s)

        agents = [
            {"name": name, "sessions": sess_list}
            for name, sess_list in sorted(agents_map.items())
        ]
        return {"agents": agents}

    # ── Results ──

    @app.get("/api/v1/results")
    def get_results(
        agent: str | None = Query(default=None, description="Filter by agent name"),
    ) -> dict[str, Any]:
        """Return composite evaluation results.

        If *agent* is given, filters session data accordingly.
        Always returns the composite report (not per-agent yet).
        """
        logger.debug("GET /api/v1/results (agent=%s)", agent)
        logger.debug("Looking for composite_report.json in: %s", results_path)

        composite_file = results_path / "composite_report.json"
        logger.debug(
            "composite_file = %s, exists = %s", composite_file, composite_file.exists()
        )

        composite = _load_json(composite_file)
        if composite is None:
            logger.error(
                "composite_report.json not found at %s — "
                "results_path=%s, DESKCLAW_RESULTS_DIR=%s",
                composite_file,
                results_path,
                os.environ.get("DESKCLAW_RESULTS_DIR"),
            )
            raise HTTPException(
                status_code=404,
                detail=f"No composite_report.json found (looked in {results_path})",
            )

        stress_details: dict[str, Any] = {}
        for st_id in ("ST-01", "ST-02", "ST-03", "ST-04"):
            st_data = _load_json(results_path / f"{st_id}_results.json")
            if st_data is not None:
                stress_details[st_id] = st_data

        sessions = _discover_sessions()
        if agent:
            sessions = [s for s in sessions if s.get("agent") == agent]

        logger.debug(
            "GET /api/v1/results -> composite keys=%s, stress_details=%s, sessions=%d",
            list(composite.keys())
            if isinstance(composite, dict)
            else type(composite).__name__,
            list(stress_details.keys()),
            len(sessions),
        )

        return {
            "composite": composite,
            "stress_details": stress_details,
            "sessions": sessions,
        }

    # ── Stress Matrix ──

    @app.get("/api/v1/stress-matrix")
    def stress_matrix() -> dict[str, Any]:
        """Return a structured stress-test matrix suitable for table rendering.

        Response::

            {
                "tests": [
                    {
                        "id": "ST-01",
                        "name": "Memory Poisoning (Dana vs Marcus)",
                        "severity": "Critical",
                        "passed": true,
                        "score": 100,
                        "max_score": 100.0,
                        "failure_modes": [],
                        "details": [ ...per-benchmark entries if available... ]
                    }
                ],
                "summary": {
                    "total": 4,
                    "passed": 4,
                    "failed": 0,
                    "pass_rate": 1.0
                }
            }
        """
        logger.debug("GET /api/v1/stress-matrix")
        composite = _load_json(results_path / "composite_report.json")
        stress_raw = composite.get("stress_tests", {}) if composite else {}

        tests: list[dict[str, Any]] = []
        passed_count = 0

        for st_id in ("ST-01", "ST-02", "ST-03", "ST-04"):
            entry = stress_raw.get(st_id)
            detail_data = _load_json(results_path / f"{st_id}_results.json")

            if entry:
                is_passed = entry.get("passed", False)
                if is_passed:
                    passed_count += 1
                tests.append(
                    {
                        "id": st_id,
                        "name": entry.get("name", st_id),
                        "severity": entry.get("severity", "Unknown"),
                        "passed": is_passed,
                        "score": entry.get("score", 0),
                        "max_score": entry.get("max_score", 100.0),
                        "failure_modes": entry.get("failure_modes", []),
                        "details": detail_data if detail_data else [],
                    }
                )
            elif detail_data:
                first = (
                    detail_data[0]
                    if isinstance(detail_data, list) and detail_data
                    else {}
                )
                is_passed = first.get("passed", False)
                if is_passed:
                    passed_count += 1
                tests.append(
                    {
                        "id": st_id,
                        "name": st_id,
                        "severity": "Critical"
                        if st_id in ("ST-01", "ST-04")
                        else "High",
                        "passed": is_passed,
                        "score": first.get("total_score", 0),
                        "max_score": 100.0,
                        "failure_modes": first.get("failure_modes", []),
                        "details": detail_data,
                    }
                )

        total = len(tests)
        logger.debug(
            "GET /api/v1/stress-matrix -> %d tests, %d passed", total, passed_count
        )
        return {
            "tests": tests,
            "summary": {
                "total": total,
                "passed": passed_count,
                "failed": total - passed_count,
                "pass_rate": passed_count / total if total else 0.0,
            },
        }

    # ── Adapter Registry (in-memory + YAML persistence) ──

    adapters_dir = _PROJECT_ROOT / "plugins" / "adapters"
    _registered_adapters: dict[str, dict[str, Any]] = {}

    def _load_existing_adapters() -> None:
        """Load adapter YAMLs from disk into the in-memory registry."""
        if not adapters_dir.is_dir():
            return
        for yaml_path in sorted(
            list(adapters_dir.glob("*.yaml")) + list(adapters_dir.glob("*.yml"))
        ):
            if yaml_path.name.startswith("_"):
                continue
            data = _load_json(yaml_path)
            if data is None:
                try:
                    import yaml as _yaml  # type: ignore[import-untyped]

                    data = _yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
                except Exception:
                    continue
            if data and isinstance(data, dict):
                adapter_block = data.get("adapter", data)
                name = adapter_block.get("name")
                if name:
                    _registered_adapters[name] = {
                        "name": name,
                        "url": adapter_block.get("url", ""),
                        "protocol": adapter_block.get("protocol", "openai"),
                        "capabilities": adapter_block.get(
                            "capabilities", ["text_generation"]
                        ),
                        "registered_at": datetime.fromtimestamp(
                            yaml_path.stat().st_mtime, tz=timezone.utc
                        ).isoformat(),
                    }

    _load_existing_adapters()
    logger.info("Pre-loaded %d adapter(s) from disk", len(_registered_adapters))

    @app.post("/api/v1/adapters", status_code=201)
    async def register_adapter(request: Request) -> dict[str, Any]:
        """Register a new BOT adapter.

        Expects JSON::

            {
                "name": "my-bot",
                "url": "http://192.168.1.100:8080/v1/chat/completions",
                "protocol": "openai",
                "capabilities": ["text_generation", "tool_use"],
                "auth_token": "optional-bearer-token"
            }
        """
        try:
            payload = await request.json()
        except Exception as exc:
            raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}")

        if not isinstance(payload, dict):
            raise HTTPException(status_code=400, detail="Body must be a JSON object")

        name = payload.get("name", "").strip()
        url = payload.get("url", "").strip()
        if not name:
            raise HTTPException(status_code=422, detail="Missing required field: name")
        if not url:
            raise HTTPException(status_code=422, detail="Missing required field: url")

        protocol = payload.get("protocol", "openai")
        capabilities = payload.get("capabilities", ["text_generation"])
        auth_token = payload.get("auth_token", "")

        adapters_dir.mkdir(parents=True, exist_ok=True)
        yaml_data: dict[str, Any] = {
            "adapter": {
                "name": name,
                "type": "http",
                "url": url,
                "protocol": protocol,
                "capabilities": capabilities,
            }
        }
        if auth_token:
            yaml_data["adapter"]["auth"] = {
                "type": "bearer",
                "token": auth_token,
            }

        out_path = adapters_dir / f"{name}.yaml"
        try:
            import yaml as _yaml  # type: ignore[import-untyped]

            out_path.write_text(
                _yaml.dump(yaml_data, default_flow_style=False), encoding="utf-8"
            )
        except ModuleNotFoundError:
            out_path.write_text(json.dumps(yaml_data, indent=2), encoding="utf-8")

        ts = datetime.now(timezone.utc).isoformat()
        _registered_adapters[name] = {
            "name": name,
            "url": url,
            "protocol": protocol,
            "capabilities": capabilities,
            "registered_at": ts,
        }

        logger.info("Registered adapter '%s' → %s (protocol=%s)", name, url, protocol)
        await _ws_broadcast(
            {"event": "adapter_registered", "data": {"name": name, "url": url}}
        )

        return {
            "name": name,
            "url": url,
            "protocol": protocol,
            "capabilities": capabilities,
            "registered_at": ts,
            "config_path": str(out_path),
        }

    @app.get("/api/v1/adapters")
    def list_registered_adapters() -> dict[str, Any]:
        """List all registered BOT adapters."""
        logger.debug("GET /api/v1/adapters")
        return {
            "adapters": list(_registered_adapters.values()),
            "total": len(_registered_adapters),
        }

    # ── Run Pipeline (background execution) ──

    _runs: dict[str, dict[str, Any]] = {}

    async def _execute_pipeline(
        run_id: str,
        agent_name: str,
        suite: str | None,
        test_ids: list[str] | None,
        swe_instance_ids: list[str] | None,
    ) -> None:
        """Run the evaluation pipeline as a subprocess."""
        import shutil

        _runs[run_id]["status"] = "running"
        _runs[run_id]["started_at"] = datetime.now(timezone.utc).isoformat()
        await _ws_broadcast(
            {"event": "run_started", "data": {"run_id": run_id, "agent": agent_name}}
        )

        run_results_dir = results_path / run_id

        deskclaw_bin = shutil.which("deskclaw")
        if deskclaw_bin:
            cmd = [deskclaw_bin]
        else:
            cmd = [sys.executable, "-c", "from deskclaw.cli.main import main; main()"]

        cmd += [
            "run",
            "full",
            "--agent",
            agent_name,
            "--config",
            str(_PROJECT_ROOT / "plugins" / "deskclaw-plugin.yaml"),
            "--results-dir",
            str(run_results_dir),
            "--reports-dir",
            str(run_results_dir / "reports"),
        ]
        if suite:
            cmd += ["--suite", suite]
        for tid in test_ids or []:
            if tid and isinstance(tid, str):
                cmd += ["--test-id", tid]
        for instance_id in swe_instance_ids or []:
            if instance_id and isinstance(instance_id, str):
                cmd += ["--swe-instance-id", instance_id]

        try:
            sub_env = os.environ.copy()
            pp = sub_env.get("PYTHONPATH", "")
            root = str(_PROJECT_ROOT)
            if root not in pp.split(os.pathsep):
                sub_env["PYTHONPATH"] = root + (os.pathsep + pp if pp else "")

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.STDOUT,
                cwd=root,
                env=sub_env,
            )

            output_lines: list[str] = []
            assert proc.stdout is not None
            async for raw_line in proc.stdout:
                line = raw_line.decode("utf-8", errors="replace").rstrip()
                output_lines.append(line)
                if "Stage" in line or "✅" in line or "⚠" in line or "Grade" in line:
                    _runs[run_id]["progress"] = line
                    await _ws_broadcast(
                        {
                            "event": "run_progress",
                            "data": {"run_id": run_id, "message": line},
                        }
                    )

            exit_code = await proc.wait()
            _runs[run_id]["exit_code"] = exit_code
            _runs[run_id]["output"] = output_lines[-50:]
            _runs[run_id]["finished_at"] = datetime.now(timezone.utc).isoformat()

            if exit_code == 0:
                _runs[run_id]["status"] = "completed"
                report = _load_json(run_results_dir / "composite_report.json")
                eval_report = _load_json(
                    run_results_dir / "reports" / "latest" / "evaluation_report.json"
                )
                _runs[run_id]["results"] = {
                    "composite": report,
                    "evaluation": eval_report,
                }
            else:
                _runs[run_id]["status"] = "failed"
                _runs[run_id]["error"] = "\n".join(output_lines[-10:])

        except Exception as exc:
            _runs[run_id]["status"] = "failed"
            _runs[run_id]["error"] = f"{type(exc).__name__}: {exc}"
            _runs[run_id]["finished_at"] = datetime.now(timezone.utc).isoformat()

        await _ws_broadcast(
            {
                "event": "run_finished",
                "data": {
                    "run_id": run_id,
                    "agent": agent_name,
                    "status": _runs[run_id]["status"],
                },
            }
        )
        logger.info(
            "Run %s for agent '%s' finished: %s",
            run_id,
            agent_name,
            _runs[run_id]["status"],
        )

    @app.post("/api/v1/run", status_code=202)
    async def trigger_run(request: Request) -> dict[str, Any]:
        """Trigger a test pipeline run for a registered adapter.

        Expects JSON::

            {
                "agent": "my-bot",
                "suite": null,
                "tests": ["SWE-LITE"],
                "swe_instance_ids": ["astropy__astropy-14995"]
            }
        """
        try:
            payload = await request.json()
        except Exception as exc:
            raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}")

        agent_name = (
            payload.get("agent", "").strip() if isinstance(payload, dict) else ""
        )
        if not agent_name:
            raise HTTPException(status_code=422, detail="Missing required field: agent")

        suite = payload.get("suite") if isinstance(payload, dict) else None
        test_ids_raw = payload.get("tests") if isinstance(payload, dict) else None
        if test_ids_raw is None:
            test_ids: list[str] | None = None
        elif isinstance(test_ids_raw, list):
            test_ids = [str(t).strip() for t in test_ids_raw if str(t).strip()]
        else:
            raise HTTPException(
                status_code=422, detail="Field 'tests' must be a string array"
            )
        swe_instance_ids_raw = (
            payload.get("swe_instance_ids") if isinstance(payload, dict) else None
        )
        if swe_instance_ids_raw is None:
            swe_instance_ids: list[str] | None = None
        elif isinstance(swe_instance_ids_raw, list):
            swe_instance_ids = [
                str(t).strip() for t in swe_instance_ids_raw if str(t).strip()
            ]
        else:
            raise HTTPException(
                status_code=422, detail="Field 'swe_instance_ids' must be a string array"
            )

        run_id = f"{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S')}_{agent_name}_{uuid.uuid4().hex[:6]}"

        _runs[run_id] = {
            "run_id": run_id,
            "agent": agent_name,
            "suite": suite,
            "tests": test_ids,
            "swe_instance_ids": swe_instance_ids,
            "status": "queued",
            "progress": "",
            "created_at": datetime.now(timezone.utc).isoformat(),
        }

        asyncio.create_task(
            _execute_pipeline(run_id, agent_name, suite, test_ids, swe_instance_ids)
        )

        logger.info("Queued run %s for agent '%s'", run_id, agent_name)
        return {"run_id": run_id, "status": "queued", "agent": agent_name}

    @app.get("/api/v1/run/{run_id}")
    def get_run_status(run_id: str) -> dict[str, Any]:
        """Check the status of a pipeline run."""
        logger.debug("GET /api/v1/run/%s", run_id)
        run = _runs.get(run_id)
        if run is None:
            raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
        return run

    @app.get("/api/v1/runs")
    def list_runs(
        agent: str | None = Query(default=None, description="Filter by agent name"),
    ) -> dict[str, Any]:
        """List all pipeline runs, optionally filtered by agent."""
        logger.debug("GET /api/v1/runs (agent=%s)", agent)
        runs = list(_runs.values())
        if agent:
            runs = [r for r in runs if r.get("agent") == agent]
        runs.sort(key=lambda r: r.get("created_at", ""), reverse=True)
        return {"runs": runs, "total": len(runs)}

    @app.get("/api/v1/experiments")
    def list_experiments() -> dict[str, Any]:
        experiments_dir = results_path / "experiments"
        exp_results_dir = results_path.parent / "experiment_results"
        experiments: list[dict[str, Any]] = []

        for search_dir in (experiments_dir, exp_results_dir, results_path):
            if not search_dir.is_dir():
                continue
            for json_file in sorted(search_dir.glob("*.json"), reverse=True):
                if json_file.name in ("composite_report.json", "dispatch_results.json"):
                    continue
                data = _load_json(json_file)
                if data and isinstance(data, dict) and "experiment_id" in data:
                    experiments.append(data)

        return {"experiments": experiments, "total": len(experiments)}

    @app.get("/api/v1/experiments/{experiment_id}")
    def get_experiment(experiment_id: str) -> dict[str, Any]:
        for search_dir in (
            results_path / "experiments",
            results_path.parent / "experiment_results",
            results_path,
        ):
            if not search_dir.is_dir():
                continue
            for json_file in search_dir.glob("*.json"):
                data = _load_json(json_file)
                if (
                    data
                    and isinstance(data, dict)
                    and data.get("experiment_id") == experiment_id
                ):
                    return data

        raise HTTPException(
            status_code=404, detail=f"Experiment '{experiment_id}' not found"
        )

    # ── WebSocket Connection Manager ──

    _ws_connections: list[Any] = []

    async def _ws_broadcast(data: dict[str, Any]) -> None:
        stale: list[Any] = []
        for conn in _ws_connections:
            try:
                await conn.send_json(data)
            except Exception:
                stale.append(conn)
        for conn in stale:
            _ws_connections.remove(conn)

    # ── Result Ingestion (POST /api/v1/ingest/result) ──

    @app.post("/api/v1/ingest/result", status_code=201)
    async def ingest_result(request: Request) -> dict[str, Any]:
        """Accept a test result pushed from a remote Bot or runner.

        Expects a JSON body with at least ``test_id`` and ``status``.
        Persists to ``results_path/ingested/`` and broadcasts via WebSocket.
        """
        try:
            payload = await request.json()
        except Exception as exc:
            raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}")

        if not isinstance(payload, dict):
            raise HTTPException(status_code=400, detail="Body must be a JSON object")

        test_id = payload.get("test_id")
        if not test_id:
            raise HTTPException(
                status_code=422, detail="Missing required field: test_id"
            )

        status_val = payload.get("status")
        if not status_val:
            raise HTTPException(
                status_code=422, detail="Missing required field: status"
            )

        ingested_dir = results_path / "ingested"
        ingested_dir.mkdir(parents=True, exist_ok=True)

        ts = datetime.now(timezone.utc)
        record_id = f"{ts.strftime('%Y%m%dT%H%M%S')}_{uuid.uuid4().hex[:8]}"
        record = {
            "record_id": record_id,
            "ingested_at": ts.isoformat(),
            **payload,
        }

        record_path = ingested_dir / f"{record_id}.json"
        record_path.write_text(json.dumps(record, indent=2), encoding="utf-8")

        logger.info(
            "Ingested result: test_id=%s status=%s -> %s",
            test_id,
            status_val,
            record_path,
        )

        await _ws_broadcast({"event": "result_ingested", "data": record})

        return {"record_id": record_id, "status": "accepted"}

    # ── WebSocket Endpoint ──

    if WebSocket is not None:

        @app.websocket("/ws")
        async def websocket_endpoint(websocket: Any) -> None:
            await websocket.accept()
            _ws_connections.append(websocket)
            logger.info("WebSocket client connected (active=%d)", len(_ws_connections))
            try:
                while True:
                    data = await websocket.receive_text()
                    if data == "ping":
                        await websocket.send_json({"event": "pong"})
            except Exception:
                pass
            finally:
                if websocket in _ws_connections:
                    _ws_connections.remove(websocket)
                logger.info(
                    "WebSocket client disconnected (active=%d)",
                    len(_ws_connections),
                )

    # ──────────────────────────────────────────────────────────────
    #  2. STATIC ASSETS — mounted AFTER API routes
    # ──────────────────────────────────────────────────────────────
    #
    #  Only /assets is mounted as a StaticFiles directory.
    #  NO catch-all /{full_path:path} route — it would swallow 404s
    #  and shadow any API route that doesn't match exactly.
    # ──────────────────────────────────────────────────────────────

    frontend_dist = Path(__file__).parent / "frontend" / "dist"
    logger.info(
        "frontend_dist = %s, exists = %s", frontend_dist, frontend_dist.is_dir()
    )

    if frontend_dist.is_dir():
        assets_dir = frontend_dist / "assets"
        if assets_dir.is_dir():
            logger.info("Mounting /assets -> %s", assets_dir)
            app.mount(
                "/assets",
                StaticFiles(directory=str(assets_dir)),
                name="assets",
            )
        else:
            logger.warning(
                "assets directory not found at %s — skipping mount", assets_dir
            )

        # ──────────────────────────────────────────────────────────
        #  3. SPA INDEX — explicit GET / only (no catch-all)
        # ──────────────────────────────────────────────────────────

        _index_html = frontend_dist / "index.html"
        logger.info("index.html = %s, exists = %s", _index_html, _index_html.exists())

        @app.get("/", response_class=HTMLResponse)
        def serve_index() -> FileResponse:
            """Serve the React SPA entry point."""
            logger.debug("GET / -> serving index.html")
            return FileResponse(str(_index_html))

        # For client-side routes used by react-router (e.g. /stress-matrix),
        # add explicit routes that also return index.html so the SPA can
        # handle them.  This avoids the catch-all pattern entirely.
        @app.get("/stress-matrix", response_class=HTMLResponse)
        def serve_stress_matrix_spa() -> FileResponse:
            """Serve index.html for the /stress-matrix client-side route."""
            logger.debug("GET /stress-matrix -> serving index.html (SPA route)")
            return FileResponse(str(_index_html))

    else:
        logger.warning(
            "Frontend dist not found at %s — "
            "only API endpoints will be available. "
            "Run 'npm run build' in frontend/ to generate the dist.",
            frontend_dist,
        )

    logger.info("--- Registered routes ---")
    for route in app.routes:
        methods = getattr(route, "methods", None)
        path = getattr(route, "path", getattr(route, "path_regex", "mount"))
        logger.info("  %s %s", methods or "MOUNT", path)
    logger.info("--- end routes ---")

    return app


app = create_app()
