"""
engine/slide_layout_checker.py
-------------------------------
Geometric spatial analysis of bank slides.

Checks whether a bank slide's label text slots are physically near their
visual infographic shapes — or floating disconnected (top-left cluster,
wrong column, etc.).

Returns True  → slide is spatially usable for label filling.
Returns False → text boxes are disconnected from visual elements; reject.

No API calls, no rendering — pure XML geometry.
"""

from __future__ import annotations
from pathlib import Path

_EMU = 914_400

_NS_P = "http://schemas.openxmlformats.org/presentationml/2006/main"
_NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main"


def _xfrm(el) -> tuple[int, int, int, int]:
    """Return (x, y, cx, cy) in EMU for a sp or grpSp element."""
    spPr = el.find(f"{{{_NS_P}}}spPr") or el.find(f"{{{_NS_P}}}grpSpPr")
    if spPr is None:
        return 0, 0, 0, 0
    xf = spPr.find(f"{{{_NS_A}}}xfrm")
    if xf is None:
        return 0, 0, 0, 0
    off = xf.find(f"{{{_NS_A}}}off")
    ext = xf.find(f"{{{_NS_A}}}ext")
    if off is None or ext is None:
        return 0, 0, 0, 0
    return (
        int(off.get("x", 0)), int(off.get("y", 0)),
        int(ext.get("cx", 0)), int(ext.get("cy", 0)),
    )


def _center(x, y, cx, cy):
    return x + cx // 2, y + cy // 2


def check_layout(bank_path: str, slide_idx: int, n_items: int) -> tuple[bool, str]:
    """
    Validate a bank slide's structure against the required item count.

    NEW (tagged-bank fast path): if the slide has at least `n_items` shapes
    with the placeholder text "Item N" (1 ≤ N ≤ n_items, case-insensitive),
    the slide is trivially usable — the new bank tags every label slot
    explicitly, so spatial heuristics are unnecessary.  Returns immediately.

    LEGACY (spatial heuristics): for the original untagged bank, fall through
    to the Pattern A/B/C checks below.
    """
    import re
    from pptx import Presentation

    _MIN_VIS   = int(0.5  * _EMU)
    _DISCONNECT = int(2.0 * _EMU)
    _COL_SPREAD = int(1.0 * _EMU)

    try:
        prs    = Presentation(bank_path)
        spTree = prs.slides[slide_idx].shapes._spTree
    except Exception as exc:
        return True, f"cannot open ({exc})"

    # ── Tagged-bank fast path ─────────────────────────────────────────────
    # Count distinct "Item N" placeholder tags found anywhere on the slide
    # (recursive — includes shapes nested inside groups).
    item_re = re.compile(r"^\s*Item\s+(\d+)\s*$", re.IGNORECASE)
    item_tags_seen = set()
    sp_tag_full = f"{{{_NS_P}}}sp"
    t_tag_full  = f"{{{_NS_A}}}t"
    for sp_el in spTree.iter(sp_tag_full):
        text = "".join(t.text or "" for t in sp_el.iter(t_tag_full)).strip()
        m = item_re.match(text)
        if m:
            n = int(m.group(1))
            if 1 <= n <= 50:
                item_tags_seen.add(n)
    if item_tags_seen:
        # This is a tagged-bank slide.  Just verify it has enough tags.
        if len(item_tags_seen) >= n_items:
            return True, f"tagged: {len(item_tags_seen)} Item-tags ≥ {n_items} needed"
        return False, f"tagged: only {len(item_tags_seen)} Item-tags, need {n_items}"

    # ── Legacy spatial check (untagged bank) ──────────────────────────────

    grp_tag = f"{{{_NS_P}}}grpSp"
    sp_tag  = f"{{{_NS_P}}}sp"

    # ── Visual group centers ───────────────────────────────────────────────────
    vis_centers: list[tuple[int, int]] = []
    for el in spTree:
        if el.tag != grp_tag:
            continue
        x, y, cx, cy = _xfrm(el)
        if cx >= _MIN_VIS and cy >= _MIN_VIS:
            vis_centers.append(_center(x, y, cx, cy))

    if not vis_centers:
        return True, "no visual groups to check"

    # ── Pattern-B: direct txBox sp in spTree ──────────────────────────────────
    txboxes: list[tuple[int, int, int, int]] = []   # (x, y, cx, cy)
    for el in spTree:
        if el.tag != sp_tag:
            continue
        if el.find(f".//{{{_NS_P}}}ph") is not None:
            continue
        nvSpPr  = el.find(f"{{{_NS_P}}}nvSpPr")
        if nvSpPr is None:
            continue
        cNvSpPr = nvSpPr.find(f"{{{_NS_P}}}cNvSpPr")
        if cNvSpPr is None or cNvSpPr.get("txBox") != "1":
            continue
        if el.find(f"{{{_NS_P}}}txBody") is None:
            continue
        x, y, cx, cy = _xfrm(el)
        if y < int(0.7 * _EMU) or cx < 45_720:
            continue
        txboxes.append((x, y, cx, cy))

    # ── Pattern A / Single-container: no direct text boxes ────────────────────
    if not txboxes:
        # New AI-bank layout: one outer grpSp that contains all content inside it.
        # Look inside the outer grpSp for txBox sp children (Pattern B equivalent)
        # and sub-grpSp children with txBox inside (Pattern C equivalent).
        outer_grps = [el for el in spTree if el.tag == grp_tag]
        if len(outer_grps) == 1:
            outer = outer_grps[0]

            def _is_txbox(sp):
                cNv = sp.find(f"{{{_NS_P}}}nvSpPr/{{{_NS_P}}}cNvSpPr")
                return cNv is not None and cNv.get("txBox") == "1"

            # Count direct txBox children inside the outer group
            inner_txbox = sum(1 for c in outer if c.tag == sp_tag and _is_txbox(c))
            # Count sub-grpSp children with at least one txBox sp inside
            inner_grp_writable = sum(
                1 for sg in outer
                if sg.tag == grp_tag and any(c.tag == sp_tag and _is_txbox(c) for c in sg)
            )
            # Either path gives enough slots → check for mixed-mode before accepting.
            # Mixed-mode: some label txBoxes are physically inside non-txBox visual
            # shapes (donuts, circles) → text appears inside the icon, not as an
            # external label. Detect by checking if any txBox center falls within any
            # non-txBox sp bounding box in the same coordinate space.
            if inner_txbox >= n_items:
                _vis_bounds = [
                    _xfrm(c) for c in outer
                    if c.tag == sp_tag and not _is_txbox(c) and _xfrm(c)[2] > 0
                ]
                if _vis_bounds:
                    _inside = 0
                    _total_tb = 0
                    for c in outer:
                        if c.tag != sp_tag or not _is_txbox(c):
                            continue
                        x_t, y_t, cx_t, cy_t = _xfrm(c)
                        if cx_t == 0:
                            continue
                        _total_tb += 1
                        ctr_x = x_t + cx_t // 2
                        ctr_y = y_t + cy_t // 2
                        for (vx, vy, vcx, vcy) in _vis_bounds:
                            if vx <= ctr_x <= vx + vcx and vy <= ctr_y <= vy + vcy:
                                _inside += 1
                                break
                    # Reject only when the MAJORITY of labels land inside visual shapes.
                    # A small fraction inside (e.g. hub designs) is acceptable;
                    # fully inside (timeline bars, donut-only) is not usable.
                    if _total_tb > 0 and _inside > _total_tb // 2:
                        return False, (
                            f"single-container: {_inside}/{_total_tb} label txboxes "
                            f"are inside visual shapes — mixed-mode layout"
                        )
                return True, f"single-container Pattern B: {inner_txbox} txboxes inside outer grp"
            if inner_grp_writable >= n_items:
                return True, f"single-container Pattern C: {inner_grp_writable} sub-grps with txbox"

            # Double-wrap: outer → 1 inner grpSp → N card grpSps each with txBox
            inner_grps = [c for c in outer if c.tag == grp_tag]
            if len(inner_grps) == 1:
                deep_writable = sum(
                    1 for sg in inner_grps[0]
                    if sg.tag == grp_tag and any(c.tag == sp_tag and _is_txbox(c) for c in sg)
                )
                if deep_writable >= n_items:
                    return True, f"double-wrap Pattern C: {deep_writable} deep sub-grps with txbox"

            if inner_txbox + inner_grp_writable > 0:
                return False, (
                    f"single-container: {inner_txbox} direct txbox + "
                    f"{inner_grp_writable} sub-grp slots, need {n_items}"
                )

        # Fallback: count top-level groups that have text-bearing sp children
        writable = 0
        for el in spTree:
            if el.tag != grp_tag:
                continue
            has_text = any(
                c.tag == sp_tag and c.find(f"{{{_NS_P}}}txBody") is not None
                for c in el.iter()
            )
            if has_text:
                writable += 1
        if writable < n_items:
            return False, f"Pattern A: only {writable} writable groups, need {n_items}"
        return True, f"Pattern A: {writable} writable groups"

    # ── Pattern B: check proximity ─────────────────────────────────────────────
    if len(txboxes) < n_items:
        return False, f"Pattern B: only {len(txboxes)} text boxes, need {n_items}"

    # Check if visual groups actually span the slide (if they don't, proximity
    # check is irrelevant — e.g. all groups are central hub)
    vis_x_vals  = [c[0] for c in vis_centers]
    vis_x_span  = max(vis_x_vals) - min(vis_x_vals)

    disconnected = 0
    for (tx, ty, tcx, tcy) in txboxes:
        tcx_c, tcy_c = _center(tx, ty, tcx, tcy)
        min_dist_sq = min(
            (tcx_c - gx) ** 2 + (tcy_c - gy) ** 2
            for gx, gy in vis_centers
        )
        if min_dist_sq > _DISCONNECT ** 2:
            disconnected += 1

    frac = disconnected / len(txboxes)
    if frac > 0.49:
        return False, (
            f"Pattern B: {disconnected}/{len(txboxes)} text boxes "
            f"are >2in from any visual shape"
        )

    # ── Column collapse: all text boxes in one narrow band ────────────────────
    if vis_x_span > _COL_SPREAD and len(txboxes) >= 2:
        tb_x_vals  = [tx for (tx, ty, tcx, tcy) in txboxes]
        tb_x_span  = max(tb_x_vals) - min(tb_x_vals)
        # Text boxes span < 20% of visual group spread → clustered
        if tb_x_span < vis_x_span * 0.20:
            return False, (
                f"column collapse: text boxes span {tb_x_span/_EMU:.1f}in "
                f"but visual groups span {vis_x_span/_EMU:.1f}in"
            )

    return True, f"Pattern B: {len(txboxes)} text boxes OK"
