"""
Area Utilization post-processing usecase.

Policy-correct behavior:
- If user provides zones -> compute zone-wise metrics
- If user does NOT provide zones -> treat full camera frame as ONE zone named "global"

Primary metric (capacity-based):
    occupancy_percent = (people_count / capacity) * 100

Time-window metrics (rolling window in seconds):
- time_occupied_percent
- avg_occupancy_percent

Membership rule:
- By default uses bbox CENTER point for zone membership
- For full-frame zone, all detections are considered in-zone
"""

from __future__ import annotations

import time
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple

from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ProcessingStatus
from ..core.config import BaseConfig, ZoneConfig, AlertConfig

from ..utils.filter_utils import apply_category_mapping, filter_by_confidence
from ..utils.geometry_utils import point_in_polygon


TARGET_CATEGORY = "person" # only checking for people

WARN_NO_ZONES = "no_zones_configured_full_frame_used"
WARN_ZONE_TOO_BIG = "zone_exceeds_stream_resolution"
WARN_INVALID_BBOX = "invalid_bbox_detected"
WARN_MISSING_STREAM_RESOLUTION = "missing_stream_resolution"

DEFAULT_CAPACITY = 10
DEFAULT_WINDOW_SECONDS = 300  # 5 minutes rolling window

def _clip(x: float, lo: float, hi: float) -> float:
    return max(lo, min(hi, x))


def _bbox_center(bbox: Any) -> Tuple[float, float]:
    """
    bbox formats supported:
    - dict xmin,ymin,xmax,ymax
    - dict x1,y1,x2,y2
    - list [x1,y1,x2,y2]
    """
    if not bbox:
        return (0.0, 0.0)

    if isinstance(bbox, (list, tuple)) and len(bbox) >= 4:
        x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3]
        return ((float(x1) + float(x2)) / 2.0, (float(y1) + float(y2)) / 2.0)

    if isinstance(bbox, dict):
        if "xmin" in bbox:
            return (
                (float(bbox.get("xmin", 0)) + float(bbox.get("xmax", 0))) / 2.0,
                (float(bbox.get("ymin", 0)) + float(bbox.get("ymax", 0))) / 2.0,
            )
        if "x1" in bbox:
            return (
                (float(bbox.get("x1", 0)) + float(bbox.get("x2", 0))) / 2.0,
                (float(bbox.get("y1", 0)) + float(bbox.get("y2", 0))) / 2.0,
            )

    return (0.0, 0.0)

# Ensure bbox exists under det['bounding_box'] if only det['bbox'] exists.
def _normalize_detection_bbox(det: Dict[str, Any]) -> Dict[str, Any]:
    d = det.copy()
    if "bounding_box" not in d and "bbox" in d:
        d["bounding_box"] = d["bbox"]
    return d

# Warn if user polygon appears outside stream resolution
def _zone_exceeds_stream_resolution(zone_poly: List[List[float]], stream_res: Dict[str, Any]) -> bool:
    w = stream_res.get("width", 0) or 0
    h = stream_res.get("height", 0) or 0
    if not w or not h:
        return False

    for p in zone_poly:
        if len(p) >= 2 and (p[0] > w or p[1] > h):
            return True
    return False

# Categorization of state buckets - vacant, low, moderate, high, overcrowded
def _occupancy_state(percent: float) -> str:
    """
    State buckets policy:
    - vacant: 0
    - low: (0, 30]
    - moderate: (30, 70]
    - high: (70, 100]
    - overcrowded: >100
    """
    if percent <= 0:
        return "vacant"
    if percent <= 30:
        return "low"
    if percent <= 70:
        return "moderate"
    if percent <= 100:
        return "high"
    return "overcrowded"


# config
@dataclass
class AreaUtilizationConfig(BaseConfig):
    """
    Zone behavior:
      - If zone_config provided -> zone-wise analytics
      - Else -> full-frame global zone

    Capacity behavior:
      zone_capacities is in extra_params:
        extra_params = {
          "zone_capacities": {"global": 10, "meeting_room": 6},
          "window_seconds": 300,
        }
    """

    # category mapping
    index_to_category: Optional[Dict[str, str]] = None

    # zone config (optional)
    zone_config: Optional[ZoneConfig] = None

    # alert config (optional)
    alert_config: Optional[AlertConfig] = None

    # by default only count persons
    target_categories: Optional[List[str]] = None

    # Use bbox center in polygon (default)
    use_center_membership: bool = True

    def validate(self) -> List[str]:
        errors = super().validate() if hasattr(super(), "validate") else []
        if self.zone_config is not None:
            errors.extend(self.zone_config.validate())
        if self.alert_config is not None:
            errors.extend(self.alert_config.validate())
        return errors


class AreaUtilizationUseCase(BaseProcessor):
    """
    Capacity + time-window based area utilization.

    Output stats per zone:
    {
      "count": int,
      "capacity": int,
      "occupancy_percent": float,
      "state": str,
      "time_occupied_percent": float,
      "avg_occupancy_percent": float
    }
    """

    def __init__(self):
        super().__init__("area_utilization")
        self.category = "general"
        

        # persistent rolling history per zone
        self._history: Dict[str, List[Tuple[float, int]]] = {}

    def create_default_config(self, **overrides) -> AreaUtilizationConfig:
        cfg = AreaUtilizationConfig(
            category="general",
            usecase="area_utilization",
            confidence_threshold=0.25,
            enable_tracking=False,
            enable_analytics=True,
            target_categories=["person"],
        )
        for k, v in overrides.items():
            setattr(cfg, k, v)
        return cfg

    def process(
        self,
        data: Any,
        config: AreaUtilizationConfig,
        context: Optional[ProcessingContext] = None,
    ) -> ProcessingResult:
        start = time.time()
        context = context or ProcessingContext()
        context.processing_start = start

        warnings: List[str] = []

        # validate config
        errors = config.validate()
        if errors:
            context.mark_completed()
            result = self.create_error_result(
                message="Invalid configuration",
                error_type="ConfigValidationError",
                usecase="area_utilization",
                category=config.category or "general",
                context=context,
            )
            return result

        # apply confidence filtering
        if config.confidence_threshold is not None:
            data = filter_by_confidence(data, threshold=config.confidence_threshold)

        # apply category mapping
        if config.index_to_category:
            data = apply_category_mapping(data, config.index_to_category)

        # default only people
        target_cats = config.target_categories or [TARGET_CATEGORY]
        data = self._filter_target_categories(data, target_cats)

        is_multiframe = self.detect_frame_structure(data)
        frame_ids = self.extract_frame_ids(data) if is_multiframe else ["0"]

        # stream resolution from context
        stream_info = (context.metadata or {}).get("stream_info", {}) if context else {}
        res = stream_info.get("stream_resolution", {}) if isinstance(stream_info, dict) else {}
        frame_w = int(res.get("width", 0) or 0)
        frame_h = int(res.get("height", 0) or 0)
        if frame_w <= 0 or frame_h <= 0:
            warnings.append(WARN_MISSING_STREAM_RESOLUTION)

        # zones
        zones = {}
        if config.zone_config and getattr(config.zone_config, "zones", None):
            zones = config.zone_config.zones
        else:
            # policy-correct: full camera frame is one zone
            zones = {"global": None}
            warnings.append(WARN_NO_ZONES)

        # check zone scale warnings
        if zones and "global" not in zones and frame_w > 0 and frame_h > 0:
            for zname, poly in zones.items():
                if poly and _zone_exceeds_stream_resolution(poly, {"width": frame_w, "height": frame_h}):
                    warnings.append(WARN_ZONE_TOO_BIG)
                    break

        # parameters from extra_params
        extra = config.extra_params or {}
        zone_capacities = extra.get("zone_capacities", {}) or {}
        window_seconds = int(extra.get("window_seconds", DEFAULT_WINDOW_SECONDS) or DEFAULT_WINDOW_SECONDS)

        # output containers per frame
        frame_incidents: Dict[str, List[Dict]] = {}
        frame_tracking_stats: Dict[str, List[Dict]] = {}
        frame_business_analytics: Dict[str, List[Dict]] = {}
        frame_alerts: Dict[str, List[Dict]] = {}
        frame_human_text: Dict[str, str] = {}

        now_ts = time.time()

        for fid in frame_ids:
            detections = self._get_frame_detections(data, fid, is_multiframe)

            zone_stats = self._compute_capacity_stats(
                detections=detections,
                zones=zones,
                capacities=zone_capacities,
                window_seconds=window_seconds,
                now_ts=now_ts,
                frame_w=frame_w,
                frame_h=frame_h,
                warnings=warnings,
                use_center_membership=config.use_center_membership,
            )

            human_text = self._build_human_text(zone_stats)

            analytics_obj = self.create_business_analytics(
                analysis_name="area_utilization",
                statistics=zone_stats,
                human_text=human_text,
                camera_info=self.get_default_camera_info(),
            )

            frame_incidents[str(fid)] = []
            frame_tracking_stats[str(fid)] = []
            frame_business_analytics[str(fid)] = [analytics_obj]
            frame_alerts[str(fid)] = []
            frame_human_text[str(fid)] = human_text

        agg_summary = self.create_frame_wise_agg_summary(
            frame_incidents=frame_incidents,
            frame_tracking_stats=frame_tracking_stats,
            frame_business_analytics=frame_business_analytics,
            frame_alerts=frame_alerts,
            frame_human_text=frame_human_text,
        )

        context.mark_completed()

        result = ProcessingResult(
            data={"agg_summary": agg_summary},
            status=ProcessingStatus.SUCCESS,
            usecase="area_utilization",
            category=config.category or "general",
            processing_time=context.processing_time or 0.0,
            context=context,
            summary="Area utilization computed successfully.",
            warnings=list(dict.fromkeys(warnings)),  # unique while preserving order
        )

        return result


    def _filter_target_categories(self, data: Any, targets: List[str]) -> Any:
        if isinstance(data, list):
            return [d for d in data if d.get("category") in targets]

        if isinstance(data, dict):
            out = {}
            for k, dets in data.items():
                if isinstance(dets, list):
                    keep = [d for d in dets if d.get("category") in targets]
                    out[k] = keep
            return out

        return data

    def _get_frame_detections(self, data: Any, frame_id: str, is_multiframe: bool) -> List[Dict[str, Any]]:
        if not is_multiframe:
            if isinstance(data, list):
                return [_normalize_detection_bbox(d) for d in data]
            return []
        if isinstance(data, dict):
            dets = data.get(frame_id) or data.get(int(frame_id), [])
            if isinstance(dets, list):
                return [_normalize_detection_bbox(d) for d in dets]
        return []

    def _compute_capacity_stats(
        self,
        detections: List[Dict[str, Any]],
        zones: Dict[str, Any],
        capacities: Dict[str, int],
        window_seconds: int,
        now_ts: float,
        frame_w: int,
        frame_h: int,
        warnings: List[str],
        use_center_membership: bool,
    ) -> Dict[str, Any]:

        stats: Dict[str, Any] = {}

        for zone_name, poly in zones.items():
            # Determine which detections belong to this zone
            if poly is None:
                # global full-frame
                in_zone = detections[:]
            else:
                in_zone = []
                if not poly or len(poly) < 3:
                    in_zone = []
                else:
                    for det in detections:
                        bbox = det.get("bounding_box") or det.get("bbox")
                        if not bbox:
                            continue
                        cx, cy = _bbox_center(bbox)
                        if use_center_membership:
                            if point_in_polygon((cx, cy), [(p[0], p[1]) for p in poly]):
                                in_zone.append(det)
                        else:
                            # fallback = still center-based (kept for future extension)
                            if point_in_polygon((cx, cy), [(p[0], p[1]) for p in poly]):
                                in_zone.append(det)

            count = len(in_zone)
            capacity = int(capacities.get(zone_name, capacities.get("global", DEFAULT_CAPACITY)) or DEFAULT_CAPACITY)

            occ_percent = 0.0
            if capacity > 0:
                occ_percent = (count / capacity) * 100.0
            occ_percent = round(occ_percent, 3)

            state = _occupancy_state(occ_percent)

            # update rolling history
            if zone_name not in self._history:
                self._history[zone_name] = []
            self._history[zone_name].append((now_ts, count))

            # prune old
            cutoff = now_ts - float(window_seconds)
            self._history[zone_name] = [(t, c) for (t, c) in self._history[zone_name] if t >= cutoff]

            # time occupied percent (count>0)
            hist = self._history[zone_name]
            if hist:
                occupied_frames = sum(1 for (_, c) in hist if c > 0)
                time_occ_percent = (occupied_frames / len(hist)) * 100.0
                avg_occ = sum(c for (_, c) in hist) / len(hist)

                avg_occ_percent = (avg_occ / capacity) * 100.0 if capacity > 0 else 0.0
            else:
                time_occ_percent = 0.0
                avg_occ_percent = 0.0

            stats[zone_name] = {
                "count": count,
                "capacity": capacity,
                "occupancy_percent": round(occ_percent, 3),
                "state": state,
                "time_occupied_percent": round(time_occ_percent, 3),
                "avg_occupancy_percent": round(avg_occ_percent, 3),
            }

        return stats

    def _build_human_text(self, zone_stats: Dict[str, Any]) -> str:
        lines = ["Area Utilization (Capacity-based):"]
        for z, s in zone_stats.items():
            lines.append(
                f"\t{z}: {s.get('occupancy_percent', 0.0)}% "
                f"({s.get('count', 0)}/{s.get('capacity', 0)}) "
                f"state={s.get('state', 'unknown')} | "
                f"time_occupied={s.get('time_occupied_percent', 0.0)}% "
                f"avg_occ={s.get('avg_occupancy_percent', 0.0)}%"
            )
        return "\n".join(lines)