"""
Landslide Detection Use Case for Post-Processing.

Model classes:
- 0: landslide

A single "landslide" category drives all incidents, alerts, analytics, and
visualisation outputs.  An alert is raised as soon as **at least 1** landslide
detection is present in a frame.
"""

from typing import Any, Dict, List, Optional, Tuple
import time
from datetime import datetime, timezone

from dataclasses import dataclass, field

from ..core.base import (
    BaseProcessor,
    ProcessingContext,
    ProcessingResult,
    ConfigProtocol,
    ResultFormat,
)
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,
)
from ..core.config import BaseConfig, AlertConfig, ZoneConfig
from ..utils.geometry_utils import (
    get_bbox_center,
    point_in_polygon,
    get_bbox_bottom25_center,
)


# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------


@dataclass
class LandslideDetectionConfig(BaseConfig):
    """Configuration for landslide detection post-processing."""

    # BBox smoothing
    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.15
    enable_class_aggregation: bool = False
    class_aggregation_window_size: int = 30

    zone_config: Optional[Dict[str, List[List[float]]]] = None

    # Only one active category for this use case.
    usecase_categories: List[str] = field(default_factory=lambda: ["landslide"])
    target_categories: List[str] = field(default_factory=lambda: ["landslide"])

    # Fire an alert as soon as ≥ 1 landslide is detected.
    alert_config: Optional[AlertConfig] = field(
        default_factory=lambda: AlertConfig(count_thresholds={"landslide": 1})
    )

    index_to_category: Optional[Dict[int, str]] = field(
        default_factory=lambda: {0: "landslide"}
    )


# ---------------------------------------------------------------------------
# Processor
# ---------------------------------------------------------------------------


class LandslideDetectionUseCase(BaseProcessor):
    """Post-processor for landslide detection model outputs."""

    CATEGORY_DISPLAY: Dict[str, str] = {"landslide": "Landslide"}

    def __init__(self) -> None:
        super().__init__("landslide_detection")
        self.category: str = "environmental"
        self.CASE_TYPE: Optional[str] = "landslide_detection"
        self.CASE_VERSION: Optional[str] = "1.0"

        self.target_categories: List[str] = ["landslide"]

        # Tracker state
        self.smoothing_tracker: Optional[BBoxSmoothingTracker] = None
        self.tracker = None
        self._total_frame_counter: int = 0
        self._global_frame_offset: int = 0
        self._tracking_start_time: Optional[float] = None
        self._track_aliases: Dict[Any, Any] = {}
        self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
        self._track_merge_iou_threshold: float = 0.6
        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: Optional[str] = None

        # Per-category cumulative and per-frame track-ID sets.
        self._per_category_total_track_ids: Dict[str, set] = {
            cat: set() for cat in self.target_categories
        }
        self._current_frame_track_ids: Dict[str, set] = {
            cat: set() for cat in self.target_categories
        }
        self._new_track_ids_this_frame: Dict[str, set] = {
            cat: set() for cat in self.target_categories
        }
        self._tracked_in_zones: set = set()
        self._total_count: int = 0
        self._last_update_time: float = time.time()
        self._total_count_list: List[Any] = []

        # Zone-based tracking storage
        self._zone_current_track_ids: Dict[str, set] = {}
        self._zone_total_track_ids: Dict[str, set] = {}
        self._zone_current_counts: Dict[str, int] = {}
        self._zone_total_counts: Dict[str, int] = {}

    # ------------------------------------------------------------------
    # Public entry point
    # ------------------------------------------------------------------

    def process(
        self,
        data: Any,
        config: ConfigProtocol,
        context: Optional[ProcessingContext] = None,
        stream_info: Optional[Dict[str, Any]] = None,
    ) -> ProcessingResult:
        """Run full landslide detection post-processing pipeline.

        Args:
            data: Raw model detections (list or YOLO-style dict).
            config: Must be a :class:`LandslideDetectionConfig` instance.
            context: Optional processing context carrying metadata.
            stream_info: Stream/video metadata used for timestamps.

        Returns:
            :class:`ProcessingResult` containing ``agg_summary`` payload.
        """
        processing_start = time.time()

        is_valid_config = isinstance(config, LandslideDetectionConfig) or (
            hasattr(config, "usecase")
            and config.usecase == "landslide_detection"
            and hasattr(config, "category")
            and config.category == "environmental"
        )
        if not is_valid_config:
            self.logger.error(
                f"Config validation failed in landslide_detection. "
                f"Got type={type(config).__name__}, module={type(config).__module__}, "
                f"usecase={getattr(config, 'usecase', 'N/A')}, "
                f"category={getattr(config, 'category', 'N/A')}"
            )
            return self.create_error_result(
                f"Invalid config type: expected LandslideDetectionConfig or config with "
                f"usecase='landslide_detection', got {type(config).__name__} with "
                f"usecase={getattr(config, 'usecase', 'N/A')}",
                usecase=self.name,
                category=self.category,
                context=context,
            )

        if context is None:
            context = ProcessingContext()

        has_zones = bool(config.zone_config and config.zone_config.get("zones"))

        # Normalise YOLO-style payloads to internal schema.
        data = self._normalize_yolo_results(data, getattr(config, "index_to_category", None))
        self.logger.debug(f"[landslide_detection] normalized_input={data}")

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

        # Confidence filtering
        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("Skipped confidence filtering – no threshold provided")

        # Index-to-category label mapping
        if config.index_to_category:
            processed_data = apply_category_mapping(processed_data, config.index_to_category)

        # Retain only target category detections
        if config.target_categories:
            processed_data = [
                d for d in processed_data if d.get("category") in config.target_categories
            ]
            self.logger.debug("Applied target category filtering")

        processed_data = [
            d for d in processed_data if d.get("category") in self.target_categories
        ]

        # Normalise alternative track-ID field names to ``track_id``
        for det in processed_data:
            if not isinstance(det, dict):
                continue
            if det.get("track_id") is not None:
                continue
            for key in ("tracker_id", "tracking_id", "trackId", "trackID", "id", "object_id"):
                candidate = det.get(key)
                if candidate is not None:
                    det["track_id"] = candidate
                    break

        # BBox smoothing
        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
            )

        # Advanced tracking for stable IDs and cumulative unique counting
        try:
            from ..advanced_tracker import AdvancedTracker
            from ..advanced_tracker.config import TrackerConfig

            if self.tracker is None:
                tracker_config = TrackerConfig(
                    track_high_thresh=0.4,
                    track_low_thresh=0.05,
                    new_track_thresh=0.3,
                    match_thresh=0.8,
                    track_buffer=int(600),
                    max_time_lost=int(1200),
                    frame_rate=25,
                    enable_class_aggregation=config.enable_class_aggregation,
                    class_aggregation_window_size=config.class_aggregation_window_size,
                )
                tracker_namespace = None
                if stream_info and stream_info.get("stream_key"):
                    tracker_namespace = str(hash(stream_info["stream_key"]) % 1_000_000)
                self.tracker = AdvancedTracker(tracker_config, namespace=tracker_namespace)
                self.tracker.restore_state()
                self.logger.info(
                    f"Initialized AdvancedTracker for Landslide Detection "
                    f"(class_aggregation={config.enable_class_aggregation}, "
                    f"namespace={tracker_namespace})"
                )
            processed_data = self.tracker.update(processed_data)
        except Exception as exc:
            self.logger.warning(f"AdvancedTracker failed: {exc}")

        self._update_tracking_state(processed_data, has_zones=has_zones)
        self._total_frame_counter += 1

        # Resolve frame number from stream_info when available
        frame_number: Optional[int] = 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

        # Counting summaries
        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
        counting_summary["categories"] = {}
        for detection in processed_data:
            category = detection.get("category", "unknown")
            counting_summary["categories"][category] = (
                counting_summary["categories"].get(category, 0) + 1
            )

        # Zone analysis
        zone_analysis: Dict[str, Any] = {}
        if has_zones:
            frame_data = processed_data
            zone_analysis = count_objects_in_zones(
                frame_data, config.zone_config["zones"], stream_info
            )
            if zone_analysis:
                enhanced_zone_analysis = self._update_zone_tracking(
                    zone_analysis, processed_data, config
                )
                for zone_name, enhanced_data in enhanced_zone_analysis.items():
                    zone_analysis[zone_name] = enhanced_data

                per_category_count = {
                    cat: len(self._current_frame_track_ids.get(cat, set()))
                    for cat in self.target_categories
                }
                counting_summary["per_category_count"] = {
                    k: v for k, v in per_category_count.items() if v > 0
                }
                counting_summary["total_count"] = sum(per_category_count.values())

        # Downstream outputs
        alerts = self._check_alerts(counting_summary, zone_analysis, frame_number, config)
        predictions = self._extract_predictions(processed_data)

        incidents_list = self._generate_incidents(
            counting_summary, zone_analysis, alerts, config, frame_number, stream_info
        )
        # Incidents are published through tracking_stats to keep the response
        # schema consistent; the separate incidents key is intentionally empty.
        incidents_list = []
        tracking_stats_list = self._generate_tracking_stats(
            counting_summary, zone_analysis, alerts, config, frame_number, stream_info
        )
        business_analytics_list = self._generate_business_analytics(
            counting_summary, zone_analysis, alerts, config, stream_info, is_empty=True
        )
        summary_list = self._generate_summary(
            counting_summary,
            zone_analysis,
            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,
                "zone_analysis": zone_analysis,
                "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() - processing_start
        processing_latency_ms = proc_time * 1000.0
        processing_fps = (1.0 / proc_time) if proc_time > 0 else None
        print(
            f"latency in ms: {processing_latency_ms} | "
            f"Throughput fps: {processing_fps} | "
            f"Frame_Number: {self._total_frame_counter}"
        )
        return result

    # ------------------------------------------------------------------
    # Zone tracking
    # ------------------------------------------------------------------

    def _update_zone_tracking(
        self,
        zone_analysis: Dict[str, Dict[str, int]],
        detections: List[Dict],
        config: LandslideDetectionConfig,
    ) -> Dict[str, Dict[str, Any]]:
        """Update per-zone track-ID sets and return enhanced zone analysis.

        Args:
            zone_analysis: Raw zone counts produced by ``count_objects_in_zones``.
            detections: Detections list for the current frame (track IDs present).
            config: Use-case configuration carrying zone polygon definitions.

        Returns:
            Enhanced zone analysis dict keyed by zone name.
        """
        if not zone_analysis or not config.zone_config or not config.zone_config.get("zones"):
            return {}

        enhanced_zone_analysis: Dict[str, Dict[str, Any]] = {}
        zones = config.zone_config["zones"]

        track_to_cat: Dict[Any, str] = {
            det.get("track_id"): det.get("category")
            for det in detections
            if det.get("track_id") is not None
        }

        current_frame_zone_tracks: Dict[str, set] = {}

        for zone_name in zones:
            current_frame_zone_tracks[zone_name] = set()
            self._zone_current_track_ids.setdefault(zone_name, set())
            self._zone_total_track_ids.setdefault(zone_name, set())

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

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

            center_point = get_bbox_bottom25_center(bbox)
            in_any_zone = False

            for zone_name, zone_polygon in zones.items():
                polygon_points = [(pt[0], pt[1]) for pt in zone_polygon]
                if point_in_polygon(center_point, polygon_points):
                    current_frame_zone_tracks[zone_name].add(track_id)
                    in_any_zone = True
                    if track_id not in self._total_count_list:
                        self._total_count_list.append(track_id)

            if in_any_zone:
                cat = track_to_cat.get(track_id)
                if cat:
                    self._current_frame_track_ids.setdefault(cat, set()).add(track_id)
                    if track_id not in self._tracked_in_zones:
                        self._tracked_in_zones.add(track_id)

        for zone_name, zone_counts in zone_analysis.items():
            current_tracks = current_frame_zone_tracks.get(zone_name, set())
            self._zone_current_track_ids[zone_name] = current_tracks
            self._zone_total_track_ids[zone_name].update(current_tracks)
            self._zone_current_counts[zone_name] = len(current_tracks)
            self._zone_total_counts[zone_name] = len(self._zone_total_track_ids[zone_name])

            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,
            }

        return enhanced_zone_analysis

    # ------------------------------------------------------------------
    # YOLO normalisation
    # ------------------------------------------------------------------

    def _normalize_yolo_results(
        self,
        data: Any,
        index_to_category: Optional[Dict[int, str]] = None,
    ) -> Any:
        """Normalise YOLO-style outputs to the internal detection schema.

        Handles both list-of-detections and frame_id→list mappings.  Each
        detection dict is normalised to contain at least:
        ``category``, ``confidence``, and ``bounding_box``.

        Args:
            data: Raw model output.
            index_to_category: Optional int-to-label mapping.

        Returns:
            Normalised detection list or dict.
        """

        def to_bbox_dict(d: Dict[str, Any]) -> Dict[str, Any]:
            if "bounding_box" in d and isinstance(d["bounding_box"], dict):
                return d["bounding_box"]
            if "bbox" in d:
                bbox = d["bbox"]
                if isinstance(bbox, dict):
                    return bbox
                if isinstance(bbox, (list, tuple)) and len(bbox) >= 4:
                    x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3]
                    return {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
            if "xyxy" in d and isinstance(d["xyxy"], (list, tuple)) and len(d["xyxy"]) >= 4:
                x1, y1, x2, y2 = d["xyxy"][0], d["xyxy"][1], d["xyxy"][2], d["xyxy"][3]
                return {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
            if "xywh" in d and isinstance(d["xywh"], (list, tuple)) and len(d["xywh"]) >= 4:
                cx, cy, w, h = d["xywh"][0], d["xywh"][1], d["xywh"][2], d["xywh"][3]
                x1, y1, x2, y2 = cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2
                return {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
            return {}

        def resolve_category(d: Dict[str, Any]) -> Tuple[str, Optional[int]]:
            raw_cls = d.get("category", d.get("category_id", d.get("class", d.get("cls"))))
            label_name = d.get("name")
            if isinstance(raw_cls, int):
                if index_to_category and raw_cls in index_to_category:
                    return index_to_category[raw_cls], raw_cls
                return str(raw_cls), raw_cls
            if isinstance(raw_cls, str):
                return raw_cls, None
            if label_name:
                return str(label_name), None
            return "unknown", None

        def normalize_det(det: Dict[str, Any]) -> Dict[str, Any]:
            category_name, category_id = resolve_category(det)
            confidence = det.get("confidence", det.get("conf", det.get("score", 0.0)))
            bbox = to_bbox_dict(det)
            normalised: Dict[str, Any] = {
                "category": category_name,
                "confidence": confidence,
                "bounding_box": bbox,
            }
            if category_id is not None:
                normalised["category_id"] = category_id
            for key in ("track_id", "frame_id", "masks", "segmentation"):
                if key in det:
                    normalised[key] = det[key]
            return normalised

        if isinstance(data, list):
            return [normalize_det(d) if isinstance(d, dict) else d for d in data]
        if isinstance(data, dict):
            normalised_dict: Dict[str, Any] = {}
            for k, v in data.items():
                if isinstance(v, list):
                    normalised_dict[k] = [normalize_det(d) if isinstance(d, dict) else d for d in v]
                elif isinstance(v, dict):
                    normalised_dict[k] = normalize_det(v)
                else:
                    normalised_dict[k] = v
            return normalised_dict
        return data

    # ------------------------------------------------------------------
    # Alerts
    # ------------------------------------------------------------------

    def _check_alerts(
        self,
        summary: Dict[str, Any],
        zone_analysis: Dict[str, Any],
        frame_number: Any,
        config: LandslideDetectionConfig,
    ) -> List[Dict[str, Any]]:
        """Evaluate alert thresholds for the current frame.

        An alert is generated when the number of detected landslides in the
        current frame meets or exceeds the configured threshold (default: 1).

        Args:
            summary: Counting summary produced by ``_count_categories``.
            zone_analysis: Zone-level counts for the current frame.
            frame_number: Current frame index or ``None``.
            config: Use-case configuration with ``alert_config``.

        Returns:
            List of alert dicts (may be empty).
        """

        def get_trend(data: List[int], lookback: int = 900, threshold: float = 0.6) -> bool:
            window = data[-lookback:] if len(data) >= lookback else data
            if len(window) < 2:
                return True
            increasing = sum(1 for i in range(1, len(window)) if window[i] >= window[i - 1])
            return (increasing / (len(window) - 1)) >= threshold

        frame_key = str(frame_number) if frame_number is not None else "current_frame"
        alerts: List[Dict[str, Any]] = []

        if not config.alert_config:
            return alerts

        total_detections = summary.get("total_count", 0)
        per_category_count = summary.get("per_category_count", {})

        if not (
            hasattr(config.alert_config, "count_thresholds")
            and config.alert_config.count_thresholds
        ):
            return alerts

        for category, threshold in config.alert_config.count_thresholds.items():
            triggered = False
            if category == "all" and total_detections >= threshold:
                triggered = True
            elif category in per_category_count and per_category_count[category] >= threshold:
                triggered = True

            if triggered:
                alerts.append(
                    {
                        "alert_type": getattr(config.alert_config, "alert_type", ["Default"]),
                        "alert_id": f"alert_{category}_{frame_key}",
                        "incident_category": self.CASE_TYPE,
                        "threshold_level": threshold,
                        "ascending": get_trend(
                            self._ascending_alert_list, lookback=900, threshold=0.8
                        ),
                        "settings": {
                            t: v
                            for t, v in zip(
                                getattr(config.alert_config, "alert_type", ["Default"]),
                                getattr(config.alert_config, "alert_value", ["JSON"]),
                            )
                        },
                    }
                )
        return alerts

    # ------------------------------------------------------------------
    # Incidents
    # ------------------------------------------------------------------

    def _generate_incidents(
        self,
        counting_summary: Dict[str, Any],
        zone_analysis: Dict[str, Any],
        alerts: List[Dict[str, Any]],
        config: LandslideDetectionConfig,
        frame_number: Optional[int] = None,
        stream_info: Optional[Dict[str, Any]] = None,
    ) -> List[Dict[str, Any]]:
        """Generate incident records for the current frame.

        Severity escalates with detection count:
        - ≥ 1  → low
        - ≥ 5  → medium
        - ≥ 10 → significant
        - ≥ 20 → critical

        Args:
            counting_summary: Frame-level counting summary.
            zone_analysis: Zone-level counts.
            alerts: Alerts generated for this frame.
            config: Use-case configuration.
            frame_number: Current frame index.
            stream_info: Stream metadata.

        Returns:
            List containing a single incident dict (may be empty dict if no
            landslides were detected).
        """
        incidents: List[Dict[str, Any]] = []
        total_detections = counting_summary.get("total_count", 0)
        current_timestamp = self._get_current_timestamp_str(stream_info)
        camera_info = self.get_camera_info_from_stream(stream_info)

        # Trim history window to avoid unbounded growth.
        self._ascending_alert_list = self._ascending_alert_list[-900:]

        if total_detections > 0:
            start_timestamp = self._get_start_timestamp_str(stream_info)

            # Track active-incident lifecycle
            if start_timestamp and self.current_incident_end_timestamp == "N/A":
                self.current_incident_end_timestamp = "Incident still active"
            elif (
                start_timestamp
                and self.current_incident_end_timestamp == "Incident still active"
            ):
                if (
                    len(self._ascending_alert_list) >= 15
                    and sum(self._ascending_alert_list[-15:]) / 15 < 1.5
                ):
                    self.current_incident_end_timestamp = current_timestamp
            elif self.current_incident_end_timestamp not in (
                "Incident still active",
                "N/A",
            ):
                self.current_incident_end_timestamp = "N/A"

            # Severity / intensity determination
            if total_detections >= 20:
                level, intensity = "critical", 10.0
                self._ascending_alert_list.append(3)
            elif total_detections >= 10:
                level, intensity = "significant", 8.0
                self._ascending_alert_list.append(2)
            elif total_detections >= 5:
                level, intensity = "medium", 6.0
                self._ascending_alert_list.append(1)
            else:
                level = "low"
                intensity = min(5.0, float(total_detections))
                self._ascending_alert_list.append(0)

            human_text_lines = [f"LANDSLIDE INCIDENT DETECTED @ {current_timestamp}:"]
            human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE, level)}")
            human_text_lines.append(f"\tLandslides Detected: {total_detections}")
            human_text = "\n".join(human_text_lines)

            alert_settings: List[Dict[str, Any]] = []
            if config.alert_config and hasattr(config.alert_config, "alert_type"):
                alert_settings.append(
                    {
                        "alert_type": getattr(config.alert_config, "alert_type", ["Default"]),
                        "incident_category": self.CASE_TYPE,
                        "threshold_level": (
                            config.alert_config.count_thresholds
                            if hasattr(config.alert_config, "count_thresholds")
                            else {}
                        ),
                        "ascending": True,
                        "settings": {
                            t: v
                            for t, v in zip(
                                getattr(config.alert_config, "alert_type", ["Default"]),
                                getattr(config.alert_config, "alert_value", ["JSON"]),
                            )
                        },
                    }
                )

            event = self.create_incident(
                incident_id=f"{self.CASE_TYPE}_{frame_number}",
                incident_type=self.CASE_TYPE,
                severity_level=level,
                human_text=human_text,
                camera_info=camera_info,
                alerts=alerts,
                alert_settings=alert_settings,
                start_time=start_timestamp,
                end_time=self.current_incident_end_timestamp,
                level_settings={"low": 1, "medium": 3, "significant": 5, "critical": 8},
            )
            incidents.append(event)
        else:
            self._ascending_alert_list.append(0)
            incidents.append({})

        return incidents

    # ------------------------------------------------------------------
    # Tracking statistics
    # ------------------------------------------------------------------

    def _generate_tracking_stats(
        self,
        counting_summary: Dict[str, Any],
        zone_analysis: Dict[str, Any],
        alerts: List[Dict[str, Any]],
        config: LandslideDetectionConfig,
        frame_number: Optional[int] = None,
        stream_info: Optional[Dict[str, Any]] = None,
    ) -> List[Dict[str, Any]]:
        """Build tracking statistics for the current frame.

        Args:
            counting_summary: Frame-level counting summary.
            zone_analysis: Zone-level counts.
            alerts: Active alerts for this frame.
            config: Use-case configuration.
            frame_number: Current frame index.
            stream_info: Stream metadata.

        Returns:
            Single-element list with a tracking stats dict.
        """
        camera_info = self.get_camera_info_from_stream(stream_info)
        tracking_stats: List[Dict[str, Any]] = []

        total_detections = counting_summary.get("total_count", 0)
        total_counts_dict = counting_summary.get("total_counts", {})
        per_category_count = counting_summary.get("per_category_count", {})
        current_timestamp = self._get_current_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
        )

        new_counts_dict = self.get_new_counts_this_frame()

        # Build per-category detection count directly from the detections list –
        # this is the most reliable source of truth.
        raw_detections = counting_summary.get("detections", [])
        detection_count_by_category: Dict[str, int] = {}
        for det in raw_detections:
            cat = det.get("category", "landslide")
            detection_count_by_category[cat] = detection_count_by_category.get(cat, 0) + 1

        total_counts = [
            {"category": cat, "count": count}
            for cat, count in total_counts_dict.items()
            if count > 0
        ]
        current_counts = [
            {"category": cat, "count": count}
            for cat, count in detection_count_by_category.items()
        ]
        if not current_counts and total_detections > 0:
            current_counts = [
                {"category": cat, "count": count}
                for cat, count in per_category_count.items()
            ]
        current_new_counts = [
            {"category": cat, "count": count} for cat, count in new_counts_dict.items()
        ]

        curr_total = sum(c.get("count", 0) for c in current_counts)
        new_total = sum(c.get("count", 0) for c in current_new_counts)
        total_total = sum(c.get("count", 0) for c in total_counts)
        print(
            f"[STATS] F{frame_number} | current={curr_total} new={new_total} "
            f"total={total_total}"
        )

        detections_output: List[Dict[str, Any]] = []
        for detection in counting_summary.get("detections", []):
            bbox = detection.get("bounding_box", {})
            category = detection.get("category", "landslide")
            if detection.get("masks"):
                det_obj = self.create_detection_object(
                    category, bbox, segmentation=detection.get("masks", [])
                )
            elif detection.get("segmentation"):
                det_obj = self.create_detection_object(
                    category, bbox, segmentation=detection.get("segmentation")
                )
            elif detection.get("mask"):
                det_obj = self.create_detection_object(
                    category, bbox, segmentation=detection.get("mask")
                )
            else:
                det_obj = self.create_detection_object(category, bbox)
            detections_output.append(det_obj)

        alert_settings: List[Dict[str, Any]] = []
        if config.alert_config and hasattr(config.alert_config, "alert_type"):
            alert_settings.append(
                {
                    "alert_type": getattr(config.alert_config, "alert_type", ["Default"]),
                    "incident_category": self.CASE_TYPE,
                    "threshold_level": (
                        config.alert_config.count_thresholds
                        if hasattr(config.alert_config, "count_thresholds")
                        else {}
                    ),
                    "ascending": True,
                    "settings": {
                        t: v
                        for t, v in zip(
                            getattr(config.alert_config, "alert_type", ["Default"]),
                            getattr(config.alert_config, "alert_value", ["JSON"]),
                        )
                    },
                }
            )

        human_text_lines: List[str] = [f"CURRENT FRAME @ {current_timestamp}:"]

        if zone_analysis:
            human_text_lines.append("\t- Landslides Detected by Zone:")
            for zone_name, zone_data in zone_analysis.items():
                if isinstance(zone_data, dict):
                    if "current_count" in zone_data:
                        zone_current = zone_data.get("current_count", 0)
                    else:
                        counts_dict = (
                            zone_data.get("original_counts")
                            if isinstance(zone_data.get("original_counts"), dict)
                            else zone_data
                        )
                        zone_current = counts_dict.get(
                            "total",
                            sum(v for v in counts_dict.values() if isinstance(v, (int, float))),
                        )
                else:
                    zone_current = 0
                human_text_lines.append(f"\t\t- {zone_name}: {int(zone_current)}")
        else:
            for cat, count in detection_count_by_category.items():
                new_count = new_counts_dict.get(cat, 0)
                human_text_lines.append(f"\t- Landslides in Frame ({cat}): {count}")
                human_text_lines.append(f"\t- New Landslides (just entered) ({cat}): {new_count}")

        human_text_lines.append("")
        human_text = "\n".join(human_text_lines)

        reset_settings = [
            {"interval_type": "daily", "reset_time": {"value": 0, "time_unit": "hour"}}
        ]
        tracking_stat = self.create_tracking_stats(
            total_counts=total_counts,
            current_counts=current_counts,
            detections=detections_output,
            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_stat["target_categories"] = self.target_categories
        tracking_stat["current_new_counts"] = current_new_counts
        tracking_stats.append(tracking_stat)
        return tracking_stats

    # ------------------------------------------------------------------
    # Business analytics
    # ------------------------------------------------------------------

    def _generate_business_analytics(
        self,
        counting_summary: Dict[str, Any],
        zone_analysis: Dict[str, Any],
        alerts: Any,
        config: LandslideDetectionConfig,
        stream_info: Optional[Dict[str, Any]] = None,
        is_empty: bool = False,
    ) -> List[Dict[str, Any]]:
        """Return business analytics payload.

        Currently returns an empty list because disaster-detection use cases
        do not require business KPIs.  Override this method to add custom
        analytics (e.g., affected area estimation, risk scoring).

        Args:
            counting_summary: Frame-level counting summary.
            zone_analysis: Zone-level counts.
            alerts: Active alerts.
            config: Use-case configuration.
            stream_info: Stream metadata.
            is_empty: If ``True``, always return empty list.

        Returns:
            Empty list.
        """
        if is_empty:
            return []
        return []

    # ------------------------------------------------------------------
    # Human-readable summary
    # ------------------------------------------------------------------

    def _generate_summary(
        self,
        summary: Dict[str, Any],
        zone_analysis: Dict[str, Any],
        incidents: List[Dict[str, Any]],
        tracking_stats: List[Dict[str, Any]],
        business_analytics: List[Dict[str, Any]],
        alerts: List[Dict[str, Any]],
    ) -> List[str]:
        """Assemble a human-readable summary string for the frame.

        Args:
            summary: Counting summary.
            zone_analysis: Zone-level counts.
            incidents: Generated incidents.
            tracking_stats: Generated tracking stats.
            business_analytics: Generated business analytics.
            alerts: Active alerts.

        Returns:
            Single-element list with the summary string.
        """
        lines: List[str] = [
            f"Application Name: {self.CASE_TYPE}",
            f"Application Version: {self.CASE_VERSION}",
        ]
        if incidents:
            lines.append(
                "Incidents: \n\t"
                + incidents[0].get("human_text", "No incidents detected")
            )
        if tracking_stats:
            lines.append(
                "Tracking Statistics: \t"
                + tracking_stats[0].get("human_text", "No tracking statistics detected")
            )
        if business_analytics:
            lines.append(
                "Business Analytics: \t"
                + business_analytics[0].get("human_text", "No business analytics detected")
            )
        if not incidents and not tracking_stats and not business_analytics:
            lines.append("Summary: No Summary Data")

        return ["\n".join(lines)]

    # ------------------------------------------------------------------
    # Tracking helpers
    # ------------------------------------------------------------------

    def _update_tracking_state(
        self, detections: List[Dict[str, Any]], has_zones: bool = False
    ) -> None:
        """Update cumulative and per-frame track-ID sets.

        The update follows a strict ordering to ensure ``new`` counts are
        computed *before* the cumulative total is updated:

        1. Build ``_current_frame_track_ids`` from detections.
        2. Compute ``_new_track_ids_this_frame`` = current − total.
        3. Update ``_per_category_total_track_ids`` with current IDs.

        Args:
            detections: Processed detection list for the current frame.
            has_zones: Whether zone analysis is active.
        """
        if not hasattr(self, "_per_category_total_track_ids"):
            self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
        if not hasattr(self, "_previous_frame_track_ids"):
            self._previous_frame_track_ids = {cat: set() for cat in self.target_categories}

        # Step 1: Build current-frame track-ID sets.
        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._current_frame_track_ids.setdefault(cat, set()).add(canonical_id)

        # Step 2: Compute new track IDs (before updating total).
        self._new_track_ids_this_frame = {
            cat: (
                self._current_frame_track_ids.get(cat, set())
                - self._per_category_total_track_ids.get(cat, set())
            )
            for cat in self.target_categories
        }

        first_cat = self.target_categories[0] if self.target_categories else "landslide"
        current_ids = sorted(list(self._current_frame_track_ids.get(first_cat, set())))
        new_ids = sorted(list(self._new_track_ids_this_frame.get(first_cat, set())))
        total_seen = len(self._per_category_total_track_ids.get(first_cat, set()))
        print(
            f"[TRACK] F{self._total_frame_counter} | det={len(detections)} "
            f"ids={current_ids[:10]}{'...' if len(current_ids) > 10 else ''} "
            f"new={new_ids} total_seen={total_seen}"
        )

        if any(len(ids) > 0 for ids in self._new_track_ids_this_frame.values()):
            print(
                f"[NEW_TRACK] F{self._total_frame_counter} | new_ids={new_ids} "
                f"total_unique={total_seen + len(new_ids)}"
            )

        # Step 3: Update cumulative totals.
        for cat, ids in self._current_frame_track_ids.items():
            self._per_category_total_track_ids.setdefault(cat, set()).update(ids)

        total_seen_after = len(self._per_category_total_track_ids.get(first_cat, set()))
        if total_seen_after > 100 and len(detections) > 0:
            ratio = total_seen_after / max(len(detections), 1)
            if ratio > 20:
                print(
                    f"[WARN] F{self._total_frame_counter} | total_seen={total_seen_after} "
                    f"vs det={len(detections)} (ratio={ratio:.1f}x) – possible tracker "
                    f"instability or use-case recreation"
                )

        self._previous_frame_track_ids = {
            cat: set(ids) for cat, ids in self._current_frame_track_ids.items()
        }

    def get_total_counts(self) -> Dict[str, int]:
        """Return cumulative unique detection counts per category."""
        return {
            cat: len(ids)
            for cat, ids in getattr(self, "_per_category_total_track_ids", {}).items()
        }

    def get_new_counts_this_frame(self) -> Dict[str, int]:
        """Return count of track IDs that appeared for the first time this frame."""
        return {
            cat: len(ids)
            for cat, ids in getattr(self, "_new_track_ids_this_frame", {}).items()
        }

    def get_current_frame_counts(self) -> Dict[str, int]:
        """Return count of all track IDs currently visible in this frame."""
        return {
            cat: len(ids)
            for cat, ids in getattr(self, "_current_frame_track_ids", {}).items()
        }

    def _get_track_ids_info(self, detections: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Summarise track-ID statistics for diagnostics."""
        frame_track_ids = {
            det.get("track_id")
            for det in detections
            if det.get("track_id") is not None
        }
        total_track_ids: set = 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),
        }

    # ------------------------------------------------------------------
    # Track merging / deduplication
    # ------------------------------------------------------------------

    def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
        """Resolve a raw tracker ID to a canonical ID, merging when IoU is high.

        High-IoU detections within the time window are assumed to be the same
        physical event and are collapsed to a single canonical track ID.

        Args:
            raw_id: ID assigned by the underlying tracker.
            bbox: Bounding box of the current detection.

        Returns:
            Canonical track ID.
        """
        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

    def _compute_iou(self, box1: Any, box2: Any) -> float:
        """Compute Intersection-over-Union between two bounding boxes.

        Accepts bounding boxes as lists/tuples ``[x1, y1, x2, y2]`` or dicts
        with keys ``x1/y1/x2/y2`` or ``xmin/ymin/xmax/ymax``.

        Args:
            box1: First bounding box.
            box2: Second bounding box.

        Returns:
            IoU score in ``[0.0, 1.0]``.
        """

        def _bbox_to_list(bbox: Any) -> List[float]:
            if bbox is None:
                return []
            if isinstance(bbox, (list, tuple)):
                return list(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

    # ------------------------------------------------------------------
    # Counting helpers
    # ------------------------------------------------------------------

    def _count_categories(
        self, detections: List[Dict[str, Any]], config: LandslideDetectionConfig
    ) -> Dict[str, Any]:
        """Count detections per category and build summary payload.

        Args:
            detections: Filtered, normalised detection list.
            config: Use-case configuration (unused directly but kept for API
                    consistency with the base class pattern).

        Returns:
            Dict with ``total_count``, ``per_category_count``, and
            ``detections`` keys.
        """
        counts: Dict[str, int] = {}
        for det in detections:
            cat = det.get("category", "unknown")
            counts[cat] = counts.get(cat, 0) + 1
        return {
            "total_count": sum(counts.values()),
            "per_category_count": counts,
            "detections": [
                {
                    "bounding_box": det.get("bounding_box"),
                    "category": det.get("category"),
                    "confidence": det.get("confidence"),
                    "track_id": det.get("track_id"),
                    "frame_id": det.get("frame_id"),
                }
                for det in detections
            ],
        }

    def _extract_predictions(
        self, detections: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """Extract a minimal prediction list from processed detections.

        Args:
            detections: Processed detection list.

        Returns:
            List of ``{category, confidence, bounding_box}`` dicts.
        """
        return [
            {
                "category": det.get("category", "unknown"),
                "confidence": det.get("confidence", 0.0),
                "bounding_box": det.get("bounding_box", {}),
            }
            for det in detections
        ]

    # ------------------------------------------------------------------
    # Timestamp utilities
    # ------------------------------------------------------------------

    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 _format_timestamp(self, timestamp: Any) -> str:
        """Format a timestamp to ``YYYY:MM:DD HH:MM:SS``.

        Accepts a numeric Unix timestamp or a string in the form
        ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.

        Args:
            timestamp: Source timestamp value.

        Returns:
            Formatted string ``YYYY:MM:DD HH:MM:SS``.
        """
        if isinstance(timestamp, (int, float)):
            dt = datetime.fromtimestamp(timestamp, timezone.utc)
            return dt.strftime("%Y:%m:%d %H:%M:%S")

        if not isinstance(timestamp, str):
            return str(timestamp)

        timestamp_clean = timestamp.replace(" UTC", "").strip()
        if "." in timestamp_clean:
            timestamp_clean = timestamp_clean.split(".")[0]

        try:
            if timestamp_clean.count("-") >= 2:
                parts = timestamp_clean.split("-")
                if len(parts) >= 4:
                    return f"{parts[0]}:{parts[1]}:{parts[2]} {'-'.join(parts[3:])}"
        except Exception:
            pass

        return timestamp_clean

    def _get_current_timestamp_str(
        self,
        stream_info: Optional[Dict[str, Any]],
        precision: bool = False,
        frame_id: Optional[str] = None,
    ) -> str:
        """Return a formatted current-frame timestamp string.

        Args:
            stream_info: Stream metadata dict.
            precision: If ``True``, return microsecond-precision ISO timestamp.
            frame_id: Optional explicit frame ID override.

        Returns:
            Formatted timestamp string.
        """
        if not stream_info:
            return "00:00:00.00"

        input_settings = stream_info.get("input_settings", {})

        if precision:
            if input_settings.get("start_frame", "na") != "na":
                return self._format_timestamp(input_settings.get("stream_time", "NA"))
            return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")

        if input_settings.get("start_frame", "na") != "na":
            if frame_id:
                start_time = int(frame_id) / input_settings.get("original_fps", 30)
            else:
                start_time = input_settings.get("start_frame", 30) / input_settings.get(
                    "original_fps", 30
                )
            _ = self._format_timestamp_for_video(start_time)
            return self._format_timestamp(input_settings.get("stream_time", "NA"))

        stream_time_str = (
            stream_info.get("input_settings", {})
            .get("stream_info", {})
            .get("stream_time", "")
        )
        if stream_time_str:
            try:
                ts_clean = stream_time_str.replace(" UTC", "")
                dt = datetime.strptime(ts_clean, "%Y-%m-%d-%H:%M:%S.%f")
                timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
                return self._format_timestamp_for_stream(timestamp)
            except Exception:
                return self._format_timestamp_for_stream(time.time())
        return self._format_timestamp_for_stream(time.time())

    def _get_start_timestamp_str(
        self,
        stream_info: Optional[Dict[str, Any]],
        precision: bool = False,
    ) -> str:
        """Return a formatted start-of-session timestamp string.

        Args:
            stream_info: Stream metadata dict.
            precision: If ``True``, return microsecond-precision ISO timestamp.

        Returns:
            Formatted timestamp string.
        """
        if not stream_info:
            return "00:00:00"

        input_settings = stream_info.get("input_settings", {})

        if precision:
            if self.start_timer is None:
                candidate = input_settings.get("stream_time")
                if not candidate or candidate == "NA":
                    candidate = datetime.now(timezone.utc).strftime(
                        "%Y-%m-%d-%H:%M:%S.%f UTC"
                    )
                self.start_timer = candidate
            elif input_settings.get("start_frame", "na") == 1:
                candidate = input_settings.get("stream_time")
                if not candidate or candidate == "NA":
                    candidate = datetime.now(timezone.utc).strftime(
                        "%Y-%m-%d-%H:%M:%S.%f UTC"
                    )
                self.start_timer = candidate
            return self._format_timestamp(self.start_timer)

        if self.start_timer is None:
            candidate = input_settings.get("stream_time")
            if not candidate or candidate == "NA":
                stream_time_str = (
                    input_settings.get("stream_info", {}).get("stream_time", "")
                )
                if stream_time_str:
                    try:
                        ts_clean = stream_time_str.replace(" UTC", "")
                        dt = datetime.strptime(ts_clean, "%Y-%m-%d-%H:%M:%S.%f")
                        self._tracking_start_time = dt.replace(
                            tzinfo=timezone.utc
                        ).timestamp()
                        candidate = datetime.fromtimestamp(
                            self._tracking_start_time, timezone.utc
                        ).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
                    except Exception:
                        candidate = datetime.now(timezone.utc).strftime(
                            "%Y-%m-%d-%H:%M:%S.%f UTC"
                        )
                else:
                    candidate = datetime.now(timezone.utc).strftime(
                        "%Y-%m-%d-%H:%M:%S.%f UTC"
                    )
            self.start_timer = candidate
            return self._format_timestamp(self.start_timer)

        if input_settings.get("start_frame", "na") == 1:
            candidate = input_settings.get("stream_time")
            if not candidate or candidate == "NA":
                stream_time_str = (
                    input_settings.get("stream_info", {}).get("stream_time", "")
                )
                if stream_time_str:
                    try:
                        ts_clean = stream_time_str.replace(" UTC", "")
                        dt = datetime.strptime(ts_clean, "%Y-%m-%d-%H:%M:%S.%f")
                        ts = dt.replace(tzinfo=timezone.utc).timestamp()
                        candidate = datetime.fromtimestamp(ts, timezone.utc).strftime(
                            "%Y-%m-%d-%H:%M:%S.%f UTC"
                        )
                    except Exception:
                        candidate = datetime.now(timezone.utc).strftime(
                            "%Y-%m-%d-%H:%M:%S.%f UTC"
                        )
                else:
                    candidate = datetime.now(timezone.utc).strftime(
                        "%Y-%m-%d-%H:%M:%S.%f UTC"
                    )
            self.start_timer = candidate
            return self._format_timestamp(self.start_timer)

        if self.start_timer is not None and self.start_timer != "NA":
            return self._format_timestamp(self.start_timer)

        if self._tracking_start_time is None:
            stream_time_str = (
                input_settings.get("stream_info", {}).get("stream_time", "")
            )
            if stream_time_str:
                try:
                    ts_clean = stream_time_str.replace(" UTC", "")
                    dt = datetime.strptime(ts_clean, "%Y-%m-%d-%H:%M:%S.%f")
                    self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
                except Exception:
                    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 _get_tracking_start_time(self) -> str:
        """Return the session start time as a formatted string."""
        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:
        """Capture the current wall-clock time as the tracking session start."""
        self._tracking_start_time = time.time()
