# acmenra_cv/inference/timing.py

import logging
from typing import Any, List, Dict, Optional

logger = logging.getLogger(__name__)


class Timing:
    """
    Represents timing information for inference pipeline stages in milliseconds.

    This class captures the duration of three critical stages in a computer vision
    inference pipeline: preprocessing (image loading and transformation), model
    prediction (neural network forward pass), and postprocessing (NMS, coordinate
    conversion, etc.). All timing values are measured in milliseconds for precision
    in performance-critical applications.

    Designed for seamless integration with inference backends (YOLO, custom models)
    and performance monitoring systems. Provides automatic total time calculation
    and supports serialization for logging, analytics, and debugging.

    Key features:
    - Millisecond precision for embedded device optimization
    - Automatic total time calculation via property
    - Strict validation (non-negative floats only)
    - JSON serialization support via to_dict/from_dict
    - Human-readable string representation

    Designed for seamless integration with downstream processing pipelines such as:
    - Performance profiling and bottleneck identification
    - Real-time FPS monitoring and alerting
    - Embedded device resource planning
    - Multi-model benchmarking and comparison

    Attributes:
        _preprocess (float): Preprocessing duration in milliseconds.
        _prediction (float): Model forward pass duration in milliseconds.
        _postprocess (float): Postprocessing duration in milliseconds.

    Note:
        - All values are in milliseconds (ms) for readability and precision
        - Typical values: preprocess (1-10ms), prediction (5-50ms), postprocess (1-5ms)
        - Total time is calculated dynamically from the three components
        - Negative values are rejected as physically impossible
    """

    def __init__(self,
                 preprocess: Optional[float] = None,
                 prediction: Optional[float] = None,
                 postprocess: Optional[float] = None): # tested (good)
        """
        Initializes timing information for inference pipeline stages.

        Args:
            preprocess (float): Preprocessing time in milliseconds.
                Includes image loading, resizing, normalization, color conversion.
            prediction (float): Model prediction time in milliseconds.
                Neural network forward pass through all layers.
            postprocess (float): Postprocessing time in milliseconds.
                NMS, coordinate conversion, confidence filtering, etc.

        Raises:
            TypeError: If any parameter is not a float.
            ValueError: If any parameter is negative.

        Note:
            - All values must be non-negative (time cannot be negative)
            - Values are stored with full float precision
            - Typical ranges depend on hardware and model complexity
        """
        self.preprocess = preprocess
        self.prediction = prediction
        self.postprocess = postprocess

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

        Returns:
            str: Formatted string with all timing components and total.
                 Format: "Timing(preprocess=X.Xms, prediction=Y.Yms, postprocess=Z.Zms, total=T.Tms)"

        Note:
            - Optimized for console logging and performance dashboards
            - Uses fixed-point precision (1 decimal place) for millisecond clarity
            - Total is calculated dynamically for accuracy
        """
        def _fmt(val: Optional[float]) -> str:
            return f"{val:.1f}ms" if val is not None else "N/A"

        return (f"Timing(preprocess={_fmt(self.preprocess)}, "
                f"prediction={_fmt(self.prediction)}, "
                f"postprocess={_fmt(self.postprocess)}, "
                f"total={_fmt(self.total)})")

    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 timing values with full float precision
            - Output can theoretically be used to recreate the object in interactive sessions
            - Essential for debugging performance issues and pipeline optimization
        """
        return (f"Timing(preprocess={self.preprocess!r}, "
                f"prediction={self.prediction!r}, "
                f"postprocess={self.postprocess!r})")

    @property
    def preprocess(self) -> Optional[float]: # tested (good)
        """
        float: Gets the preprocessing time in milliseconds.

        Returns:
            float: Preprocessing duration (image loading, resizing, normalization).

        Note:
            - Typical values: 1-10ms on modern hardware
            - Higher values may indicate I/O bottleneck or inefficient preprocessing
            - Includes color conversion (BGR→RGB), resizing, normalization
        """
        return self._preprocess

    @preprocess.setter
    def preprocess(self, preprocess: Optional[float]) -> None: # tested (good)
        """
        Sets the preprocessing time with validation.

        Args:
            preprocess (float): Preprocessing time in milliseconds.

        Raises:
            TypeError: If preprocess is not a float.
            ValueError: If preprocess is negative.

        Note:
            - Time cannot be negative (physical impossibility)
            - Zero is valid (instant preprocessing)
        """
        if preprocess is not None:
            if not isinstance(preprocess, float):
                raise TypeError(f"'preprocess' must be '<float>', but got {type(preprocess).__name__}")
            if preprocess < 0:
                raise ValueError(f"'preprocess' must be >= 0, got {preprocess}")
        self._preprocess = preprocess

    @property
    def prediction(self) -> Optional[float]: # tested (good)
        """
        float: Gets the model prediction time in milliseconds.

        Returns:
            float: Model forward pass duration (neural network inference).

        Note:
            - Typical values: 5-50ms depending on model and hardware
            - This is the core inference time (GPU/CPU compute)
            - Critical metric for FPS calculation and optimization
        """
        return self._prediction

    @prediction.setter
    def prediction(self, prediction: Optional[float]) -> None: # tested (good)
        """
        Sets the model prediction time with validation.

        Args:
            prediction (float): Prediction time in milliseconds.

        Raises:
            TypeError: If prediction is not a float.
            ValueError: If prediction is negative.

        Note:
            - Time cannot be negative (physical impossibility)
            - Zero is valid (instant prediction, though unrealistic)
        """
        if prediction is not None:
            if not isinstance(prediction, float):
                raise TypeError(f"'prediction' must be '<float>', but got {type(prediction).__name__}")
            if prediction < 0:
                raise ValueError(f"'prediction' must be >= 0, got {prediction}")
        self._prediction = prediction

    @property
    def postprocess(self) -> Optional[float]: # tested (good)
        """
        float: Gets the postprocessing time in milliseconds.

        Returns:
            float: Postprocessing duration (NMS, coordinate conversion, filtering).

        Note:
            - Typical values: 1-5ms on modern hardware
            - Higher values may indicate inefficient NMS or coordinate conversion
            - Includes confidence filtering, NMS, coordinate denormalization
        """
        return self._postprocess

    @postprocess.setter
    def postprocess(self, postprocess: Optional[float]) -> None: # tested (good)
        """
        Sets the postprocessing time with validation.

        Args:
            postprocess (float): Postprocessing time in milliseconds.

        Raises:
            TypeError: If postprocess is not a float.
            ValueError: If postprocess is negative.

        Note:
            - Time cannot be negative (physical impossibility)
            - Zero is valid (instant postprocessing)
        """
        if postprocess is not None:
            if not isinstance(postprocess, float):
                raise TypeError(f"'postprocess' must be '<float>', but got {type(postprocess).__name__}")
            if postprocess < 0:
                raise ValueError(f"'postprocess' must be >= 0, got {postprocess}")
        self._postprocess = postprocess

    @property
    def total(self) -> Optional[float]: # tested (good)
        """
        float: Gets the total inference time in milliseconds.

        Returns:
            float: Sum of preprocess, prediction, and postprocess times.

        Note:
            - Calculated dynamically from the three components
            - Use for FPS calculation: fps = 1000.0 / timing.total
            - Critical metric for real-time performance requirements
        """
        values = [v for v in (self.preprocess, self.prediction, self.postprocess) if v is not None]
        return sum(values) if values else None

    def to_dict(self) -> Dict[str, Any]: # tested (good)
        """
        Serializes the timing information to a JSON-compatible dictionary.

        Returns:
            Dict[str, Any]: Dictionary containing all timing components and total.

        Note:
            - Ready for direct DB insertion or API transmission
            - All values are primitive floats requiring no complex conversion
            - Maintains perfect round-trip fidelity with from_dict()
            - Includes calculated 'total' for convenience
        """
        return {
            "preprocess": self.preprocess,
            "prediction": self.prediction,
            "postprocess": self.postprocess,
            "total": self.total
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'Timing': # tested (good)
        """
        Reconstructs a Timing instance from a serialized dictionary.

        Args:
            data: Dictionary with timing configuration.
                  Required: preprocess, prediction, postprocess.
                  Optional: total (ignored, calculated automatically).

        Returns:
            Timing: Fully initialized and validated instance.

        Raises:
            KeyError: If any required field is missing from input dictionary.
            TypeError: If any parameter is not a float.
            ValueError: If any parameter is negative.

        Note:
            - Delegates validation to property setters
            - 'total' field is ignored if present (calculated automatically)
            - Handles missing keys with descriptive error messages
            - Maintains perfect round-trip fidelity with to_dict()
        """
        required_fields = ['preprocess', 'prediction', 'postprocess']
        for field in required_fields:
            if field not in data:
                raise KeyError(f"Required field '{field}' is missing from input dictionary: {data}")

        return cls(
            preprocess=float(data.get("preprocess")) if data.get("preprocess") is not None else None,
            prediction=float(data.get("prediction")) if data.get("prediction") is not None else None,
            postprocess=float(data.get("postprocess")) if data.get("postprocess") is not None else None
        )