"""Evolution API — read-only surfaces for the Bayesian self-improvement layer.

Phase 1 exposes enough to inspect that the subsystem is functioning:
  GET  /api/evolution/status            — healthy flag, counts, weights, kill switch
  GET  /api/evolution/runs              — paginated list of recent runs + joined outcomes
  GET  /api/evolution/runs/{run_id}     — one run with outcome + telemetry
  GET  /api/evolution/reflexes          — Phase 2+ list (Phase 1 returns [])
  GET  /api/evolution/anti-reflexes     — same, filtered to kind='anti'
  GET  /api/evolution/promotions        — Phase 4+ (Phase 1 returns [])
  GET  /api/evolution/drift             — Phase 5 placeholder returning []

Write surfaces (replay, manual promotion, rollback, kill switch) come in
Phase 4. This Phase 1 API is intentionally read-only so inspection is safe
before the learning loop has accumulated enough data to trust.

All endpoints degrade gracefully when ``app.state.evolution`` isn't wired
(e.g. early boot, test rigs) — they return an empty/health-unknown shape
rather than 500.
"""

from __future__ import annotations

from fastapi import APIRouter, HTTPException, Query, Request

router = APIRouter(prefix="/evolution", tags=["evolution"])


def _service(request: Request):
    """Return the EvolutionService from app.state, or None if not wired."""
    return getattr(request.app.state, "evolution", None)


@router.get("/status")
async def get_evolution_status(request: Request) -> dict:
    """Subsystem health + counts + selector config.

    Shape::

        {
          "healthy": true,
          "phase": "1-2",
          "kill_switch": false,
          "counts": {
            "runs": 42, "outcomes": 42,
            "reflexes": 0, "anti_reflexes": 0,
            "active": 0, "candidates": 0, "drafts": 0,
            "quarantined": 0, "retired": 0,
            "promotions": 0, "replays": 0
          },
          "weights": {...},
          "config_path": "/vault/system/skills/_meta/evolution/config/evolution.yaml"
        }
    """
    svc = _service(request)
    if svc is None:
        return {
            "healthy": False,
            "phase": "unwired",
            "kill_switch": False,
            "counts": {
                "runs": 0, "outcomes": 0, "reflexes": 0, "anti_reflexes": 0,
                "active": 0, "candidates": 0, "drafts": 0,
                "quarantined": 0, "retired": 0, "promotions": 0, "replays": 0,
            },
            "weights": {},
            "config_path": "",
        }
    return await svc.get_status()


@router.get("/explain")
async def explain_selector(
    request: Request,
    skill: str = Query(..., description="Skill name to simulate selection for"),
    caller: str = Query(default="system", description="Caller context"),
    trigger_type: str = Query(default="", description="Trigger type (defaults to skill)"),
    parent_run_id: str = Query(default="", description="Set non-empty to simulate a nested invoke"),
    rng_seed: int | None = Query(default=None, description="Deterministic sampling seed"),
) -> dict:
    """Preview what the selector would pick for this (skill, caller, ...) right now.

    Returns the eligible candidate list with their posterior + sampled
    scores, which fallback rule would fire (if any), and the chosen
    policy_id + rationale. Pure read — does NOT persist a run or
    telemetry. Use ``rng_seed`` for reproducible explanations.

    This is the "why did the selector pick X?" debugging surface. Without
    it, users would have to query the selector_telemetry table by
    correlation_id to understand any individual decision.
    """
    svc = _service(request)
    if svc is None:
        return {
            "skill_name": skill,
            "caller": caller,
            "chosen": {
                "policy_id": "incumbent_default",
                "rationale": "fallback:service_unwired",
                "sampled_score": 0.0,
                "is_fallback": True,
            },
            "fallback_rule_triggered": "service_unwired",
            "eligible": [],
            "config": {},
        }
    return await svc.explain(
        skill,
        caller=caller,
        trigger_type=trigger_type,
        parent_run_id=parent_run_id,
        rng_seed=rng_seed,
    )


@router.get("/health")
async def get_evolution_health(request: Request) -> dict:
    """Subsystem self-monitoring — meta-health for the evolution layer.

    Surfaces lifetime counters + recency markers + last-handler-summaries
    + last-error so the admin UI can show "is the evolution layer itself
    working correctly?" without needing to query the ledger.

    Returns ``{"initialized": false, ...}`` with a defensive shape when
    the service isn't wired (early boot, test rigs).
    """
    svc = _service(request)
    if svc is None:
        return {
            "initialized": False,
            "kill_switch": False,
            "runs_recorded": 0,
            "runs_record_failures": 0,
            "runs_failure_rate": 0.0,
            "feedback_recorded": 0,
            "kernel_violations_caught": 0,
            "last_run_at": "",
            "last_mined_at": "",
            "last_promoted_at": "",
            "last_drift_audit_at": "",
            "last_feedback_at": "",
            "last_mining_summary": {},
            "last_promotion_summary": {},
            "last_drift_summary": {},
            "last_error": "evolution service not wired",
            "last_error_ts": "",
        }
    return svc.health()


@router.get("/runs")
async def list_runs(
    request: Request,
    skill_name: str | None = Query(default=None),
    policy_id: str | None = Query(default=None),
    caller: str | None = Query(default=None),
    since: str | None = Query(default=None),
    limit: int = Query(default=50, ge=1, le=500),
    offset: int = Query(default=0, ge=0),
) -> dict:
    """Paginated list of runs, newest first, with joined outcome + telemetry."""
    svc = _service(request)
    if svc is None:
        return {"runs": [], "count": 0}
    runs = await svc.list_runs(
        skill_name=skill_name, policy_id=policy_id, caller=caller,
        since=since, limit=limit, offset=offset,
    )
    return {"runs": runs, "count": len(runs)}


@router.get("/runs/{run_id}")
async def get_run(request: Request, run_id: str) -> dict:
    """Full detail for one run — manifest + outcome + selector telemetry."""
    svc = _service(request)
    if svc is None:
        raise HTTPException(status_code=404, detail="evolution service not wired")
    run = await svc.get_run(run_id)
    if not run:
        raise HTTPException(status_code=404, detail=f"run {run_id} not found")
    return run


@router.get("/reflexes")
async def list_reflexes(
    request: Request,
    status: str | None = Query(default=None),
    limit: int = Query(default=100, ge=1, le=500),
) -> dict:
    """List Reflex records. Phase 1 returns [] because mining is Phase 2."""
    svc = _service(request)
    if svc is None:
        return {"reflexes": [], "count": 0}
    reflexes = await svc.list_reflexes(kind="reflex", status=status, limit=limit)
    return {"reflexes": reflexes, "count": len(reflexes)}


@router.get("/anti-reflexes")
async def list_anti_reflexes(
    request: Request,
    status: str | None = Query(default=None),
    limit: int = Query(default=100, ge=1, le=500),
) -> dict:
    """List AntiReflex records. Phase 1 returns [] because mining is Phase 2."""
    svc = _service(request)
    if svc is None:
        return {"anti_reflexes": [], "count": 0}
    reflexes = await svc.list_reflexes(kind="anti", status=status, limit=limit)
    return {"anti_reflexes": reflexes, "count": len(reflexes)}


@router.get("/promotions")
async def list_promotions(
    request: Request,
    limit: int = Query(default=50, ge=1, le=500),
) -> dict:
    """List promotion decisions (promote | reject | defer). Phase 4+ populated."""
    svc = _service(request)
    if svc is None:
        return {"promotions": [], "count": 0}
    proms = await svc.list_promotions(limit=limit)
    return {"promotions": proms, "count": len(proms)}


@router.get("/drift")
async def list_drift_events(
    request: Request,
    limit: int = Query(default=50, ge=1, le=500),
) -> dict:
    """Drift audit recent findings. Phase 5 active — returns last audit output."""
    svc = _service(request)
    if svc is None:
        return {"drift": [], "count": 0, "status": "unwired"}
    # Phase 5: return list of currently-quarantined reflexes as the
    # "drift events" snapshot. The drift audit runs async; callers
    # wanting to TRIGGER an audit should POST /api/evolution/drift/run.
    quarantined = await svc.list_reflexes(kind="reflex", status="quarantined", limit=limit)
    return {"drift": quarantined, "count": len(quarantined), "status": "phase_5_active"}


# ── Phase 4 / 5 write endpoints ──
#
# Each endpoint is paired with an activity-middleware classification
# (see api/activity_middleware.py) that marks it user_override on high
# importance — these are reversals of the system's autonomous decisions.


@router.post("/promote/run")
async def run_promotion_arena(request: Request) -> dict:
    """Trigger the promotion arena — evaluate every candidate reflex once.

    Idempotent: the gates are deterministic, so running this twice on the
    same data produces the same decisions. Typically called from the
    heartbeat, but exposed here so a user can force an evaluation.
    """
    svc = _service(request)
    if svc is None:
        raise HTTPException(status_code=503, detail="evolution service not wired")
    return await svc.handle_promote({"source": "api"})


@router.post("/promote/{reflex_id}")
async def promote_reflex_manually(
    request: Request, reflex_id: str,
) -> dict:
    """Force-evaluate a specific candidate (no gate shortcuts — still runs all gates).

    This is useful for "I think this candidate is ready, please look now"
    — NOT "I declare this winner regardless of evidence". The gates still
    apply; if they fail, the decision is ``defer`` or ``reject``.
    """
    svc = _service(request)
    if svc is None:
        raise HTTPException(status_code=503, detail="evolution service not wired")
    candidate = await svc.ledger.get_reflex(reflex_id)
    if candidate is None:
        raise HTTPException(status_code=404, detail=f"reflex {reflex_id} not found")
    from agntspace.evolution.promotion import PromotionArena
    arena = PromotionArena(
        ledger=svc.ledger, config=svc.config,
        writer=svc._writer, decision_logger=svc._decisions,
        activity_logger=svc._activity,
    )
    promotion = await arena.evaluate_one(candidate)
    return {
        "promotion_id": promotion.promotion_id,
        "decision": promotion.decision,
        "confidence": promotion.confidence,
        "rationale": promotion.rationale,
        "rollback_target": promotion.rollback_target,
    }


@router.post("/rollback/{promotion_id}")
async def rollback_promotion(request: Request, promotion_id: str) -> dict:
    """Revert a prior promotion. Restores the rollback_target to active."""
    svc = _service(request)
    if svc is None:
        raise HTTPException(status_code=503, detail="evolution service not wired")
    result = await svc.rollback_promotion(promotion_id)
    if result.get("status") == "not_found":
        raise HTTPException(status_code=404, detail=result)
    return result


@router.post("/replay")
async def enqueue_replay(
    request: Request,
    candidate_id: str = Query(..., description="Reflex ID to replay"),
    correlation_id: str = Query(..., alias="correlation_id_original",
                                description="Original correlation_id to replay"),
) -> dict:
    """Enqueue a replay job for a candidate reflex against a historical correlation_id."""
    svc = _service(request)
    if svc is None:
        raise HTTPException(status_code=503, detail="evolution service not wired")
    return await svc.enqueue_replay_for_candidate(
        candidate_id=candidate_id,
        correlation_id_original=correlation_id,
    )


@router.post("/drift/run")
async def run_drift_audit(request: Request) -> dict:
    """Trigger the drift detector once, synchronously. Returns the audit summary."""
    svc = _service(request)
    if svc is None:
        raise HTTPException(status_code=503, detail="evolution service not wired")
    return await svc.handle_drift_scan({"source": "api"})


@router.post("/mine")
async def run_mining(request: Request) -> dict:
    """Trigger the reflex miner once, synchronously. Returns the mining summary."""
    svc = _service(request)
    if svc is None:
        raise HTTPException(status_code=503, detail="evolution service not wired")
    return await svc.handle_mine_reflexes({"source": "api"})


@router.post("/kill-switch")
async def toggle_kill_switch(
    request: Request,
    enabled: bool = Query(..., description="True = disable selector experimentation"),
) -> dict:
    """Toggle the selector kill switch (runtime mirror — restart restores YAML).

    Use this to disable evolution in an emergency without waiting for a
    YAML edit to propagate. The change is AUDIT-LOGGED at high importance.
    """
    svc = _service(request)
    if svc is None:
        raise HTTPException(status_code=503, detail="evolution service not wired")
    return await svc.set_kill_switch(enabled)


@router.post("/feedback/{run_id}")
async def record_run_feedback(
    request: Request,
    run_id: str,
    score: float = Query(..., ge=0.0, le=1.0, description="Feedback in [0, 1]"),
) -> dict:
    """Attach a feedback score to a completed run's outcome (closes the loop).

    Called from approval endpoints (memory / reflection / relationship /
    any other user-approval surface) once they can map the approval to a
    run_id. Recomputes utility + updates reflex posterior if the run
    was attributed to a non-incumbent policy.
    """
    svc = _service(request)
    if svc is None:
        raise HTTPException(status_code=503, detail="evolution service not wired")
    result = await svc.record_feedback(run_id, score)
    if result.get("status") == "not_found":
        raise HTTPException(status_code=404, detail=result)
    return result
