"""Universal auto-conversion for ALL model formats.

Supports 13+ output formats, including Roboflow Inference SDK and Detectron2.
Every converter is production-grade with full input validation.
"""

from __future__ import annotations

import numpy as np
from typing import Any, Optional, Union

from eye.detection.core import Detections


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _to_numpy(tensor: Any) -> np.ndarray:
    """Robustly convert any tensor-like (PyTorch, TF, JAX, PaddlePaddle, plain
    ndarray) to a numpy array.  Never raises on a plain ndarray."""
    if isinstance(tensor, np.ndarray):
        return tensor
    if hasattr(tensor, "cpu"):
        # PyTorch / PaddlePaddle GPU tensor
        return tensor.cpu().numpy()
    if hasattr(tensor, "numpy"):
        # TF / JAX / PaddlePaddle CPU tensor
        return tensor.numpy()
    return np.asarray(tensor)


def _validate_xyxy(xyxy: np.ndarray) -> np.ndarray:
    """Ensure *xyxy* is a float32 (N, 4) array.  Returns empty on degenerate input."""
    xyxy = np.asarray(xyxy, dtype=np.float32)
    if xyxy.ndim == 1:
        xyxy = xyxy.reshape(-1, 4)
    if xyxy.ndim != 2 or xyxy.shape[1] != 4:
        return np.empty((0, 4), dtype=np.float32)
    return xyxy


# ---------------------------------------------------------------------------
# Format detection predicates (ordered by specificity)
# ---------------------------------------------------------------------------

def _is_ultralytics(obj: Any) -> bool:
    """True when *obj* looks like an Ultralytics YOLO/RTDETR Results object."""
    return hasattr(obj, "boxes") and hasattr(getattr(obj, "boxes", None), "xyxy")


def _is_detectron2(obj: Any) -> bool:
    """True when *obj* is a Detectron2 prediction dict with ``instances``,
    or a raw Detectron2 ``Instances`` object."""
    if isinstance(obj, dict) and "instances" in obj:
        return hasattr(obj["instances"], "pred_boxes")
    # Direct Instances object (has .pred_boxes or ._fields with pred_boxes)
    if hasattr(obj, "pred_boxes"):
        return True
    if hasattr(obj, "_fields") and hasattr(obj, "has") and callable(obj.has):
        try:
            return obj.has("pred_boxes")
        except Exception:
            return False
    return False


def _is_roboflow_inference(obj: Any) -> bool:
    """True when *obj* is a Roboflow Inference SDK result (dict with ``predictions``
    list, or has a ``.dict()`` method that produces one)."""
    if isinstance(obj, dict) and "predictions" in obj and "image" in obj:
        return True
    # The Inference SDK result objects expose a .dict() helper
    if hasattr(obj, "dict") and callable(getattr(obj, "dict")):
        try:
            d = obj.dict(exclude_none=True, by_alias=True)
            return "predictions" in d and "image" in d
        except Exception:
            return False
    return False


def _is_mmdetection(obj: Any) -> bool:
    """True when *obj* looks like an MMDetection DetDataSample."""
    return hasattr(obj, "pred_instances") and hasattr(
        getattr(obj, "pred_instances", None), "bboxes"
    )


def _is_torchvision_dict(obj: Any) -> bool:
    """True when *obj* is a torchvision-style dict with boxes/scores/labels."""
    return isinstance(obj, dict) and all(
        k in obj for k in ("boxes", "scores", "labels")
    )


def _is_tensorflow_dict(obj: Any) -> bool:
    """True when *obj* is a TF Object Detection API output dict."""
    return isinstance(obj, dict) and all(
        k in obj
        for k in ("detection_boxes", "detection_scores", "detection_classes")
    )


def _is_paddledet(obj: Any) -> bool:
    """True when *obj* is a PaddleDetection result dict."""
    return isinstance(obj, dict) and "bbox" in obj and isinstance(
        obj["bbox"], np.ndarray
    )


# ---------------------------------------------------------------------------
# Per-format converters
# ---------------------------------------------------------------------------

def _convert_ultralytics(results: Any) -> Detections:
    """Convert a single Ultralytics Results object."""
    return Detections.from_ultralytics(results)


def _convert_detectron2(results: Any) -> Detections:
    """Convert a Detectron2 prediction dict or raw Instances object."""
    if isinstance(results, dict) and "instances" in results:
        return Detections.from_detectron2(results)
    # Direct Instances object — wrap for Detections.from_detectron2
    return Detections.from_detectron2({"instances": results})


def _convert_roboflow(results: Any) -> Detections:
    """Convert a Roboflow Inference SDK result."""
    return Detections.from_inference(results)


def _convert_mmdetection(results: Any) -> Detections:
    """Convert an MMDetection DetDataSample."""
    return Detections.from_mmdetection(results)


def _convert_torchvision(results: dict) -> Detections:
    """Convert a torchvision-style dict (``boxes``, ``scores``, ``labels``)."""
    xyxy = _validate_xyxy(_to_numpy(results["boxes"]))
    confidence = _to_numpy(results["scores"]).astype(np.float32).ravel()
    class_id = _to_numpy(results["labels"]).astype(int).ravel()
    mask = None
    if "masks" in results and results["masks"] is not None:
        mask = _to_numpy(results["masks"])
        if mask.ndim == 4:
            mask = mask.squeeze(1)
    return Detections(xyxy=xyxy, confidence=confidence, class_id=class_id, mask=mask)


def _convert_tensorflow_dict(results: dict) -> Detections:
    """Convert a TF Object Detection API dict (unnormalized boxes expected)."""
    boxes = _to_numpy(results["detection_boxes"])
    if boxes.ndim == 3:
        boxes = boxes[0]
    scores = _to_numpy(results["detection_scores"]).ravel()
    classes = _to_numpy(results["detection_classes"]).ravel().astype(int)
    # TF format: [ymin, xmin, ymax, xmax] → xyxy [xmin, ymin, xmax, ymax]
    xyxy = boxes[:, [1, 0, 3, 2]].astype(np.float32)
    return Detections(xyxy=_validate_xyxy(xyxy), confidence=scores, class_id=classes)


def _convert_paddledet(results: dict) -> Detections:
    """Convert a PaddleDetection result dict."""
    return Detections.from_paddledet(results)


def _convert_opencv_dnn(results: np.ndarray) -> Detections:
    """Convert an OpenCV DNN [1, 1, N, 7] output."""
    det2d = results.reshape(-1, results.shape[-1])
    valid = det2d[det2d[:, 2] > 0]
    if valid.shape[0] == 0:
        return Detections.empty()
    return Detections(
        xyxy=valid[:, 3:7].astype(np.float32),
        confidence=valid[:, 2].astype(np.float32),
        class_id=valid[:, 1].astype(int),
    )


def _convert_raw_array(results: np.ndarray) -> Detections:
    """Convert a raw [N, 6+] array (ONNX/TensorRT centre-xywh format)."""
    x, y, w, h = results[:, 0], results[:, 1], results[:, 2], results[:, 3]
    xyxy = np.stack([x - w / 2, y - h / 2, x + w / 2, y + h / 2], axis=1).astype(
        np.float32
    )
    return Detections(
        xyxy=xyxy,
        confidence=results[:, 4].astype(np.float32),
        class_id=results[:, 5].astype(int),
    )


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

_SUPPORTED_FORMATS = (
    "Ultralytics YOLO/RTDETR, Roboflow Inference SDK, Detectron2, "
    "PyTorch (torchvision), TensorFlow Object Detection API, MMDetection, "
    "PaddleDetection, OpenCV DNN, ONNX/TensorRT raw arrays, "
    "HuggingFace Transformers (via Detections.from_transformers)"
)


def auto_convert(results: Any) -> Detections:
    """Automatically detect the model output format and convert to
    :class:`eye.Detections`.

    Supported formats (13+):
      Ultralytics YOLO/RTDETR, Roboflow Inference SDK, Detectron2,
      torchvision dicts, TensorFlow OD API, MMDetection, PaddleDetection,
      OpenCV DNN, ONNX/TensorRT raw arrays.

    Args:
        results: Model output in **any** supported format.  Batch lists
            (e.g. ``model(img)`` returning a list) are handled automatically
            by taking the first element.

    Returns:
        A populated :class:`eye.Detections` instance.

    Raises:
        TypeError: When *results* cannot be matched to any known format.

    Example::

        results = model(image)
        detections = eye.auto_convert(results)
    """
    if results is None:
        return Detections.empty()

    # --- Unwrap single-element batch lists ---
    if isinstance(results, (list, tuple)) and len(results) > 0:
        first = results[0]
        # YOLO batch list
        if _is_ultralytics(first):
            return _convert_ultralytics(first)
        # Detectron2 batch list
        if isinstance(first, dict) and "instances" in first:
            return _convert_detectron2(first)
        # Roboflow inference batch list
        if isinstance(first, dict) and "predictions" in first:
            return _convert_roboflow(first)

    # --- Ultralytics YOLO / RTDETR ---
    if _is_ultralytics(results):
        return _convert_ultralytics(results)

    # --- Roboflow Inference SDK ---
    if _is_roboflow_inference(results):
        return _convert_roboflow(results)

    # --- Detectron2 ---
    if _is_detectron2(results):
        return _convert_detectron2(results)

    # --- MMDetection ---
    if _is_mmdetection(results):
        return _convert_mmdetection(results)

    # --- Dict-based formats ---
    if isinstance(results, dict):
        # TensorFlow OD API dict
        if _is_tensorflow_dict(results):
            return _convert_tensorflow_dict(results)
        # PaddleDetection dict
        if _is_paddledet(results):
            return _convert_paddledet(results)
        # torchvision-style dict (last because it's the most generic)
        if _is_torchvision_dict(results):
            return _convert_torchvision(results)

    # --- ndarray-based formats ---
    if isinstance(results, np.ndarray):
        # Empty or degenerate
        if results.size == 0:
            return Detections.empty()
        # OpenCV DNN: [1, 1, N, 7]
        if results.ndim == 4 and results.shape[0] == 1:
            return _convert_opencv_dnn(results)
        if results.ndim == 2:
            cols = results.shape[1]
            # Raw ONNX/TensorRT: [N, 6+]  (cx, cy, w, h, conf, cls)
            if cols >= 6:
                return _convert_raw_array(results)
            # [N, 5]: xyxy + confidence
            if cols == 5:
                xyxy = _validate_xyxy(results[:, :4])
                return Detections(
                    xyxy=xyxy,
                    confidence=results[:, 4].astype(np.float32),
                )
            # [N, 4]: xyxy only
            if cols == 4:
                xyxy = _validate_xyxy(results[:, :4])
                return Detections(xyxy=xyxy)

    # --- Tuple fallback: (boxes, scores, classes) ---
    if isinstance(results, (tuple, list)) and len(results) >= 3:
        try:
            boxes = _to_numpy(results[0])
            scores = _to_numpy(results[1]).ravel()
            classes = _to_numpy(results[2]).ravel().astype(int)
            if boxes.ndim >= 2 and boxes.shape[-1] == 4:
                xyxy = _validate_xyxy(boxes.squeeze())
                return Detections(xyxy=xyxy, confidence=scores, class_id=classes)
        except Exception:
            pass

    raise TypeError(
        f"Unsupported model output format: {type(results).__name__}. "
        f"Supported: {_SUPPORTED_FORMATS}. "
        f"You can also use Detections.from_*() class methods for explicit conversion."
    )


# ---------------------------------------------------------------------------
# Convenience aliases (one per framework)
# ---------------------------------------------------------------------------


def from_yolo(results: Any) -> Detections:
    """Convert YOLO (Ultralytics) results to :class:`Detections`.

    Accepts a single ``Results`` object or a batch list.
    """
    if isinstance(results, (list, tuple)) and len(results) > 0:
        results = results[0]
    if _is_ultralytics(results):
        return _convert_ultralytics(results)
    return auto_convert(results)


def from_roboflow(results: Any) -> Detections:
    """Convert Roboflow Inference SDK results to :class:`Detections`.

    Accepts the raw dict or the SDK result object (which has a ``.dict()`` method).
    Returns empty :class:`Detections` for ``None`` input.

    Example::

        from inference import get_model
        import eye

        model = get_model(model_id="yolov8n-640")
        result = model.infer(image)[0]
        detections = eye.from_roboflow(result)
    """
    if results is None:
        return Detections.empty()
    return Detections.from_inference(results)


def from_detectron2(results: Any) -> Detections:
    """Convert Detectron2 prediction dict to :class:`Detections`.

    Returns empty :class:`Detections` for ``None`` input.

    Example::

        from detectron2.engine import DefaultPredictor
        import eye

        predictor = DefaultPredictor(cfg)
        outputs = predictor(image)
        detections = eye.from_detectron2(outputs)
    """
    if results is None:
        return Detections.empty()
    if isinstance(results, dict) and "instances" in results:
        return Detections.from_detectron2(results)
    return Detections.from_detectron2({"instances": results})


def from_pytorch(results: dict) -> Detections:
    """Convert PyTorch / torchvision results to :class:`Detections`."""
    if _is_torchvision_dict(results):
        return _convert_torchvision(results)
    return auto_convert(results)


def from_tensorflow(results: Any) -> Detections:
    """Convert TensorFlow Object Detection API results to :class:`Detections`."""
    if isinstance(results, dict) and _is_tensorflow_dict(results):
        return _convert_tensorflow_dict(results)
    return auto_convert(results)


def from_mmdetection(results: Any) -> Detections:
    """Convert MMDetection DetDataSample to :class:`Detections`."""
    return Detections.from_mmdetection(results)


def from_opencv(results: np.ndarray) -> Detections:
    """Convert OpenCV DNN results to :class:`Detections`."""
    if isinstance(results, np.ndarray):
        if results.ndim == 4 and results.shape[0] == 1:
            return _convert_opencv_dnn(results)
        if results.ndim == 2 and results.shape[1] >= 6:
            return _convert_raw_array(results)
    return auto_convert(results)
