from typing import Any, Dict, List, Optional
from dataclasses import asdict, dataclass, field
import time
from datetime import datetime, timezone
from collections import Counter, defaultdict

from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol
from ..utils import (
    filter_by_confidence,
    apply_category_mapping,
    calculate_counting_summary,
    match_results_structure,
    bbox_smoothing,
    BBoxSmoothingConfig,
    BBoxSmoothingTracker
)
from ..core.config import BaseConfig, AlertConfig


@dataclass
class WeaponDetectionConfig(BaseConfig):
    """Configuration for weapon detection use case."""
    enable_smoothing: bool = True
    smoothing_algorithm: str = "observability"
    smoothing_window_size: int = 20
    smoothing_cooldown_frames: int = 5
    smoothing_confidence_range_factor: float = 0.5
    confidence_threshold: float = 0.3
    weapon_categories: List[str] = field(default_factory=lambda: ['billete', 'bluntweapon', 'glass', 'gun', 'knife', 'monedero', 'pistol', 'smartphone', 'tarjeta'])
    target_weapon_categories: List[str] = field(default_factory=lambda: ['bluntweapon', 'glass', 'gun', 'knife', 'monedero', 'pistol', 'tarjeta'])
    alert_config: Optional[AlertConfig] = None
    index_to_category: Optional[Dict[int, str]] = field(
        default_factory=lambda: {
            0: "billete",
            1: "bluntweapon",
            2: "glass",
            3: "gun",
            4: "knife",
            5: "monedero",
            6: "pistol",
            7: "smartphone",
            8: "tarjeta"
        }
    )

class WeaponDetectionUseCase(BaseProcessor):
    CATEGORY_DISPLAY = {
        "billete": "Billete",
        "bluntweapon": "Blunt Weapon",
        "glass": "Glass",
        "gun": "Gun",
        "knife": "Knife",
        "monedero": "Monedero",
        "pistol": "Pistol",
        "smartphone": "Smartphone",
        "tarjeta": "Tarjeta"
    }

    def __init__(self):
        super().__init__("weapon_detection")
        self.category = "security"
        self.CASE_TYPE: Optional[str] = 'weapon_detection'
        self.CASE_VERSION: Optional[str] = '1.0'
        self.target_categories = ['bluntweapon', 'glass', 'gun', 'knife', 'monedero', 'pistol', 'tarjeta']
        self.smoothing_tracker = None
        self.tracker = None
        self._total_frame_counter = 0
        self._global_frame_offset = 0
        self._tracking_start_time = None
        self._track_aliases: Dict[Any, Any] = {}
        self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
        self._track_merge_iou_threshold: float = 0.05
        self._track_merge_time_window: float = 7.0
        self._ascending_alert_list: List[int] = []
        self.current_incident_end_timestamp: str = "N/A"
        self.start_timer = None
        # NEW: Track unique weapons per track_id
        self._unique_weapon_instances: Dict[Any, str] = {}  # track_id -> dominant category
        self._track_weapon_counts: Dict[Any, Counter] = defaultdict(Counter)  # track_id -> Counter(category -> count)
        self.helper: Dict[Any, List[str]] = {}  # track_id -> list of categories
        self._text_history: Dict[str, int] = {}  # category -> consecutive frame count
        self._stable_frames_required = 3  # Number of frames for stability

    def reset_tracker(self) -> None:
        """Reset the advanced tracker instance."""
        if self.tracker is not None:
            self.tracker.reset()
            self.logger.info("AdvancedTracker reset for new tracking session")

    def reset_weapon_tracking(self) -> None:
        """Reset weapon tracking state."""
        self._unique_weapon_instances = {}
        self._track_weapon_counts = defaultdict(Counter)
        self.helper = {}
        self._text_history = {}
        self._total_frame_counter = 0
        self._global_frame_offset = 0
        self.logger.info("Weapon tracking state reset")

    def reset_all_tracking(self) -> None:
        """Reset both advanced tracker and weapon tracking state."""
        self.reset_tracker()
        self.reset_weapon_tracking()
        self.logger.info("All weapon tracking state reset")

    def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None,
                stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
        start_time = time.time()
        if not isinstance(config, WeaponDetectionConfig):
            return self.create_error_result("Invalid config type", usecase=self.name, category=self.category, context=context)
        if context is None:
            context = ProcessingContext()

        input_format = match_results_structure(data)
        context.input_format = input_format
        context.confidence_threshold = config.confidence_threshold

        # Normalize alert_config if provided as a plain dict
        if isinstance(getattr(config, 'alert_config', None), dict):
            try:
                config.alert_config = AlertConfig(**config.alert_config)
            except Exception:
                pass

        if config.confidence_threshold is not None:
            processed_data = filter_by_confidence(data, config.confidence_threshold)
            self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
        else:
            processed_data = data
            self.logger.debug("No confidence filtering applied")

        if config.index_to_category:
            processed_data = apply_category_mapping(processed_data, config.index_to_category)
            self.logger.debug("Applied category mapping")

        if config.target_weapon_categories:
            processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
            self.logger.debug("Applied category filtering")

        # Alert for detected weapons
        for detection in processed_data:
            if detection.get('category') in config.target_weapon_categories:
                self.logger.warning(f"ALERT: {detection.get('category')} detected at {self._get_current_timestamp_str(stream_info)}")

        if config.enable_smoothing:
            if self.smoothing_tracker is None:
                smoothing_config = BBoxSmoothingConfig(
                    smoothing_algorithm=config.smoothing_algorithm,
                    window_size=config.smoothing_window_size,
                    cooldown_frames=config.smoothing_cooldown_frames,
                    confidence_threshold=config.confidence_threshold,
                    confidence_range_factor=config.smoothing_confidence_range_factor,
                    enable_smoothing=True
                )
                self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
            processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)

        try:
            from ..advanced_tracker import AdvancedTracker
            from ..advanced_tracker.config import TrackerConfig
            if self.tracker is None:
                tracker_config = TrackerConfig(
                    track_high_thresh=float(config.confidence_threshold),
                    track_low_thresh=max(0.05, float(config.confidence_threshold) / 2),
                    new_track_thresh=float(config.confidence_threshold)
                )
                self.tracker = AdvancedTracker(tracker_config)
                self.logger.info(f"Initialized AdvancedTracker with thresholds: high={tracker_config.track_high_thresh}, "
                                 f"low={tracker_config.track_low_thresh}, new={tracker_config.new_track_thresh}")
            processed_data = self.tracker.update(processed_data)
        except Exception as e:
            self.logger.warning(f"AdvancedTracker failed: {e}")

        self._update_tracking_state(processed_data)
        self._update_weapon_instances(processed_data)
        self._total_frame_counter += 1

        frame_number = None
        if stream_info:
            input_settings = stream_info.get("input_settings", {})
            start_frame = input_settings.get("start_frame")
            end_frame = input_settings.get("end_frame")
            if start_frame is not None and end_frame is not None and start_frame == end_frame:
                frame_number = start_frame

        general_counting_summary = calculate_counting_summary(data)
        counting_summary = self._count_categories(processed_data, config)
        total_counts = self.get_total_counts()
        counting_summary['total_counts'] = total_counts
        alerts = self._check_alerts(counting_summary, frame_number, config)
        predictions = self._extract_predictions(processed_data)

        incidents_list = self._generate_incidents(counting_summary, alerts, config, frame_number, stream_info)
        tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number, stream_info)
        business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, frame_number, stream_info, is_empty=True)
        summary_list = self._generate_summary(counting_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)

        incidents = incidents_list[0] if incidents_list else {}
        tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
        business_analytics = business_analytics_list[0] if business_analytics_list else {}
        summary = summary_list[0] if summary_list else {}
        agg_summary = {str(frame_number): {
            "incidents": incidents,
            "tracking_stats": tracking_stats,
            "business_analytics": business_analytics,
            "alerts": alerts,
            "human_text": summary}
        }

        context.mark_completed()
        result = self.create_result(
            data={"agg_summary": agg_summary},
            usecase=self.name,
            category=self.category,
            context=context
        )
        proc_time = time.time() - start_time
        processing_latency_ms = proc_time * 1000.0
        processing_fps = (1.0 / proc_time) if proc_time > 0 else None
        self.logger.info(f"Processing latency: {processing_latency_ms:.2f} ms, Throughput: {processing_fps:.2f} fps, Frame: {self._total_frame_counter}")
        return result

    def _count_categories(self, detections: List[Dict], config: WeaponDetectionConfig) -> Dict[str, Any]:
        """Count unique weapon detections per frame and attach detections."""
        unique_instances: set = set()
        valid_detections: List[Dict[str, Any]] = []

        # Group detections by track_id for per-track dominance
        tracks: Dict[Any, List[Dict[str, Any]]] = {}
        for det in detections:
            if not all(k in det for k in ['category', 'confidence', 'bounding_box']):
                continue
            tid = det.get('track_id')
            if tid is None:
                # If no track id, treat as its own pseudo-track keyed by bbox
                tid = (det.get("bounding_box") or det.get("bbox"))
            tracks.setdefault(tid, []).append(det)

        for tid, dets in tracks.items():
            # Pick a representative detection (first occurrence)
            rep = dets[0]
            cat = rep.get('category', '')
            if cat not in self.target_categories:
                continue
            bbox = rep.get('bounding_box')
            conf = rep.get('confidence')
            frame_id = rep.get('frame_id')

            # Compute dominant category for this track from last 50% of history
            dominant_category = None
            history = self.helper.get(tid, [])
            if history:
                half = max(1, len(history) // 2)
                window = history[-half:]
                dominant_category, _ = Counter(window).most_common(1)[0]
            elif rep.get('category'):
                dominant_category = rep.get('category')

            # Fallback to already computed per-track mapping
            if not dominant_category:
                dominant_category = self._unique_weapon_instances.get(tid)

            if dominant_category:
                unique_instances.add((tid, dominant_category))
                valid_detections.append({
                    "bounding_box": bbox,
                    "category": dominant_category,
                    "confidence": conf,
                    "track_id": rep.get('track_id'),
                    "frame_id": frame_id,
                    "masks": rep.get("masks", [])
                })

        counts = {self.CATEGORY_DISPLAY.get(cat, cat): len([t for t, c in unique_instances if c == cat]) for cat in self.target_categories}

        return {
            "total_count": len(unique_instances),
            "per_category_count": counts,
            "detections": valid_detections
        }

    def _update_weapon_instances(self, detections: List[Dict]) -> None:
        """Update set of seen weapon instances and track the dominant category per track_id."""
        current_frame_categories = set()
        for det in detections:
            category = det.get('category')
            track_id = det.get('track_id')
            if not category or track_id is None or category not in self.target_categories:
                continue

            # Append to per-track rolling history
            history = self.helper.get(track_id)
            if history is None:
                history = []
                self.helper[track_id] = history
            history.append(category)
            if len(history) > 200:
                del history[: len(history) - 200]

            # Update per-track frequency counter
            self._track_weapon_counts[track_id][category] += 1

            # Update consecutive frame counter for stability
            self._text_history[category] = self._text_history.get(category, 0) + 1

            # Once stable, decide dominant category from last 50% of history
            if self._text_history[category] >= self._stable_frames_required:
                half = max(1, len(history) // 2)
                window = history[-half:]
                dominant, _ = Counter(window).most_common(1)[0]
                self._unique_weapon_instances[track_id] = dominant

        # Reset counters for categories NOT seen in this frame
        current_frame_categories = {det.get('category', '') for det in detections if det.get('category') in self.target_categories}
        for t in list(self._text_history.keys()):
            if t not in current_frame_categories:
                self._text_history[t] = 0

    def _generate_tracking_stats(self, counting_summary: Dict, alerts: List, config: WeaponDetectionConfig,
                                frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
        """Generate structured tracking stats with frame-based keys, labeling target categories as Weapon."""
        tracking_stats = []
        total_detections = counting_summary.get("total_count", 0)
        total_counts = counting_summary.get("total_counts", {})
        cumulative_total = sum(set(total_counts.values())) if total_counts else 0
        per_category_count = counting_summary.get("per_category_count", {})
        track_ids_info = self._get_track_ids_info(counting_summary.get("detections", []))
        current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
        start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
        high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
        high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
        camera_info = self.get_camera_info_from_stream(stream_info)

        human_text_lines = []
        human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
        if total_detections > 0:
            # Count total weapons and list specific categories
            weapon_count = sum(count for cat, count in per_category_count.items() if count > 0)
            category_details = [f"{count} {self.CATEGORY_DISPLAY.get(cat, cat)}" for cat, count in per_category_count.items() if count > 0]
            detection_text = f"{weapon_count} Weapon{'s' if weapon_count != 1 else ''} detected"
            # if category_details:
            #     detection_text += f" ({', '.join(category_details)})"
            # human_text_lines.append(f"\t- {detection_text}")
            # # Show dominant per-track weapons for current frame
            # seen = set()
            # display_categories = []
            # for det in counting_summary.get("detections", []):
            #     t = det.get("track_id")
            #     cat = det.get("category")
            #     if not cat or t in seen or cat not in self.target_categories:
            #         continue
            #     seen.add(t)
            #     display_categories.append(self.CATEGORY_DISPLAY.get(cat, cat))
            # if display_categories:
            #     human_text_lines.append(f"\t- Weapon Types: {', '.join(display_categories)}")
        else:
            human_text_lines.append(f"\t- No weapons detected")

        human_text_lines.append("")
        human_text_lines.append(f"TOTAL SINCE {start_timestamp}:")
        human_text_lines.append(f"\t- Total Weapons Detected: {cumulative_total}")
        # if self._unique_weapon_instances:
        #     human_text_lines.append("\t- Unique Weapon Types:")
        #     for cat in sorted(set(self._unique_weapon_instances.values())):
        #         if cat in self.target_categories:
        #             human_text_lines.append(f"\t\t- {self.CATEGORY_DISPLAY.get(cat, cat)}")

        # Use "Weapon" as the primary category for counts, with details in per_category_count
        current_counts = [{"category": "Weapon", "count": sum(count for cat, count in per_category_count.items() if count > 0)}] if total_detections > 0 else []
        if current_counts:
            current_counts.append({"category": "Weapon Types", "details": [
                {"type": self.CATEGORY_DISPLAY.get(cat, cat), "count": count} for cat, count in per_category_count.items() if count > 0
            ]})
        total_counts_list = [{"category": "Weapon", "count": sum(count for cat, count in total_counts.items() if count > 0)}] if cumulative_total > 0 else []
        if total_counts_list:
            total_counts_list.append({"category": "Weapon Types", "details": [
                {"type": self.CATEGORY_DISPLAY.get(cat, cat), "count": count} for cat, count in total_counts.items() if count > 0
            ]})

        human_text = "\n".join(human_text_lines)
        detections = []
        for detection in counting_summary.get("detections", []):
            cat = detection.get("category", "")
            if cat not in self.target_categories:
                continue
            bbox = detection.get("bounding_box", {})
            segmentation = detection.get("masks", [])
            detection_obj = self.create_detection_object(
                category="Weapon",
                bbox=bbox,
                segmentation=segmentation,
                weapon_type=self.CATEGORY_DISPLAY.get(cat, cat)  # Add specific weapon type
            )
            detections.append(detection_obj)

        alert_settings = []
        if config.alert_config:
            alert_cfg = config.alert_config
            alert_type = getattr(alert_cfg, 'alert_type', None) if not isinstance(alert_cfg, dict) else alert_cfg.get('alert_type')
            alert_value = getattr(alert_cfg, 'alert_value', None) if not isinstance(alert_cfg, dict) else alert_cfg.get('alert_value')
            count_thresholds = getattr(alert_cfg, 'count_thresholds', None) if not isinstance(alert_cfg, dict) else alert_cfg.get('count_thresholds')
            alert_type = alert_type if isinstance(alert_type, list) else (list(alert_type) if alert_type is not None else ['Default'])
            alert_value = alert_value if isinstance(alert_value, list) else (list(alert_value) if alert_value is not None else ['JSON'])
            alert_settings.append({
                "alert_type": alert_type,
                "incident_category": self.CASE_TYPE,
                "threshold_level": count_thresholds or {},
                "ascending": True,
                "settings": {t: v for t, v in zip(alert_type, alert_value)}
            })

        if alerts:
            human_text_lines.append(f"Alerts: {alerts[0].get('settings', {})}")
        else:
            human_text_lines.append("Alerts: None")

        human_text = "\n".join(human_text_lines)
        reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]

        tracking_stat = self.create_tracking_stats(
            total_counts=total_counts_list,
            current_counts=current_counts,
            detections=detections,
            human_text=human_text,
            camera_info=camera_info,
            alerts=alerts,
            alert_settings=alert_settings,
            reset_settings=reset_settings,
            start_time=high_precision_start_timestamp,
            reset_time=high_precision_reset_timestamp
        )
        tracking_stats.append(tracking_stat)
        return tracking_stats

    def _generate_business_analytics(self, counting_summary: Dict, alerts: List, config: WeaponDetectionConfig,
                                    frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None,
                                    is_empty=False) -> List[Dict]:
        if is_empty:
            return []
        return []

    def _generate_summary(self, counting_summary: Dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[Dict]:
        """Generate a human-readable summary."""
        lines = []
        lines.append("Application Name: "+self.CASE_TYPE)
        lines.append("Application Version: "+self.CASE_VERSION)
        if len(incidents) > 0:
            lines.append("Incidents: "+f"\n\t{incidents[0].get('human_text', 'No incidents detected')}")
        if len(tracking_stats) > 0:
            lines.append("Tracking Statistics: "+f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}")
        if len(business_analytics) > 0:
            lines.append("Business Analytics: "+f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}")
        if not (incidents or tracking_stats or business_analytics):
            lines.append("Summary: "+"No Summary Data")
        return ["\n".join(lines)]

    def _get_track_ids_info(self, detections: List[Dict]) -> Dict[str, Any]:
        frame_track_ids = {det.get('track_id') for det in detections if det.get('track_id') is not None}
        total_track_ids = set()
        for s in getattr(self, '_per_category_total_track_ids', {}).values():
            total_track_ids.update(s)
        return {
            "total_count": len(total_track_ids),
            "current_frame_count": len(frame_track_ids),
            "total_unique_track_ids": len(total_track_ids),
            "current_frame_track_ids": list(frame_track_ids),
            "last_update_time": time.time(),
            "total_frames_processed": getattr(self, '_total_frame_counter', 0)
        }

    def _update_tracking_state(self, detections: List[Dict]):
        if not hasattr(self, "_per_category_total_track_ids"):
            self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
        self._current_frame_track_ids = {cat: set() for cat in self.target_categories}

        for det in detections:
            cat = det.get("category")
            raw_track_id = det.get("track_id")
            if cat not in self.target_categories or raw_track_id is None:
                continue
            bbox = det.get("bounding_box", det.get("bbox"))
            canonical_id = self._merge_or_register_track(raw_track_id, bbox)
            det["track_id"] = canonical_id
            self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
            self._current_frame_track_ids[cat].add(canonical_id)

    def get_total_counts(self):
        return {self.CATEGORY_DISPLAY.get(cat, cat): len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}

    def _format_timestamp_for_stream(self, timestamp: float) -> str:
        dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
        return dt.strftime('%Y:%m:%d %H:%M:%S')

    def _format_timestamp_for_video(self, timestamp: float) -> str:
        hours = int(timestamp // 3600)
        minutes = int((timestamp % 3600) // 60)
        seconds = round(float(timestamp % 60), 2)
        return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"

    def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
        if not stream_info:
            return "00:00:00.00"
        if precision:
            if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
                if frame_id:
                    start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
                else:
                    start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
                stream_time_str = self._format_timestamp_for_video(start_time)
                return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
            else:
                return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
        if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
            if frame_id:
                start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
            else:
                start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
            stream_time_str = self._format_timestamp_for_video(start_time)
            return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
        else:
            stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
            if stream_time_str:
                try:
                    timestamp_str = stream_time_str.replace(" UTC", "")
                    dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
                    timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
                    return self._format_timestamp_for_stream(timestamp)
                except:
                    return self._format_timestamp_for_stream(time.time())
            else:
                return self._format_timestamp_for_stream(time.time())

    def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
        if not stream_info:
            return "00:00:00"
        if precision:
            if self.start_timer is None:
                self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
                return self._format_timestamp(self.start_timer)
            elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
                self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
                return self._format_timestamp(self.start_timer)
            else:
                return self._format_timestamp(self.start_timer)
        if self.start_timer is None:
            self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
            return self._format_timestamp(self.start_timer)
        elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
            self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
            return self._format_timestamp(self.start_timer)
        else:
            if self.start_timer is not None:
                return self._format_timestamp(self.start_timer)
            if self._tracking_start_time is None:
                stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
                if stream_time_str:
                    try:
                        timestamp_str = stream_time_str.replace(" UTC", "")
                        dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
                        self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
                    except:
                        self._tracking_start_time = time.time()
                else:
                    self._tracking_start_time = time.time()
            dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
            dt = dt.replace(minute=0, second=0, microsecond=0)
            return dt.strftime('%Y:%m:%d %H:%M:%S')

    def _format_timestamp(self, timestamp: Any) -> str:
        if isinstance(timestamp, (int, float)):
            timestamp = datetime.fromtimestamp(timestamp, timezone.utc).strftime(
                '%Y-%m-%d-%H:%M:%S.%f UTC'
            )
        if not isinstance(timestamp, str):
            return str(timestamp)
        if '.' not in timestamp:
            return timestamp
        main_part, fractional_and_suffix = timestamp.split('.', 1)
        if ' ' in fractional_and_suffix:
            fractional_part, suffix = fractional_and_suffix.split(' ', 1)
            suffix = ' ' + suffix
        else:
            fractional_part, suffix = fractional_and_suffix, ''
        fractional_part = (fractional_part + '00')[:2]
        return f"{main_part}.{fractional_part}{suffix}"

    def _get_tracking_start_time(self) -> str:
        if self._tracking_start_time is None:
            return "N/A"
        return self._format_timestamp(self._tracking_start_time)

    def _set_tracking_start_time(self) -> None:
        self._tracking_start_time = time.time()

    def _compute_iou(self, box1: Any, box2: Any) -> float:
        def _bbox_to_list(bbox):
            if bbox is None:
                return []
            if isinstance(bbox, list):
                return bbox[:4] if len(bbox) >= 4 else []
            if isinstance(bbox, dict):
                if "xmin" in bbox:
                    return [bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"]]
                if "x1" in bbox:
                    return [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]]
                values = [v for v in bbox.values() if isinstance(v, (int, float))]
                return values[:4] if len(values) >= 4 else []
            return []
        l1 = _bbox_to_list(box1)
        l2 = _bbox_to_list(box2)
        if len(l1) < 4 or len(l2) < 4:
            return 0.0
        x1_min, y1_min, x1_max, y1_max = l1
        x2_min, y2_min, x2_max, y2_max = l2
        x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
        y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
        x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
        y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
        inter_x_min = max(x1_min, x2_min)
        inter_y_min = max(y1_min, y2_min)
        inter_x_max = min(x1_max, x2_max)
        inter_y_max = min(y1_max, y2_max)
        inter_w = max(0.0, inter_x_max - inter_x_min)
        inter_h = max(0.0, inter_y_max - inter_y_min)
        inter_area = inter_w * inter_h
        area1 = (x1_max - x1_min) * (y1_max - y1_min)
        area2 = (x2_max - x2_min) * (y2_max - y2_min)
        union_area = area1 + area2 - inter_area
        return (inter_area / union_area) if union_area > 0 else 0.0

    def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
        if raw_id is None or bbox is None:
            return raw_id
        now = time.time()
        if raw_id in self._track_aliases:
            canonical_id = self._track_aliases[raw_id]
            track_info = self._canonical_tracks.get(canonical_id)
            if track_info is not None:
                track_info["last_bbox"] = bbox
                track_info["last_update"] = now
                track_info["raw_ids"].add(raw_id)
            return canonical_id
        for canonical_id, info in self._canonical_tracks.items():
            if now - info["last_update"] > self._track_merge_time_window:
                continue
            iou = self._compute_iou(bbox, info["last_bbox"])
            if iou >= self._track_merge_iou_threshold:
                self._track_aliases[raw_id] = canonical_id
                info["last_bbox"] = bbox
                info["last_update"] = now
                info["raw_ids"].add(raw_id)
                return canonical_id
        canonical_id = raw_id
        self._track_aliases[raw_id] = canonical_id
        self._canonical_tracks[canonical_id] = {
            "last_bbox": bbox,
            "last_update": now,
            "raw_ids": {raw_id},
        }
        return canonical_id