"""
Unified bounding box utilities for consistent bbox handling across services.

Supports multiple bbox formats:
- Nested polygon: [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
- Flat rectangle: [x0, y0, x1, y1]
- Flat polygon: [x1, y1, x2, y2, x3, y3, x4, y4]
- Dict format: {"x0": ..., "y0": ..., "x1": ..., "y1": ...}
"""

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

logger = logging.getLogger(__name__)

BboxCoords = Tuple[float, float, float, float]  # (x0, y0, x1, y1)


def normalize_bbox(
    bbox: Union[Dict, List, Tuple, None]
) -> Optional[BboxCoords]:
    """
    Normalize any bbox format to (x0, y0, x1, y1) tuple.

    Handles:
    - Nested polygon: [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
    - Flat rectangle: [x0, y0, x1, y1]
    - Flat polygon: [x1, y1, x2, y2, x3, y3, x4, y4]
    - Dict format: {"x0": ..., "y0": ..., "x1": ..., "y1": ...}

    Args:
        bbox: Bounding box in any supported format

    Returns:
        Normalized (x0, y0, x1, y1) tuple or None if invalid
    """
    if bbox is None:
        return None

    try:
        # Dict format: {"x0": ..., "y0": ..., "x1": ..., "y1": ...}
        if isinstance(bbox, dict):
            if 'x0' in bbox and 'y0' in bbox and 'x1' in bbox and 'y1' in bbox:
                return (
                    float(bbox['x0']),
                    float(bbox['y0']),
                    float(bbox['x1']),
                    float(bbox['y1'])
                )
            # Alternative dict keys
            if 'x_min' in bbox or 'y_min' in bbox:
                return (
                    float(bbox.get('x_min', bbox.get('x0', 0))),
                    float(bbox.get('y_min', bbox.get('y0', 0))),
                    float(bbox.get('x_max', bbox.get('x1', 0))),
                    float(bbox.get('y_max', bbox.get('y1', 0)))
                )
            logger.warning(f"Dict bbox missing required fields: {bbox}")
            return None

        # List/tuple formats
        if isinstance(bbox, (list, tuple)):
            if len(bbox) == 0:
                return None

            # Nested polygon format: [[x1,y1], [x2,y2], ...]
            if isinstance(bbox[0], (list, tuple)):
                xs = [pt[0] for pt in bbox if len(pt) >= 2]
                ys = [pt[1] for pt in bbox if len(pt) >= 2]
                if xs and ys:
                    return (
                        float(min(xs)),
                        float(min(ys)),
                        float(max(xs)),
                        float(max(ys))
                    )
                return None

            # Flat rectangle: [x0, y0, x1, y1]
            if len(bbox) == 4:
                return (
                    float(bbox[0]),
                    float(bbox[1]),
                    float(bbox[2]),
                    float(bbox[3])
                )

            # Flat polygon: [x1, y1, x2, y2, x3, y3, x4, y4, ...]
            if len(bbox) >= 8 and len(bbox) % 2 == 0:
                xs = [bbox[i] for i in range(0, len(bbox), 2)]
                ys = [bbox[i] for i in range(1, len(bbox), 2)]
                return (
                    float(min(xs)),
                    float(min(ys)),
                    float(max(xs)),
                    float(max(ys))
                )

        logger.warning(f"Unknown bbox format: {type(bbox).__name__}, value: {bbox}")
        return None

    except (TypeError, ValueError, IndexError) as e:
        logger.warning(f"Failed to normalize bbox {bbox}: {e}")
        return None


def get_bbox_center(bbox: Union[Dict, List, Tuple, BboxCoords, None]) -> Optional[Tuple[float, float]]:
    """
    Get the center point of a bounding box.

    Args:
        bbox: Bounding box in any supported format or already normalized

    Returns:
        (center_x, center_y) tuple or None if invalid
    """
    # If already normalized tuple
    if isinstance(bbox, tuple) and len(bbox) == 4:
        x0, y0, x1, y1 = bbox
        return ((x0 + x1) / 2, (y0 + y1) / 2)

    # Otherwise normalize first
    coords = normalize_bbox(bbox)
    if coords is None:
        return None

    x0, y0, x1, y1 = coords
    return ((x0 + x1) / 2, (y0 + y1) / 2)


def get_bbox_area(bbox: Union[Dict, List, Tuple, BboxCoords, None]) -> float:
    """
    Calculate the area of a bounding box.

    Args:
        bbox: Bounding box in any supported format

    Returns:
        Area in square pixels, 0 if invalid
    """
    coords = normalize_bbox(bbox) if not (isinstance(bbox, tuple) and len(bbox) == 4) else bbox
    if coords is None:
        return 0.0

    x0, y0, x1, y1 = coords
    return max(0, x1 - x0) * max(0, y1 - y0)


def calculate_ioa(
    inner_bbox: Union[Dict, List, Tuple, BboxCoords, None],
    outer_bbox: Union[Dict, List, Tuple, BboxCoords, None]
) -> float:
    """
    Calculate Intersection over Area (IoA) of inner bbox with respect to outer bbox.

    IoA = intersection_area / inner_area

    Args:
        inner_bbox: The bbox to check (numerator area)
        outer_bbox: The reference bbox

    Returns:
        IoA ratio (0.0 to 1.0), 0.0 if either bbox is invalid
    """
    inner_coords = normalize_bbox(inner_bbox) if not (isinstance(inner_bbox, tuple) and len(inner_bbox) == 4) else inner_bbox
    outer_coords = normalize_bbox(outer_bbox) if not (isinstance(outer_bbox, tuple) and len(outer_bbox) == 4) else outer_bbox

    if inner_coords is None or outer_coords is None:
        return 0.0

    inner_x0, inner_y0, inner_x1, inner_y1 = inner_coords
    outer_x0, outer_y0, outer_x1, outer_y1 = outer_coords

    # Calculate intersection
    inter_x0 = max(inner_x0, outer_x0)
    inter_y0 = max(inner_y0, outer_y0)
    inter_x1 = min(inner_x1, outer_x1)
    inter_y1 = min(inner_y1, outer_y1)

    if inter_x1 <= inter_x0 or inter_y1 <= inter_y0:
        return 0.0

    intersection_area = (inter_x1 - inter_x0) * (inter_y1 - inter_y0)
    inner_area = (inner_x1 - inner_x0) * (inner_y1 - inner_y0)

    if inner_area <= 0:
        return 0.0

    return intersection_area / inner_area


def calculate_iou(
    bbox1: Union[Dict, List, Tuple, BboxCoords, None],
    bbox2: Union[Dict, List, Tuple, BboxCoords, None]
) -> float:
    """
    Calculate Intersection over Union (IoU) of two bounding boxes.

    Args:
        bbox1: First bounding box
        bbox2: Second bounding box

    Returns:
        IoU ratio (0.0 to 1.0), 0.0 if either bbox is invalid
    """
    coords1 = normalize_bbox(bbox1) if not (isinstance(bbox1, tuple) and len(bbox1) == 4) else bbox1
    coords2 = normalize_bbox(bbox2) if not (isinstance(bbox2, tuple) and len(bbox2) == 4) else bbox2

    if coords1 is None or coords2 is None:
        return 0.0

    x0_1, y0_1, x1_1, y1_1 = coords1
    x0_2, y0_2, x1_2, y1_2 = coords2

    # Calculate intersection
    inter_x0 = max(x0_1, x0_2)
    inter_y0 = max(y0_1, y0_2)
    inter_x1 = min(x1_1, x1_2)
    inter_y1 = min(y1_1, y1_2)

    if inter_x1 <= inter_x0 or inter_y1 <= inter_y0:
        return 0.0

    intersection_area = (inter_x1 - inter_x0) * (inter_y1 - inter_y0)
    area1 = (x1_1 - x0_1) * (y1_1 - y0_1)
    area2 = (x1_2 - x0_2) * (y1_2 - y0_2)
    union_area = area1 + area2 - intersection_area

    if union_area <= 0:
        return 0.0

    return intersection_area / union_area


def is_bbox_inside(
    inner_bbox: Union[Dict, List, Tuple, BboxCoords, None],
    outer_bbox: Union[Dict, List, Tuple, BboxCoords, None],
    tolerance: float = 0.0
) -> bool:
    """
    Check if inner_bbox is completely inside outer_bbox (with optional tolerance).

    Args:
        inner_bbox: The bbox to check
        outer_bbox: The containing bbox
        tolerance: Allowed overflow in pixels

    Returns:
        True if inner is inside outer (within tolerance)
    """
    inner_coords = normalize_bbox(inner_bbox) if not (isinstance(inner_bbox, tuple) and len(inner_bbox) == 4) else inner_bbox
    outer_coords = normalize_bbox(outer_bbox) if not (isinstance(outer_bbox, tuple) and len(outer_bbox) == 4) else outer_bbox

    if inner_coords is None or outer_coords is None:
        return False

    inner_x0, inner_y0, inner_x1, inner_y1 = inner_coords
    outer_x0, outer_y0, outer_x1, outer_y1 = outer_coords

    return (
        inner_x0 >= outer_x0 - tolerance and
        inner_y0 >= outer_y0 - tolerance and
        inner_x1 <= outer_x1 + tolerance and
        inner_y1 <= outer_y1 + tolerance
    )
