import json
import os
import sqlite3
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

import requests
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
from requests import HTTPError

from synthneura.ui.web.run_pipeline import run_pipeline


def _default_db_path() -> str:
    env_path = os.getenv("SYNTHNEURA_DB_PATH")
    if env_path:
        return env_path
    repo_root = os.path.abspath(
        os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
    )
    return os.path.join(repo_root, "trials.db")


def _frontend_dist_path() -> str:
    repo_root = os.path.abspath(
        os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
    )
    return os.path.join(repo_root, "frontend", "dist")


DEFAULT_DB_PATH = _default_db_path()
FRONTEND_DIST = _frontend_dist_path()
OPENAI_URL = "https://api.openai.com/v1/chat/completions"
CHAT_MODEL = os.getenv("SYNTHNEURA_CHAT_MODEL", "gpt-4.1-nano")
CHAT_DAILY_TOKENS = int(os.getenv("SYNTHNEURA_CHAT_DAILY_TOKENS", "10000"))
CHAT_MAX_OUTPUT_TOKENS = int(os.getenv("SYNTHNEURA_CHAT_MAX_OUTPUT_TOKENS", "400"))

app = Flask(__name__, static_folder=FRONTEND_DIST, static_url_path="")
CORS(app)


def _connect(db_path: str) -> sqlite3.Connection:
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    return conn


def _safe_json_loads(value: Any, fallback: Any) -> Any:
    if value is None:
        return fallback
    try:
        return json.loads(value)
    except json.JSONDecodeError:
        return fallback


def _ensure_chat_tables(conn: sqlite3.Connection) -> None:
    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS chat_usage (
            user_id TEXT NOT NULL,
            usage_date TEXT NOT NULL,
            tokens_used INTEGER NOT NULL,
            PRIMARY KEY (user_id, usage_date)
        )
        """
    )
    conn.commit()


def _estimate_tokens(text: str) -> int:
    return max(1, len(text) // 4)


def _chat_usage(conn: sqlite3.Connection, user_id: str, usage_date: str) -> int:
    row = conn.execute(
        """
        SELECT tokens_used
        FROM chat_usage
        WHERE user_id = ? AND usage_date = ?
        """,
        (user_id, usage_date),
    ).fetchone()
    return row["tokens_used"] if row else 0


def _update_chat_usage(
    conn: sqlite3.Connection, user_id: str, usage_date: str, tokens: int
) -> int:
    current = _chat_usage(conn, user_id, usage_date)
    updated = current + tokens
    conn.execute(
        """
        INSERT OR REPLACE INTO chat_usage (user_id, usage_date, tokens_used)
        VALUES (?, ?, ?)
        """,
        (user_id, usage_date, updated),
    )
    conn.commit()
    return updated


def _retrieve_context(
    conn: sqlite3.Connection, run_id: Optional[str], query: str, limit: int = 8
) -> List[Dict[str, Any]]:
    if not run_id:
        return []
    terms = [term.strip() for term in query.lower().split() if term.strip()]
    like = f"%{terms[0]}%" if terms else "%"
    rows = conn.execute(
        """
        SELECT nct_id, title, status, phase, sponsor, conditions, outcomes
        FROM clinical_trials
        WHERE run_id = ?
          AND (
            LOWER(title) LIKE ?
            OR LOWER(conditions) LIKE ?
            OR LOWER(outcomes) LIKE ?
            OR LOWER(sponsor) LIKE ?
            OR LOWER(nct_id) LIKE ?
          )
        ORDER BY ingested_at DESC
        LIMIT ?
        """,
        (run_id, like, like, like, like, like, limit),
    ).fetchall()

    context: List[Dict[str, Any]] = []
    for row in rows:
        extraction = conn.execute(
            """
            SELECT endpoints_json, biomarkers_json
            FROM trial_extractions
            WHERE nct_id = ? AND run_id = ?
            """,
            (row["nct_id"], run_id),
        ).fetchone()
        endpoints = (
            _safe_json_loads(extraction["endpoints_json"], []) if extraction else []
        )
        biomarkers = (
            _safe_json_loads(extraction["biomarkers_json"], []) if extraction else []
        )
        context.append(
            {
                "nct_id": row["nct_id"],
                "title": row["title"],
                "status": row["status"],
                "phase": row["phase"],
                "sponsor": row["sponsor"],
                "conditions": row["conditions"],
                "outcomes": row["outcomes"],
                "endpoints": endpoints,
                "biomarkers": biomarkers,
            }
        )
    return context


def _latest_runs_summary(
    conn: sqlite3.Connection, limit: int = 5
) -> List[Dict[str, Any]]:
    rows = conn.execute(
        """
        SELECT
            run_id,
            ingested_at,
            query,
            total,
            new_count,
            updated_count,
            unchanged_count
        FROM runs
        ORDER BY ingested_at DESC
        LIMIT ?
        """,
        (limit,),
    ).fetchall()
    return [dict(row) for row in rows]


def _latest_run_summary(
    conn: sqlite3.Connection, query: Optional[str]
) -> Optional[Dict[str, Any]]:
    if query:
        row = conn.execute(
            """
            SELECT
                run_id,
                ingested_at,
                query,
                total,
                new_count,
                updated_count,
                unchanged_count
            FROM runs
            WHERE query = ?
            ORDER BY ingested_at DESC
            LIMIT 1
            """,
            (query,),
        ).fetchone()
    else:
        row = conn.execute(
            """
            SELECT
                run_id,
                ingested_at,
                query,
                total,
                new_count,
                updated_count,
                unchanged_count
            FROM runs
            ORDER BY ingested_at DESC
            LIMIT 1
            """
        ).fetchone()
    return dict(row) if row else None


def _latest_run(conn: sqlite3.Connection) -> Dict[str, Any]:
    row = conn.execute(
        """
        SELECT run_id, ingested_at, source, query, total, new_count, updated_count,
               unchanged_count, failed_count
        FROM runs
        ORDER BY ingested_at DESC
        LIMIT 1
        """
    ).fetchone()
    return dict(row) if row else {}


def _top_extractions(
    conn: sqlite3.Connection, run_id: str, limit: int
) -> Dict[str, Any]:
    rows = conn.execute(
        """
        SELECT endpoints_json, biomarkers_json
        FROM trial_extractions
        WHERE run_id = ?
        """,
        (run_id,),
    ).fetchall()

    endpoint_counts: Dict[str, int] = {}
    biomarker_counts: Dict[str, int] = {}
    for row in rows:
        endpoints = _safe_json_loads(row["endpoints_json"], [])
        biomarkers = _safe_json_loads(row["biomarkers_json"], [])
        for value in endpoints:
            endpoint_counts[value] = endpoint_counts.get(value, 0) + 1
        for value in biomarkers:
            biomarker_counts[value] = biomarker_counts.get(value, 0) + 1

    top_endpoints = sorted(
        endpoint_counts.items(), key=lambda item: item[1], reverse=True
    )
    top_biomarkers = sorted(
        biomarker_counts.items(), key=lambda item: item[1], reverse=True
    )

    return {
        "endpoints": [{"value": v, "count": c} for v, c in top_endpoints[:limit]],
        "biomarkers": [{"value": v, "count": c} for v, c in top_biomarkers[:limit]],
    }


def _phase_weight(phase: Any) -> int:
    if not phase:
        return 0
    value = str(phase).upper()
    if "PHASE4" in value:
        return 4
    if "PHASE3" in value:
        return 3
    if "PHASE2" in value:
        return 2
    if "PHASE1" in value:
        return 1
    return 0


def _status_weight(status: Any) -> int:
    if not status:
        return 0
    value = str(status).lower()
    if "recruit" in value:
        return 2
    if "active" in value:
        return 1
    return 0


def _novel_biomarker(
    conn: sqlite3.Connection, run_id: str, biomarkers: List[str]
) -> bool:
    if not biomarkers:
        return False
    for biomarker in biomarkers:
        row = conn.execute(
            """
            SELECT 1
            FROM trial_extractions
            WHERE biomarkers_json LIKE ?
              AND run_id != ?
            LIMIT 1
            """,
            (f"%{biomarker}%", run_id),
        ).fetchone()
        if row is None:
            return True
    return False


def _summarize_signal(
    nct_id: str,
    status: Any,
    phase: Any,
    endpoints: List[str],
    biomarkers: List[str],
    changed_fields: List[str],
) -> str:
    endpoint = endpoints[0] if endpoints else "an endpoint"
    biomarker = biomarkers[0] if biomarkers else "a biomarker"
    status_text = status or "status update"
    phase_text = phase or "unphased"
    delta = ", ".join(changed_fields) if changed_fields else "trial fields"
    return (
        f"{nct_id} is a {phase_text} trial with {biomarker}. "
        f"Recent changes in {delta} and {endpoint} may shift signal "
        f"as the status is now {status_text}."
    )


def _confidence_score(
    endpoints: List[str], biomarkers: List[str], changed_fields: List[str]
) -> float:
    score = 0.0
    if endpoints:
        score += 0.4
    if biomarkers:
        score += 0.4
    if changed_fields:
        score += 0.2
    return round(score, 2)


def _build_narratives(
    run: Dict[str, Any],
    changes_payload: List[Dict[str, Any]],
    novel_biomarkers: List[str],
) -> List[str]:
    narratives: List[str] = []
    new_count = run.get("new_count") or 0
    updated_count = run.get("updated_count") or 0

    if new_count:
        narratives.append(f"{new_count} new trials added in this run.")
    if updated_count:
        narratives.append(f"{updated_count} trials updated since the last run.")

    transition_counts: Dict[str, int] = {}
    for change in changes_payload:
        if "status" not in (change.get("changed_fields") or []):
            continue
        before = (change.get("before") or {}).get("status")
        after = (change.get("after") or {}).get("status")
        if before and after and before != after:
            key = f"{before} → {after}"
            transition_counts[key] = transition_counts.get(key, 0) + 1

    if transition_counts:
        top_transition = max(transition_counts.items(), key=lambda item: item[1])
        narratives.append(f"{top_transition[1]} trials moved from {top_transition[0]}.")

    if novel_biomarkers:
        sample = ", ".join(novel_biomarkers[:2])
        label = f"{len(novel_biomarkers)} new biomarker(s) detected"
        if sample:
            label = f"{label} (e.g., {sample})."
        else:
            label = f"{label}."
        narratives.append(label)

    if not narratives:
        narratives.append("Baseline captured. Deltas appear after the next run.")

    return narratives


@app.get("/api/health")
def health() -> Any:
    return jsonify({"status": "ok"})


@app.get("/api/overview")
def overview() -> Any:
    db_path = request.args.get("db", DEFAULT_DB_PATH)
    limit = int(request.args.get("limit", 8))
    with _connect(db_path) as conn:
        run = _latest_run(conn)
        if not run:
            return jsonify({"run": {}, "changes": [], "extractions": {}})
        run_id = request.args.get("run_id") or run.get("run_id")
        if run_id and run_id != run.get("run_id"):
            row = conn.execute(
                """
                SELECT
                    run_id,
                    ingested_at,
                    source,
                    query,
                    total,
                    new_count,
                    updated_count,
                    unchanged_count,
                    failed_count
                FROM runs
                WHERE run_id = ?
                """,
                (run_id,),
            ).fetchone()
            if row:
                run = dict(row)
        run_id = run.get("run_id")
        if not run_id:
            return jsonify(
                {
                    "run": run,
                    "changes": [],
                    "extractions": {"endpoints": [], "biomarkers": []},
                    "novel_biomarkers": [],
                    "baseline": False,
                    "narratives": ["No runs yet."],
                    "signals": [],
                }
            )
        if run_id:
            total_row = conn.execute(
                "SELECT COUNT(*) AS total FROM clinical_trials WHERE run_id = ?",
                (run_id,),
            ).fetchone()
            if total_row:
                run["total"] = total_row["total"]
        changes = conn.execute(
            """
            SELECT nct_id, changed_fields, before_json, after_json, ingested_at
            FROM trial_diffs
            WHERE run_id = ?
            ORDER BY ingested_at DESC
            LIMIT ?
            """,
            (run_id, limit),
        ).fetchall()
        changes_payload = []
        for row in changes:
            changes_payload.append(
                {
                    "nct_id": row["nct_id"],
                    "changed_fields": _safe_json_loads(row["changed_fields"], []),
                    "before": _safe_json_loads(row["before_json"], {}),
                    "after": _safe_json_loads(row["after_json"], {}),
                    "ingested_at": row["ingested_at"],
                }
            )
        extractions = _top_extractions(conn, run_id, limit)
        novel_biomarkers_set = set()
        extraction_rows = conn.execute(
            """
            SELECT biomarkers_json
            FROM trial_extractions
            WHERE run_id = ?
            """,
            (run_id,),
        ).fetchall()
        for row in extraction_rows:
            biomarkers = _safe_json_loads(row["biomarkers_json"], [])
            for biomarker in biomarkers:
                if _novel_biomarker(conn, run_id, [biomarker]):
                    novel_biomarkers_set.add(biomarker)
        novel_biomarkers = sorted(novel_biomarkers_set)

        signals: List[Dict[str, Any]] = []
        for change in changes_payload:
            nct_id = change["nct_id"]
            trial = conn.execute(
                """
                SELECT status, phase
                FROM clinical_trials
                WHERE nct_id = ?
                """,
                (nct_id,),
            ).fetchone()
            extraction = conn.execute(
                """
                SELECT endpoints_json, biomarkers_json
                FROM trial_extractions
                WHERE nct_id = ? AND run_id = ?
                """,
                (nct_id, run_id),
            ).fetchone()
            endpoints = (
                _safe_json_loads(extraction["endpoints_json"], []) if extraction else []
            )
            biomarkers = (
                _safe_json_loads(extraction["biomarkers_json"], [])
                if extraction
                else []
            )
            novelty = _novel_biomarker(conn, run_id, biomarkers)
            phase = trial["phase"] if trial else None
            status = trial["status"] if trial else None
            score = (
                _phase_weight(phase) * 2
                + _status_weight(status) * 2
                + len(change["changed_fields"]) * 2
                + (3 if novelty else 0)
            )
            signals.append(
                {
                    "nct_id": nct_id,
                    "score": score,
                    "status": status,
                    "phase": phase,
                    "endpoints": endpoints,
                    "biomarkers": biomarkers,
                    "summary": _summarize_signal(
                        nct_id,
                        status,
                        phase,
                        endpoints,
                        biomarkers,
                        change["changed_fields"],
                    ),
                    "changed_fields": change["changed_fields"],
                    "novel_biomarker": novelty,
                    "confidence": _confidence_score(
                        endpoints, biomarkers, change["changed_fields"]
                    ),
                }
            )
        signals.sort(key=lambda item: item["score"], reverse=True)
        if not signals:
            new_trials = conn.execute(
                """
                SELECT nct_id, status, phase
                FROM clinical_trials
                WHERE run_id = ?
                ORDER BY ingested_at DESC
                LIMIT ?
                """,
                (run_id, limit),
            ).fetchall()
            for row in new_trials:
                nct_id = row["nct_id"]
                extraction = conn.execute(
                    """
                    SELECT endpoints_json, biomarkers_json
                    FROM trial_extractions
                    WHERE nct_id = ? AND run_id = ?
                    """,
                    (nct_id, run_id),
                ).fetchone()
                endpoints = (
                    _safe_json_loads(extraction["endpoints_json"], [])
                    if extraction
                    else []
                )
                biomarkers = (
                    _safe_json_loads(extraction["biomarkers_json"], [])
                    if extraction
                    else []
                )
                novelty = _novel_biomarker(conn, run_id, biomarkers)
                phase = row["phase"]
                status = row["status"]
                score = (
                    _phase_weight(phase)
                    + _status_weight(status)
                    + (2 if novelty else 0)
                )
                signals.append(
                    {
                        "nct_id": nct_id,
                        "score": score,
                        "status": status,
                        "phase": phase,
                        "endpoints": endpoints,
                        "biomarkers": biomarkers,
                        "summary": _summarize_signal(
                            nct_id, status, phase, endpoints, biomarkers, []
                        ),
                        "changed_fields": [],
                        "novel_biomarker": novelty,
                        "confidence": _confidence_score(endpoints, biomarkers, []),
                        "signal_type": "new",
                    }
                )
            signals.sort(key=lambda item: item["score"], reverse=True)
    return jsonify(
        {
            "run": run,
            "changes": changes_payload,
            "extractions": extractions,
            "novel_biomarkers": novel_biomarkers,
            "baseline": bool(
                run.get("new_count")
                and (run.get("updated_count") or 0) == 0
                and len(changes_payload) == 0
            ),
            "narratives": _build_narratives(run, changes_payload, novel_biomarkers),
            "signals": signals[:limit],
        }
    )


@app.get("/api/alerts")
def alerts() -> Any:
    return jsonify(
        {
            "alerts": [
                {
                    "name": "Recruiting status changes",
                    "description": "Notify when recruiting trials change status.",
                },
                {
                    "name": "New biomarker detected",
                    "description": "Flag new biomarkers appearing in the latest run.",
                },
                {
                    "name": "Phase 3 updates",
                    "description": "Track changes in Phase 3 trials only.",
                },
            ]
        }
    )


@app.post("/api/run")
def run_query() -> Any:
    payload = request.get_json(silent=True) or {}
    query = (payload.get("query") or "").strip()
    max_results = int(payload.get("max_results") or 25)
    if not query:
        return jsonify({"status": "error", "detail": "query is required"}), 400
    result = run_pipeline(query=query, max_results=max_results)
    status = 200 if result.get("status") == "ok" else 500
    return jsonify(result), status


@app.get("/api/trials")
def trials() -> Any:
    db_path = request.args.get("db", DEFAULT_DB_PATH)
    limit = int(request.args.get("limit", 25))
    page = int(request.args.get("page", 1))
    page_size = int(request.args.get("page_size", limit))
    page = 1 if page < 1 else page
    page_size = 1 if page_size < 1 else page_size
    offset = (page - 1) * page_size
    with _connect(db_path) as conn:
        latest = _latest_run(conn)
        run_id = request.args.get("run_id") or latest.get("run_id") if latest else None
        total_count = 0
        if run_id:
            total_row = conn.execute(
                "SELECT COUNT(*) AS total FROM clinical_trials WHERE run_id = ?",
                (run_id,),
            ).fetchone()
            total_count = total_row["total"] if total_row else 0
            rows = conn.execute(
                """
                SELECT nct_id, title, status, phase, sponsor, conditions, interventions,
                       outcomes, ingested_at
                FROM clinical_trials
                WHERE run_id = ?
                ORDER BY ingested_at DESC
                LIMIT ? OFFSET ?
                """,
                (run_id, page_size, offset),
            ).fetchall()
        else:
            total_row = conn.execute(
                "SELECT COUNT(*) AS total FROM clinical_trials"
            ).fetchone()
            total_count = total_row["total"] if total_row else 0
            rows = conn.execute(
                """
                SELECT nct_id, title, status, phase, sponsor, conditions, interventions,
                       outcomes, ingested_at
                FROM clinical_trials
                ORDER BY ingested_at DESC
                LIMIT ? OFFSET ?
                """,
                (page_size, offset),
            ).fetchall()
        extractions_by_id = {}
        if run_id:
            extractions = conn.execute(
                """
                SELECT nct_id, endpoints_json, biomarkers_json
                FROM trial_extractions
                WHERE run_id = ?
                """,
                (run_id,),
            ).fetchall()
            for row in extractions:
                extractions_by_id[row["nct_id"]] = {
                    "endpoints": _safe_json_loads(row["endpoints_json"], []),
                    "biomarkers": _safe_json_loads(row["biomarkers_json"], []),
                }
    payload: List[Dict[str, Any]] = []
    for row in rows:
        extraction = extractions_by_id.get(row["nct_id"], {})
        payload.append(
            {
                "nct_id": row["nct_id"],
                "title": row["title"],
                "status": row["status"],
                "phase": row["phase"],
                "sponsor": row["sponsor"],
                "conditions": (
                    (row["conditions"] or "").split(",") if row["conditions"] else []
                ),
                "interventions": (
                    (row["interventions"] or "").split(",")
                    if row["interventions"]
                    else []
                ),
                "outcomes": (
                    (row["outcomes"] or "").split(",") if row["outcomes"] else []
                ),
                "ingested_at": row["ingested_at"],
                "endpoints": extraction.get("endpoints", []),
                "biomarkers": extraction.get("biomarkers", []),
            }
        )
    return jsonify(
        {"trials": payload, "total": total_count, "page": page, "page_size": page_size}
    )


@app.post("/api/chat")
def chat() -> Any:
    payload = request.get_json(silent=True) or {}
    message = (payload.get("message") or "").strip()
    user_id = (payload.get("user_id") or "demo").strip()
    if not message:
        return jsonify({"status": "error", "detail": "message is required"}), 400

    api_key = os.getenv("SYNTHNEURA_OPENAI_API_KEY")
    if not api_key:
        return jsonify({"status": "error", "detail": "OpenAI API key missing"}), 400

    usage_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    estimated_input_tokens = _estimate_tokens(message)

    with _connect(DEFAULT_DB_PATH) as conn:
        _ensure_chat_tables(conn)
        latest = _latest_run(conn)
        run_id = latest.get("run_id") if latest else None
        lowered = message.lower()
        if (
            "latest run" in lowered
            or "latest runs" in lowered
            or "recent runs" in lowered
        ):
            runs = _latest_runs_summary(conn, limit=5)
            reply_lines = []
            for run in runs:
                reply_lines.append(
                    (
                        f"{run.get('query')} · {run.get('ingested_at')} · "
                        f"total {run.get('total')} (new {run.get('new_count')}, "
                        f"updated {run.get('updated_count')}, "
                        f"unchanged {run.get('unchanged_count')})"
                    )
                )
            reply = "Latest runs:\n" + (
                "\n".join(reply_lines) if reply_lines else "No runs yet."
            )
            return jsonify(
                {
                    "status": "ok",
                    "reply": reply,
                    "tokens_used": _chat_usage(
                        conn,
                        user_id,
                        datetime.now(timezone.utc).strftime("%Y-%m-%d"),
                    ),
                    "tokens_limit": CHAT_DAILY_TOKENS,
                    "model": CHAT_MODEL,
                }
            )
        if "latest" in lowered and "run" in lowered and "melanoma" in lowered:
            summary = _latest_run_summary(conn, "melanoma")
            if summary:
                reply = (
                    "Latest melanoma run: "
                    f"{summary.get('ingested_at')} · total {summary.get('total')} "
                    f"(new {summary.get('new_count')}, "
                    f"updated {summary.get('updated_count')}, "
                    f"unchanged {summary.get('unchanged_count')})."
                )
            else:
                reply = "No melanoma runs found yet."
            return jsonify(
                {
                    "status": "ok",
                    "reply": reply,
                    "tokens_used": _chat_usage(
                        conn,
                        user_id,
                        datetime.now(timezone.utc).strftime("%Y-%m-%d"),
                    ),
                    "tokens_limit": CHAT_DAILY_TOKENS,
                    "model": CHAT_MODEL,
                }
            )
        if (
            "what does that mean" in lowered
            or "what does this mean" in lowered
            or ("what do" in lowered and "mean" in lowered)
        ):
            summary = _latest_run_summary(conn, None)
            if summary:
                new_count = summary.get("new_count") or 0
                updated_count = summary.get("updated_count") or 0
                unchanged_count = summary.get("unchanged_count") or 0
                if updated_count == 0:
                    meaning = (
                        "This run is effectively a baseline snapshot: "
                        "no trials changed since the previous run. "
                        f"It recorded {new_count} new trials and "
                        f"{unchanged_count} unchanged."
                    )
                else:
                    meaning = (
                        "This run includes changes compared to the previous "
                        "snapshot, "
                        f"with {updated_count} updates detected."
                    )
                reply = (
                    f"Latest run summary: {summary.get('query')} · "
                    f"{summary.get('ingested_at')} · total {summary.get('total')}. "
                    f"{meaning}"
                )
            else:
                reply = "There are no runs yet to interpret."
            return jsonify(
                {
                    "status": "ok",
                    "reply": reply,
                    "tokens_used": _chat_usage(
                        conn,
                        user_id,
                        datetime.now(timezone.utc).strftime("%Y-%m-%d"),
                    ),
                    "tokens_limit": CHAT_DAILY_TOKENS,
                    "model": CHAT_MODEL,
                }
            )

        context_rows = _retrieve_context(conn, run_id, message, limit=8)
        context_block = ""
        if context_rows:
            context_lines = []
            for row in context_rows:
                context_lines.append(
                    " | ".join(
                        [
                            row.get("nct_id") or "",
                            row.get("title") or "",
                            row.get("phase") or "",
                            row.get("status") or "",
                            f"biomarkers={','.join(row.get('biomarkers') or [])}",
                            f"endpoints={','.join(row.get('endpoints') or [])}",
                        ]
                    )
                )
            context_block = "\n".join(context_lines)
        used = _chat_usage(conn, user_id, usage_date)
        if used + estimated_input_tokens >= CHAT_DAILY_TOKENS:
            return (
                jsonify(
                    {
                        "status": "error",
                        "detail": "Daily token limit reached.",
                        "tokens_used": used,
                        "tokens_limit": CHAT_DAILY_TOKENS,
                    }
                ),
                429,
            )

        system_prompt = (
            "You are SynthNeura's clinical intelligence assistant. "
            "Answer concisely, cite trial identifiers when relevant, "
            "and do not invent facts. Use only the provided context if available."
        )
        user_prompt = message
        if context_block:
            user_prompt = (
                "Context (latest run trials):\n"
                f"{context_block}\n\n"
                f"User question: {message}"
            )
        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        }
        body = {
            "model": CHAT_MODEL,
            "temperature": 0.2,
            "max_tokens": CHAT_MAX_OUTPUT_TOKENS,
            "messages": [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt},
            ],
        }
        try:
            response = requests.post(OPENAI_URL, headers=headers, json=body, timeout=60)
            response.raise_for_status()
        except HTTPError as exc:
            detail = str(exc)
            return jsonify({"status": "error", "detail": detail}), 502
        except requests.RequestException as exc:
            return jsonify({"status": "error", "detail": str(exc)}), 502

        data = response.json()
        reply = data["choices"][0]["message"]["content"].strip()
        estimated_output_tokens = _estimate_tokens(reply)
        total_tokens = estimated_input_tokens + estimated_output_tokens
        new_total = _update_chat_usage(conn, user_id, usage_date, total_tokens)

    return jsonify(
        {
            "status": "ok",
            "reply": reply,
            "tokens_used": new_total,
            "tokens_limit": CHAT_DAILY_TOKENS,
            "model": CHAT_MODEL,
        }
    )


@app.get("/api/runs")
def runs() -> Any:
    db_path = request.args.get("db", DEFAULT_DB_PATH)
    with _connect(db_path) as conn:
        rows = conn.execute(
            """
            SELECT run_id, ingested_at, query, source, total
            FROM runs
            ORDER BY ingested_at DESC
            LIMIT 50
            """
        ).fetchall()
    payload = [dict(row) for row in rows]
    return jsonify({"runs": payload})


if __name__ == "__main__":
    port = int(os.getenv("SYNTHNEURA_API_PORT", "5001"))
    app.run(host="0.0.0.0", port=port, debug=True)


@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")
def serve_frontend(path: str) -> Any:
    if path.startswith("api"):
        return jsonify({"detail": "Not found"}), 404
    if path and os.path.exists(os.path.join(FRONTEND_DIST, path)):
        return send_from_directory(FRONTEND_DIST, path)
    return send_from_directory(FRONTEND_DIST, "index.html")


@app.errorhandler(404)
def spa_fallback(error: Exception) -> Any:
    if request.path.startswith("/api"):
        return jsonify({"detail": "Not found"}), 404
    if os.path.exists(os.path.join(FRONTEND_DIST, "index.html")):
        return send_from_directory(FRONTEND_DIST, "index.html")
    return jsonify({"detail": "Not found"}), 404
