"""Fill token → OOXML DrawingML fill element emission.

Maps semantic fill tokens (solid, gradient, pattern, placeholder) to native
OOXML fill primitives with ``a:schemeClr`` theme-linking so fills recolor
automatically when the theme changes.
"""

from __future__ import annotations

from typing import Literal

from lxml import etree

from tokenmoulds._compat import strip_hex
from tokenmoulds.emitters.xml_constants import A_NS, R_NS

ColorGradeName = Literal["duotone", "screen", "desaturated", "monochrome", "none"]
FillTypeName = Literal[
    "solid", "gradient", "pattern", "placeholder", "photo", "illustration"
]


def _is_theme_color(color: str) -> bool:
    """Check if a color string is a theme slot name (not a hex value)."""
    return not color.startswith("#")


def _create_color(
    parent: etree._Element, color: str, alpha: int | None = None
) -> etree._Element:
    """Create a color element — ``a:schemeClr`` for theme slots, ``a:srgbClr`` for hex."""
    if _is_theme_color(color):
        el = etree.SubElement(parent, f"{{{A_NS}}}schemeClr")
        el.set("val", color)
    else:
        el = etree.SubElement(parent, f"{{{A_NS}}}srgbClr")
        el.set("val", strip_hex(color))
    if alpha is not None:
        a_el = etree.SubElement(el, f"{{{A_NS}}}alpha")
        a_el.set("val", str(alpha))
    return el


def _opacity_to_ooxml(opacity: float) -> int:
    """Convert 0-1 opacity to OOXML 0-100000 units."""
    return int(round(opacity * 100000))


def _emit_solid_fill(parent: etree._Element, tokens: dict) -> etree._Element:
    fill = etree.SubElement(parent, f"{{{A_NS}}}solidFill")
    alpha = _opacity_to_ooxml(tokens["opacity"]) if "opacity" in tokens else None
    _create_color(fill, tokens["color"], alpha=alpha)
    return fill


def _emit_gradient_fill(parent: etree._Element, tokens: dict) -> etree._Element:
    fill = etree.SubElement(parent, f"{{{A_NS}}}gradFill")
    fill.set("rotWithShape", "1")

    alpha = _opacity_to_ooxml(tokens["opacity"]) if "opacity" in tokens else None

    gs_lst = etree.SubElement(fill, f"{{{A_NS}}}gsLst")

    # Start stop
    gs0 = etree.SubElement(gs_lst, f"{{{A_NS}}}gs")
    gs0.set("pos", "0")
    _create_color(gs0, tokens["from"], alpha=alpha)

    # End stop
    gs1 = etree.SubElement(gs_lst, f"{{{A_NS}}}gs")
    gs1.set("pos", "100000")
    _create_color(gs1, tokens["to"], alpha=alpha)

    # Linear direction — OOXML angle is in 60000ths of a degree
    angle_deg = tokens.get("angle", 0)
    lin = etree.SubElement(fill, f"{{{A_NS}}}lin")
    lin.set("ang", str(int(round(angle_deg * 60000))))
    lin.set("scaled", "0")

    return fill


def _emit_pattern_fill(parent: etree._Element, tokens: dict) -> etree._Element:
    fill = etree.SubElement(parent, f"{{{A_NS}}}pattFill")
    fill.set("prst", tokens["preset"])

    alpha = _opacity_to_ooxml(tokens["opacity"]) if "opacity" in tokens else None

    fg_clr = etree.SubElement(fill, f"{{{A_NS}}}fgClr")
    _create_color(fg_clr, tokens["foreground"], alpha=alpha)

    bg_clr = etree.SubElement(fill, f"{{{A_NS}}}bgClr")
    _create_color(bg_clr, tokens["background"])

    return fill


def _emit_placeholder_fill(parent: etree._Element, tokens: dict) -> etree._Element:
    """Placeholder fills render as solid — the label is metadata for the content pipeline."""
    return _emit_solid_fill(parent, tokens)


def _emit_photo_fill(parent: etree._Element, tokens: dict) -> etree._Element:
    """Emit a:blipFill with embedded image reference and color grade effects."""
    fill = etree.SubElement(parent, f"{{{A_NS}}}blipFill")
    fill.set("dpi", "150")
    fill.set("rotWithShape", "1")

    blip = etree.SubElement(fill, f"{{{A_NS}}}blip")
    blip.set(f"{{{R_NS}}}embed", tokens["relationship_id"])
    blip.set("cstate", "print")

    grade = tokens.get("colorGrade", "none")
    intensity = tokens.get("gradeIntensity", 0.7)
    apply_color_grade(blip, grade, intensity)

    # Smart crop
    crop = tokens.get("crop")
    if crop:
        src_rect = etree.SubElement(fill, f"{{{A_NS}}}srcRect")
        for attr in ("l", "t", "r", "b"):
            if attr in crop:
                src_rect.set(attr, str(crop[attr]))

    # Stretch to fill
    stretch = etree.SubElement(fill, f"{{{A_NS}}}stretch")
    etree.SubElement(stretch, f"{{{A_NS}}}fillRect")

    return fill


def _emit_illustration_fill(parent: etree._Element, tokens: dict) -> etree._Element:
    """Emit vector illustration shapes from svg2ooxml DrawingML output.

    When ``shape_xml`` is present (svg2ooxml output), parses and embeds the
    shapes directly as DrawingML elements. The shapes already contain
    ``a:schemeClr`` for theme-aware colors — no post-processing needed.

    Falls back to ``a:solidFill`` when no shape XML is available.
    """
    shape_xml = tokens.get("shape_xml")
    if not shape_xml:
        return _emit_solid_fill(parent, {"color": "lt1"})

    return _embed_shape_xml(parent, shape_xml)


def _embed_shape_xml(parent: etree._Element, shape_xml: str) -> etree._Element:
    """Parse svg2ooxml shape XML and embed as child elements of parent.

    Returns the first embedded element (for API compatibility with fill emitters).
    """
    # Wrap in a root so lxml can parse multiple shape fragments
    wrapped = f"<shapes xmlns:a='{A_NS}' xmlns:r='{R_NS}'>{shape_xml}</shapes>"
    root = etree.fromstring(wrapped)  # noqa: S320
    first: etree._Element | None = None
    for shape in root:
        parent.append(shape)
        if first is None:
            first = shape
    # If parsing produced nothing, fall back to a solid placeholder
    if first is None:
        return _emit_solid_fill(parent, {"color": "lt1"})
    return first


# PresentationML namespace for shape wrapping
P_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"


def emit_illustration_group(
    parent: etree._Element,
    shape_xml: str,
) -> etree._Element:
    """Wrap svg2ooxml shape XML in a PresentationML group shape (``p:grpSp``).

    Use this when placing vector illustrations on a slide canvas.
    """
    grp = etree.SubElement(parent, f"{{{P_NS}}}grpSp")
    _embed_shape_xml(grp, shape_xml)
    return grp


# WordprocessingML wrapping namespaces
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
WP_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
GRAPHIC_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"


def emit_illustration_inline(
    parent: etree._Element,
    shape_xml: str,
    *,
    cx: int = 5486400,
    cy: int = 3657600,
) -> etree._Element:
    """Wrap svg2ooxml shapes for WordprocessingML inline drawing.

    Produces ``w:drawing > wp:inline > a:graphic > a:graphicData`` wrapping.

    Args:
        parent: Parent element (typically a ``w:r`` run element).
        shape_xml: DrawingML shape XML from svg2ooxml.
        cx: Extent width in EMU (default ~14.4cm).
        cy: Extent height in EMU (default ~9.6cm).
    """
    drawing = etree.SubElement(parent, f"{{{W_NS}}}drawing")
    inline = etree.SubElement(drawing, f"{{{WP_NS}}}inline")
    inline.set("distT", "0")
    inline.set("distB", "0")
    inline.set("distL", "0")
    inline.set("distR", "0")

    extent = etree.SubElement(inline, f"{{{WP_NS}}}extent")
    extent.set("cx", str(cx))
    extent.set("cy", str(cy))

    doc_pr = etree.SubElement(inline, f"{{{WP_NS}}}docPr")
    doc_pr.set("id", "1")
    doc_pr.set("name", "Illustration")

    graphic = etree.SubElement(inline, f"{{{GRAPHIC_NS}}}graphic")
    graphic_data = etree.SubElement(graphic, f"{{{GRAPHIC_NS}}}graphicData")
    graphic_data.set("uri", GRAPHIC_NS)

    _embed_shape_xml(graphic_data, shape_xml)

    return drawing


_FILL_EMITTERS = {
    "solid": _emit_solid_fill,
    "gradient": _emit_gradient_fill,
    "pattern": _emit_pattern_fill,
    "placeholder": _emit_placeholder_fill,
    "photo": _emit_photo_fill,
    "illustration": _emit_illustration_fill,
}


# =============================================================================
# COLOR GRADE RECIPES for a:blip elements
# =============================================================================


def _lerp(a: int, b: int, t: float) -> int:
    """Linear interpolation between two OOXML unit values."""
    return int(round(a + (b - a) * t))


def _grade_duotone(blip: etree._Element, intensity: float) -> None:
    """Brand duotone: shadows → dk1, highlights → accent1."""
    dt = etree.SubElement(blip, f"{{{A_NS}}}duotone")

    shadow = etree.SubElement(dt, f"{{{A_NS}}}schemeClr")
    shadow.set("val", "dk1")
    shade = etree.SubElement(shadow, f"{{{A_NS}}}shade")
    shade.set("val", str(_lerp(60000, 20000, intensity)))

    highlight = etree.SubElement(dt, f"{{{A_NS}}}schemeClr")
    highlight.set("val", "accent1")
    tint = etree.SubElement(highlight, f"{{{A_NS}}}tint")
    tint.set("val", str(_lerp(94000, 40000, intensity)))


def _grade_screen(blip: etree._Element, intensity: float) -> None:
    """Semi-transparent brand color overlay."""
    # Slight darken for contrast
    lum = etree.SubElement(blip, f"{{{A_NS}}}lum")
    lum.set("bright", "-10000")
    lum.set("contrast", "10000")

    overlay = etree.SubElement(blip, f"{{{A_NS}}}fillOverlay")
    overlay.set("blend", "over")
    solid = etree.SubElement(overlay, f"{{{A_NS}}}solidFill")
    scheme = etree.SubElement(solid, f"{{{A_NS}}}schemeClr")
    scheme.set("val", "accent1")
    alpha = etree.SubElement(scheme, f"{{{A_NS}}}alpha")
    alpha.set("val", str(_lerp(15000, 60000, intensity)))


def _grade_desaturated(blip: etree._Element, intensity: float) -> None:
    """Muted editorial look — reduced saturation, boosted contrast."""
    sat = etree.SubElement(blip, f"{{{A_NS}}}sat")
    sat.set("val", str(_lerp(80000, 20000, intensity)))
    lum = etree.SubElement(blip, f"{{{A_NS}}}lum")
    lum.set("bright", "5000")
    lum.set("contrast", str(_lerp(5000, 20000, intensity)))


def _grade_monochrome(blip: etree._Element, intensity: float) -> None:
    """Grayscale then duotone — full brand color lock."""
    etree.SubElement(blip, f"{{{A_NS}}}grayscl")
    _grade_duotone(blip, intensity)


def _grade_none(blip: etree._Element, intensity: float) -> None:
    """No effects — raw photo."""


_COLOR_GRADES = {
    "duotone": _grade_duotone,
    "screen": _grade_screen,
    "desaturated": _grade_desaturated,
    "monochrome": _grade_monochrome,
    "none": _grade_none,
}


def apply_color_grade(
    blip: etree._Element,
    grade: ColorGradeName,
    intensity: float = 0.7,
) -> None:
    """Apply a color grade recipe to an ``a:blip`` element.

    Args:
        blip: The ``a:blip`` element to add effects to.
        grade: Recipe name (duotone, screen, desaturated, monochrome, none).
        intensity: Effect strength 0.0 (subtle) to 1.0 (full).

    Raises:
        ValueError: If grade name is unknown.
    """
    fn = _COLOR_GRADES.get(grade)
    if fn is None:
        msg = f"Unknown color grade: {grade!r}. Valid: {', '.join(_COLOR_GRADES)}"
        raise ValueError(msg)
    fn(blip, max(0.0, min(1.0, intensity)))


def emit_fill(parent: etree._Element, fill_tokens: dict) -> etree._Element:
    """Emit an OOXML fill element from fill tokens.

    Photo fills degrade gracefully to a solid placeholder fill when the
    image could not be fetched (``relationship_id`` missing). Illustration
    fills degrade when ``shape_xml`` is missing.

    Args:
        parent: Parent XML element to append the fill to.
        fill_tokens: Dict with ``type`` key and type-specific properties.

    Returns:
        The created fill element.

    Raises:
        ValueError: If fill type is unknown.
        KeyError: If required keys are missing.
    """
    fill_type = fill_tokens["type"]

    # Graceful degradation: photo without an embedded image falls back to solid.
    # Illustrations handle their own fallback (shape_xml or solid).
    if fill_type == "photo" and not fill_tokens.get("relationship_id"):
        return _emit_solid_fill(parent, {"color": "lt1"})

    emitter = _FILL_EMITTERS.get(fill_type)
    if emitter is None:
        msg = (
            f"Unsupported fill type: {fill_type!r}. Valid: {', '.join(_FILL_EMITTERS)}"
        )
        raise ValueError(msg)
    return emitter(parent, fill_tokens)
