#!/usr/bin/env python3
"""Shared diagnostic contract for unsupported imported object effects."""

from __future__ import annotations

import json
from xml.etree import ElementTree as ET


EFFECT_STATUS_ATTR = "data-pptx-effect-status"
EFFECT_REASON_ATTR = "data-pptx-effect-reason"
UNSUPPORTED_EFFECT_STATUS = "unsupported"
_EFFECT_OBJECT_IDENTITY_ATTRS = (
    "data-pptx-object",
    "data-pptx-shape-id",
    "data-pptx-shape-scope",
)
_DML_NAMESPACE = "http://schemas.openxmlformats.org/drawingml/2006/main"
_TEXT_PROPERTY_TAGS = frozenset({
    f"{{{_DML_NAMESPACE}}}defRPr",
    f"{{{_DML_NAMESPACE}}}endParaRPr",
    f"{{{_DML_NAMESPACE}}}rPr",
})
_RUN_EFFECT_CONTAINER_TAGS = frozenset({
    f"{{{_DML_NAMESPACE}}}effectLst",
    f"{{{_DML_NAMESPACE}}}effectDag",
})


def project_effect_status_errors(root: ET.Element) -> list[str]:
    """Return blocking diagnostics for invalid or unsupported effect metadata."""
    errors: set[str] = set()
    parents = {
        child: parent
        for parent in root.iter()
        for child in parent
    }
    for elem in root.iter():
        raw_status = elem.get(EFFECT_STATUS_ATTR)
        raw_reason = elem.get(EFFECT_REASON_ATTR)
        if raw_status is None and raw_reason is None:
            continue
        parent = parents.get(elem)
        if (
            parent is not None
            and parent.get(EFFECT_STATUS_ATTR) == raw_status
            and parent.get(EFFECT_REASON_ATTR) == raw_reason
            and _same_source_object(parent, elem)
        ):
            # Import duplicates the marker on the logical object and carrier
            # so stripping either copy cannot erase the block. Report it once.
            continue
        label = _element_label(elem)
        status = (raw_status or "").strip()
        if status != UNSUPPORTED_EFFECT_STATUS:
            errors.add(
                f'{label} {EFFECT_STATUS_ATTR} must equal '
                f'{UNSUPPORTED_EFFECT_STATUS!r}; got {raw_status!r}'
            )
            continue
        reason = (raw_reason or "").strip()
        if not reason:
            errors.add(
                f'{label} {EFFECT_REASON_ATTR} requires a non-empty reason'
            )
            continue
        errors.add(f'{label} has unsupported source PPTX effect: {reason}')
    return sorted(errors)


def unsupported_effect_metadata(*reasons: str) -> dict[str, str]:
    """Build one canonical import marker without dropping compound reasons."""
    normalized: set[str] = set()
    for reason in reasons:
        reason = reason.strip()
        if not reason:
            raise ValueError("Unsupported PPTX effect reason must not be empty")
        items: object = reason
        if reason.startswith("["):
            try:
                items = json.loads(reason)
            except json.JSONDecodeError:
                pass
        if not isinstance(items, list):
            items = [reason]
        if not all(isinstance(item, str) and item.strip() for item in items):
            raise ValueError("Unsupported PPTX effect reasons must be strings")
        normalized.update(item.strip() for item in items)
    if not normalized:
        raise ValueError("Unsupported PPTX effect reason must not be empty")
    ordered = sorted(normalized)
    encoded = (
        ordered[0]
        if len(ordered) == 1
        else json.dumps(ordered, separators=(",", ":"))
    )
    return {
        EFFECT_STATUS_ATTR: UNSUPPORTED_EFFECT_STATUS,
        EFFECT_REASON_ATTR: encoded,
    }


def txbody_has_run_effects(*text_style_roots: ET.Element | None) -> bool:
    """Return whether rebuilding any supplied text style would lose an effect."""
    for root in text_style_roots:
        if root is None:
            continue
        for properties in root.iter():
            if properties.tag not in _TEXT_PROPERTY_TAGS:
                continue
            for child in properties:
                if child.tag in _RUN_EFFECT_CONTAINER_TAGS and any(
                    isinstance(effect.tag, str)
                    for effect in child
                ):
                    return True
    return False


def _element_label(elem: ET.Element) -> str:
    tag = elem.tag.rsplit("}", 1)[-1]
    elem_id = elem.get("id") or elem.get("data-name")
    if elem_id:
        return f'<{tag} id="{elem_id}">'
    shape_id = elem.get("data-pptx-shape-id")
    if shape_id:
        object_kind = elem.get("data-pptx-object") or "object"
        scope = elem.get("data-pptx-shape-scope") or "unknown"
        return (
            f'<{tag} data-pptx-object="{object_kind}" '
            f'data-pptx-shape-id="{shape_id}" '
            f'data-pptx-shape-scope="{scope}">'
        )
    return f"<{tag}>"


def _same_source_object(parent: ET.Element, child: ET.Element) -> bool:
    """Return whether a parent/child marker describes one imported object."""
    return all(
        child.get(attr) is not None
        and child.get(attr) == parent.get(attr)
        for attr in _EFFECT_OBJECT_IDENTITY_ATTRS
    )
