"""
Road-Sequencing Decision Agent
==============================

Autonomous controller for the `routing_use_road_distance` feature. Instead of a
human flipping a config flag, this agent periodically *measures* whether road-aware
sequencing (road travel-time matrix + OR-Tools open-TSP) actually beats the default
straight-line ordering on REAL recent batches, and turns the feature on or off by
itself — re-checking every cycle so it self-corrects if the gain ever disappears.

Decision (with hysteresis, so it doesn't flap):
    mean travel-time gain >= routing_auto_enable_gain_pct  -> enable
    mean travel-time gain <  routing_auto_disable_gain_pct -> disable
    in between                                             -> leave as-is

Every decision is stored (auditable) in DynamicConfig under `routing_road_eval`
and exposed via GET /api/v1/ml/road-eval. Cost is bounded: it evaluates only
`routing_eval_sample_batches` batches once per `routing_eval_interval_hours`.
"""

import asyncio
import json
import logging
import threading
import time
from datetime import datetime
from typing import Any, Dict, List, Optional

import numpy as np

logger = logging.getLogger(__name__)


def _total(order_idx: List[int], matrix: List[List[float]]) -> float:
    """Total open-route cost: origin(0) -> drops in `order_idx` (1-based into matrix)."""
    seq = [0] + [i + 1 for i in order_idx]
    return sum(matrix[seq[k]][seq[k + 1]] for k in range(len(seq) - 1))


class RoadSequencingAgent:
    """Measures road-vs-aerial sequencing on real batches and auto-toggles the flag."""

    def __init__(self):
        self._opt = None  # lazy RouteOptimizer
        self._scheduler_started = False
        self._lock = threading.Lock()
        self.last_decision: Dict[str, Any] = {}

    def _optimizer(self):
        if self._opt is None:
            from app.services.routing.route_optimizer import RouteOptimizer
            self._opt = RouteOptimizer()
        return self._opt

    async def evaluate(self, sample_batches: int = 8, days: int = 14) -> Dict[str, Any]:
        """
        Compare aerial-order vs road-order travel time on sampled real batches.
        Returns mean gain % and per-batch detail. No flag changes here.
        """
        from app.services.routing.delivery_history_service import get_delivery_history_service
        from app.core.arrow_utils import calculate_haversine_matrix_vectorized

        opt = self._optimizer()
        if not opt.use_google_maps:
            return {"evaluated": 0, "reason": "no_google_key", "mean_gain_pct": 0.0}

        batches = get_delivery_history_service().sample_batches(
            days=days, limit=sample_batches
        )
        if not batches:
            return {"evaluated": 0, "reason": "no_local_batches", "mean_gain_pct": 0.0}

        per_batch: List[Dict[str, Any]] = []
        for b in batches:
            origin, drops = b["origin"], b["drops"]
            locs = [origin] + drops
            matrix = await opt._road_duration_matrix(locs)
            if matrix is None:
                continue
            # aerial order (current production behaviour)
            lats = np.array([p[0] for p in locs])
            lons = np.array([p[1] for p in locs])
            aerial = calculate_haversine_matrix_vectorized(lats, lons)
            aer = [i - 1 for i in opt._two_opt_improve(opt._solve_greedy(locs, aerial), aerial) if i != 0]
            # road order (OR-Tools open-TSP on the road travel-time matrix)
            road = [i - 1 for i in opt._solve_tsp_ortools(locs, matrix) if i != 0]

            # Human's ACTUAL route: drops are already in delivered order
            # (_load_raw_rows is ORDER BY deliverytime), so identity = what the
            # rider really drove. This is the "do we beat the humans?" baseline.
            human = list(range(len(drops)))

            t_aer = _total(aer, matrix)
            t_road = _total(road, matrix)
            t_human = _total(human, matrix)
            gain = (100.0 * (t_aer - t_road) / t_aer) if t_aer > 0 else 0.0
            gain_vs_human = (100.0 * (t_human - t_road) / t_human) if t_human > 0 else 0.0
            per_batch.append({
                "rider": b["rider"], "day": b["day"], "drops": len(drops),
                "aerial_min": round(t_aer, 1), "road_min": round(t_road, 1),
                "human_min": round(t_human, 1),
                "gain_pct": round(gain, 1),
                "gain_vs_human_pct": round(gain_vs_human, 1),
                "beat_human": t_road <= t_human + 1e-9,
            })

        if not per_batch:
            return {"evaluated": 0, "reason": "matrix_unavailable", "mean_gain_pct": 0.0}

        n = len(per_batch)
        mean_gain = sum(x["gain_pct"] for x in per_batch) / n
        mean_vs_human = sum(x["gain_vs_human_pct"] for x in per_batch) / n
        beats = sum(1 for x in per_batch if x["beat_human"])
        return {
            "evaluated": n,
            "mean_gain_pct": round(mean_gain, 2),
            "median_gain_pct": round(sorted(x["gain_pct"] for x in per_batch)[n // 2], 2),
            "human_beat_rate_pct": round(100.0 * beats / n, 1),
            "human_beat_count": f"{beats}/{n}",
            "mean_gain_vs_human_pct": round(mean_vs_human, 2),
            "per_batch": per_batch,
        }

    def decide_and_apply(self) -> Dict[str, Any]:
        """Run an evaluation and autonomously enable/disable road sequencing."""
        from app.config.dynamic_config import get_config
        cfg = get_config()

        sample = int(cfg.get("routing_eval_sample_batches", 8))
        days = int(cfg.get("routing_eval_days", 14))
        enable_thr = float(cfg.get("routing_auto_enable_gain_pct", 3.0))
        disable_thr = float(cfg.get("routing_auto_disable_gain_pct", 1.0))
        min_batches = int(cfg.get("routing_eval_min_batches", 3))
        auto_manage = bool(cfg.get("routing_auto_manage", True))

        try:
            result = asyncio.run(self.evaluate(sample_batches=sample, days=days))
        except RuntimeError:
            # An event loop is already running in this thread — use a fresh one.
            loop = asyncio.new_event_loop()
            try:
                result = loop.run_until_complete(self.evaluate(sample_batches=sample, days=days))
            finally:
                loop.close()

        current = bool(cfg.get("routing_use_road_distance", False))
        evaluated = result.get("evaluated", 0)
        gain = result.get("mean_gain_pct", 0.0)
        action = "kept"
        new_state = current

        if not auto_manage:
            action = "auto_manage_off"
        elif evaluated < min_batches:
            action = "insufficient_data"
        else:
            if gain >= enable_thr and not current:
                new_state = True
                cfg.set("routing_use_road_distance", True, source="road_agent")
                action = "enabled"
            elif gain < disable_thr and current:
                new_state = False
                cfg.set("routing_use_road_distance", False, source="road_agent")
                action = "disabled"

        decision = {
            "decided_at": datetime.utcnow().isoformat(),
            "action": action,
            "flag_before": current,
            "flag_after": new_state,
            "mean_gain_pct": gain,
            "enable_threshold_pct": enable_thr,
            "disable_threshold_pct": disable_thr,
            "evaluation": result,
        }
        self.last_decision = decision
        try:
            # Persist a compact copy for audit (without the full per-batch list).
            compact = {k: v for k, v in decision.items() if k != "evaluation"}
            compact["evaluated"] = evaluated
            compact["human_beat_rate_pct"] = result.get("human_beat_rate_pct")
            compact["mean_gain_vs_human_pct"] = result.get("mean_gain_vs_human_pct")
            cfg.set("routing_road_eval", compact, source="road_agent")
        except Exception:
            pass

        logger.info(
            f"[RoadAgent] action={action} gain_vs_aerial={gain}% evaluated={evaluated} "
            f"flag {current}->{new_state}"
        )
        if evaluated:
            logger.info(
                f"[RoadAgent] beat humans on {result.get('human_beat_count', '?')} batches "
                f"(avg {result.get('mean_gain_vs_human_pct', 0)}% faster than actual delivered order)"
            )
        return decision

    def ensure_background_agent(self, interval_hours: int = 24,
                               warmup_seconds: int = 120) -> bool:
        """Start the autonomous decision loop once (daemon thread)."""
        with self._lock:
            if self._scheduler_started:
                return False
            self._scheduler_started = True

        def _loop():
            # Let the ETA sync agent populate the local mirror first.
            time.sleep(max(0, warmup_seconds))
            logger.info(f"[RoadAgent] autonomous road-sequencing decision loop started "
                        f"(interval={interval_hours}h)")
            while True:
                try:
                    self.decide_and_apply()
                except Exception as e:
                    logger.warning(f"[RoadAgent] decision cycle failed (will retry): {e}")
                from app.config.dynamic_config import get_config
                hrs = int(get_config().get("routing_eval_interval_hours", interval_hours))
                time.sleep(max(1, hrs) * 3600)

        threading.Thread(target=_loop, daemon=True, name="road-seq-agent").start()
        return True


_agent: Optional[RoadSequencingAgent] = None
_agent_lock = threading.Lock()


def get_road_agent() -> RoadSequencingAgent:
    global _agent
    with _agent_lock:
        if _agent is None:
            _agent = RoadSequencingAgent()
        return _agent
