"""
People counting use case implementation.

This module provides a clean implementation of people counting functionality
with zone-based analysis, tracking, and alerting capabilities.
"""

from typing import Any, Dict, List, Optional, Set
from dataclasses import asdict
import time

from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol, ResultFormat
from ..core.config import PeopleCountingConfig, ZoneConfig, AlertConfig
from ..utils import (
    filter_by_confidence,
    filter_by_categories,
    apply_category_mapping,
    count_objects_by_category,
    count_objects_in_zones,
    calculate_counting_summary,
    match_results_structure,
    bbox_smoothing,
    BBoxSmoothingConfig,
    BBoxSmoothingTracker,
    calculate_iou
)
from ..utils.geometry_utils import get_bbox_center, point_in_polygon, get_bbox_bottom25_center


class PeopleCountingUseCase(BaseProcessor):
    """People counting use case with zone analysis and alerting."""
    
    def __init__(self):
        """Initialize people counting use case."""
        super().__init__("people_counting")
        self.category = "general"
        
        # Track ID storage for total count calculation
        self._total_track_ids = set()  # Store all unique track IDs seen across calls
        self._current_frame_track_ids = set()  # Store track IDs from current frame
        self._total_count = 0  # Cached total count
        self._last_update_time = time.time()  # Track when last updated
        
        # Zone-based tracking storage
        self._zone_current_track_ids = {}  # zone_name -> set of current track IDs in zone
        self._zone_total_track_ids = {}  # zone_name -> set of all track IDs that have been in zone
        self._zone_current_counts = {}  # zone_name -> current count in zone
        self._zone_total_counts = {}  # zone_name -> total count that have been in zone
        
        # Frame counter for tracking total frames processed
        self._total_frame_counter = 0  # Total frames processed across all calls
        
        # Global frame offset for video chunk processing
        self._global_frame_offset = 0  # Offset to add to local frame IDs for global frame numbering
        self._frames_in_current_chunk = 0  # Number of frames in current chunk
        
        # Initialize smoothing tracker
        self.smoothing_tracker = None

        # --------------------------------------------------------------------- #
        # Tracking aliasing structures to merge fragmented IDs                   #
        # --------------------------------------------------------------------- #
        # Maps raw tracker IDs generated by ByteTrack to a stable canonical ID
        # that represents a real-world person. This helps avoid double counting
        # when the tracker loses a target temporarily and assigns a new ID.
        self._track_aliases: Dict[Any, Any] = {}

        # Stores metadata about each canonical track such as its last seen
        # bounding box, last update timestamp and all raw IDs that have been
        # merged into it.
        self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}

        # IoU threshold above which two bounding boxes are considered to belong
        # to the same person (empirically chosen; adjust in production if
        # needed).
        self._track_merge_iou_threshold: float = 0.04

        # Only merge with canonical tracks that were updated within this time
        # window (in seconds). This prevents accidentally merging tracks that
        # left the scene long ago.
        self._track_merge_time_window: float = 10.0
    
    def get_config_schema(self) -> Dict[str, Any]:
        """Get configuration schema for people counting."""
        return {
            "type": "object",
            "properties": {
                "confidence_threshold": {
                    "type": "number",
                    "minimum": 0.0,
                    "maximum": 1.0,
                    "default": 0.5,
                    "description": "Minimum confidence threshold for detections"
                },
                "enable_tracking": {
                    "type": "boolean",
                    "default": False,
                    "description": "Enable tracking for unique counting"
                },
                "zone_config": {
                    "type": "object",
                    "properties": {
                        "zones": {
                            "type": "object",
                            "additionalProperties": {
                                "type": "array",
                                "items": {
                                    "type": "array",
                                    "items": {"type": "number"},
                                    "minItems": 2,
                                    "maxItems": 2
                                },
                                "minItems": 3
                            },
                            "description": "Zone definitions as polygons"
                        },
                        "zone_confidence_thresholds": {
                            "type": "object",
                            "additionalProperties": {"type": "number", "minimum": 0.0, "maximum": 1.0},
                            "description": "Per-zone confidence thresholds"
                        }
                    }
                },
                "person_categories": {
                    "type": "array",
                    "items": {"type": "string"},
                    "default": ["person", "people"],
                    "description": "Category names that represent people"
                },
                "enable_unique_counting": {
                    "type": "boolean",
                    "default": True,
                    "description": "Enable unique people counting using tracking"
                },
                "time_window_minutes": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 60,
                    "description": "Time window for counting analysis in minutes"
                },
                "alert_config": {
                    "type": "object",
                    "properties": {
                        "count_thresholds": {
                            "type": "object",
                            "additionalProperties": {"type": "integer", "minimum": 1},
                            "description": "Count thresholds for alerts"
                        },
                        "occupancy_thresholds": {
                            "type": "object", 
                            "additionalProperties": {"type": "integer", "minimum": 1},
                            "description": "Zone occupancy thresholds for alerts"
                        }
                    }
                }
            },
            "required": ["confidence_threshold"],
            "additionalProperties": False
        }
    
    def create_default_config(self, **overrides) -> PeopleCountingConfig:
        """Create default configuration with optional overrides."""
        defaults = {
            "category": self.category,
            "usecase": self.name,
            "confidence_threshold": 0.5,
            "enable_tracking": False,
            "enable_analytics": True,
            "enable_unique_counting": True,
            "time_window_minutes": 60,
            "person_categories": ["person", "people"],
        }
        defaults.update(overrides)
        return PeopleCountingConfig(**defaults)
    
    def process(self, data: Any, config: ConfigProtocol, 
                context: Optional[ProcessingContext] = None, stream_info: Optional[Any] = None) -> ProcessingResult:
        """
        Process people counting use case.
        
        Args:
            data: Raw model output (detection or tracking format)
            config: People counting configuration
            context: Processing context
            stream_info: Stream information containing frame details (optional, for compatibility)
            
        Returns:
            ProcessingResult: Processing result with people counting analytics
        """
        start_time = time.time()
        
        try:
            # Ensure we have the right config type
            if not isinstance(config, PeopleCountingConfig):
                return self.create_error_result(
                    "Invalid configuration type for people counting",
                    usecase=self.name,
                    category=self.category,
                    context=context
                )
            
            # Initialize processing context if not provided
            if context is None:
                context = ProcessingContext()
            
            # Detect input format
            input_format = match_results_structure(data)
            context.input_format = input_format
            context.confidence_threshold = config.confidence_threshold
            
            self.logger.info(f"Processing people counting with format: {input_format.value}")
            
            # Check if data is frame-based tracking format
            is_frame_based_tracking = (isinstance(data, dict) and 
                                     all(isinstance(k, (str, int)) for k in data.keys()) and
                                     input_format == ResultFormat.OBJECT_TRACKING)
            
            if is_frame_based_tracking:
                # Apply smoothing to tracking results if enabled
                if config.enable_smoothing:
                    # Initialize smoothing tracker if not exists
                    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 or 0.5,
                            confidence_range_factor=config.smoothing_confidence_range_factor,
                            enable_smoothing=True
                        )
                        self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
                    
                    # Apply smoothing to tracking results (handles frame-based format)
                    smoothed_tracking_data = bbox_smoothing(data, self.smoothing_tracker.config, self.smoothing_tracker)
                    self.logger.debug(f"Applied bbox smoothing to tracking results with {len(smoothed_tracking_data)} frames")
                    
                    # Ensure smoothed_tracking_data is a dict for frame-wise tracking
                    # if isinstance(smoothed_tracking_data, list):
                    #     smoothed_tracking_data = {"0": smoothed_tracking_data}
                    
                    return self._process_frame_wise_tracking(smoothed_tracking_data, config, context)
                else:
                    # Process frame-wise tracking data without smoothing
                    return self._process_frame_wise_tracking(data, config, context)
            else:
                # Process single frame or detection data (existing logic)
                return self._process_single_frame_data(data, config, context)
                
        except Exception as e:
            self.logger.error(f"People counting failed: {str(e)}", exc_info=True)
            
            if context:
                context.mark_completed()
            
            return self.create_error_result(
                str(e), 
                type(e).__name__,
                usecase=self.name,
                category=self.category,
                context=context
            )
    
    def _process_frame_wise_tracking(self, data: Dict, config: PeopleCountingConfig, context: ProcessingContext) -> ProcessingResult:
        """Process frame-wise tracking data to generate frame-specific events and tracking stats."""
        
        frame_events = {}
        frame_tracking_stats = {}
        all_events = []
        all_tracking_stats = []
        
        # Increment total frame counter
        frames_in_this_call = len(data)
        self._total_frame_counter += frames_in_this_call
        
        # Process each frame individually
        for frame_key, frame_detections in data.items():
            # Extract frame ID from tracking data
            local_frame_id = self._extract_frame_id_from_tracking(frame_detections, frame_key)
            
            # Convert to global frame ID
            global_frame_id = self.get_global_frame_id(local_frame_id)
            
            # Process this single frame's detections
            frame_result = self._process_single_frame_detections(frame_detections, config, context, global_frame_id)
            
            if frame_result.is_success():
                # Get events and tracking stats for this frame
                events = frame_result.data.get("events", [])
                tracking_stats = frame_result.data.get("tracking_stats", [])
                
                # Add global frame_id to each event
                for event in events:
                    event["frame_id"] = global_frame_id
                
                # Store frame-wise results
                if events:  # Only add if events exist
                    frame_events[global_frame_id] = events
                    all_events.extend(events)
                
                frame_tracking_stats[global_frame_id] = tracking_stats
                all_tracking_stats.extend(tracking_stats)
        
        # Update global frame offset after processing this chunk
        self.update_global_frame_offset(frames_in_this_call)
        
        # Create comprehensive result with frame-wise structure
        result_data = {
            "events": frame_events,           # Frame-wise events
            "tracking_stats": frame_tracking_stats,  # Frame-wise tracking stats
            "all_events": all_events,         # All events combined
            "all_tracking_stats": all_tracking_stats,  # All tracking stats combined
            "total_count": self.get_total_count(),
            "zone_tracking": self.get_zone_tracking_info(),
            "global_frame_offset": self._global_frame_offset,
            "frames_in_chunk": frames_in_this_call
        }
        
        # Create result
        result = self.create_result(
            result_data,
            self.name,
            self.category,
            context
        )
        
        return result
    
    def _extract_frame_id_from_tracking(self, frame_detections: List[Dict], frame_key: str) -> str:
        """Extract frame ID from tracking data."""
        # Priority 1: Check if detections have frame information
        if frame_detections and len(frame_detections) > 0:
            first_detection = frame_detections[0]
            if "frame" in first_detection:
                return str(first_detection["frame"])
            elif "timestamp" in first_detection:
                return str(int(first_detection["timestamp"]))
        
        # Priority 2: Use frame_key from input data
        return str(frame_key)
    
    def _process_single_frame_detections(self, frame_detections: List[Dict], config: PeopleCountingConfig, context: ProcessingContext, frame_id: Optional[str] = None) -> ProcessingResult:
        """Process detections from a single frame using existing logic."""
        
        # Step 1: Apply confidence filtering to this frame
        if config.confidence_threshold is not None:
            frame_detections = [d for d in frame_detections if d.get("confidence", 0) >= config.confidence_threshold]
        
        # Step 2: Apply category mapping if provided
        if config.index_to_category:
            frame_detections = apply_category_mapping(frame_detections, config.index_to_category)
        
        # Step 3: Filter to person categories
        if config.person_categories:
            frame_detections = [d for d in frame_detections if d.get("category") in config.person_categories]
        
        # Step 4: Create counting summary for this frame
        counting_summary = {
            "total_objects": len(frame_detections),
            "detections": frame_detections,
            "categories": {}
        }
        
        # Count by category
        for detection in frame_detections:
            category = detection.get("category", "unknown")
            counting_summary["categories"][category] = counting_summary["categories"].get(category, 0) + 1
        
        # Step 5: Zone analysis for this frame
        zone_analysis = {}
        if config.zone_config and config.zone_config.zones:
            # Convert single frame to format expected by count_objects_in_zones
            frame_data = frame_detections #[frame_detections]
            zone_analysis = count_objects_in_zones(frame_data, config.zone_config.zones)
            
            # Update zone tracking with current frame data
            if zone_analysis and config.enable_tracking:
                enhanced_zone_analysis = self._update_zone_tracking(zone_analysis, frame_detections, config)
                # Merge enhanced zone analysis with original zone analysis
                for zone_name, enhanced_data in enhanced_zone_analysis.items():
                    zone_analysis[zone_name] = enhanced_data
        
        # Step 4.5: Always update tracking state (regardless of enable_unique_counting setting)
        self._update_tracking_state(counting_summary)
        
        # Step 5: Generate insights and alerts for this frame
        insights = self._generate_insights(counting_summary, zone_analysis, config)
        alerts = self._check_alerts(counting_summary, zone_analysis, config)
        
        # Step 6: Generate events for this frame
        events = self._generate_events(counting_summary, zone_analysis, alerts, config)
        
        # Step 7: Generate tracking stats for this frame
        summary = self._generate_summary(counting_summary, zone_analysis, alerts)
        tracking_stats = self._generate_tracking_stats(counting_summary, zone_analysis, insights, summary, config, frame_id=frame_id)
        
        return self.create_result(
            data={
                "events": events,
                "tracking_stats": tracking_stats,
                "counting_summary": counting_summary,
                "zone_analysis": zone_analysis
            },
            usecase=self.name,
            category=self.category,
            context=context
        )
    
    def _process_single_frame_data(self, data: Any, config: PeopleCountingConfig, context: ProcessingContext) -> ProcessingResult:
        """Process single frame data (existing logic unchanged)."""
        start_time = time.time()
        
        try:
            # Step 1: Apply confidence filtering
            processed_data = data
            if config.confidence_threshold is not None:
                processed_data = filter_by_confidence(processed_data, config.confidence_threshold)
                self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
            
            # Step 2: Apply category mapping if provided
            if config.index_to_category:
                processed_data = apply_category_mapping(processed_data, config.index_to_category)
                self.logger.debug("Applied category mapping")
            
            # Step 2.5: Filter to only include person categories
            person_processed_data = processed_data
            if config.person_categories:
                person_processed_data = filter_by_categories(processed_data.copy(), config.person_categories)
                self.logger.debug(f"Applied person category filtering for: {config.person_categories}")
            
            # Step 2.6: Apply bbox smoothing if enabled
            if config.enable_smoothing:
                # Initialize smoothing tracker if not exists
                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 or 0.5,
                        confidence_range_factor=config.smoothing_confidence_range_factor,
                        enable_smoothing=True
                    )
                    self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
                
                # Apply smoothing to person detections (handles both list and dict formats)
                smoothed_persons = bbox_smoothing(person_processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
                person_processed_data = smoothed_persons
                self.logger.debug(f"Applied bbox smoothing with {len(smoothed_persons) if isinstance(smoothed_persons, list) else len(smoothed_persons)} smoothed detections")
            
            # Step 3: Calculate comprehensive counting summary
            zones = config.zone_config.zones if config.zone_config else None
            person_counting_summary = calculate_counting_summary(
                person_processed_data,
                zones=zones
            )
            general_counting_summary = calculate_counting_summary(
                processed_data,
                zones=zones
            )
            
            # Add detections to the counting summary for tracking
            person_counting_summary["detections"] = person_processed_data
            general_counting_summary["detections"] = processed_data
            
            # Step 4: Zone-based analysis if zones are configured
            zone_analysis = {}
            if config.zone_config and config.zone_config.zones:
                zone_analysis = count_objects_in_zones(
                    person_processed_data, 
                    config.zone_config.zones
                )
                self.logger.debug(f"Analyzed {len(config.zone_config.zones)} zones")
                
                # Step 4.5: Update zone tracking with current frame data
                if zone_analysis and config.enable_tracking:
                    detections = self._get_detections_with_confidence(person_counting_summary)
                    enhanced_zone_analysis = self._update_zone_tracking(zone_analysis, detections, config)
                    # Merge enhanced zone analysis with original zone analysis
                    for zone_name, enhanced_data in enhanced_zone_analysis.items():
                        zone_analysis[zone_name] = enhanced_data
            
            # Step 4.6: Always update tracking state (regardless of enable_unique_counting setting)
            self._update_tracking_state(person_counting_summary)
            
            # Step 5: Generate insights and alerts
            insights = self._generate_insights(person_counting_summary, zone_analysis, config)
            alerts = self._check_alerts(person_counting_summary, zone_analysis, config)
            
            # Step 6: Calculate detailed metrics
            metrics = self._calculate_metrics(person_counting_summary, zone_analysis, config, context)
            
            # Step 7: Extract predictions for API compatibility
            predictions = self._extract_predictions(processed_data)
            
            # Step 8: Generate human-readable summary
            summary = self._generate_summary(person_counting_summary, zone_analysis, alerts)
            
            # Step 9: Generate structured events and tracking stats
            events = self._generate_events(person_counting_summary, zone_analysis, alerts, config)
            tracking_stats = self._generate_tracking_stats(person_counting_summary, zone_analysis, insights, summary, config)
            
            # Mark processing as completed
            context.mark_completed()
            
            # Create successful result
            result = self.create_result(
                data={
                    "general_counting_summary": general_counting_summary,
                    "counting_summary": person_counting_summary,
                    "zone_analysis": zone_analysis,
                    "alerts": alerts,
                    "total_people": person_counting_summary.get("total_objects", 0),
                    "zones_count": len(config.zone_config.zones) if config.zone_config else 0,
                    "events": events,
                    "tracking_stats": tracking_stats
                },
                usecase=self.name,
                category=self.category,
                context=context
            )
            
            # Add human-readable information
            result.summary = summary
            result.insights = insights
            result.predictions = predictions
            result.metrics = metrics
            
            # Add warnings for low confidence detections
            if config.confidence_threshold and config.confidence_threshold < 0.3:
                result.add_warning(f"Low confidence threshold ({config.confidence_threshold}) may result in false positives")
            
            processing_time = context.processing_time or time.time() - start_time
            self.logger.info(f"People counting completed successfully in {processing_time:.2f}s")
            return result
            
        except Exception as e:
            self.logger.error(f"People counting failed: {str(e)}", exc_info=True)
            
            if context:
                context.mark_completed()
            
            return self.create_error_result(
                str(e), 
                type(e).__name__,
                usecase=self.name,
                category=self.category,
                context=context
            )
    
    def _generate_insights(self, counting_summary: Dict, zone_analysis: Dict, 
                          config: PeopleCountingConfig) -> List[str]:
        """Generate human-readable insights from counting results."""
        insights = []
        
        total_people = counting_summary.get("total_objects", 0)
        
        if total_people == 0:
            insights.append("No people detected in the scene")
            return insights
        
        # Basic count insight
        insights.append(f"EVENT : Detected {total_people} people in the scene")
        
        # Intensity calculation based on threshold percentage
        intensity_threshold = None
        if (config.alert_config and 
            config.alert_config.count_thresholds and 
            "all" in config.alert_config.count_thresholds):
            intensity_threshold = config.alert_config.count_thresholds["all"]
        
        if intensity_threshold is not None:
            # Calculate percentage relative to threshold
            percentage = (total_people / intensity_threshold) * 100
            
            if percentage < 20:
                insights.append(f"INTENSITY: Low occupancy in the scene ({percentage:.1f}% of capacity)")
            elif percentage <= 50:
                insights.append(f"INTENSITY: Medium occupancy in the scene ({percentage:.1f}% of capacity)")
            elif percentage <= 70:
                insights.append(f"INTENSITY: High occupancy in the scene ({percentage:.1f}% of capacity)")
            else:
                insights.append(f"INTENSITY: Very high density in the scene ({percentage:.1f}% of capacity)")
        else:
            # Fallback to hardcoded thresholds if no alert config is set
            if total_people > 10:
                insights.append(f"INTENSITY: High density in the scene with {total_people} people")
            elif total_people == 1:
                insights.append("INTENSITY: Low occupancy in the scene")
        
        # Zone-specific insights
        if zone_analysis:
            def robust_zone_total(zone_counts):
                if isinstance(zone_counts, dict):
                    total = 0
                    for v in zone_counts.values():
                        if isinstance(v, int):
                            total += v
                        elif isinstance(v, list):
                            total += len(v)
                    return total
                elif isinstance(zone_counts, list):
                    return len(zone_counts)
                elif isinstance(zone_counts, int):
                    return zone_counts
                else:
                    return 0
            zones_with_people = sum(1 for zone_counts in zone_analysis.values() if robust_zone_total(zone_counts) > 0)
            insights.append(f"People detected across {zones_with_people}/{len(zone_analysis)} zones")
        
        # Category breakdown insights
        if "by_category" in counting_summary:
            category_counts = counting_summary["by_category"]
            for category, count in category_counts.items():
                if count > 0 and category in config.person_categories:
                    percentage = (count / total_people) * 100
                    insights.append(f"Category '{category}': {count} detections")
        
        # Time-based insights
        if config.time_window_minutes:
            rate_per_hour = (total_people / config.time_window_minutes) * 60
            insights.append(f"Detection rate: {rate_per_hour:.1f} people per hour")
        
        # Unique counting insights
        if config.enable_unique_counting:
            unique_count = self._count_unique_tracks(counting_summary, config)
            if unique_count is not None:
                insights.append(f"Unique people count: {unique_count}")
                if unique_count != total_people:
                    insights.append(f"Detection efficiency: {unique_count}/{total_people} unique tracks")
        
        return insights
    
    def _check_alerts(self, counting_summary: Dict, zone_analysis: Dict, 
                     config: PeopleCountingConfig) -> List[Dict]:
        """Check for alert conditions and generate alerts."""
        alerts = []
        
        if not config.alert_config:
            return alerts
        
        total_people = counting_summary.get("total_objects", 0)
        
        # Count threshold alerts
        if config.alert_config.count_thresholds:
            for category, threshold in config.alert_config.count_thresholds.items():
                if category == "all" and total_people >= threshold:
                    alerts.append({
                        "type": "count_threshold",
                        "severity": "warning",
                        "message": f"Total people count ({total_people}) exceeds threshold ({threshold})",
                        "category": category,
                        "current_count": total_people,
                        "threshold": threshold
                    })
                elif category in counting_summary.get("by_category", {}):
                    count = counting_summary["by_category"][category]
                    if count >= threshold:
                        alerts.append({
                            "type": "count_threshold",
                            "severity": "warning",
                            "message": f"{category} count ({count}) exceeds threshold ({threshold})",
                            "category": category,
                            "current_count": count,
                            "threshold": threshold
                        })
        
        # Zone occupancy threshold alerts
        if config.alert_config.occupancy_thresholds:
            for zone_name, threshold in config.alert_config.occupancy_thresholds.items():
                if zone_name in zone_analysis:
                    zone_count = sum(zone_analysis[zone_name].values()) if isinstance(zone_analysis[zone_name], dict) else zone_analysis[zone_name]
                    if zone_count >= threshold:
                        alerts.append({
                            "type": "occupancy_threshold",
                            "severity": "warning",
                            "message": f"Zone '{zone_name}' occupancy ({zone_count}) exceeds threshold ({threshold})",
                            "zone": zone_name,
                            "current_occupancy": zone_count,
                            "threshold": threshold
                        })
        
        return alerts
    
    def _calculate_metrics(self, counting_summary: Dict, zone_analysis: Dict, 
                          config: PeopleCountingConfig, context: ProcessingContext) -> Dict[str, Any]:
        """Calculate detailed metrics for analytics."""
        total_people = counting_summary.get("total_objects", 0)
        
        metrics = {
            "total_people": total_people,
            "processing_time": context.processing_time or 0.0,
            "input_format": context.input_format.value,
            "confidence_threshold": config.confidence_threshold,
            "zones_analyzed": len(zone_analysis),
            "detection_rate": 0.0,
            "coverage_percentage": 0.0
        }
        
        # Calculate detection rate
        if config.time_window_minutes and config.time_window_minutes > 0:
            metrics["detection_rate"] = (total_people / config.time_window_minutes) * 60
        
        # Calculate zone coverage
        if zone_analysis and total_people > 0:
            people_in_zones = 0
            for zone_counts in zone_analysis.values():
                if isinstance(zone_counts, dict):
                    for v in zone_counts.values():
                        if isinstance(v, int):
                            people_in_zones += v
                        elif isinstance(v, list):
                            people_in_zones += len(v)
                elif isinstance(zone_counts, list):
                    people_in_zones += len(zone_counts)
                elif isinstance(zone_counts, int):
                    people_in_zones += zone_counts
            metrics["coverage_percentage"] = (people_in_zones / total_people) * 100
        
        # Unique tracking metrics
        if config.enable_unique_counting:
            unique_count = self._count_unique_tracks(counting_summary, config)
            if unique_count is not None:
                metrics["unique_people"] = unique_count
                metrics["tracking_efficiency"] = (unique_count / total_people) * 100 if total_people > 0 else 0
        
        # Per-zone metrics
        if zone_analysis:
            zone_metrics = {}
            for zone_name, zone_counts in zone_analysis.items():
                # Robustly sum counts, handling dicts with int or list values
                if isinstance(zone_counts, dict):
                    zone_total = 0
                    for v in zone_counts.values():
                        if isinstance(v, int):
                            zone_total += v
                        elif isinstance(v, list):
                            zone_total += len(v)
                elif isinstance(zone_counts, list):
                    zone_total = len(zone_counts)
                elif isinstance(zone_counts, int):
                    zone_total = zone_counts
                else:
                    zone_total = 0
                zone_metrics[zone_name] = {
                    "count": zone_total,
                    "percentage": (zone_total / total_people) * 100 if total_people > 0 else 0
                }
            metrics["zone_metrics"] = zone_metrics
        
        return metrics
    
    def _extract_predictions(self, data: Any) -> List[Dict[str, Any]]:
        """Extract predictions from processed data for API compatibility."""
        predictions = []
        
        try:
            if isinstance(data, list):
                # Detection format
                for item in data:
                    prediction = self._normalize_prediction(item)
                    if prediction:
                        predictions.append(prediction)
            
            elif isinstance(data, dict):
                # Frame-based or tracking format
                for frame_id, items in data.items():
                    if isinstance(items, list):
                        for item in items:
                            prediction = self._normalize_prediction(item)
                            if prediction:
                                prediction["frame_id"] = frame_id
                                predictions.append(prediction)
        
        except Exception as e:
            self.logger.warning(f"Failed to extract predictions: {str(e)}")
        
        return predictions
    
    def _normalize_prediction(self, item: Dict[str, Any]) -> Dict[str, Any]:
        """Normalize a single prediction item."""
        if not isinstance(item, dict):
            return {}
        
        return {
            "category": item.get("category", item.get("class", "unknown")),
            "confidence": item.get("confidence", item.get("score", 0.0)),
            "bounding_box": item.get("bounding_box", item.get("bbox", {})),
            "track_id": item.get("track_id")
        }
    
    def _get_detections_with_confidence(self, counting_summary: Dict) -> List[Dict]:
        """Extract detection items with confidence scores."""
        return counting_summary.get("detections", [])
    
    def _count_unique_tracks(self, counting_summary: Dict, config: PeopleCountingConfig = None) -> Optional[int]:
        """Count unique tracks if tracking is enabled."""
        # Always update tracking state regardless of enable_unique_counting setting
        self._update_tracking_state(counting_summary)
        
        # Only return the count if unique counting is enabled
        if config and config.enable_unique_counting:
            return self._total_count if self._total_count > 0 else None
        else:
            return None
    
    def _update_tracking_state(self, counting_summary: Dict) -> None:
        """Update tracking state with current frame data (always called)."""
        detections = self._get_detections_with_confidence(counting_summary)

        if not detections:
            return

        # Map raw tracker IDs to canonical IDs to avoid duplicate counting
        current_frame_tracks: Set[Any] = set()

        for detection in detections:
            raw_track_id = detection.get("track_id")
            if raw_track_id is None:
                continue

            bbox = detection.get("bounding_box", detection.get("bbox"))
            if not bbox:
                continue

            canonical_id = self._merge_or_register_track(raw_track_id, bbox)

            # Propagate canonical ID so that downstream logic (including zone
            # tracking and event generation) operates on the de-duplicated ID.
            detection["track_id"] = canonical_id
            current_frame_tracks.add(canonical_id)

        # Update total track IDs with new canonical IDs from current frame
        old_total_count = len(self._total_track_ids)
        self._total_track_ids.update(current_frame_tracks)
        self._current_frame_track_ids = current_frame_tracks

        # Update total count
        self._total_count = len(self._total_track_ids)
        self._last_update_time = time.time()

        # Log tracking state updates
        if len(current_frame_tracks) > 0:
            new_tracks = current_frame_tracks - (self._total_track_ids - current_frame_tracks)
            if new_tracks:
                self.logger.debug(
                    f"Tracking state updated: {len(new_tracks)} new canonical track IDs added, total unique tracks: {self._total_count}")
            else:
                self.logger.debug(
                    f"Tracking state updated: {len(current_frame_tracks)} current frame canonical tracks, total unique tracks: {self._total_count}")
    
    def get_total_count(self) -> int:
        """Get the total count of unique people tracked across all calls."""
        return self._total_count
    
    def get_current_frame_count(self) -> int:
        """Get the count of people in the current frame."""
        return len(self._current_frame_track_ids)
    
    def get_total_frames_processed(self) -> int:
        """Get the total number of frames processed across all calls."""
        return self._total_frame_counter
    
    def set_global_frame_offset(self, offset: int) -> None:
        """Set the global frame offset for video chunk processing."""
        self._global_frame_offset = offset
        self.logger.info(f"Global frame offset set to: {offset}")
    
    def get_global_frame_offset(self) -> int:
        """Get the current global frame offset."""
        return self._global_frame_offset
    
    def update_global_frame_offset(self, frames_in_chunk: int) -> None:
        """Update global frame offset after processing a chunk."""
        old_offset = self._global_frame_offset
        self._global_frame_offset += frames_in_chunk
        self.logger.info(f"Global frame offset updated: {old_offset} -> {self._global_frame_offset} (added {frames_in_chunk} frames)")
    
    def get_global_frame_id(self, local_frame_id: str) -> str:
        """Convert local frame ID to global frame ID."""
        try:
            # Try to convert local_frame_id to integer
            local_frame_num = int(local_frame_id)
            global_frame_num = local_frame_num #+ self._global_frame_offset
            return str(global_frame_num)
        except (ValueError, TypeError):
            # If local_frame_id is not a number (e.g., timestamp), return as is
            return local_frame_id
    
    def get_track_ids_info(self) -> Dict[str, Any]:
        """Get detailed information about track IDs."""
        return {
            "total_count": self._total_count,
            "current_frame_count": len(self._current_frame_track_ids),
            "total_unique_track_ids": len(self._total_track_ids),
            "current_frame_track_ids": list(self._current_frame_track_ids),
            "last_update_time": self._last_update_time,
            "total_frames_processed": self._total_frame_counter
        }
    
    def get_tracking_debug_info(self) -> Dict[str, Any]:
        """Get detailed debugging information about tracking state."""
        return {
            "total_track_ids": list(self._total_track_ids),
            "current_frame_track_ids": list(self._current_frame_track_ids),
            "total_count": self._total_count,
            "current_frame_count": len(self._current_frame_track_ids),
            "total_frames_processed": self._total_frame_counter,
            "last_update_time": self._last_update_time,
            "zone_current_track_ids": {zone: list(tracks) for zone, tracks in self._zone_current_track_ids.items()},
            "zone_total_track_ids": {zone: list(tracks) for zone, tracks in self._zone_total_track_ids.items()},
            "zone_current_counts": self._zone_current_counts.copy(),
            "zone_total_counts": self._zone_total_counts.copy(),
            "global_frame_offset": self._global_frame_offset,
            "frames_in_current_chunk": self._frames_in_current_chunk
        }
    
    def get_frame_info(self) -> Dict[str, Any]:
        """Get detailed information about frame processing and global frame offset."""
        return {
            "global_frame_offset": self._global_frame_offset,
            "total_frames_processed": self._total_frame_counter,
            "frames_in_current_chunk": self._frames_in_current_chunk,
            "next_global_frame": self._global_frame_offset + self._frames_in_current_chunk
        }
    
    def reset_tracking_state(self) -> None:
        """
        ⚠️  WARNING: This completely resets ALL tracking data including cumulative totals!
        
        This should ONLY be used when:
        - Starting a completely new tracking session
        - Switching to a different video/stream
        - Manual reset requested by user
        
        For clearing expired/stale tracks, use clear_current_frame_tracking() instead.
        """
        self._total_track_ids.clear()
        self._current_frame_track_ids.clear()
        self._total_count = 0
        self._last_update_time = time.time()
        
        # Clear zone tracking data
        self._zone_current_track_ids.clear()
        self._zone_total_track_ids.clear()
        self._zone_current_counts.clear()
        self._zone_total_counts.clear()
        
        # Reset frame counter and global frame offset
        self._total_frame_counter = 0
        self._global_frame_offset = 0
        self._frames_in_current_chunk = 0

        # Clear aliasing information
        self._canonical_tracks.clear()
        self._track_aliases.clear()
        
        self.logger.warning("⚠️  FULL tracking state reset - all track IDs, zone data, frame counter, and global frame offset cleared. Cumulative totals lost!")
    
    def clear_current_frame_tracking(self) -> int:
        """
        MANUAL USE ONLY: Clear only current frame tracking data while preserving cumulative totals.
        
        ⚠️  This method is NOT called automatically anywhere in the code.
        
        This is the SAFE method to use for manual clearing of stale/expired current frame data.
        The cumulative total (self._total_count) is always preserved.
        
        In streaming scenarios, you typically don't need to call this at all.
        
        Returns:
            Number of current frame tracks cleared
        """
        old_current_count = len(self._current_frame_track_ids)
        self._current_frame_track_ids.clear()
        
        # Clear current zone tracking (but keep total zone tracking)
        cleared_zone_tracks = 0
        for zone_name in list(self._zone_current_track_ids.keys()):
            cleared_zone_tracks += len(self._zone_current_track_ids[zone_name])
            self._zone_current_track_ids[zone_name].clear()
            self._zone_current_counts[zone_name] = 0
        
        # Update timestamp
        self._last_update_time = time.time()
        
        self.logger.info(f"Cleared {old_current_count} current frame tracks and {cleared_zone_tracks} zone current tracks. Cumulative total preserved: {self._total_count}")
        return old_current_count
    
    def reset_frame_counter(self) -> None:
        """Reset only the frame counter."""
        old_count = self._total_frame_counter
        self._total_frame_counter = 0
        self.logger.info(f"Frame counter reset from {old_count} to 0")
    
    def clear_expired_tracks(self, max_age_seconds: float = 300.0) -> int:
        """
        MANUAL USE ONLY: Clear current frame tracking data if no updates for a while.
        
        ⚠️  This method is NOT called automatically anywhere in the code.
        It's provided as a utility function for manual cleanup if needed.
        
        In streaming scenarios, you typically don't need to call this at all.
        The cumulative total should keep growing as new unique people are detected.
        
        This method only clears current frame tracking data while preserving
        the cumulative total count. The cumulative total should never decrease.
        
        Args:
            max_age_seconds: Maximum age in seconds before clearing current frame tracks
            
        Returns:
            Number of current frame tracks cleared
        """
        current_time = time.time()
        if current_time - self._last_update_time > max_age_seconds:
            # Use the safe method that preserves cumulative totals
            cleared_count = self.clear_current_frame_tracking()
            self.logger.info(f"Manual cleanup: cleared {cleared_count} expired current frame tracks (age > {max_age_seconds}s)")
            return cleared_count
        return 0
    
    def _update_zone_tracking(self, zone_analysis: Dict[str, Dict[str, int]], detections: List[Dict], config: PeopleCountingConfig) -> Dict[str, Dict[str, Any]]:
        """
        Update zone tracking with current frame data.
        
        Args:
            zone_analysis: Current zone analysis results
            detections: List of detections with track IDs
            config: People counting configuration with zone polygons
            
        Returns:
            Enhanced zone analysis with tracking information
        """
        if not zone_analysis or not config.zone_config or not config.zone_config.zones:
            return {}
        
        enhanced_zone_analysis = {}
        zones = config.zone_config.zones
        
        # Get current frame track IDs in each zone
        current_frame_zone_tracks = {}
        
        # Initialize zone tracking for all zones
        for zone_name in zones.keys():
            current_frame_zone_tracks[zone_name] = set()
            if zone_name not in self._zone_current_track_ids:
                self._zone_current_track_ids[zone_name] = set()
            if zone_name not in self._zone_total_track_ids:
                self._zone_total_track_ids[zone_name] = set()
        
        # Check each detection against each zone
        for detection in detections:
            track_id = detection.get("track_id")
            if track_id is None:
                continue
            
            # Get detection bbox
            bbox = detection.get("bounding_box", detection.get("bbox"))
            if not bbox:
                continue
            
            # Get detection center point
            center_point = get_bbox_bottom25_center(bbox) #get_bbox_center(bbox)
            
            # Check which zone this detection is in using actual zone polygons
            for zone_name, zone_polygon in zones.items():
                # Convert polygon points to tuples for point_in_polygon function
                # zone_polygon format: [[x1, y1], [x2, y2], [x3, y3], ...]
                polygon_points = [(point[0], point[1]) for point in zone_polygon]
                
                # Check if detection center is inside the zone polygon using ray casting algorithm
                if point_in_polygon(center_point, polygon_points):
                    current_frame_zone_tracks[zone_name].add(track_id)
        
        # Update zone tracking for each zone
        for zone_name, zone_counts in zone_analysis.items():
            # Get current frame tracks for this zone
            current_tracks = current_frame_zone_tracks.get(zone_name, set())
            
            # Update current zone tracks
            self._zone_current_track_ids[zone_name] = current_tracks
            
            # Update total zone tracks (accumulate all track IDs that have been in this zone)
            self._zone_total_track_ids[zone_name].update(current_tracks)
            
            # Update counts
            self._zone_current_counts[zone_name] = len(current_tracks)
            self._zone_total_counts[zone_name] = len(self._zone_total_track_ids[zone_name])
            
            # Create enhanced zone analysis
            enhanced_zone_analysis[zone_name] = {
                "current_count": self._zone_current_counts[zone_name],
                "total_count": self._zone_total_counts[zone_name],
                "current_track_ids": list(current_tracks),
                "total_track_ids": list(self._zone_total_track_ids[zone_name]),
                "original_counts": zone_counts  # Preserve original zone counts
            }
        
        return enhanced_zone_analysis
    
    def get_zone_tracking_info(self) -> Dict[str, Dict[str, Any]]:
        """Get detailed zone tracking information."""
        return {
            zone_name: {
                "current_count": self._zone_current_counts.get(zone_name, 0),
                "total_count": self._zone_total_counts.get(zone_name, 0),
                "current_track_ids": list(self._zone_current_track_ids.get(zone_name, set())),
                "total_track_ids": list(self._zone_total_track_ids.get(zone_name, set()))
            }
            for zone_name in set(self._zone_current_counts.keys()) | set(self._zone_total_counts.keys())
        }
    
    def get_zone_current_count(self, zone_name: str) -> int:
        """Get current count of people in a specific zone."""
        return self._zone_current_counts.get(zone_name, 0)
    
    def get_zone_total_count(self, zone_name: str) -> int:
        """Get total count of people who have been in a specific zone."""
        return self._zone_total_counts.get(zone_name, 0)
    
    def get_all_zone_counts(self) -> Dict[str, Dict[str, int]]:
        """Get current and total counts for all zones."""
        return {
            zone_name: {
                "current": self._zone_current_counts.get(zone_name, 0),
                "total": self._zone_total_counts.get(zone_name, 0)
            }
            for zone_name in set(self._zone_current_counts.keys()) | set(self._zone_total_counts.keys())
        }
    
    def _generate_summary(self, counting_summary: Dict, zone_analysis: Dict, alerts: List) -> str:
        """Generate human-readable summary."""
        total_people = counting_summary.get("total_objects", 0)
        
        if total_people == 0:
            return "No people detected in the scene"
        
        summary_parts = [f"{total_people} people detected"]
        
        if zone_analysis:
            def robust_zone_total(zone_counts):
                if isinstance(zone_counts, dict):
                    total = 0
                    for v in zone_counts.values():
                        if isinstance(v, int):
                            total += v
                        elif isinstance(v, list):
                            total += len(v)
                    return total
                elif isinstance(zone_counts, list):
                    return len(zone_counts)
                elif isinstance(zone_counts, int):
                    return zone_counts
                else:
                    return 0
            zones_with_people = sum(1 for zone_counts in zone_analysis.values() if robust_zone_total(zone_counts) > 0)
            summary_parts.append(f"across {zones_with_people}/{len(zone_analysis)} zones")
        
        if alerts:
            alert_count = len(alerts)
            summary_parts.append(f"with {alert_count} alert{'s' if alert_count != 1 else ''}")
        
        return ", ".join(summary_parts)
    
    def _generate_events(self, counting_summary: Dict, zone_analysis: Dict, alerts: List, config: PeopleCountingConfig) -> List[Dict]:
        """Generate structured events for the output format."""
        from datetime import datetime, timezone
        
        events = []
        total_people = counting_summary.get("total_objects", 0)
        
        if total_people > 0:
            # Determine event level based on thresholds
            level = "info"
            intensity = 5.0
            
            if config.alert_config and config.alert_config.count_thresholds:
                threshold = config.alert_config.count_thresholds.get("all", 10)
                intensity = min(10.0, (total_people / threshold) * 10)
                
                if intensity >= 7:
                    level = "critical"
                elif intensity >= 5:
                    level = "warning"
                else:
                    level = "info"
            else:
                if total_people > 20:
                    level = "critical"
                    intensity = 9.0
                elif total_people > 10:
                    level = "warning" 
                    intensity = 7.0
                else:
                    level = "info"
                    intensity = min(10.0, total_people / 2.0)
            
            # Main people counting event
            event = {
                "type": "people_counting",
                "stream_time": datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S UTC"),
                "level": level,
                "intensity": round(intensity, 1),
                "config": {
                    "min_value": 0,
                    "max_value": 10,
                    "level_settings": {"info": 2, "warning": 5, "critical": 7}
                },
                "application_name": "People Counting System",
                "application_version": "1.2",
                "location_info": None,
                "human_text": f"Event: People Counting\nCount: {total_people} people detected"
            }
            events.append(event)
        
        # Add zone-specific events if applicable
        if zone_analysis:
            def robust_zone_total(zone_count):
                if isinstance(zone_count, dict):
                    total = 0
                    for v in zone_count.values():
                        if isinstance(v, int):
                            total += v
                        elif isinstance(v, list) and total==0:
                            total += len(v)
                    return total
                elif isinstance(zone_count, list):
                    return len(zone_count)
                elif isinstance(zone_count, int):
                    return zone_count
                else:
                    return 0
            for zone_name, zone_count in zone_analysis.items():
                zone_total = robust_zone_total(zone_count)
                if zone_total > 0:
                    zone_intensity = min(10.0, zone_total / 5.0)
                    zone_level = "info"
                    if zone_intensity >= 7:
                        zone_level = "warning"
                    elif zone_intensity >= 5:
                        zone_level = "info"
                    zone_event = {
                        "type": "zone_occupancy",
                        "stream_time": datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S UTC"),
                        "level": zone_level,
                        "intensity": round(zone_intensity, 1),
                        "config": {
                            "min_value": 0,
                            "max_value": 10,
                            "level_settings": {"info": 2, "warning": 5, "critical": 7}
                        },
                        "application_name": "Zone Monitoring System",
                        "application_version": "1.2",
                        "location_info": zone_name,
                        "human_text": f"Event: Zone Occupancy\nZone: {zone_name}\nCount: {zone_total} people"
                    }
                    events.append(zone_event)
        
        # Add alert events
        for alert in alerts:
            alert_event = {
                "type": alert.get("type", "alert"),
                "stream_time": datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S UTC"),
                "level": alert.get("severity", "warning"),
                "intensity": 8.0,
                "config": {
                    "min_value": 0,
                    "max_value": 10,
                    "level_settings": {"info": 2, "warning": 5, "critical": 7}
                },
                "application_name": "Alert System",
                "application_version": "1.2",
                "location_info": alert.get("zone"),
                "human_text": 'Alert triggered'
            }
            events.append(alert_event)
        
        return events
    
    def _generate_tracking_stats(self, counting_summary: Dict, zone_analysis: Dict, insights: List[str], summary: str, config: PeopleCountingConfig, frame_id: Optional[str] = None) -> List[Dict]:
        """Generate tracking stats in the old format structure with simplified keywords."""
        from datetime import datetime, timezone
        
        tracking_stats = []
        human_text_list= []

        total_people = counting_summary.get("total_objects", 0)
        
        
        if total_people > 0 or zone_analysis:
            # Get total count from cached tracking state
            total_unique_count = self.get_total_count()
            current_frame_count = self.get_current_frame_count()
            
            # Use provided frame_id or generate timestamp
            frame_identifier = frame_id if frame_id else datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")

                        # Create tracking stats in old format structure
            tracking_stat = {
                "tracking_start_time": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
                "all_results_for_tracking": {
                    "total_people": total_people,
                    "zone_analysis": zone_analysis,
                    "counting_summary": counting_summary,
                    "detection_rate": (total_people / config.time_window_minutes * 60) if config.time_window_minutes else 0.0,
                    "zones_count": len(zone_analysis),
                    "people_in_frame": total_people,  # Clear keyword for people in this frame
                    "cumulative_total": total_unique_count,  # Clear keyword for total unique people
                    "current_frame_count": current_frame_count,
                    "track_ids_info": self.get_track_ids_info(),
                    "zone_tracking_info": self.get_zone_tracking_info(),
                    "global_frame_offset": self._global_frame_offset,
                    "local_frame_id": frame_identifier if frame_id else None
                }}

            if zone_analysis:
                def robust_zone_total(zone_count):
                    if isinstance(zone_count, dict):
                        total = 0
                        for v in zone_count.values():
                            if isinstance(v, int):
                                total += v
                            elif isinstance(v, list) and total==0:
                                total += len(v)
                        return total
                    elif isinstance(zone_count, list):
                        return len(zone_count)
                    elif isinstance(zone_count, int):
                        return zone_count
                    else:
                        return 0
                human_text_list.append( f"People Detected: {total_people}") 
                for zone_name, zone_count in zone_analysis.items():
                        zone_total = robust_zone_total(zone_count)
                        human_text_list.append( f"Zone Occupancy\nZone: {zone_name}\n Total Count in zone: {zone_total}")
                       
                human_message = "\n".join(human_text_list)  
                tracking_stat["human_text"]= human_message
            else:
                tracking_stat["human_text"]= self._generate_human_text_for_tracking(total_people, total_unique_count, config)    
            tracking_stat["frame_id"]= frame_identifier
            tracking_stat["frames_in_this_call"]= 1  # Single frame processing
            tracking_stat["total_frames_processed"]= self._total_frame_counter
            tracking_stat["current_frame_number"]= frame_identifier
            tracking_stat["global_frame_offset"]= self._global_frame_offset
            
            tracking_stats.append(tracking_stat)
        
        return tracking_stats
    
    def _generate_human_text_for_tracking(self, total_people: int, total_unique_count: int, config: PeopleCountingConfig) -> str:
        """Generate human-readable text for tracking stats in old format."""
        from datetime import datetime, timezone
        
        timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M")
        
        text_parts = [
            f"Tracking Start Time: {timestamp}",
            f"People Detected: {total_people}",
            f"Total Unique People: {total_unique_count}"
        ]
        
        return "\n".join(text_parts)
    
    def test_tracking_persistence(self) -> Dict[str, Any]:
        """Test method to verify tracking state persists across calls."""
        test_data = [
            # First call - 2 people
            [
                {"category": "person", "confidence": 0.8, "track_id": "track_1", "bounding_box": [100, 100, 200, 200]},
                {"category": "person", "confidence": 0.9, "track_id": "track_2", "bounding_box": [300, 300, 400, 400]}
            ],
            # Second call - 3 people (1 new, 2 existing)
            [
                {"category": "person", "confidence": 0.8, "track_id": "track_1", "bounding_box": [110, 110, 210, 210]},
                {"category": "person", "confidence": 0.9, "track_id": "track_2", "bounding_box": [310, 310, 410, 410]},
                {"category": "person", "confidence": 0.7, "track_id": "track_3", "bounding_box": [500, 500, 600, 600]}
            ],
            # Third call - 1 new person
            [
                {"category": "person", "confidence": 0.8, "track_id": "track_4", "bounding_box": [700, 700, 800, 800]}
            ]
        ]
        
        config = self.create_default_config(enable_unique_counting=True)
        results = []
        
        for i, frame_data in enumerate(test_data):
            # Create a simple counting summary
            counting_summary = {
                "total_objects": len(frame_data),
                "detections": frame_data,
                "categories": {"person": len(frame_data)}
            }
            
            # Update tracking state
            self._update_tracking_state(counting_summary)
            
            result = {
                "call_number": i + 1,
                "frame_people": len(frame_data),
                "total_unique_tracks": self._total_count,
                "track_ids": list(self._total_track_ids),
                "current_frame_tracks": list(self._current_frame_track_ids)
            }
            results.append(result)
        
        return {
            "test_results": results,
            "final_total_count": self._total_count,
            "final_track_ids": list(self._total_track_ids),
            "debug_info": self.get_tracking_debug_info()
        }

    # --------------------------------------------------------------------- #
    # Private helpers for canonical track aliasing                           #
    # --------------------------------------------------------------------- #

    def _compute_iou(self, box1: Any, box2: Any) -> float:
        """Compute IoU between two bounding boxes that may be either list or dict.
        Falls back to geometry_utils.calculate_iou when both boxes are dicts.
        """
        # Handle dict format directly with calculate_iou (supports many keys)
        if isinstance(box1, dict) and isinstance(box2, dict):
            return calculate_iou(box1, box2)

        # Helper to convert bbox (dict or list) to a list [x1,y1,x2,y2]
        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"]]
                # Fallback: take first four values in insertion order
                values = list(bbox.values())
                return values[:4] if len(values) >= 4 else []
            # Unsupported type
            return []

        list1 = _bbox_to_list(box1)
        list2 = _bbox_to_list(box2)

        if len(list1) < 4 or len(list2) < 4:
            return 0.0

        x1_min, y1_min, x1_max, y1_max = list1
        x2_min, y2_min, x2_max, y2_max = list2

        # Ensure correct ordering of coordinates
        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 _get_canonical_id(self, raw_id: Any) -> Any:
        """Return the canonical ID for a raw tracker-generated ID."""
        return self._track_aliases.get(raw_id, raw_id)

    def _merge_or_register_track(self, raw_id: Any, bbox: List[float]) -> Any:
        """Merge the raw track into an existing canonical track if possible,
        otherwise register it as a new canonical track. Returns the canonical
        ID to use for counting.
        """
        now = time.time()

        # Fast path: raw_id already mapped
        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

        # Attempt to merge with an existing canonical track
        for canonical_id, info in self._canonical_tracks.items():
            # Only consider recently updated tracks to avoid stale matches
            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:
                # Merge raw_id into canonical track
                self._track_aliases[raw_id] = canonical_id
                info["last_bbox"] = bbox
                info["last_update"] = now
                info["raw_ids"].add(raw_id)
                self.logger.debug(
                    f"Merged raw track {raw_id} into canonical track {canonical_id} (IoU={iou:.2f})")
                return canonical_id

        # No match found – create a new canonical track
        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},
        }
        self.logger.debug(f"Registered new canonical track {canonical_id}")
        return canonical_id 