"""
Rider Affinity Service (learned, agentic)
=========================================

Learns each rider's real kitchen affinity and operating area from the local
delivery mirror (`delivery_raw`) and exposes them MERGED with the curated config
in `app/config/rider_preferences.py`:

  * get_preferred_kitchens() = curated RIDER_PREFERRED_KITCHENS  ∪  learned
    (rider served a kitchen >= `rider_affinity_min_deliveries` times). Union only —
    never drops a curated owner.
  * get_home_locations()     = curated RIDER_HOME_LOCATIONS, with a learned drop
    centroid filled in ONLY for riders missing from the config.

These feed SOFT steering (preference discount / home bonus / distance bypass) in
the optimizer + assignment service. HARD kitchen locks and BLOCKED_RIDERS remain
sourced from the curated config — learned data can only broaden preference, never
change who is *eligible* for a kitchen. Recomputed by the autonomous agent loop.
"""

import logging
import threading
from collections import defaultdict
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple

from app.config.rider_preferences import RIDER_PREFERRED_KITCHENS, RIDER_HOME_LOCATIONS

logger = logging.getLogger(__name__)


def _to_int_rid(v: Any) -> Optional[int]:
    try:
        return int(v)
    except (TypeError, ValueError):
        return None


class RiderAffinityService:
    def __init__(self):
        self._learned_pref: Dict[int, List[str]] = {}
        self._learned_home: Dict[int, Tuple[float, float]] = {}
        self._last_refreshed: Optional[datetime] = None
        self._lock = threading.Lock()

    # ------------------------------------------------------------------

    def refresh(self, days: Optional[int] = None) -> Dict[str, Any]:
        """Recompute learned affinity from the local delivery mirror (no DB hit)."""
        from app.config.dynamic_config import get_config
        from app.services.routing.delivery_history_service import (
            get_delivery_history_service, normalize_kitchen,
        )
        cfg = get_config()
        window = int(days if days is not None else cfg.get("eta_history_days", 14))
        min_n = int(cfg.get("rider_affinity_min_deliveries", 10))

        rows = get_delivery_history_service()._load_raw_rows(window)
        counts: Dict[int, Dict[str, int]] = defaultdict(lambda: defaultdict(int))
        coords: Dict[int, List[Tuple[float, float]]] = defaultdict(list)
        for r in rows:
            rid = _to_int_rid(r.get("userid"))
            if rid is None:
                continue
            kitchen = normalize_kitchen(r.get("pickupcustomer"))
            if kitchen and kitchen != "?":
                counts[rid][kitchen] += 1
            try:
                la, lo = float(r.get("dlat")), float(r.get("dlon"))
                if la and lo:
                    coords[rid].append((la, lo))
            except (TypeError, ValueError):
                continue

        learned_pref = {
            rid: [k for k, n in kc.items() if n >= min_n]
            for rid, kc in counts.items()
        }
        learned_pref = {rid: ks for rid, ks in learned_pref.items() if ks}
        learned_home = {
            rid: (round(sum(p[0] for p in pts) / len(pts), 6),
                  round(sum(p[1] for p in pts) / len(pts), 6))
            for rid, pts in coords.items() if pts
        }

        with self._lock:
            self._learned_pref = learned_pref
            self._learned_home = learned_home
            self._last_refreshed = datetime.utcnow()

        summary = {
            "status": "ok",
            "riders_with_learned_kitchens": len(learned_pref),
            "riders_with_learned_home": len(learned_home),
            "min_deliveries": min_n,
            "window_days": window,
            "refreshed_at": self._last_refreshed.isoformat(),
        }
        logger.info(
            f"[Affinity] learned kitchens for {len(learned_pref)} riders, "
            f"home for {len(learned_home)} (min_deliveries={min_n})"
        )
        return summary

    def _ensure_loaded(self) -> None:
        if self._last_refreshed is None:
            try:
                self.refresh()
            except Exception as e:
                logger.debug(f"[Affinity] lazy refresh failed: {e}")

    # ------------------------------------------------------------------

    def get_preferred_kitchens(self) -> Dict[int, List[str]]:
        """Curated config ∪ learned (union — never drops a curated owner)."""
        from app.config.dynamic_config import get_config
        if not bool(get_config().get("rider_affinity_enabled", True)):
            return {rid: list(v) for rid, v in RIDER_PREFERRED_KITCHENS.items()}
        self._ensure_loaded()
        merged: Dict[int, List[str]] = {rid: list(v) for rid, v in RIDER_PREFERRED_KITCHENS.items()}
        with self._lock:
            learned = {rid: list(v) for rid, v in self._learned_pref.items()}
        for rid, kitchens in learned.items():
            base = merged.setdefault(rid, [])
            base_lower = {b.lower() for b in base}
            for k in kitchens:
                if k.lower() not in base_lower:
                    base.append(k)
        return merged

    def get_home_locations(self) -> Dict[int, Tuple[float, float]]:
        """Curated home primary; learned drop centroid only for riders absent from config."""
        from app.config.dynamic_config import get_config
        if not bool(get_config().get("rider_affinity_enabled", True)):
            return dict(RIDER_HOME_LOCATIONS)
        self._ensure_loaded()
        merged: Dict[int, Tuple[float, float]] = dict(RIDER_HOME_LOCATIONS)
        with self._lock:
            learned = dict(self._learned_home)
        for rid, home in learned.items():
            if rid not in merged or merged.get(rid) in (None, (0.0, 0.0)):
                merged[rid] = home
        return merged

    def get_summary(self) -> Dict[str, Any]:
        """Learned-vs-config diff for the admin endpoint."""
        self._ensure_loaded()
        with self._lock:
            learned_pref = {rid: list(v) for rid, v in self._learned_pref.items()}
            learned_home = dict(self._learned_home)
        config_riders = set(RIDER_PREFERRED_KITCHENS)
        new_pref_riders = sorted(set(learned_pref) - config_riders)
        return {
            "last_refreshed": self._last_refreshed.isoformat() if self._last_refreshed else None,
            "config_preferred_riders": sorted(config_riders),
            "learned_preferred": {str(k): v for k, v in sorted(learned_pref.items())},
            "riders_newly_learned_not_in_config": new_pref_riders,
            "learned_home_count": len(learned_home),
        }


_affinity: Optional[RiderAffinityService] = None
_affinity_lock = threading.Lock()


def get_rider_affinity() -> RiderAffinityService:
    global _affinity
    with _affinity_lock:
        if _affinity is None:
            _affinity = RiderAffinityService()
        return _affinity
