# acmenra_cv/inference/backend.py

import logging
import numpy as np
from enum import EnumType
from abc import ABC, abstractmethod

from acmenra_cv.inference import Result, Timing
from acmenra_cv.instance.enums import TaskType
from acmenra_cv.instance import Point, Box, Obb, Polygon, Instance, DeviceType


logger = logging.getLogger(__name__)


class Backend(ABC):
    """
    Abstract base class for all inference backends.

    Defines the contract that any backend (YOLO, custom model, OpenVINO, TensorRT)
    must implement to integrate with the Tracker. Provides a unified interface
    for detection and optional tracking, ensuring backend-agnostic architecture.

    Key design decisions (contracts):
    - All backends must implement `predict()` returning a universal `Result` container
    - Optional `track()` method for backends with built-in tracking (e.g., YOLO)
    - Common properties (device, category, threshold, task_type) with strict validation
    - Flexible parameter passing via `**kwargs` for backend-specific parameters
    - Optional `warmup()` method for model initialization
    - Optional `info()` method for debugging and diagnostics

    Designed for seamless integration with downstream processing pipelines such as:
    - Multi-object tracking (Tracker)
    - Real-time ADAS monitoring and alert visualization
    - Business logic for event detection and zone analysis
    - Performance profiling and benchmarking

    Attributes:
        _device (DeviceType): Hardware backend used for inference.
        _category (EnumType): Semantic category enumeration for label mapping.
        _threshold (float): Detection confidence threshold [0.0, 1.0].
        _task_type (TaskType): Type of task the model performs (detect, segment, obb, etc.).

    Note:
        - All spatial coordinates in Result use normalized space [0.0, 1.0]
        - Backend-specific parameters should be passed via **kwargs in predict/track
        - Subclasses must implement at least predict() method
        - Optional track() can be overridden for built-in tracking support
    """

    def __init__(self,
                 device: DeviceType,
                 category: EnumType,
                 task_type: TaskType,
                 threshold: float = 0.5) -> None: # tested (good)
        """
        Initializes the inference backend with common configuration.

        Args:
            category (EnumType): Semantic category enumeration for label mapping.
            task_type (TaskType): Type of task the model performs.
            device (DeviceType, optional): Hardware backend. Defaults to MPS.
            threshold (float, optional): Detection confidence threshold [0.0, 1.0].
                Defaults to 0.5.

        Raises:
            TypeError: If any parameter has incorrect type.
            ValueError: If threshold is outside range [0.0, 1.0].

        Note:
            - Assignment is delegated to property setters for immediate validation
            - Backend-specific parameters should be passed in subclass __init__
        """
        self.device = device
        self.category = category
        self.task_type = task_type
        self.threshold = threshold

    def __str__(self) -> str: # tested (good)
        """
        Returns a human-readable string representation of the inference backend.

        Returns:
            str: Concise summary with device, category, task_type, and threshold.
                 Format: "Backend(device=mps, category=CocoClass, task_type=detect, threshold=0.35)"

        Note:
            - Optimized for console logging and operator dashboards
            - Uses enum value for device (e.g., "mps", "cuda:0")
            - Uses class name for category (e.g., "CocoClass", "ADASClass")
            - Uses enum value for task_type (e.g., "detect", "segment")
        """
        return (f"Backend(device={self.device.value}, "
                f"category={self.category.__name__}, "
                f"task_type={self.task_type.value}, "
                f"threshold={self.threshold:.2f})")

    def __repr__(self) -> str: # tested (good)
        """
        Returns a detailed string representation suitable for debugging and reproduction.

        Returns:
            str: Exact representation including all canonical parameters.
                 Uses repr-safe formatting (!r) for unambiguous parsing.

        Note:
            - Includes all configuration parameters with full precision
            - Output can theoretically be used to recreate the object in interactive sessions
            - Essential for debugging pipeline configuration and backend selection
        """
        return (f"Backend(device={self.device!r}, "
                f"category={self.category!r}, "
                f"task_type={self.task_type!r}, "
                f"threshold={self.threshold!r})")

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

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

        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)
            - Changing device may require model reload in subclass
        """
        return self._device

    @device.setter
    def device(self, device: DeviceType) -> None: # tested (good)
        """
        Sets the hardware backend for 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
            - Subclasses should handle model reload on device change
            - Existing tracks remain valid; only inference backends changes
        """
        if not isinstance(device, DeviceType):
            raise TypeError(f"'device' must be '<DeviceType>', but got {type(device).__name__}")
        self._device = device

    @property
    def category(self) -> EnumType: # tested (good)
        """
        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`.
            - Used to construct `Instance.label` via `category(class_id)` call.
            - Example: `CocoClass(0)` returns `CocoClass.person` enum member.

            Important distinction:
            - Backend.category holds the enum CLASS (e.g., `CocoClass`)
            - Instance.label holds the enum MEMBER (e.g., `CocoClass.person`)
            - Instance.class_id holds the numeric ID (e.g., `0`)
        """
        return self._category

    @category.setter
    def category(self, category: EnumType) -> None: # tested (good)
        """
        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 model'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 task_type(self) -> TaskType: # tested (good)
        """
        TaskType: Gets the type of task the model performs.

        Returns:
            TaskType: Task type (e.g., `detect`, `segment`, `obb`).

        Note:
            - Determines what type of output the model produces
            - Used for selecting appropriate post-processing logic
            - Example: `detect` produces boxes, `segment` produces masks
        """
        return self._task_type

    @task_type.setter
    def task_type(self, task_type: TaskType) -> None: # tested (good)
        """
        Sets the task type with validation.

        Args:
            task_type (TaskType): New task type.

        Raises:
            TypeError: If task_type is not a `TaskType` enum member.

        Note:
            - Changing task type may require model reload in subclass
            - Must match the actual model's capabilities
        """
        if not isinstance(task_type, TaskType):
            raise TypeError(f"'task_type' must be '<TaskType>', but got {type(task_type).__name__}")
        self._task_type = task_type

    @property
    def threshold(self) -> float: # tested (good)
        """
        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.5` (standard), `0.5+` (high confidence)
            - Lower values = more detections but more false positives
            - Higher values = fewer detections but fewer false positives
        """
        return self._threshold

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

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

        Raises:
            TypeError: If threshold is not a float.
            ValueError: If threshold 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
            - Changes affect subsequent predict/track calls immediately
        """
        if not isinstance(threshold, float):
            raise TypeError(f"'threshold' must be '<float>', but got {type(threshold).__name__}")
        if not 0.0 <= threshold <= 1.0:
            raise ValueError(f"'threshold' must be in range [0.0, 1.0], got {threshold}")
        self._threshold = threshold

    @abstractmethod
    def predict(self, frame: np.ndarray, **kwargs) -> Result:
        """
        Performs inference on a single image.

        Args:
            frame (np.ndarray): Input image in BGR format (H, W, 3).
            **kwargs: Backend-specific parameters (e.g., temperature, num_queries).
                Documented in subclass implementations.

        Returns:
            Result: Universal result container with instances and timing.

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

        Note:
            - This is the only required method for all backends
            - Backend-specific parameters should be passed via **kwargs
            - Result contains normalized coordinates [0.0, 1.0] and timing metrics
            - Subclasses must implement this method with backends-specific logic
        """
        pass

    def track(self, frame: np.ndarray, **kwargs) -> Result:
        """
        Performs inference with built-in tracking (if supported).

        Default implementation delegates to predict(). Override in subclasses
        that support built-in tracking (e.g., YOLO with BoT-SORT).

        Args:
            frame (np.ndarray): Input image in BGR format (H, W, 3).
            **kwargs: Backend-specific parameters for tracking.
                Documented in subclass implementations.

        Returns:
            Result: Universal result container with instances (with tracking IDs) and timing.

        Note:
            - Default behavior: calls predict() and returns result without tracking IDs
            - Subclasses with built-in tracking should override this method
            - Tracking IDs are assigned by the backends and stored in Instance.id
            - If backends doesn't support tracking, Tracker will handle ID assignment
        """
        return self.predict(frame, **kwargs)




