import logging
from datetime import datetime

import numpy as np
from enum import EnumType
from typing import List, Dict

from ultralytics import YOLO
from acmenra_cv.instance.enums import DeviceType
from acmenra_cv.instance.box import Box
from acmenra_cv.instance.obb import Obb
from acmenra_cv.instance.point import Point
from acmenra_cv.instance.polygon import Polygon

from acmenra_cv.instance.instance import Instance
from acmenra_cv.tracker.timed_point import TimedPoint
from acmenra_cv.tracker.timed_point_queue import TimedPointQueue
from acmenra_cv.tracker.tracked_object import TrackedObject


logger = logging.getLogger(__name__)


class Tracker:
    """
    Multi-object tracking system for YOLO detection models with trajectory management.

    Provides persistent object IDs, configurable trajectory history, and support for
    multiple YOLO task types (segmentation, bounding box, oriented bounding box).
    Designed for seamless integration with ADAS pipelines and real-time monitoring systems.

    Key features:
    - Persistent tracking IDs across frames (when `enable_tracking=True`)
    - Configurable trajectory history length via `max_length`
    - Support for YOLO task modes: `segment`, `detect`, `obb`
    - Bi-YOLO mode for ultra-wide panoramic images (detection-only)
    - Automatic cleanup of stale/inactive tracks
    - Dynamic confidence/IoU threshold adjustment for varying conditions

    Designed for seamless integration with downstream processing pipelines such as:
    - ADAS zone analysis and collision detection
    - Multi-camera object re-identification and tracking
    - Real-time trajectory smoothing and motion prediction
    - Event triggering and safety alert generation

    Attributes:
        _model (YOLO): Ultralytics instance instance for inference.
        _device (DeviceType): Hardware backend (CPU/CUDA/MPS).
        _category (EnumType): Semantic class enumeration for label mapping.
        _conf, _iou (float): Detection confidence and NMS thresholds.
        _use_bi_yolo (bool): Flag for panoramic dual-inference mode.
        _max_length (int): Maximum trajectory history length per object.
        _objects (Dict[int, TrackedObject]): Active track state dictionary.

    Note:
        - All spatial coordinates use normalized space `[0.0, 1.0]` unless specified
        - Tracking state is maintained internally via `_objects` dictionary
        - Bi-YOLO mode (`use_bi_yolo=True`) disables persistent tracking (detection-only)
        - Thread-safety: Not inherently thread-safe; use external locks for concurrent access
    """

    def __init__(self,
                 model: YOLO,
                 category: EnumType,
                 device: DeviceType = DeviceType.MPS,
                 conf: float = 0.35,
                 iou: float = 0.7,
                 max_length: int = 30,
                 use_bi_yolo: bool = False) -> None:
        """
        Initializes the Tracker with YOLO instance and configuration parameters.

        Args:
            model (YOLO): Ultralytics YOLO instance instance.
                Must be loaded with appropriate task type (`segment`/`detect`/`obb`).
            category (EnumType): Enumeration class for semantic category mapping.
                Used to convert numeric `class_id` to human-readable labels.
            device (DeviceType, optional): Hardware backend for inference.
                Defaults to `DeviceType.MPS`. Options: `CPU`, `CUDA`, `MPS`.
            conf (float, optional): Detection confidence threshold `[0.0, 1.0]`.
                Defaults to `0.35`. Lower values increase recall but may add false positives.
            iou (float, optional): IoU threshold for Non-Maximum Suppression `[0.0, 1.0]`.
                Defaults to `0.7`. Lower values suppress more overlapping detections.
            max_length (int, optional): Maximum trajectory history length per object.
                Defaults to `30`. Higher values smooth trajectories but increase memory usage.
            use_bi_yolo (bool, optional): Enable panoramic dual-inference mode.
                Defaults to `False`. When `True`, splits ultra-wide images into left/right halves.

        Raises:
            TypeError: If any parameter has incorrect type.

        Note:
            - Bi-YOLO mode disables persistent tracking (detection-only)
            - `max_length` affects both memory footprint and trajectory smoothness
            - Confidence/IoU thresholds can be adjusted dynamically at runtime via properties
        """
        self.model = model
        self.device = device
        self.category = category
        self.conf = conf
        self.iou = iou
        self.use_bi_yolo = use_bi_yolo
        self.max_length = max_length
        self._is_update = True
        self._objects = {}

    @property
    def model(self) -> YOLO:
        """
        YOLO: Gets the object detection instance used for inference.

        Returns:
            YOLO: Ultralytics YOLO instance instance.

        Note:
            - Changing the instance automatically invalidates previous tracking state
            - Via the `_is_update` flag to ensure consistent behavior
            - Model must support the configured task type (`segment`/`detect`/`obb`)
        """
        return self._model

    @model.setter
    def model(self, model: YOLO) -> None:
        """
        Sets the object detection instance with type validation.

        Args:
            model (YOLO): New YOLO instance instance for detection.

        Raises:
            TypeError: If instance is not a YOLO instance.

        Note:
            - Setting a new instance automatically triggers state refresh
            - Via `_is_update = True` to ensure subsequent processing uses updated configuration
            - Existing tracks are NOT automatically migrated; consider resetting `_objects` if needed
        """
        if not isinstance(model, YOLO):
            raise TypeError(f"'instance' must be '<YOLO>', but got {type(model).__name__}")
        self._model = model
        self._is_update = True

    @property
    def device(self) -> DeviceType:
        """
        DeviceType: Gets the hardware backend used for instance inference.

        Returns:
            DeviceType: Current device (`CPU`, `CUDA`, or `MPS`).

        Note:
            - Must match available hardware resources on the target system
            - `CUDA` requires NVIDIA GPU with compatible drivers
            - `MPS` requires Apple Silicon (M1/M2/M3/M4 series)
        """
        return self._device

    @device.setter
    def device(self, device: DeviceType) -> None:
        """
        Sets the hardware backend for instance inference with validation.

        Args:
            device (DeviceType): Target device for inference.

        Raises:
            TypeError: If device is not a `DeviceType` enum member.

        Note:
            - Changing device automatically invalidates previous processing state
            - Via `_is_update` flag to ensure instance is properly reloaded on new device
            - Existing tracks remain valid; only inference backend changes
        """
        if not isinstance(device, DeviceType):
            raise TypeError(f"'device' must be '<DeviceType>', but got {type(device).__name__}")
        self._device = device
        self._is_update = True

    @property
    def category(self) -> EnumType:
        """
        EnumType: Gets the semantic category enumeration class.

        Returns:
            EnumType: Category enum (e.g., `ADASClass`, `CocoClass`).

        Note:
            - Provides human-readable semantic meaning to numeric `class_id`
            - Always corresponds to the current `class_id` property values
            - Example: `ADASClass.car.value == 2` if car is class 2 in the instance
        """
        return self._category

    @category.setter
    def category(self, category: EnumType) -> None:
        """
        Sets the semantic category enumeration with type validation.

        Args:
            category (EnumType): New category enum class.

        Raises:
            TypeError: If category is not an `EnumType` instance.

        Note:
            - Category enum must match the instance's output class indices
            - Used for mapping `class_id` to semantic labels in business logic
            - Changing category does NOT affect existing tracks; only new detections
        """
        if not isinstance(category, EnumType):
            raise TypeError(f"'category' must be '<EnumType>', but got {type(category).__name__}")
        self._category = category

    @property
    def use_bi_yolo(self) -> bool:
        """
        bool: Gets whether bi-YOLO panoramic mode is enabled.

        Returns:
            bool: `True` if bi-YOLO is enabled, `False` otherwise.

        Note:
            - When `True`, system splits ultra-wide images into left/right halves
            - For improved detection accuracy on panoramic views, then merges results
            - Designed for resolutions wider than 1920px (e.g., 3840×1080)
            - Disables persistent tracking functionality (detection-only mode)
        """
        return self._use_bi_yolo

    @use_bi_yolo.setter
    def use_bi_yolo(self, use_bi_yolo: bool) -> None:
        """
        Enables or disables bi-YOLO panoramic processing.

        Args:
            use_bi_yolo (bool): Whether to use bi-YOLO processing.

        Raises:
            TypeError: If use_bi_yolo is not a boolean.

        Note:
            - Enabling bi-YOLO automatically disables object tracking
            - As it's incompatible with the split-processing approach
            - This is a trade-off: improved detection in wide scenes vs. no persistent IDs
        """
        if not isinstance(use_bi_yolo, bool):
            raise TypeError(f"'use_bi_yolo' must be '<bool>', but got {type(use_bi_yolo).__name__}")
        self._use_bi_yolo = use_bi_yolo

    @property
    def max_length(self) -> int:
        """
        int: Gets the maximum trajectory history length per tracked object.

        Returns:
            int: Maximum number of historical points retained per object.

        Note:
            - Specifies how many historical positions are stored per tracked object
            - Affects trajectory visualization smoothness and memory usage
            - Higher values create smoother trajectories but consume more memory
            - Lower values may cause jittery visualization but reduce resource usage
        """
        return self._max_length

    @max_length.setter
    def max_length(self, max_length: int) -> None:
        """
        Sets the maximum trajectory length with validation.

        Args:
            max_length (int): New maximum trajectory length. Must be non-negative.

        Raises:
            TypeError: If max_length is not an integer.
            ValueError: If max_length is negative.

        Note:
            - This change affects only NEW trajectories; existing ones maintain current length
            - No state refresh is needed as this is a pure tracking parameter
            - Without instance implications; can be adjusted at runtime
        """
        if not isinstance(max_length, int) or isinstance(max_length, bool):
            raise TypeError(f"'max_length' must be 'int', but got {type(max_length).__name__}")
        if max_length < 0:
            raise ValueError(f"'max_length' must be >= 0")
        self._max_length = max_length

    @property
    def conf(self) -> float:
        """
        float: Gets the detection confidence threshold.

        Returns:
            float: Confidence threshold in range `[0.0, 1.0]`.

        Note:
            - Detections with confidence below this threshold are filtered out
            - Typical values: `0.25-0.35` (standard), `0.5+` (high confidence)
            - Lower values = more detections but more false positives
            - Higher values = fewer detections but fewer false positives
        """
        return self._conf

    @conf.setter
    def conf(self, conf: float) -> None:
        """
        Sets the detection confidence threshold with validation.

        Args:
            conf (float): Confidence threshold to set `[0.0, 1.0]`.

        Raises:
            TypeError: If conf is not a float.
            ValueError: If conf is outside range `[0.0, 1.0]`.

        Note:
            - Confidence threshold filters low-confidence detections at inference time
            - Adaptive thresholding can be used for varying lighting conditions
            - See `adjust_threshold_by_illumination()` for dynamic adjustment strategies
        """
        if not isinstance(conf, float):
            raise TypeError(f"'conf' must be '<float>', but got {type(conf).__name__}")
        if not 0.0 <= conf <= 1.0:
            raise ValueError(f"'conf' must be in range [0.0, 1.0], got {conf}")
        self._conf = conf

    @property
    def iou(self) -> float:
        """
        float: Gets the IoU (Intersection over Union) threshold for NMS.

        Returns:
            float: IoU threshold in range `[0.0, 1.0]`.

        Note:
            - Used for Non-Maximum Suppression (NMS) during detection post-processing
            - Higher values = more overlapping boxes retained (less aggressive suppression)
            - Lower values = more aggressive suppression of duplicate detections
            - Typical values: `0.5-0.7` for most applications
        """
        return self._iou

    @iou.setter
    def iou(self, iou: float) -> None:
        """
        Sets the IoU threshold for NMS with validation.

        Args:
            iou (float): IoU threshold to set `[0.0, 1.0]`.

        Raises:
            TypeError: If iou is not a float.
            ValueError: If iou is outside range `[0.0, 1.0]`.

        Note:
            - IoU threshold controls duplicate detection suppression aggressiveness
            - Higher values allow more overlapping detections (useful for crowded scenes)
            - Lower values are more aggressive at removing duplicate boxes
        """
        if not isinstance(iou, float):
            raise TypeError(f"'iou' must be '<float>', but got {type(iou).__name__}")
        if not 0 <= iou <= 1.0:
            raise ValueError(f"'iou' must be more then zero!")
        self._iou = iou

    @property
    def objects(self) -> Dict[int, TrackedObject]:
        """
        Dict[int, TrackedObject]: Gets the dictionary of currently tracked objects.

        Returns:
            Dict[int, TrackedObject]: Mapping of track IDs to `TrackedObject` instances.

        Note:
            - Keys are track IDs (integers assigned by YOLO tracker)
            - Values are `TrackedObject` instances with full trajectory history
            - Dictionary is updated every frame during `track()` calls
            - Stale tracks are automatically removed after timeout (configurable)
        """
        return self._objects

    def track(self, frame: np.ndarray, enable_tracking: bool = False) -> List[TrackedObject]:
        """
        Processes a video frame and returns tracking data for all detected objects.

        This method is the main entry point for frame processing, handling:
        1. Frame validation and preparation
        2. Object detection (using appropriate method based on configuration)
        3. Tracking state management (ID assignment, trajectory updates)
        4. Result aggregation for downstream processing

        Args:
            frame (np.ndarray): Input video frame in BGR format `(H, W, 3)`.
            enable_tracking (bool): Whether to use persistent object tracking.
                If `False`, uses detection-only mode (no ID persistence).
                Ignored when `use_bi_yolo=True` (always detection-only).

        Returns:
            List[TrackedObject]: All detected objects with complete tracking data:
                - `id`: Tracking ID from YOLO (skipped if `None`)
                - `instance`: `YOLOInstance` with full detection data
                - `trajectory`: `TimedPointQueue` with historical positions

        Raises:
            TypeError: If frame is not a valid numpy array.
            ValueError: If frame is empty or has invalid dimensions.

        Note:
            - This method modifies the tracker's internal state by updating
              trajectory histories and maintaining object references between frames
            - For stateless detection, use `predict()` or `double_predict()` directly
            - Objects with `None` IDs are skipped (not tracked)
            - Thread-safety: Not inherently thread-safe; use external locks for concurrent access
        """
        if frame is None or not isinstance(frame, np.ndarray):
            raise TypeError(f"Frame must be a valid numpy array, got {type(frame)}!")
        if frame.size == 0:
            raise ValueError(f"Frame is empty!")
        if len(frame.shape) < 2:
            raise ValueError(f"Frame has invalid dimensions!")

        objects = []
        track_time = datetime.now()
        for obj in self.double_predict(image=frame) if self.use_bi_yolo else self.predict(image=frame,
                                                                                          track=enable_tracking):
            if obj.id is None:
                continue

            if obj.id not in self._objects:
                self._objects[obj.id] = TrackedObject(id=obj.id,
                                                      instance=obj,
                                                      trajectory=TimedPointQueue(max_length=self.max_length))
            self._objects[obj.id].instance = obj
            self._objects[obj.id].trajectory.enqueue(TimedPoint(x=obj.box.center.X,
                                                                y=obj.box.center.Y,
                                                                z=obj.box.center.Z,
                                                                timestamp=track_time))
            objects.append(self._objects[obj.id])
        return objects

    def predict(self,
                image: np.ndarray,
                track: bool = False,
                imgsz: int = 1280,
                verbose: bool = False,
                max_det: int = 640,
                half: bool = False,
                iou: float = 0.7) -> List[Instance]:
        """
        Performs YOLO inference and converts results to normalized `YOLOInstance` objects.

        This function handles:
        - Model inference on the provided image
        - Processing segmentation masks, bounding boxes, or OBB results
        - Conversion of normalized coordinates to `YOLOInstance` objects
        - Semantic class mapping via category enum

        Args:
            image (np.ndarray): Input image in BGR format with shape `(H, W, 3)`.
            track (bool): Whether to use tracking mode (persistent IDs).
            imgsz (int): Inference image size. Defaults to `1280`.
            verbose (bool): Enable verbose logging. Defaults to `False`.
            max_det (int): Maximum detections per frame. Defaults to `640`.
            half (bool): Use FP16 inference. Defaults to `False`.
            iou (float): IoU threshold for NMS. Defaults to `0.7`.

        Returns:
            List[Instance]: Detected objects with normalized coordinates.
                Empty list if no valid detections are found.

        Raises:
            TypeError: If any parameter has incorrect type.
            ValueError: If numeric parameters have invalid values.

        Note:
            - Supports three detection modes: segmentation, bbox, OBB
            - Polygon coordinates are normalized to `[0.0, 1.0]`
            - Requires Ultralytics YOLO instance with appropriate task type
            - Use `track=True` for persistent object IDs across frames
        """
        if image is None or not isinstance(image, np.ndarray):
            raise TypeError(f"'image' must be a valid numpy array, got {type(image)}!")
        if not isinstance(track, bool):
            raise TypeError(f"'track' must be a valid numpy array, got {type(image)}!")
        if not isinstance(verbose, bool):
            raise TypeError(f"'verbose' must be a valid numpy array, got {type(image)}!")
        if not isinstance(half, bool):
            raise TypeError(f"'half' must be a valid numpy array, got {type(half)}!")

        if not isinstance(imgsz, int):
            raise TypeError(f"'imgsz' must be '<int>', but got {type(imgsz).__name__}")
        if imgsz < 0:
            raise ValueError(f"'imgsz' must be >= 0, got {imgsz}")

        if not isinstance(max_det, int):
            raise TypeError(f"'max_det' must be '<int>', but got {type(max_det).__name__}")
        if max_det < 0:
            raise ValueError(f"'max_det' must be >= 0, got {max_det}")

        if not isinstance(iou, float):
            raise TypeError(f"'iou' must be '<float>', but got {type(iou).__name__}")
        if iou < 0:
            raise ValueError(f"'iou' must be >= 0, got {iou}")


        if track:
            results = self.model.track(source=image,
                                       device=self.device.value,
                                       conf=self.conf,
                                       imgsz=imgsz,
                                       verbose=verbose,
                                       max_det=max_det,
                                       half=half,
                                       iou=iou,
                                       persist=True)
        else:
            results = self.model.predict(source=image,
                                         device=self.device.value,
                                         conf=self.conf,
                                         imgsz=imgsz,
                                         verbose=verbose,
                                         max_det=max_det,
                                         half=half,
                                         iou=iou)

        detected_objects = []
        for result in results:
            if result.masks is not None:
                detected_objects += self._process_seg_results(result=result)
            elif result.boxes is not None and result.masks is None:
                detected_objects += self._process_bbox_results(result=result)
            elif result.obb is not None:
                detected_objects += self._process_obb_results(result=result)

        return detected_objects

    def double_predict(self,
                       image: np.ndarray,
                       imgsz: int = 640,
                       verbose: bool = False,
                       max_det: int = 640,
                       half: bool = False,
                       iou: float = 0.7) -> List[Instance]:
        """
        Performs YOLO detection on left/right halves of ultra-wide images and merges results.

        This method improves detection accuracy on panoramic images by:
        1. Processing left/right halves independently at full resolution
        2. Correctly mapping normalized coordinates to global image space
        3. Combining results into a unified object list

        Args:
            image (np.ndarray): Input image in BGR format with shape `(H, W, 3)`.
            imgsz (int): Inference image size. Defaults to `640`.
            verbose (bool): Enable verbose logging. Defaults to `False`.
            max_det (int): Maximum detections per frame. Defaults to `640`.
            half (bool): Use FP16 inference. Defaults to `False`.
            iou (float): IoU threshold for NMS. Defaults to `0.7`.

        Returns:
            List[Instance]: Detected objects with coordinates normalized to full image.
                Empty list if no valid detections are found in either half.

        Raises:
            TypeError: If image is not a valid numpy array.
            ValueError: If image width is less than 2 pixels.

        Note:
            - Left half: `[0.0, 0.5]` global normalized X
            - Right half: `[0.5, 1.0]` global normalized X
            - Requires segmentation-capable instance for mask processing
            - Automatically handles empty results from either half
            - Detection-only mode (no tracking IDs assigned)
        """
        if image is None or not isinstance(image, np.ndarray):
            raise TypeError(f"Image must be a valid numpy array, got {type(image)}!")
        if image.shape[1] < 2:
            raise ValueError(f"Image width must be >= 2 pixels, got {image.shape[1]}")

        image_height, image_width = image.shape[:2]
        half_width = image_width // 2

        # Run inference on both halves
        results_left = self.model.predict(source=image[:, :half_width],
                                          device=self.device.value,
                                          conf=self.conf,
                                          imgsz=imgsz,
                                          verbose=verbose,
                                          max_det=max_det,
                                          half=half,
                                          iou=iou)
        results_right = self.model.predict(source=image[:, half_width:],
                                           device=self.device.value,
                                           conf=self.conf,
                                           imgsz=imgsz,
                                           verbose=verbose,
                                           max_det=max_det,
                                           half=half,
                                           iou=iou)

        detected_objects = []

        # Process left half results (scale X: [0.0, 1.0] → [0.0, 0.5])
        for result in results_left:
            if result.masks is None:
                continue

            for box, mask in zip(result.boxes, result.masks.xyn):
                mask_global = mask.copy()
                mask_global[:, 0] *= 0.5  # Scale X-axis
                x1, y1, x2, y2 = box.xyxyn.cpu().numpy().flatten().tolist()
                detected_objects.append(Instance(id=None,
                                                 class_id=box.cls.int().tolist()[0],
                                                 category=self.category(box.cls.int().tolist()[0]),
                                                 conf=box.conf.cpu().numpy().flatten().tolist()[0],
                                                 polygon=Polygon.from_yolo_xyn(xyn=mask_global),
                                                 obb=Obb.from_yolo_obb(xyxyxyxyn=mask_global),
                                                 box=Box(left=x1 * 0.5, right=x2 * 0.5, top=y1, bottom=y2)
                                                 )
                                        )

        # Process right half results (scale + shift X: [0.0, 1.0] → [0.5, 1.0])
        for result in results_right:
            if result.masks is None:
                continue

            for box, mask in zip(result.boxes, result.masks.xyn):
                mask_global = mask.copy()
                mask_global[:, 0] = mask_global[:, 0] * 0.5 + 0.5  # Scale + shift
                x1, y1, x2, y2 = box.xyxyn.cpu().numpy().flatten().tolist()
                detected_objects.append(Instance(id=None,
                                                 class_id=box.cls.int().tolist()[0],
                                                 category=self.category(box.cls.int().tolist()[0]).value,
                                                 conf=box.conf.cpu().numpy().flatten().tolist()[0],
                                                 polygon=Polygon.from_yolo_xyn(xyn=mask_global),
                                                 obb=Obb.from_yolo_obb(xyxyxyxyn=mask_global),
                                                 box=Box(left=x1 * 0.5 + 0.5, right=x2 * 0.5 + 0.5, top=y1, bottom=y2)
                                                 )
                                        )

        return detected_objects

    def remove(self, object_id: int):
        """
        Removes a tracked object by ID from internal state.

        Args:
            object_id (int): Track ID to remove.

        Note:
            - Used for manual cleanup of specific tracks
            - Does not raise error if ID doesn't exist (idempotent)
            - Automatically called for stale tracks during `track()` processing
        """
        if object_id in self._objects:
            del self._objects[object_id]

    def _process_bbox_results(self, result) -> List[Instance]:
        """
        Processes YOLO bounding box results into `YOLOInstance` objects.

        Args:
            result: YOLO result object with boxes.

        Returns:
            List[Instance]: List of processed instances.

        Note:
            - Converts normalized box coordinates to `Polygon` (4 corners)
            - Creates pseudo-OBB from box corners (not true rotated box)
            - Used for detection-only models (`task='detect'`)
        """
        instances = []
        if result.boxes is not None:
            for box in result.boxes:
                x1, y1, x2, y2 = box.xyxyn.cpu().numpy().flatten().tolist()
                points = [Point.safe(x=x1, y=y1),
                          Point.safe(x=x2, y=y1),
                          Point.safe(x=x2, y=y2),
                          Point.safe(x=x1, y=y2)]
                instances.append(Instance(id=box.id.int().tolist()[0] if box.id is not None else None,
                                          class_id=box.cls.int().tolist()[0],
                                          category=self.category(box.cls.int().tolist()[0]),
                                          conf=box.conf.cpu().numpy().flatten().tolist()[0],
                                          polygon=Polygon(points),
                                          obb=Obb(points),
                                          box=Box(left=x1, right=x2, top=y1, bottom=y2)
                                          )
                                 )
        return instances

    def _process_obb_results(self, result) -> List[Instance]:
        """
        Processes YOLO OBB results into `YOLOInstance` objects.

        Args:
            result: YOLO result object with OBB detections.

        Returns:
            List[Instance]: List of processed instances with OBB data.

        Note:
            - Uses true rotated bounding boxes from YOLO OBB models
            - Coordinates are normalized `[0.0, 1.0]`
            - Used for OBB detection models (`task='obb'`)
            - YOLOv8/v11 OBB models only
        """
        instances = []
        if result.obb is not None:
            for obb in result.obb:
                points = obb.xyxyxyxyn.cpu().numpy().reshape(4, 2).tolist()
                xs = [p[0] for p in points]
                ys = [p[1] for p in points]
                instances.append(Instance(id=obb.id.int().tolist()[0] if obb.id is not None else None,
                                          class_id=obb.cls.int().tolist()[0],
                                          category=self.category(obb.cls.int().tolist()[0]),
                                          conf=obb.conf.cpu().numpy().flatten().tolist()[0],
                                          polygon=Polygon(points=[Point.safe(x=p[0], y=p[1]) for p in points]),
                                          obb=Obb.from_yolo_obb(xyxyxyxyn=obb.xyxyxyxyn.cpu().numpy().reshape(4, 2)),
                                          box=Box(left=min(xs), right=max(xs), top=min(ys), bottom=max(ys))
                                          )
                                 )
        return instances

    def _process_seg_results(self, result) -> List[Instance]:
        """
        Processes YOLO segmentation results into `YOLOInstance` objects.

        Args:
            result: YOLO result object with segmentation masks.

        Returns:
            List[Instance]: List of processed instances with polygon masks.

        Note:
            - Uses segmentation masks from YOLO segmentation models
            - Polygon coordinates are normalized `[0.0, 1.0]`
            - Creates pseudo-OBB from polygon bounding box (not true rotated box)
            - Used for segmentation models (`task='segment'`)
        """
        instances = []
        if result.masks is not None and result.boxes is not None:
            for box, mask in zip(result.boxes, result.masks.xyn):
                polygon = Polygon.from_yolo_xyn(xyn=mask)
                xs = [p.X for p in polygon.points]
                ys = [p.Y for p in polygon.points]
                obb_points = [
                    Point.safe(x=min(xs), y=min(ys)),
                    Point.safe(x=max(xs), y=min(ys)),
                    Point.safe(x=max(xs), y=max(ys)),
                    Point.safe(x=min(xs), y=max(ys))
                ]
                instances.append(Instance(id=box.id.int().tolist()[0] if box.id is not None else None,
                                          class_id=box.cls.int().tolist()[0],
                                          category=self.category(box.cls.int().tolist()[0]),
                                          conf=box.conf.cpu().numpy().flatten().tolist()[0],
                                          polygon=polygon,
                                          obb=Obb(obb_points),
                                          box=Box(left=min(xs), right=max(xs), top=min(ys), bottom=max(ys))
                                          )
                                 )
        return instances