"""
Contextra AI Analytics — AbstractScenario 베이스 클래스

역할:
  - 모든 시나리오 엔진의 공통 인터페이스 정의
  - trajectory buffer, cooldown 상태머신, zone/ROI 판정 제공
  - 프레임 간 상태 관리를 하위 클래스가 재사용 가능하게 추상화

설계 원칙:
  - 모델 독립적: detection 결과는 {"class_name", "confidence", "bbox"} dict만 요구
  - BoT-SORT 연동: track_id는 optional (없으면 bbox-centric 모드로 동작)
  - 쿨다운 상태머신: MONITOR → TRIGGERED → COOLDOWN → MONITOR
"""

from __future__ import annotations

import time as _time_mod
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple


logger = logging.getLogger(__name__)


# ============================================================================
# 데이터 클래스
# ============================================================================

@dataclass
class Detection:
    """단일 탐지 결과 (모델 독립적 인터페이스)"""
    track_id: Optional[int]
    class_name: str
    confidence: float
    bbox: Tuple[float, float, float, float]


@dataclass
class ZoneROI:
    """영역 정의 (polygon 또는 rectangle)"""
    name: str
    points: List[Tuple[float, float]] = field(default_factory=list)
    is_rectangle: bool = False

    def __post_init__(self):
        if len(self.points) == 4 and self.points[0][0] == self.points[2][0]:
            self.is_rectangle = True


@dataclass
class ScenarioEvent:
    """시나리오 이벤트 출력"""
    event_type: str
    severity: str
    private_key: str
    stream_id: str
    site_id: str
    timestamp_utc_ms: int
    track_id: Optional[int]
    class_name: str
    confidence: float
    bbox: Tuple[float, ...]
    detection_result: Dict[str, Any] = field(default_factory=dict)
    metadata: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "event_type": self.event_type,
            "severity": self.severity,
            "private_key": self.private_key,
            "stream_id": self.stream_id,
            "site_id": self.site_id,
            "timestamp_utc_ms": self.timestamp_utc_ms,
            "track_id": self.track_id,
            "class_name": self.class_name,
            "confidence": round(self.confidence, 4),
            "bbox": list(self.bbox),
            "detection_result": self.detection_result,
            "metadata": self.metadata,
        }


# ============================================================================
# Trajectory Buffer — BoT-SORT 연동용 궤적 관리
# ============================================================================

@dataclass
class TrajectoryPoint:
    """궤적의 한 점"""
    timestamp_ms: int
    bbox: Tuple[float, float, float, float]
    class_name: str
    confidence: float


class TrajectoryBuffer:
    def __init__(self, max_points: int = 60, max_age_seconds: float = 30.0):
        self.max_points = max_points
        self.max_age_ms = int(max_age_seconds * 1000)
        self._points: List[TrajectoryPoint] = []

    def add(self, timestamp_ms: int, bbox: Tuple[float, ...],
            class_name: str, confidence: float):
        self._points.append(TrajectoryPoint(
            timestamp_ms=timestamp_ms, bbox=bbox,
            class_name=class_name, confidence=confidence,
        ))
        while len(self._points) > self.max_points:
            self._points.pop(0)
        cutoff = timestamp_ms - self.max_age_ms
        while self._points and self._points[0].timestamp_ms < cutoff:
            self._points.pop(0)

    @property
    def points(self) -> List[TrajectoryPoint]:
        return list(self._points)

    @property
    def length(self) -> int:
        return len(self._points)

    @property
    def duration_ms(self) -> int:
        if len(self._points) < 2:
            return 0
        return self._points[-1].timestamp_ms - self._points[0].timestamp_ms

    @property
    def last_bbox(self) -> Optional[Tuple[float, ...]]:
        return self._points[-1].bbox if self._points else None

    def get_speed_and_direction(self, frames_ago: int = 30) -> Tuple[float, Tuple[float, float]]:
        if len(self._points) < frames_ago + 1:
            return 0.0, (0.0, 0.0)
        start = self._points[-(frames_ago + 1)]
        end = self._points[-1]
        sx = (start.bbox[0] + start.bbox[2]) / 2.0
        sy = (start.bbox[1] + start.bbox[3]) / 2.0
        ex = (end.bbox[0] + end.bbox[2]) / 2.0
        ey = (end.bbox[1] + end.bbox[3]) / 2.0
        dx = ex - sx
        dy = ey - sy
        displacement = (dx ** 2 + dy ** 2) ** 0.5
        dt = max(end.timestamp_ms - start.timestamp_ms, 1)
        speed = displacement / dt if dt > 0 else 0.0
        direction = (dx / displacement, dy / displacement) if displacement > 0 else (0.0, 0.0)
        return speed, direction


# ============================================================================
# Cooldown StateMachine
# ============================================================================

class CooldownState:
    STATE_MONITOR = "MONITOR"
    STATE_TRIGGERED = "TRIGGERED"
    STATE_COOLDOWN = "COOLDOWN"

    def __init__(self, cooldown_seconds: float = 30.0):
        self.cooldown_ms = int(cooldown_seconds * 1000)
        self.state = self.STATE_MONITOR
        self._triggered_at_ms: Optional[int] = None

    def check(self, now_ms: int, condition_met: bool) -> Tuple[str, bool]:
        # 2026-08-17 수정: TRIGGERED→COOLDOWN 전이가 누락되어 조건 유지 중
        # 매 프레임 재발화되던 버그 — 발화 시 COOLDOWN 진입, cooldown_ms 동안 차단.
        if self.state == self.STATE_COOLDOWN:
            elapsed = now_ms - (self._triggered_at_ms or 0)
            if elapsed >= self.cooldown_ms:
                self.state = self.STATE_MONITOR
                self._triggered_at_ms = None
            else:
                return self.STATE_COOLDOWN, False
        if condition_met and self.state == self.STATE_MONITOR:
            self.state = self.STATE_COOLDOWN
            self._triggered_at_ms = now_ms
            return self.STATE_TRIGGERED, True
        return self.state, False

    @property
    def remaining_seconds(self) -> float:
        if self.state != self.STATE_COOLDOWN or not self._triggered_at_ms:
            return 0.0
        elapsed = _time_mod.time() * 1000 - self._triggered_at_ms
        return max(0.0, (self.cooldown_ms - elapsed) / 1000.0)


# ============================================================================
# Zone/ROI Geometry Utilities
# ============================================================================

def point_in_polygon(px: float, py: float, polygon: List[Tuple[float, float]]) -> bool:
    n = len(polygon)
    inside = False
    x1, y1 = polygon[0]
    for i in range(1, n + 1):
        x2, y2 = polygon[i % n]
        if py > min(y1, y2):
            if py <= max(y1, y2):
                if px <= max(x1, x2):
                    if y1 != y2:
                        xinters = (py - y1) * (x2 - x1) / (y2 - y1) + x1
                    if p1 := x1 == x2 or px <= xinters:
                        inside = not inside
        x1, y1 = x2, y2
    return inside


def bbox_center_in_zone(bbox: Tuple[float, float, float, float], zone_points: List[Tuple[float, float]]) -> bool:
    cx = (bbox[0] + bbox[2]) / 2.0
    cy = (bbox[1] + bbox[3]) / 2.0
    return point_in_polygon(cx, cy, zone_points)


def bbox_area(bbox: Tuple[float, float, float, float]) -> float:
    return max(0.0, (bbox[2] - bbox[0])) * max(0.0, (bbox[3] - bbox[1]))


# ============================================================================
# AbstractScenario — 시나리오 엔진 베이스 클래스
# ============================================================================

class AbstractScenario(ABC):
    name: str = "base"
    kr_name: str = "기본"
    required_classes: set = {"person"}
    cooldown_seconds: float = 30.0
    zones: List[ZoneROI] = []
    severity_map: Dict[str, str] = {}

    def __init__(self):
        self._cooldowns: Dict[Any, CooldownState] = {}
        self._trajectories: Dict[Any, TrajectoryBuffer] = {}
        self._event_count: int = 0

    def evaluate(
        self, detections: List[Detection], private_key: str,
        stream_id: str, site_id: str, timestamp_utc_ms: int,
    ) -> List[ScenarioEvent]:
        self._update_trajectories(detections, timestamp_utc_ms)
        events = self._evaluate_frame(
            detections=detections, private_key=private_key,
            stream_id=stream_id, site_id=site_id, timestamp_utc_ms=timestamp_utc_ms,
        )
        return self._postprocess_events(events)

    def get_state_summary(self) -> Dict[str, Any]:
        active_tracks = len([t for t in self._trajectories.values() if t.length > 0])
        cooldown_active = sum(1 for c in self._cooldowns.values() if c.state == CooldownState.STATE_COOLDOWN)
        return {
            "scenario": self.name, "active_tracks": active_tracks,
            "total_events": self._event_count, "cooldown_active": cooldown_active,
        }

    def reset(self):
        self._cooldowns.clear()
        self._trajectories.clear()
        self._event_count = 0

    @abstractmethod
    def _evaluate_frame(
        self, detections: List[Detection], private_key: str,
        stream_id: str, site_id: str, timestamp_utc_ms: int,
    ) -> List[ScenarioEvent]:
        pass

    def get_trajectory(self, track_id: Optional[int]) -> TrajectoryBuffer:
        if track_id not in self._trajectories:
            self._trajectories[track_id] = TrajectoryBuffer()
        return self._trajectories[track_id]

    def get_cooldown(self, track_id: Optional[int], severity: str = "INFO") -> CooldownState:
        key = (track_id, severity) if isinstance(track_id, int) else track_id
        if key not in self._cooldowns:
            cd_secs = self.cooldown_seconds
            for sev, s_cd in {"INFO": 15.0, "WARN": 30.0, "CRITICAL": 60.0}.items():
                if severity == sev and s_cd > cd_secs:
                    cd_secs = s_cd
            self._cooldowns[key] = CooldownState(cooldown_seconds=cd_secs)
        return self._cooldowns[key]

    def check_zone(self, bbox: Tuple[float, ...], zone_name: str) -> bool:
        for zone in self.zones:
            if zone.name == zone_name:
                return bbox_center_in_zone(bbox, zone.points)
        return False

    def get_detections_by_class(self, detections: List[Detection], class_name: str) -> List[Detection]:
        return [d for d in detections if d.class_name == class_name]

    def build_event(
        self, severity: str, track_id: Optional[int], detection: Detection,
        private_key: str, stream_id: str, site_id: str, timestamp_utc_ms: int,
        extra_result: Dict[str, Any] = None,
    ) -> ScenarioEvent:
        event_type = f"{self.name}_{severity.lower()}"
        self._event_count += 1
        return ScenarioEvent(
            event_type=event_type, severity=severity, private_key=private_key,
            stream_id=stream_id, site_id=site_id, timestamp_utc_ms=timestamp_utc_ms,
            track_id=track_id, class_name=detection.class_name,
            confidence=detection.confidence, bbox=detection.bbox,
            detection_result={"scenario": self.name, "event_index": self._event_count, **(extra_result or {})},
        )

    def _update_trajectories(self, detections: List[Detection], timestamp_ms: int):
        for d in detections:
            if d.class_name not in self.required_classes:
                continue
            traj = self.get_trajectory(d.track_id)
            traj.add(timestamp_ms=timestamp_ms, bbox=d.bbox, class_name=d.class_name, confidence=d.confidence)

    def _postprocess_events(self, events: List[ScenarioEvent]) -> List[ScenarioEvent]:
        return events
