"""
Analytics & ML Admin API
=========================
Exposes historical assignment quality data, the empirical-ETA pipeline, and the
autonomous road-sequencing / rider-affinity agents. (The XGBoost/Optuna hypertuner,
ID3 risk tree, profit predictor, and Thompson-Sampling strategy bandit have all
been retired — none of them ever affected assignment or routing behavior.)

Endpoints:
  GET  /api/v1/ml/status          – quality trend, analytics DB + history stats
  GET  /api/v1/ml/analytics       – hourly stats, zone stats, histogram
  GET  /api/v1/ml/config          – active config values
  PATCH /api/v1/ml/config         – manual config override
  POST /api/v1/ml/reset           – reset config to defaults
  POST /api/v1/ml/refresh-eta     – sync nearledb + rebuild empirical ETA stats
  GET  /api/v1/ml/eta-accuracy    – formula vs empirical ETA backtest
  GET/POST /api/v1/ml/road-eval   – autonomous road-sequencing decision
  GET  /api/v1/ml/rider-affinity  – learned vs configured rider→kitchen affinity
  GET  /api/v1/ml/export          – download assignment log as CSV
  GET  /api/v1/ml/history         – delivery-history FAISS store stats
"""

import json
import logging
import os

from fastapi import APIRouter, Body, HTTPException
from fastapi.responses import PlainTextResponse, FileResponse

logger = logging.getLogger(__name__)

router = APIRouter(
    prefix="/api/v1/ml",
    tags=["Analytics & ML"],
    responses={500: {"description": "Internal server error"}},
)

web_router = APIRouter(tags=["ML Monitor Web Dashboard"])


# ---------------------------------------------------------------------------
# Dashboard (HTML)
# ---------------------------------------------------------------------------

@web_router.get("/ml-ops", summary="Visual ML monitoring dashboard")
def ml_dashboard():
    path = os.path.join(os.getcwd(), "app/templates/ml_dashboard.html")
    if not os.path.isfile(path):
        raise HTTPException(status_code=404, detail=f"Dashboard template not found at {path}")
    return FileResponse(path)


# ---------------------------------------------------------------------------
# GET /status
# ---------------------------------------------------------------------------

@router.get("/status", summary="Assignment quality trend & store stats")
def ml_status():
    """
    Returns:
    - How many assignment events are logged
    - Recent quality score trend (last 50 calls)
    - Delivery history FAISS store record count
    - Active config values
    """
    try:
        from app.services.ml.ml_data_collector import get_collector
        from app.services.vector.delivery_history_store import get_delivery_history_store
        from app.config.dynamic_config import get_config

        collector = get_collector()
        history   = get_delivery_history_store()
        cfg       = get_config()

        return {
            "status": "ok",
            "db_records":       collector.count_records(),
            "quality_trend":    collector.get_recent_quality_trend(last_n=50),
            "delivery_history": {
                "record_count": history.record_count(),
                "status": "ready" if history.record_count() > 0 else "empty",
            },
            "config": cfg.get_all(),
        }
    except Exception as e:
        logger.error(f"[ML API] status: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# GET /analytics
# ---------------------------------------------------------------------------

@router.get("/analytics", summary="Hourly stats, zone stats, quality histogram")
def ml_analytics():
    """Operational analytics from historical assignment logs."""
    try:
        from app.services.ml.ml_data_collector import get_collector
        collector = get_collector()
        return {
            "status":             "ok",
            "hourly_stats":       collector.get_hourly_stats(),
            "zone_stats":         collector.get_zone_stats(),
            "quality_histogram":  collector.get_quality_histogram(),
        }
    except Exception as e:
        logger.error(f"[ML API] analytics: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# GET /config
# ---------------------------------------------------------------------------

@router.get("/config", summary="Current active configuration values")
def ml_config():
    from app.config.dynamic_config import get_config, DEFAULTS
    try:
        cfg         = get_config()
        all_values  = cfg.get_all()
        cached_keys = set(cfg._cache.keys())
        annotated   = {
            k: {"value": v, "source": "override" if k in cached_keys else "default"}
            for k, v in all_values.items()
        }
        return {
            "status":          "ok",
            "hyperparameters": annotated,
            "total_params":    len(annotated),
            "override_count":  sum(1 for x in annotated.values() if x["source"] == "override"),
        }
    except Exception as e:
        logger.error(f"[ML API] config: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


@router.patch("/config", summary="Override specific config values")
def ml_config_patch(payload: dict = Body(...)):
    """Manually set any config key, e.g. {\"road_factor\": 1.4}"""
    from app.config.dynamic_config import get_config
    try:
        get_config().set_bulk(payload, source="ml_admin")
        return {"status": "ok", "updated": list(payload.keys())}
    except Exception as e:
        logger.error(f"[ML API] config patch: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# POST /reset
# ---------------------------------------------------------------------------

@router.post("/reset", summary="Reset all config overrides to factory defaults")
def ml_reset():
    from app.config.dynamic_config import get_config
    try:
        get_config().reset_to_defaults()
        return {"status": "ok", "message": "All config values reset to factory defaults."}
    except Exception as e:
        logger.error(f"[ML API] reset: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))




# ---------------------------------------------------------------------------
# GET /rider-efficiency
# ---------------------------------------------------------------------------

@router.get("/rider-efficiency", summary="Per-rider efficiency scores from 30-day CSV history")
def ml_rider_efficiency():
    """
    Computes efficiency scores for each rider from the delivery_details.csv.

    Metrics:
      delivery_count  — total deliveries in the 30-day window
      avg_km          — average km per delivery
      unique_zones    — number of distinct 1.1 km delivery cells served
      efficiency_score — normalised 0..1 composite (high = efficient)

    Used internally as tiebreaker during solo rider consolidation
    and Phase-0 pattern pre-assignment host selection.
    """
    try:
        from app.services.vector.delivery_history_store import get_delivery_history_store
        scores = get_delivery_history_store().get_rider_efficiency_scores()
        ranked = sorted(scores.items(), key=lambda x: x[1]["efficiency_score"], reverse=True)
        return {
            "status": "ok",
            "rider_count": len(scores),
            "riders": [{"rider_id": rid, **data} for rid, data in ranked],
        }
    except Exception as e:
        logger.error(f"[ML API] rider-efficiency: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# GET /history
# ---------------------------------------------------------------------------

@router.get("/history", summary="Delivery-history FAISS store stats + pattern table")
def ml_history(patterns: bool = False):
    """
    Shows the delivery history store status.

    Add ?patterns=true to include the full pattern table
    (all clear dominant-rider zones, sorted by pattern score).
    """
    from app.services.vector.delivery_history_store import (
        get_delivery_history_store, _paths_for, _pattern_source, CSV_PATH
    )
    try:
        store  = get_delivery_history_store()
        source = _pattern_source()
        vectors_path, _records_path, meta_path = _paths_for(source)
        meta   = {}
        if os.path.isfile(meta_path):
            with open(meta_path, "r", encoding="utf-8") as f:
                meta = json.load(f)

        resp = {
            "status":         "ok",
            "pattern_source": source,
            "record_count":   store.record_count(),
            "pattern_count":  store.pattern_count(),
            "index_ready":    store.record_count() > 0,
            "disk_index":     os.path.isfile(vectors_path),
            "csv_path":       CSV_PATH,
            "csv_exists":     os.path.isfile(CSV_PATH),
            "saved_meta":     meta,
        }
        if patterns:
            resp["patterns"] = store.get_pattern_stats()
        return resp
    except Exception as e:
        logger.error(f"[ML API] history: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# POST /inject-corrections
# ---------------------------------------------------------------------------

@router.post("/inject-corrections", summary="Inject manually-corrected delivery CSV as high-weight priors")
def ml_inject_corrections(
    corrections_path: str = Body(default="delivery_corrections.csv", embed=True),
    weight: int = Body(default=5, embed=True),
):
    """
    Loads a manually-corrected delivery CSV and injects it into the live
    delivery-history pattern table as high-weight prior evidence.

    Each record in the corrections file is counted `weight` times (default 5),
    so 5 correction votes easily override 1-2 noise votes from regular history.

    The correction CSV must have the same columns as delivery_details.csv:
      pickupcustomer, pickuplat, pickuplon, deliverylat, deliverylong,
      userid, ridername

    After injection the pattern table is rebuilt in-memory and saved to disk.
    No server restart needed.

    Parameters:
      corrections_path  Path inside the container (default: delivery_corrections.csv)
      weight            How many times each correction record is counted (default: 5)
    """
    from app.services.vector.delivery_history_store import get_delivery_history_store
    try:
        store  = get_delivery_history_store()
        result = store.inject_corrections(corrections_path, weight=weight)
        return {"status": "ok", **result}
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))
    except Exception as e:
        logger.error(f"[ML API] inject-corrections: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# POST /reload-history
# ---------------------------------------------------------------------------

@router.post("/reload-history", summary="Rebuild FAISS delivery-history index from CSV")
def ml_reload_history():
    """
    Forces a full rebuild of the delivery-history FAISS index from
    delivery_details.csv, then saves the new index to disk.

    Call this whenever you update the CSV with a fresh 30-day export.
    No server restart needed — the in-memory store is hot-swapped.

    Returns the number of records now loaded.
    """
    from app.services.vector.delivery_history_store import get_delivery_history_store
    try:
        store = get_delivery_history_store()
        n     = store.reload_from_csv()
        return {
            "status":       "ok",
            "record_count": n,
            "message":      f"FAISS history index rebuilt from CSV — {n} records loaded and saved to disk.",
        }
    except Exception as e:
        logger.error(f"[ML API] reload-history: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))



# ---------------------------------------------------------------------------
# POST /refresh-eta
# ---------------------------------------------------------------------------

@router.post("/refresh-eta", summary="Sync nearledb -> local mirror and rebuild empirical ETA stats")
def ml_refresh_eta(
    days: int = Body(default=None, embed=True),
    tenant_id: int = Body(default=916, embed=True),
    full: bool = Body(default=False, embed=True),
):
    """
    Manually trigger the empirical-ETA pipeline (the autonomous agent does this
    on a schedule too): incremental READ-ONLY pull of new completed deliveries
    from nearledb into the local mirror, then rebuild the learned medians.

    `days` defaults to `eta_history_days`. Set `full=true` to force a full
    backfill of the window instead of an incremental sync.
    """
    from app.services.routing.delivery_history_service import get_delivery_history_service
    from app.config.dynamic_config import get_config
    try:
        window = int(days if days is not None else get_config().get("eta_history_days", 14))
        result = get_delivery_history_service().refresh_eta_stats(
            days=window, tenant_id=tenant_id, full=full
        )
        return {"status": "ok", "result": result}
    except Exception as e:
        logger.error(f"[ML API] refresh-eta: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# GET /eta-accuracy
# ---------------------------------------------------------------------------

@router.get("/eta-accuracy", summary="Backtest: formula ETA MAE vs empirical ETA MAE")
def ml_eta_accuracy(days: int = None, tenant_id: int = 916):
    """
    Honest time-split backtest. Trains empirical medians on the older 80% of the
    history window and reports mean-absolute-error (minutes) of the formula vs
    the empirical model on the held-out recent 20%.

    Use this as the gate before trusting empirical ETAs: if `interpretation` is
    not "empirical_better", leave `eta_empirical_enabled=false` and investigate.
    """
    from app.services.routing.delivery_history_service import get_delivery_history_service
    from app.config.dynamic_config import get_config
    try:
        cfg = get_config()
        window = int(days if days is not None else cfg.get("eta_history_days", 14))
        result = get_delivery_history_service().backtest(
            days=window,
            tenant_id=tenant_id,
            min_samples=int(cfg.get("eta_min_samples", 20)),
            stat=str(cfg.get("eta_stat", "median")),
        )
        return {"status": "ok", "backtest": result}
    except Exception as e:
        logger.error(f"[ML API] eta-accuracy: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# GET/POST /road-eval  (autonomous road-sequencing decision agent)
# ---------------------------------------------------------------------------

@router.get("/road-eval", summary="Last autonomous road-sequencing decision")
def ml_road_eval_get():
    """
    Show the road-sequencing agent's most recent decision: whether road-aware
    ordering beat straight-line ordering on real batches, the measured mean
    travel-time gain, and whether it auto-enabled/disabled `routing_use_road_distance`.
    """
    from app.services.routing.road_sequencing_agent import get_road_agent
    from app.config.dynamic_config import get_config
    try:
        agent = get_road_agent()
        return {
            "status": "ok",
            "road_distance_enabled": bool(get_config().get("routing_use_road_distance", False)),
            "auto_manage": bool(get_config().get("routing_auto_manage", True)),
            "last_decision": agent.last_decision or get_config().get("routing_road_eval", {}),
        }
    except Exception as e:
        logger.error(f"[ML API] road-eval get: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/road-eval", summary="Run the road-sequencing decision now")
def ml_road_eval_run():
    """
    Trigger an immediate evaluation + autonomous decision (the agent also does
    this on a schedule). Measures road vs aerial sequencing on sampled real
    batches and may flip `routing_use_road_distance` based on the measured gain.
    Also reports whether we beat the riders' actual delivered order.
    """
    from app.services.routing.road_sequencing_agent import get_road_agent
    try:
        return {"status": "ok", "decision": get_road_agent().decide_and_apply()}
    except Exception as e:
        logger.error(f"[ML API] road-eval run: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# GET /rider-affinity  (learned vs configured rider→kitchen affinity)
# ---------------------------------------------------------------------------

@router.get("/rider-affinity", summary="Learned vs configured rider→kitchen affinity")
def ml_rider_affinity(refresh: bool = False):
    """
    Show the learned rider→kitchen affinity (from real delivery history) merged
    with the curated config. `?refresh=true` recomputes from the local mirror
    first. Learned data only augments SOFT steering — hard kitchen locks and
    BLOCKED_RIDERS stay sourced from the curated config.
    """
    from app.services.routing.rider_affinity_service import get_rider_affinity
    try:
        aff = get_rider_affinity()
        if refresh:
            aff.refresh()
        return {"status": "ok", "affinity": aff.get_summary()}
    except Exception as e:
        logger.error(f"[ML API] rider-affinity: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


# ---------------------------------------------------------------------------
# GET /export
# ---------------------------------------------------------------------------

@router.get("/export", summary="Download all assignment logs as CSV")
def ml_export():
    from app.services.ml.ml_data_collector import get_collector
    try:
        csv_data = get_collector().export_csv()
        response = PlainTextResponse(content=csv_data, media_type="text/csv")
        response.headers["Content-Disposition"] = 'attachment; filename="assignment_log.csv"'
        return response
    except Exception as e:
        logger.error(f"[ML API] export: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))
