"""Provider payload optimization endpoints."""

import asyncio as _asyncio
import logging
import os
import time
from concurrent.futures import ThreadPoolExecutor
from math import radians, cos, sin, asin, sqrt as _math_sqrt
from typing import Any
from fastapi import APIRouter, Body, Request, Depends, status, HTTPException, Query

from app.controllers.route_controller import RouteController
from app.core.exceptions import APIException

logger = logging.getLogger(__name__)

# ── Thread pool for parallel per-rider TSP route optimization ────────────────
#
# Problem: asyncio.gather on async coroutines that call OR-Tools internally
# does NOT give true CPU parallelism. Python's GIL means only one thread
# runs Python bytecode at a time, so "parallel" async TSP calls actually
# execute sequentially on a single core.
#
# Solution: run each rider's optimize_provider_payload in a ThreadPoolExecutor.
# OR-Tools is a C++ extension that RELEASES the GIL during solving, so threads
# genuinely run on separate CPU cores simultaneously.
#
# Result: 8 riders' routes are solved in parallel instead of serially.
# Typical gain: 8 × 200ms serial → ~200ms parallel (4-8× speedup).
#
# Pool size: capped at 12 workers (enough for max expected simultaneous riders;
# more workers than cores still helps because OR-Tools releases the GIL).
_route_executor = ThreadPoolExecutor(
    max_workers=min(12, (os.cpu_count() or 4)),
    thread_name_prefix="route-tsp",
)


def _sync_optimize_route(rider_orders: list) -> list:
    """
    Thread-pool worker: run optimize_provider_payload inside an isolated event loop.

    Design decisions:
    - Fresh RouteOptimizer per call: the class is cheap to construct (just
      config reads) and creates no shared mutable state, so this is safe
      and avoids any future thread-safety risk from shared instances.
    - Fresh asyncio event loop per thread: event loops are NOT thread-safe;
      each OS thread must own its own loop.  asyncio.new_event_loop() +
      run_until_complete is the documented way to call async code from a
      synchronous thread context.
    """
    from app.services.routing.route_optimizer import RouteOptimizer as _RO
    _opt = _RO()
    _loop = _asyncio.new_event_loop()
    try:
        return _loop.run_until_complete(
            _opt.optimize_provider_payload(rider_orders, start_coords=None)
        )
    except Exception:
        logger.exception("[ThreadTSP] Route optimization failed in worker thread")
        return []
    finally:
        _loop.close()

# ── Background thread pool for ML tasks (logging, retraining, auto-tuning) ───
# Kept deliberately small (2 workers) — these jobs are lightweight SQLite
# writes and in-memory ID3 fits that finish in < 1 s each.  They must never
# compete with the route-tsp pool for CPU during a live request.
_ml_executor = ThreadPoolExecutor(
    max_workers=2,
    thread_name_prefix="ml-bg",
)


def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    """Great-circle distance in km (inline, no external dep)."""
    try:
        la1, lo1, la2, lo2 = map(radians, [float(lat1), float(lon1), float(lat2), float(lon2)])
        dlat = la2 - la1
        dlon = lo2 - lo1
        a = sin(dlat / 2) ** 2 + cos(la1) * cos(la2) * sin(dlon / 2) ** 2
        return 2 * asin(min(1.0, _math_sqrt(a))) * 6371.0
    except Exception:
        return float("inf")


def _bg_log_assignment(
    num_orders: int,
    num_riders: int,
    hyperparams: dict,
    assignments: dict,
    unassigned_count: int,
    elapsed_ms: float,
) -> None:
    """Background worker: log the assignment event. Runs in _ml_executor so
    it never blocks the API response."""
    try:
        from app.services.ml.ml_data_collector import get_collector as _gc
        collector = _gc()
        quality_score = collector.log_assignment_event(
            num_orders=num_orders,
            num_riders=num_riders,
            hyperparams=hyperparams,
            assignments=assignments,
            unassigned_count=unassigned_count,
            elapsed_ms=elapsed_ms,
        ) or 50.0
        count = collector.count_records()
        logger.debug(f"[ML BG] Logged event #{count}, quality={quality_score:.1f}")
    except Exception as _e:
        logger.warning(f"[ML BG] Background task failed (non-fatal): {_e}")


router = APIRouter(
    prefix="/api/v1/optimization",
    tags=["Route Optimization"],
    responses={
        400: {"description": "Bad request - Invalid input parameters"},
        422: {"description": "Validation error - Request validation failed"},
        500: {"description": "Internal server error"},
    },
)


def get_route_controller() -> RouteController:
    """Dependency injection for route controller."""
    return RouteController()


# Legacy single-route endpoint removed; provider flow only.
@router.post(
    "/createdeliveries",
    status_code=status.HTTP_200_OK,
    summary="Optimize provider payload (forwarding paused)",
    description="""
    Accepts the provider's orders array, reorders it using greedy nearest-neighbor, adds only:
    - step (1..N)
    - previouskms (distance from previous stop in km)
    - cumulativekms (total distance so far in km)
    - actualkms (direct pickup-to-delivery distance)

    Forwarding is temporarily paused: returns the optimized array in the response.
    """,
    responses={
        200: {
            "description": "Upstream response",
            "content": {
                "application/json": {
                    "example": {
                        "code": 200,
                        "details": [],
                        "message": "Success",
                        "status": True,
                    }
                }
            },
        }
    },
)
async def provider_optimize_forward(
    body: list[dict], controller: RouteController = Depends(get_route_controller)
):
    """
    Accept provider JSON array, reorder by greedy nearest-neighbor, annotate each item with:
    - step (1..N)
    - previouskms (km from previous point)
    - cumulativekms (km so far)
    - actualkms (pickup to delivery distance)
    Then forward the optimized array to the external API and return only its response.
    """
    try:
        url = "https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries"
        result = await controller.optimize_and_forward_provider_payload(body, url)
        return result
    except APIException:
        raise
    except Exception as e:
        logger.error(
            f"Unexpected error in provider_optimize_forward: {e}", exc_info=True
        )
        raise HTTPException(status_code=500, detail="Internal server error")


@router.get("/createdeliveries", summary="Usage info for provider optimize forward")
async def provider_optimize_forward_info():
    """Return usage info; this endpoint accepts POST only for processing."""
    return {
        "message": "Use POST with a JSON array of orders to optimize and forward.",
        "method": "POST",
        "path": "/api/v1/optimization/provider-optimize-forward",
    }


# ---------------------------------------------------------------------------
# STEP RECONCILIATION
# ---------------------------------------------------------------------------

def _detect_step_anomaly(orders: list[dict]) -> tuple[bool, str]:
    """
    Check whether a rider's order list has a clean sequential step sequence.

    Returns (anomaly_found, reason_string).

    Anomaly cases:
      - duplicate step numbers
      - gap in step numbers  (e.g. 1,2,3,5,6 — missing 4)
      - step numbers don't start at 1 for pending-only list
      - an order has no "step" field at all
    """
    steps = []
    for o in orders:
        s = o.get("step")
        if s is None:
            return True, "missing_step"
        try:
            steps.append(int(s))
        except (ValueError, TypeError):
            return True, "invalid_step"

    if not steps:
        return False, "empty"

    steps_sorted = sorted(steps)

    # Duplicate check
    if len(steps_sorted) != len(set(steps_sorted)):
        return True, "duplicate"

    # Gap / non-sequential check
    expected = list(range(steps_sorted[0], steps_sorted[0] + len(steps_sorted)))
    if steps_sorted != expected:
        return True, "gap"

    # Should start from 1 (not from some arbitrary offset)
    if steps_sorted[0] != 1:
        return True, "wrong_start"

    return False, "ok"


@router.post(
    "/reconcile-steps",
    status_code=status.HTTP_200_OK,
    summary="Reconcile step numbers after manual rider reassignment",
    description="""
    When the operations team manually moves an order from one rider to another,
    step numbers become inconsistent:
      - Donor rider gets a gap  (steps 1,2,3,5,6 — step 4 was transferred)
      - Recipient rider gets a new order with a conflicting or missing step

    This endpoint:
      1. Detects anomalies per rider (gap, duplicate, missing, wrong order)
      2. Separates each rider's already-delivered orders from pending ones
      3. Re-runs the route optimizer on the pending orders to get the best sequence
      4. Returns clean sequential steps starting from 1 for each rider

    Input:
      {
        "riders": [
          {
            "rider_id": 883,
            "orders": [ ...full order objects with current step values... ]
          }
        ]
      }

    The "orders" array for each rider should include ALL current orders
    (delivered + pending). The endpoint determines delivered status from
    the "deliverytime" field (non-null/non-empty = delivered).
    """,
)
async def reconcile_steps(body: Any = Body(default=None)):
    if not body or not isinstance(body, dict):
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail='Body must be {"riders": [{"rider_id": X, "orders": [...]}]}',
        )

    riders_input: list = body.get("riders") or []
    if not riders_input:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="No riders provided.",
        )

    from app.services.routing.route_optimizer import RouteOptimizer

    results = []

    for rider_block in riders_input:
        rider_id = rider_block.get("rider_id") or rider_block.get("userid")
        all_orders: list[dict] = rider_block.get("orders") or []

        # Resolve rider name: explicit input > order username field > fallback
        rider_name = (
            rider_block.get("rider_name")
            or rider_block.get("username")
            or next(
                (o.get("username") or o.get("rider") for o in all_orders
                 if o.get("username") or o.get("rider")),
                None,
            )
            or f"Rider {rider_id}"
        )

        if not all_orders:
            results.append({
                "rider_id": rider_id,
                "rider_name": rider_name,
                "anomaly_detected": False,
                "anomaly_type": "empty",
                "delivered_count": 0,
                "pending_count": 0,
                "resequenced": False,
                "orders": [],
            })
            continue

        # ── Split delivered vs pending ──────────────────────────────────────
        # Delivered = deliverytime is non-null and non-empty string.
        # Pending   = everything else (in-transit or not yet started).
        delivered: list[dict] = []
        pending: list[dict]   = []
        for o in all_orders:
            dt = o.get("deliverytime")
            if dt and str(dt).strip() not in ("", "0", "null", "None"):
                delivered.append(dict(o))
            else:
                pending.append(dict(o))

        # ── Detect anomaly on the FULL order list ───────────────────────────
        anomaly, reason = _detect_step_anomaly(all_orders)

        if not anomaly and not pending:
            # Everything delivered, steps clean — nothing to do
            results.append({
                "rider_id": rider_id,
                "rider_name": rider_name,
                "anomaly_detected": False,
                "anomaly_type": "ok",
                "delivered_count": len(delivered),
                "pending_count": 0,
                "resequenced": False,
                "orders": all_orders,
            })
            continue

        # ── Step-value helper (used by both fallback sort and delivered sort) ─
        def _step_val(o: dict) -> int:
            try:
                return int(o.get("step") or 0)
            except (ValueError, TypeError):
                return 0

        # ── Coordinate normalizer ───────────────────────────────────────────
        # optimize_provider_payload reads only deliverylat / deliverylong.
        # Orders from reconcile callers may carry coords under droplat/droplon
        # or dlat/dlon.  Normalise so the optimizer always gets valid coords.
        def _norm_coords(o: dict) -> dict:
            o = dict(o)
            def _fv(v):
                try:    return float(v or 0)
                except: return 0.0
            if _fv(o.get("deliverylat")) == 0:
                o["deliverylat"] = o.get("droplat") or o.get("dlat") or o.get("deliverylat") or ""
            if _fv(o.get("deliverylong")) == 0:
                o["deliverylong"] = (
                    o.get("droplon") or o.get("dlon")
                    or o.get("deliverylong") or ""
                )
            return o

        # ── Re-optimise pending orders ──────────────────────────────────────
        # Run the same route optimizer used at assignment time so step order
        # matches the greedy + 2-opt algorithm (now with crossings eliminated).
        # Fallback: sort by existing step value so at least the sequence is
        # deterministic when the optimizer cannot run.
        resequenced_pending: list[dict] = sorted(pending, key=_step_val)

        if pending:
            try:
                _opt = RouteOptimizer()
                _optimized = await _opt.optimize_provider_payload(
                    [_norm_coords(o) for o in pending],
                    start_coords=None,
                )
                if _optimized:
                    # Optimizer returns list in route order (index 0 = first stop).
                    # Use it; otherwise keep the sorted fallback.
                    resequenced_pending = _optimized
                else:
                    logger.warning(
                        f"[Reconcile] Optimizer returned empty result for rider "
                        f"{rider_id} — keeping step-sorted fallback."
                    )
            except Exception as _oe:
                logger.warning(
                    f"[Reconcile] Route re-optimize failed for rider {rider_id} "
                    f"(non-fatal, keeping step-sorted fallback): {_oe}"
                )

        # ── Renumber: delivered keep positions 1..N, pending continue after ─
        # Sort delivered by their existing step so the sequence is stable.
        delivered_sorted = sorted(delivered, key=_step_val)

        # Reassign delivered steps cleanly (fills any gap in delivered portion)
        for i, o in enumerate(delivered_sorted, start=1):
            o["step"] = i
        max_delivered_step = len(delivered_sorted)

        # Pending steps start immediately after the last delivered step.
        # resequenced_pending is already in route order (optimizer output or
        # step-sorted fallback), so enumerate order == correct delivery order.
        for i, o in enumerate(resequenced_pending, start=max_delivered_step + 1):
            o["step"] = i

        final_orders = delivered_sorted + resequenced_pending

        results.append({
            "rider_id": rider_id,
            "rider_name": rider_name,
            "anomaly_detected": anomaly,
            "anomaly_type": reason,
            "delivered_count": len(delivered),
            "pending_count": len(pending),
            "resequenced": True,
            "orders": final_orders,
        })

        logger.info(
            f"[Reconcile] Rider {rider_id}: anomaly={reason} "
            f"delivered={len(delivered)} pending={len(pending)} "
            f"resequenced={len(resequenced_pending)}"
        )

    return {
        "status": True,
        "reconciled_riders": len(results),
        "riders": results,
    }


@router.post(
    "/riderassign",
    status_code=status.HTTP_200_OK,
    summary="Assign created orders to active riders",
    description="""
    Assigns orders to riders based on kitchen preferences, proximity, and load.
    
    - If a payload of orders is provided, processes those.
    - If payload is empty, fetches all 'created' orders from the external API.
    - Fetches active riders and matches them.
    """,
    responses={
        200: {
            "description": "Assignment Result",
            "content": {
                "application/json": {
                    "example": {
                        "code": 200,
                        "details": {"1234": [{"orderid": "..."}]},
                        "message": "Success",
                        "status": True,
                    }
                }
            },
        }
    },
)
async def assign_orders_to_riders(
    request: Request,
    body: Any = Body(default=None),
    reshuffle: bool = Query(False, alias="reshuffle"),
):
    """
    Smart assignment of orders to riders.

    Accepts two payload formats:
      - Legacy: flat JSON array of order objects
      - New:    {"deliveries": [...], "absent_riders": [{"userid": 123, "username": "..."}]}

    Riders listed in absent_riders are excluded from all assignment phases for
    this call — even if they are the best pattern match.
    """
    from app.services.rider.get_active_riders import (
        fetch_active_riders,
        fetch_created_orders,
    )
    from app.services.core.assignment_service import AssignmentService
    from app.services.routing.route_optimizer import RouteOptimizer
    from app.services.routing.empirical_eta_calculator import EmpiricalETACalculator
    from datetime import datetime, timedelta
    from dateutil.parser import parse as parse_date
    import asyncio

    eta_calculator = EmpiricalETACalculator()

    try:
        _t0 = time.time()   # wall-clock start for elapsed_ms logging

        # ── Parse new payload format ─────────────────────────────────────────
        # {"deliveries": [...], "absent_riders": [{"userid": X}, ...]}
        # vs legacy flat list [...].
        absent_rider_ids: set = set()
        if isinstance(body, dict):
            _absent_list = body.get("absent_riders") or []
            for _ar in _absent_list:
                try:
                    _arid = int(_ar.get("userid") or _ar.get("id") or 0)
                    if _arid:
                        absent_rider_ids.add(_arid)
                except (ValueError, TypeError):
                    pass
            body = body.get("deliveries") or []
            if absent_rider_ids:
                logger.info(
                    f"[Absent] Excluding {len(absent_rider_ids)} absent riders "
                    f"from this batch: {sorted(absent_rider_ids)}"
                )

        # Accept both ?reshuffle and legacy typo variants in URL for backwards compat
        q_params = request.query_params
        do_reshuffle = reshuffle or any(
            k in q_params for k in ["resuffle", "rehuffle"]
        )

        # 1. Fetch riders and (if body is empty) orders in parallel.
        if body:
            riders = await fetch_active_riders()
            orders = body
            logger.info(f"[PROCESS] Received {len(orders)} orders from payload.")
        else:
            logger.info(
                "[PROCESS] No payload — fetching riders and orders in parallel."
            )
            riders, orders = await asyncio.gather(
                fetch_active_riders(),
                fetch_created_orders(),
            )
            if orders:
                logger.info(f"[PROCESS] Fetched {len(orders)} created orders from external API.")
            else:
                logger.info("[PROCESS] No created orders returned from external API.")

        fuel_charge = 2.5
        base_pay = 0.0

        # 2. Validate absent_rider_ids against the live roster
        _live_ids: set = {
            int(r.get("userid") or r.get("riderid") or r.get("id") or 0)
            for r in riders
        } - {0}
        if absent_rider_ids:
            _unknown_absent  = absent_rider_ids - _live_ids
            _confirmed_absent = absent_rider_ids & _live_ids
            if _unknown_absent:
                logger.warning(
                    f"[Absent] Rider ID(s) not in active roster (typo?): "
                    f"{sorted(_unknown_absent)} — these will have no effect."
                )
            if _confirmed_absent:
                logger.info(
                    f"[Absent] Confirmed absent (in roster, now excluded): "
                    f"{sorted(_confirmed_absent)}"
                )
        else:
            _unknown_absent  = set()
            _confirmed_absent = set()

        # 3. Log summary after all data is in hand
        mode_str = "reshuffle" if do_reshuffle else "normal"
        logger.info(
            f"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
            f"[API HIT] POST /api/v1/optimization/riderassign\n"
            f"[CONFIG]  Mode: {mode_str.upper()} | Active Riders: {len(riders)}\n"
            f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        )
        logger.info(f"[API] riderassign ▶ orders={len(orders)} riders={len(riders)} mode={mode_str}")

        if not orders:
            return {
                "code": 200,
                "details": {},
                "message": "No orders found to assign.",
                "status": True,
                "meta": {"active_riders_count": len(riders)},
            }

        _faiss_corrected = 0
        _faiss_store = None

        # 2b. FAISS coordinate verification — correct noisy delivery coords
        #      using historically verified rider delivery positions.
        try:
            from app.services.vector.faiss_customer_store import get_faiss_store
            _faiss_store = get_faiss_store()
            _faiss_corrected = 0

            for _order in orders:
                # Resolve phone number across common field name variants
                _phone = (
                    _order.get("customerphonenumber")
                    or _order.get("customerphone")
                    or _order.get("phonenumber")
                    or _order.get("mobile")
                    or _order.get("phone")
                    or _order.get("customer_phone")
                )
                _name = (
                    _order.get("customername")
                    or _order.get("customer_name")
                    or _order.get("deliveryname")
                    or ""
                )
                try:
                    _raw_lat = float(
                        _order.get("deliverylat") or _order.get("droplat") or 0
                    )
                    _raw_lon = float(
                        _order.get("deliverylong") or _order.get("droplon") or 0
                    )
                except (ValueError, TypeError):
                    _raw_lat, _raw_lon = 0.0, 0.0

                if not _phone or _raw_lat == 0.0 or _raw_lon == 0.0:
                    continue

                _v_lat, _v_lon, _corrected = _faiss_store.get_verified_coords(
                    _phone, _name, _raw_lat, _raw_lon
                )

                if _corrected:
                    # Replace delivery coords with verified historical coords
                    _order["deliverylat"] = _v_lat
                    _order["deliverylong"] = _v_lon
                    if "droplat" in _order:
                        _order["droplat"] = _v_lat
                    if "droplon" in _order:
                        _order["droplon"] = _v_lon
                    _order["_coord_source"] = "faiss_verified"
                    _faiss_corrected += 1
                else:
                    _order["_coord_source"] = "input"

            logger.info(
                f"[FAISS] Verified {_faiss_corrected} out of {len(orders)} orders "
                f"using historical coordinates (from {_faiss_store.record_count()} stored records)."
            )
        except Exception as _fe:
            logger.warning(f"[FAISS] Coord verification failed (non-fatal): {_fe}")

        # ── PHASE 0: FAISS DELIVERY HISTORY — pre-assign well-known routes ────
        # For each order look up the rider who covered that kitchen → delivery
        # area most in the last 30-day CSV.  Only pre-assigns when:
        #   (a) that rider wins ≥ 50 % of filtered historical neighbours
        #   (b) that rider is currently active
        #   (c) that rider is not blocked
        # Remaining orders go to the VRP solver as normal.
        history_assignments: dict = {}
        history_pre_assigned_ids: set = set()
        _history_hits = 0

        _KITCHEN_KEYS_PH0 = [
            "pickupcustomer", "locationname", "storename", "store_name",
            "restaurantname", "restaurant_name", "kitchenname", "kitchen_name",
            "partnername", "partner_name", "tenantname",
        ]

        try:
            from app.services.vector.delivery_history_store import get_delivery_history_store
            from app.config.rider_preferences import BLOCKED_RIDERS as _BLOCKED_H

            _hist_store = get_delivery_history_store()

            if _hist_store.record_count() > 0:
                # Build set of active, non-blocked, non-absent rider IDs for validation
                _active_rider_ids: set = set()
                for _r in riders:
                    try:
                        _rid0 = int(_r.get("userid") or _r.get("riderid") or _r.get("id") or 0)
                        if _rid0 and _rid0 not in _BLOCKED_H and _rid0 not in absent_rider_ids:
                            _active_rider_ids.add(_rid0)
                    except (ValueError, TypeError):
                        pass

                for _order in orders:
                    # Extract kitchen name
                    _kitchen_h = ""
                    for _kk in _KITCHEN_KEYS_PH0:
                        _kv = _order.get(_kk)
                        if _kv and str(_kv).strip():
                            _kitchen_h = str(_kv).strip()
                            break

                    try:
                        _h_plat = float(_order.get("pickuplat") or 0)
                        _h_plon = float(_order.get("pickuplon") or _order.get("pickuplong") or 0)
                        _h_dlat = float(_order.get("deliverylat") or _order.get("droplat") or 0)
                        _h_dlon = float(_order.get("deliverylong") or _order.get("droplon") or 0)
                    except (TypeError, ValueError):
                        continue

                    if not _h_dlat:
                        continue  # no delivery coords, skip

                    _hit = _hist_store.find_rider(
                        _kitchen_h, _h_plat, _h_plon, _h_dlat, _h_dlon
                    )
                    if _hit and _hit["userid"] in _active_rider_ids:
                        _h_rid = _hit["userid"]
                        history_assignments.setdefault(_h_rid, []).append(_order)
                        history_pre_assigned_ids.add(id(_order))
                        _history_hits += 1

                logger.info(
                    f"[FAISS History] Pre-assigned {_history_hits}/{len(orders)} orders "
                    f"from delivery history ({_hist_store.record_count()} records). "
                    f"Riders: {list(history_assignments.keys())}"
                )
            else:
                logger.info("[FAISS History] Store empty — skipping history pre-assignment.")
        except Exception as _he:
            logger.warning(f"[FAISS History] Pre-assignment failed (non-fatal): {_he}")

        # Orders not covered by FAISS history go to the VRP / 2-phase solver.
        vrp_orders = [o for o in orders if id(o) not in history_pre_assigned_ids]

        # 3. Run Assignment (AssignmentService)
        from app.config.dynamic_config import get_config

        _cfg = get_config()

        optimizer = RouteOptimizer()

        # ── PHASE 3a: TRUE VRP (primary solver) ──────────────────────────────
        # Attempt to solve assignment + routing simultaneously with OR-Tools VRP.
        # This is globally optimal — one model for all riders and all orders.
        # Falls back to 2-phase if OR-Tools unavailable or solver finds no solution.
        # ─────────────────────────────────────────────────────────────────────
        vrp_assignments: dict = {}
        unassigned_orders: list = []
        used_vrp = False

        if not do_reshuffle:  # VRP not meaningful during reshuffle (intentional exploration)
            try:
                # Build minimal rider info for VRP
                from app.services.routing.gps_smoother import smooth_rider_locations
                _smooth_riders = smooth_rider_locations(list(riders))

                from app.services.core.assignment_service import AssignmentService as _AS
                _svc_tmp = _AS()

                # Import the single source-of-truth blocked-rider set.
                from app.config.rider_preferences import BLOCKED_RIDERS as _BLOCKED_RIDERS

                rider_data_for_vrp = []
                for r in _smooth_riders:
                    rid_raw = r.get("userid") or r.get("riderid") or r.get("id")
                    try:
                        rid = int(rid_raw)
                    except (ValueError, TypeError):
                        continue
                    if rid in _BLOCKED_RIDERS or rid in absent_rider_ids:
                        continue
                    lat, lon = _svc_tmp.get_lat_lon(r)
                    if lat == 0 or lon == 0:
                        continue
                    rider_data_for_vrp.append({"id": rid, "lat": lat, "lon": lon})

                if rider_data_for_vrp:
                    import math as _math
                    # Hard cap at 12 orders/rider regardless of what ML tunes.
                    # This gives ceil(35/12) = 3 riders for a typical 35-order load,
                    # which matches the real-world target for this fleet.
                    # Preference discounts (below) steer each of those 3 riders
                    # toward their own kitchen's orders — so cap=12 is safe.
                    _max_opr = int(_cfg.get("max_orders_per_rider", 12))
                    _vrp_cap = min(_max_opr, 12)   # never let ML push this above 12

                    logger.info(
                        f"[VRP] Capacity: {_vrp_cap}/rider | "
                        f"{len(vrp_orders)} orders (of {len(orders)} total, "
                        f"{_history_hits} pre-assigned by FAISS history) / "
                        f"{len(rider_data_for_vrp)} riders → "
                        f"expect ≈{_math.ceil(max(1, len(vrp_orders)) / _vrp_cap)} active riders"
                    )

                    vrp_result = optimizer.solve_vrp_multi_rider(
                        orders=vrp_orders,   # excludes Phase-0 pre-assigned orders
                        rider_data=rider_data_for_vrp,
                        max_orders_per_rider=_vrp_cap,
                        soft_prefs=_svc_tmp.soft_preferences,
                        soft_home=_svc_tmp.home_locations,
                    )
                    if vrp_result:
                        vrp_assignments = vrp_result
                        # Orders not in any VRP route AND not in Phase-0 → unassigned
                        _vrp_assigned_ids = {
                            id(o) for rider_ords in vrp_assignments.values() for o in rider_ords
                        }
                        unassigned_orders = [
                            o for o in orders
                            if id(o) not in _vrp_assigned_ids
                            and id(o) not in history_pre_assigned_ids
                        ]
                        used_vrp = True
                        # Removed logging from here, moved to after both solvers run
            except Exception as _vrp_err:
                logger.warning(f"[VRP] Primary solver skipped: {_vrp_err}")

        # ── PHASE 3b: 2-PHASE FALLBACK (cluster → score → assign) ────────────
        if not used_vrp:
            service = AssignmentService()
            _available_riders = [
                r for r in riders
                if int(r.get("userid") or r.get("riderid") or r.get("id") or 0)
                not in absent_rider_ids
            ] if absent_rider_ids else riders
            vrp_assignments, _fb_unassigned = await service.assign_orders(
                riders=_available_riders,
                orders=vrp_orders,   # excludes Phase-0 pre-assigned orders
                fuel_charge=fuel_charge,
                base_pay=base_pay,
                reshuffle=do_reshuffle,
            )
            # Unassigned = 2-phase leftovers (history orders are always assigned)
            unassigned_orders = list(_fb_unassigned)

        # Merge Phase-0 FAISS history pre-assignments into VRP/fallback result
        for _h_rid, _h_ords in history_assignments.items():
            if _h_rid in vrp_assignments:
                # Prepend history orders (rider knows these routes best)
                vrp_assignments[_h_rid] = list(_h_ords) + vrp_assignments[_h_rid]
            else:
                vrp_assignments[_h_rid] = list(_h_ords)

        assignments = vrp_assignments

        # ── SOLO RIDER CONSOLIDATION ─────────────────────────────────────────
        # A rider deployed for a single order is almost always a net loss.
        # Rule: if a rider has exactly 1 order and another rider is already
        # running a route from the same (or nearby) kitchen, transfer the
        # order there instead of deploying a solo trip.
        #
        # Priority for matching a host:
        #   0 → same pickuplocationid   (same kitchen counter, zero extra pickup)
        #   1 → same kitchen name       (logical same kitchen, different ID edge-case)
        #   2 → pickup within 1.5 km   (very close kitchen cluster)
        #
        # Among equal-priority hosts: pick the one whose delivery centroid is
        # nearest to the solo order's drop point (minimises route extension).
        #
        # Pass 1: merge solos into multi-order riders (≥ 2 orders).
        # Pass 2: merge remaining solos with each other (same kitchen only).
        #
        # Hard cap: never pile > 10 orders onto one host.
        # Safety: if no suitable host found, keep the solo assignment as-is
        #         (unassigned is worse than a solo trip).
        # ─────────────────────────────────────────────────────────────────────
        _SOLO_MAX_PICKUP_KM = 1.5
        _SOLO_ORDER_CAP     = 10
        _consolidation_moves = 0

        def _ord_pickup_locid(o: dict) -> int:
            try:   return int(o.get("pickuplocationid") or 0)
            except: return 0

        def _ord_kitchen(o: dict) -> str:
            for _k in ("pickupcustomer", "locationname", "tenantname"):
                _v = o.get(_k, "")
                if _v: return str(_v).strip().lower()
            return ""

        def _ord_pickup_coords(o: dict):
            try:
                return (float(o.get("pickuplat") or 0),
                        float(o.get("pickuplon") or o.get("pickuplong") or 0))
            except: return (0.0, 0.0)

        def _delivery_centroid(ords: list):
            lats = [float(o.get("deliverylat") or o.get("droplat") or 0) for o in ords]
            lons = [float(o.get("deliverylong") or o.get("droplon") or 0) for o in ords]
            lats = [x for x in lats if x]; lons = [x for x in lons if x]
            if not lats: return (0.0, 0.0)
            return (sum(lats) / len(lats), sum(lons) / len(lons))

        # Rider efficiency scores (from 30-day CSV history) used as tiebreaker
        _rider_efficiency: dict = {}
        try:
            from app.services.vector.delivery_history_store import get_delivery_history_store as _gds
            _rider_efficiency = _gds().get_rider_efficiency_scores()
        except Exception:
            pass

        def _best_host(solo_order: dict, host_pool: dict,
                        min_host_orders: int) -> "tuple | None":
            """Return (host_rider_id, score) or None if no match.
            Score tuple: (priority, drop_dist, -efficiency)
              lower priority = better kitchen match
              lower drop_dist = less route extension
              higher efficiency = prefer that rider as tiebreaker
            """
            _s_locid = _ord_pickup_locid(solo_order)
            _s_kname = _ord_kitchen(solo_order)
            _s_plat, _s_plon = _ord_pickup_coords(solo_order)
            try:
                _s_dlat = float(solo_order.get("deliverylat") or solo_order.get("droplat") or 0)
                _s_dlon = float(solo_order.get("deliverylong") or solo_order.get("droplon") or 0)
            except: _s_dlat = _s_dlon = 0.0

            _best_rid, _best_score = None, None
            for _hrid, _hords in host_pool.items():
                if len(_hords) < min_host_orders or len(_hords) >= _SOLO_ORDER_CAP:
                    continue
                _h0 = _hords[0]
                _h_locid = _ord_pickup_locid(_h0)
                _h_kname = _ord_kitchen(_h0)
                _h_plat, _h_plon = _ord_pickup_coords(_h0)
                _pickup_dist = (
                    _haversine_km(_s_plat, _s_plon, _h_plat, _h_plon)
                    if _s_plat and _h_plat else 999.0
                )
                if   _s_locid and _h_locid and _s_locid == _h_locid: _pri = 0
                elif _s_kname and _h_kname and _s_kname == _h_kname:  _pri = 1
                elif _pickup_dist <= _SOLO_MAX_PICKUP_KM:              _pri = 2
                else: continue

                _centroid = _delivery_centroid(_hords)
                _drop_dist = (
                    _haversine_km(_s_dlat, _s_dlon, _centroid[0], _centroid[1])
                    if _s_dlat and _centroid[0] else 999.0
                )
                # Efficiency tiebreaker: higher score = better host (negate for min-sort)
                _eff = _rider_efficiency.get(_hrid, {}).get("efficiency_score", 0.5)
                _score = (_pri, round(_drop_dist, 2), round(1.0 - _eff, 4))
                if _best_score is None or _score < _best_score:
                    _best_score = _score; _best_rid = _hrid
            return (_best_rid, _best_score) if _best_rid is not None else None

        # --- Pass 1: solo → multi-order host (≥ 2 existing orders) ----------
        _solo_rids   = [rid for rid, ords in assignments.items() if len(ords) == 1]
        _active_pool = {rid: ords for rid, ords in assignments.items() if len(ords) >= 2}

        for _srid in _solo_rids:
            if not assignments.get(_srid): continue   # already cleared
            _solo_ord = assignments[_srid][0]
            _match = _best_host(_solo_ord, _active_pool, min_host_orders=2)
            if _match:
                _hrid, _hscore = _match
                _active_pool[_hrid].append(_solo_ord)
                assignments[_hrid] = _active_pool[_hrid]
                assignments[_srid]  = []
                _consolidation_moves += 1
                logger.info(
                    f"[Consolidate] Rider {_srid} (1 order) → merged into rider "
                    f"{_hrid} (now {len(_active_pool[_hrid])} orders, "
                    f"kitchen-priority={_hscore[0]}, drop-dist={_hscore[1]:.1f}km)"
                )

        # --- Pass 2: remaining solos → each other (same kitchen only) --------
        _still_solo = [rid for rid in _solo_rids if len(assignments.get(rid, [])) == 1]
        _solo_pool   = {rid: assignments[rid] for rid in _still_solo}

        _merged_in_p2: set = set()
        for _srid in _still_solo:
            if _srid in _merged_in_p2: continue
            _solo_ord = assignments[_srid][0]
            # Only same-kitchen merges in pass 2 (pickup_locid or name match)
            _candidates = {
                rid: ords for rid, ords in _solo_pool.items()
                if rid != _srid and rid not in _merged_in_p2
            }
            _match = _best_host(_solo_ord, _candidates, min_host_orders=1)
            if _match:
                _hrid, _hscore = _match
                if _hscore[0] <= 1:   # only locid/name matches in pass 2
                    assignments[_hrid].append(_solo_ord)
                    assignments[_srid] = []
                    _merged_in_p2.add(_srid)
                    _consolidation_moves += 1
                    logger.info(
                        f"[Consolidate-P2] Solo {_srid} merged into solo "
                        f"{_hrid} (now {len(assignments[_hrid])} orders, "
                        f"kitchen-priority={_hscore[0]})"
                    )

        if _consolidation_moves:
            logger.info(
                f"[Consolidate] {_consolidation_moves} solo order(s) merged into "
                f"existing routes — {len([r for r in _solo_rids if not assignments.get(r)])} "
                f"rider(s) freed up."
            )

        # Log outcome for whichever solver was used
        assigned_count = sum(len(v) for v in assignments.values())
        solver_name = "VRP Optimal" if used_vrp else "2-Phase Heuristic"
        logger.info(
            f"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
            f"[SUMMARY]  Solver Used: {solver_name}\n"
            f"[OUTCOME]  Assigned {assigned_count} out of {len(orders)} orders.\n"
            f"[RIDERS]   Utilized {len([v for v in assignments.values() if v])} out of {len(riders)} active riders.\n"
            f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        )

        # No explicit restore needed — contextvars are scoped to this async task.
        # When the task ends the ContextVar value is automatically discarded.

        if do_reshuffle:
            logger.info(
                "[RESHUFFLE] Retry mode active - exploring alternative rider assignments."
            )

        # 4. Optimize Routes for Each Rider and Flatten Response
        # (OR-Tools TSP + 2-opt per rider, now with time windows if pickupSlot present)
        flat_orders_list = []

        # Build an ordered list of (rider_id, orders) pairs so we can align
        # task_contexts with results after the parallel gather.
        task_items = [
            (rider_id, rider_orders)
            for rider_id, rider_orders in assignments.items()
            if rider_orders
        ]
        task_contexts = [rider_id for rider_id, _ in task_items]

        total_assigned = 0

        if task_items:
            # ── TRUE PARALLEL TSP via ThreadPoolExecutor ─────────────────────
            # OR-Tools (C++ extension) releases the GIL during solving, so
            # threads genuinely run on separate CPU cores at the same time.
            # asyncio.get_running_loop().run_in_executor schedules each worker
            # on the thread pool and yields control to the event loop while
            # the threads compute, keeping FastAPI fully responsive.
            loop = asyncio.get_running_loop()
            results = await asyncio.gather(*[
                loop.run_in_executor(
                    _route_executor,
                    _sync_optimize_route,
                    rider_orders,
                )
                for _, rider_orders in task_items
            ])

            # Create a lookup for rider details
            rider_info_map = {}
            for r in riders:
                # Use string conversion for robust ID matching
                r_id = str(r.get("userid") or r.get("_id", ""))
                if r_id:
                    rider_info_map[r_id] = {
                        "name": r.get("username", ""),
                        "contactno": r.get("contactno", ""),
                    }

            # Process results matching them back to riders
            for stored_rider_id, optimized_route in zip(task_contexts, results):
                r_id_str = str(stored_rider_id)
                r_info = rider_info_map.get(r_id_str, {})
                rider_name = r_info.get("name", "")
                rider_contact = r_info.get("contactno", "")

                # Calculate total distance for this rider
                total_rider_kms = 0
                if optimized_route:
                    # Usually the last order has the max cumulative kms if steps are 1..N
                    try:
                        total_rider_kms = max(
                            [float(o.get("cumulativekms", 0)) for o in optimized_route]
                        )
                    except:
                        total_rider_kms = sum(
                            [
                                float(o.get("actualkms", o.get("kms", 0)))
                                for o in optimized_route
                            ]
                        )

                for order in optimized_route:
                    order["userid"] = stored_rider_id
                    order["username"] = rider_name
                    # Populate the specific fields requested by the user
                    order["rider"] = rider_name
                    order["ridercontactno"] = rider_contact
                    order["riderkms"] = str(round(total_rider_kms, 2))

                    # --- DYNAMIC ETA COMPUTATION -----------------------------
                    # Try various cases and names for pickup slot
                    pickup_slot_str = (
                        order.get("pickupSlot")
                        or order.get("pickupslot")
                        or order.get("pickup_slot")
                        or order.get("pickuptime")
                    )

                    if pickup_slot_str:
                        try:
                            # Robust date parsing (handles almost any format magically)
                            pickup_time = parse_date(str(pickup_slot_str))

                            # Use cumulative_eta (total time from kitchen → this stop)
                            # if the route optimizer produced it; fall back to
                            # a fresh per-leg calculation using cumulativekms.
                            if order.get("cumulative_eta"):
                                eta_mins = int(order["cumulative_eta"])
                            else:
                                dist_km = float(
                                    order.get("cumulativekms")
                                    or order.get("actualkms", order.get("kms", 0))
                                )
                                step = int(order.get("step", 1))
                                order_type = order.get("ordertype", "Economy")
                                from app.services.routing.realistic_eta_calculator import get_time_of_day_category
                                _dcoords = None
                                try:
                                    _dlat = float(order.get("deliverylat") or order.get("droplat") or 0)
                                    _dlon = float(order.get("deliverylong") or order.get("droplon") or 0)
                                    if _dlat and _dlon:
                                        _dcoords = (_dlat, _dlon)
                                except (TypeError, ValueError):
                                    _dcoords = None
                                eta_mins = eta_calculator.calculate_eta(
                                    distance_km=dist_km,
                                    is_first_order=(step == 1),
                                    order_type=order_type,
                                    time_of_day=get_time_of_day_category(),
                                    kitchen=order.get("pickupcustomer") or order.get("locationname"),
                                    drop_coords=_dcoords,
                                    rider_id=order.get("userid"),
                                )

                            expected_time = pickup_time + timedelta(minutes=eta_mins)

                            # Format output as requested: "2026-03-24 08:25 AM"
                            order["expectedDeliveryTime"] = expected_time.strftime(
                                "%Y-%m-%d %I:%M %p"
                            )
                            order["transitMinutes"] = eta_mins
                            order["calculationDistanceKm"] = round(
                                float(order.get("cumulativekms") or order.get("actualkms", 0)), 2
                            )
                        except Exception as e:
                            logger.warning(
                                f"Could not calculate ETA from pickupSlot '{pickup_slot_str}': {e}"
                            )
                    # ---------------------------------------------------------

                    flat_orders_list.append(order)
                total_assigned += len(optimized_route)

        # The ID3 "risk" tree was retired (it never affected assignment). Key kept
        # static so existing response consumers don't break.
        risk_meta: dict = {"label": "n/a", "model_trained": False, "deprecated": True}

        # ── BACKGROUND ML LOGGING ────────────────────────────────────────────
        # Fire-and-forget: log this assignment event to SQLite.
        # Uses _ml_executor so the API response is never delayed.
        try:
            _elapsed_ms = (time.time() - _t0) * 1000
            _hyp_snapshot = get_config().get_all()   # frozen copy for this call
            _ml_executor.submit(
                _bg_log_assignment,
                len(orders),
                len(riders),
                _hyp_snapshot,
                {rid: list(ords) for rid, ords in assignments.items()},
                len(unassigned_orders),
                round(_elapsed_ms, 1),
            )
        except Exception as _mle:
            logger.debug(f"[ML BG] Submit failed (non-fatal): {_mle}")

        # ── Compact exit trace (grep one request_id to see the whole request) ──
        try:
            from app.services.routing.delivery_history_service import get_delivery_history_service
            _road_on = bool(get_config().get("routing_use_road_distance", False))
            _eta_src = (
                "empirical"
                if (get_config().get("eta_empirical_enabled", True)
                    and get_delivery_history_service().has_data())
                else "formula"
            )
            logger.info(
                f"[API] riderassign ◀ solver={'vrp_optimal' if used_vrp else '2phase'} "
                f"assigned={total_assigned}/{len(orders)} unassigned={len(unassigned_orders)} "
                f"riders_used={len([v for v in assignments.values() if v])}/{len(riders)} "
                f"road_seq={'on' if _road_on else 'off'} eta={_eta_src} "
                f"elapsed={round(_elapsed_ms)}ms"
            )
        except Exception:
            pass

        # 5. Zone Processing
        from app.services.routing.zone_service import ZoneService

        zone_service = ZoneService()
        zone_data = zone_service.group_by_zones(
            flat_orders_list,
            unassigned_orders,
            fuel_charge=fuel_charge,
            base_pay=base_pay,
        )

        zones_structure = zone_data["detailed_zones"]
        zone_analysis = zone_data["zone_analysis"]

        return {
            "code": 200,
            "zone_summary": zone_analysis,  # High-level zone metrics
            "zones": zones_structure,  # Detailed data
            "details": flat_orders_list,  # Flat list
            "message": "Success",
            "status": True,
            "meta": {
                "total_orders": len(orders),
                "utilized_riders": len([rid for rid, rl in assignments.items() if rl]),
                "active_riders_pool": len(riders),
                "assigned_orders": total_assigned,
                "unassigned_orders": len(unassigned_orders),
                "total_profit": round(sum(z["total_profit"] for z in zone_analysis), 2),
                "fuel_charge_base": fuel_charge,
                "unassigned_details": [
                    {
                        "orderid": o.get("orderid") or o.get("_id"),
                        "reason": o.get(
                            "unassigned_reason", "Unknown capacity/proximity issue"
                        ),
                    }
                    for o in unassigned_orders
                ],
                "distribution_summary": {
                    rid: len(rl) for rid, rl in assignments.items() if rl
                },
                "reshuffle_mode": do_reshuffle,
                "solver_mode": "vrp_optimal" if used_vrp else "2phase_heuristic",
                "faiss_coord_corrections": _faiss_corrected,
                "faiss_customer_records": _faiss_store.record_count() if _faiss_store else 0,
                "faiss_history_preassigned": _history_hits,
                "solo_consolidations": _consolidation_moves,
                "absent_riders_excluded": sorted(_confirmed_absent),
                "absent_riders_unknown":  sorted(_unknown_absent),
                "risk_assessment": risk_meta,
            },
        }

    except Exception as e:
        logger.error(f"Error in rider assignment: {e}", exc_info=True)
        raise HTTPException(
            status_code=500, detail="Internal server error during assignment"
        )

