"""Spatial validation rules — overlap, bounds, spacing, and text overflow checks."""

from __future__ import annotations

from typing import Any

from pptx_mcp.models.brand import BrandConfig
from pptx_mcp.models.review import ReviewCategory, ReviewIssue, Severity

# EMU-to-inches constant
_EMU_PER_INCH = 914400


def _shape_rect(shape: Any) -> tuple[float, float, float, float] | None:
    """Extract bounding rect (left, top, right, bottom) in inches.

    Returns None if the shape lacks position attributes.
    """
    try:
        left = shape.left
        top = shape.top
        width = shape.width
        height = shape.height
        if left is None or top is None or width is None or height is None:
            return None
        return (
            left / _EMU_PER_INCH,
            top / _EMU_PER_INCH,
            (left + width) / _EMU_PER_INCH,
            (top + height) / _EMU_PER_INCH,
        )
    except (AttributeError, TypeError):
        return None


def _shape_label(shape: Any, index: int) -> str:
    """Human-readable label for a shape."""
    name = getattr(shape, "name", None)
    if name:
        return f"'{name}' (index {index})"
    return f"shape index {index}"


def _rects_overlap(
    r1: tuple[float, float, float, float],
    r2: tuple[float, float, float, float],
    threshold: float = 0.05,
) -> bool:
    """Check if two rects overlap by more than threshold inches."""
    overlap_x = min(r1[2], r2[2]) - max(r1[0], r2[0])
    overlap_y = min(r1[3], r2[3]) - max(r1[1], r2[1])
    return overlap_x > threshold and overlap_y > threshold


_MAX_SHAPES_FOR_SPATIAL = 200


def check_element_overlap(
    slide: Any,
    slide_index: int,
    brand: BrandConfig,
    **kwargs: Any,
) -> list[ReviewIssue]:
    """Detect overlapping shapes on a slide."""
    issues: list[ReviewIssue] = []
    shapes = list(slide.shapes)
    if len(shapes) > _MAX_SHAPES_FOR_SPATIAL:
        return issues  # skip — too many shapes for pairwise checks
    rects: list[tuple[int, Any, tuple[float, float, float, float]]] = []

    for i, shape in enumerate(shapes):
        rect = _shape_rect(shape)
        if rect is not None:
            rects.append((i, shape, rect))

    for a_idx in range(len(rects)):
        for b_idx in range(a_idx + 1, len(rects)):
            i_a, shape_a, rect_a = rects[a_idx]
            i_b, shape_b, rect_b = rects[b_idx]
            if _rects_overlap(rect_a, rect_b):
                issues.append(ReviewIssue(
                    severity=Severity.WARNING,
                    category=ReviewCategory.LAYOUT,
                    message=(
                        f"Elements overlap: {_shape_label(shape_a, i_a)} "
                        f"and {_shape_label(shape_b, i_b)}."
                    ),
                    slide_index=slide_index,
                    suggestion="Move or resize elements so they don't overlap.",
                ))

    return issues


def check_element_bounds(
    slide: Any,
    slide_index: int,
    brand: BrandConfig,
    *,
    slide_width: float = 13.333,
    slide_height: float = 7.5,
    **kwargs: Any,
) -> list[ReviewIssue]:
    """Check that shapes stay within slide boundaries."""
    issues: list[ReviewIssue] = []
    tolerance = 0.05  # inches

    for i, shape in enumerate(slide.shapes):
        rect = _shape_rect(shape)
        if rect is None:
            continue

        left, top, right, bottom = rect
        out_parts: list[str] = []

        if left < -tolerance:
            out_parts.append(f"left edge at {left:.2f}\"")
        if top < -tolerance:
            out_parts.append(f"top edge at {top:.2f}\"")
        if right > slide_width + tolerance:
            out_parts.append(f"right edge at {right:.2f}\" (slide width {slide_width:.3f}\")")
        if bottom > slide_height + tolerance:
            out_parts.append(f"bottom edge at {bottom:.2f}\" (slide height {slide_height:.3f}\")")

        if out_parts:
            issues.append(ReviewIssue(
                severity=Severity.WARNING,
                category=ReviewCategory.LAYOUT,
                message=(
                    f"Element {_shape_label(shape, i)} extends beyond slide: "
                    f"{'; '.join(out_parts)}."
                ),
                slide_index=slide_index,
                suggestion="Reposition or resize the element to fit within the slide.",
            ))

    return issues


def check_element_spacing(
    slide: Any,
    slide_index: int,
    brand: BrandConfig,
    *,
    min_gap: float = 0.1,
    **kwargs: Any,
) -> list[ReviewIssue]:
    """Warn when non-overlapping shapes are too close together."""
    issues: list[ReviewIssue] = []
    shapes = list(slide.shapes)
    if len(shapes) > _MAX_SHAPES_FOR_SPATIAL:
        return issues  # skip — too many shapes for pairwise checks
    rects: list[tuple[int, Any, tuple[float, float, float, float]]] = []

    for i, shape in enumerate(shapes):
        rect = _shape_rect(shape)
        if rect is not None:
            rects.append((i, shape, rect))

    for a_idx in range(len(rects)):
        for b_idx in range(a_idx + 1, len(rects)):
            i_a, shape_a, rect_a = rects[a_idx]
            i_b, shape_b, rect_b = rects[b_idx]

            # Skip if overlapping (handled by overlap rule)
            if _rects_overlap(rect_a, rect_b, threshold=0.0):
                continue

            # Compute minimum gap between edges
            gap_x = max(0.0, max(rect_a[0], rect_b[0]) - min(rect_a[2], rect_b[2]))
            gap_y = max(0.0, max(rect_a[1], rect_b[1]) - min(rect_a[3], rect_b[3]))

            # Only flag shapes that are nearby (not on opposite sides)
            gap = max(gap_x, gap_y)
            if gap_x > 0 and gap_y > 0:
                # Diagonal — use Euclidean-ish distance
                gap = (gap_x ** 2 + gap_y ** 2) ** 0.5

            if 0 < gap < min_gap:
                issues.append(ReviewIssue(
                    severity=Severity.INFO,
                    category=ReviewCategory.WHITESPACE,
                    message=(
                        f"Tight spacing ({gap:.2f}\") between "
                        f"{_shape_label(shape_a, i_a)} and {_shape_label(shape_b, i_b)}."
                    ),
                    slide_index=slide_index,
                    suggestion=f"Increase spacing to at least {min_gap}\" for visual clarity.",
                ))

    return issues


def check_text_overflow(
    slide: Any,
    slide_index: int,
    brand: BrandConfig,
    **kwargs: Any,
) -> list[ReviewIssue]:
    """Heuristic check for text likely overflowing its container."""
    issues: list[ReviewIssue] = []

    for i, shape in enumerate(slide.shapes):
        if not shape.has_text_frame:
            continue

        rect = _shape_rect(shape)
        if rect is None:
            continue

        width_in = rect[2] - rect[0]
        height_in = rect[3] - rect[1]

        if width_in <= 0 or height_in <= 0:
            continue

        tf = shape.text_frame
        full_text = tf.text or ""
        if not full_text.strip():
            continue

        # Estimate font size from first run
        font_size_pt = 12.0  # default
        for para in tf.paragraphs:
            for run in para.runs:
                if run.font.size is not None:
                    font_size_pt = run.font.size / 12700  # EMU to pt
                    break
            else:
                continue
            break

        # Heuristic: average char width ≈ font_size * 0.6 / 72 inches
        avg_char_width = font_size_pt * 0.6 / 72.0
        chars_per_line = max(1, int(width_in / avg_char_width))

        # Line height ≈ font_size * 1.2 / 72 inches
        line_height = font_size_pt * 1.2 / 72.0
        max_lines = max(1, int(height_in / line_height))

        # Estimate lines needed
        lines_needed = 0
        for para in tf.paragraphs:
            para_text = para.text or ""
            para_lines = max(1, (len(para_text) + chars_per_line - 1) // chars_per_line)
            lines_needed += para_lines

        # Warn if estimated lines exceed capacity by >30%
        if lines_needed > max_lines * 1.3:
            issues.append(ReviewIssue(
                severity=Severity.WARNING,
                category=ReviewCategory.LAYOUT,
                message=(
                    f"Text likely overflows {_shape_label(shape, i)}: "
                    f"~{lines_needed} lines needed, ~{max_lines} fit."
                ),
                slide_index=slide_index,
                suggestion="Reduce text, decrease font size, or enlarge the text box.",
            ))

    return issues


# Registry of spatial validation rules
SPATIAL_RULES = [
    check_element_overlap,
    check_element_bounds,
    check_element_spacing,
    check_text_overflow,
]
