"""Word table style generation with theme-aware colors.

This module generates Word table styles that use themeFill attributes
instead of hardcoded colors. This makes tables responsive to theme changes.

Uses SSOT (Single Source of Truth) pattern: XML structure from templates,
Python modifies attribute values via lxml DOM manipulation.

Key attributes:
- w:themeFill: Theme color slot (accent1, dk1, etc.)
- w:themeFillTint: Hex tint value (FF=full color, 00=white)
- w:themeFillShade: Hex shade value (FF=full color, 00=black)

Example XML:
    <w:shd w:val="clear"
           w:color="auto"
           w:fill="4472C4"
           w:themeFill="accent1"
           w:themeFillTint="66"/>

The w:fill attribute contains a fallback hex color for compatibility with
older Word versions and non-Microsoft applications. This color is computed
using the ThemePalette which provides pre-computed tint/shade values.
"""

from __future__ import annotations

import copy
from pathlib import Path
from typing import Optional

from lxml import etree

from tokenmoulds.colors.palette import ThemePalette, TransformType
from tokenmoulds.design.style_recipes import (
    ColorRecipe,
    TableStyleRecipe,
)
from tokenmoulds.emitters.xml_helpers import (
    clone_template_element,
    replace_placeholders,
)


NS_W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
W = f"{{{NS_W}}}"  # Namespace prefix for attribute names
NSMAP = {"w": NS_W}

# DrawingML → WordprocessingML themeColor value mapping
# DrawingML uses dk1/lt1/dk2/lt2; WordprocessingML uses dark1/light1/dark2/light2
_DML_TO_WML_THEME = {"dk1": "dark1", "lt1": "light1", "dk2": "dark2", "lt2": "light2"}


def _wml_theme_color(slot_value: str) -> str:
    """Convert DrawingML theme slot value to WordprocessingML themeColor value."""
    return _DML_TO_WML_THEME.get(slot_value, slot_value)


# Template path for SSOT approach
_TEMPLATE_PATH = (
    Path(__file__).resolve().parents[3]
    / "xml-structures"
    / "ecma-376"
    / "wordprocessingml"
    / "templates"
    / "table_style.xml"
)

# Cache for parsed template
_TEMPLATE_CACHE: etree._Element | None = None


def _load_template() -> etree._Element:
    """Load and cache the table style template."""
    global _TEMPLATE_CACHE
    if _TEMPLATE_CACHE is None:
        root = etree.parse(str(_TEMPLATE_PATH)).getroot()
        assert root is not None
        _TEMPLATE_CACHE = root
    # Return deep copy to avoid mutation
    assert _TEMPLATE_CACHE is not None
    return copy.deepcopy(_TEMPLATE_CACHE)


def _get_fallback_fill(
    recipe: ColorRecipe,
    palette: Optional[ThemePalette],
) -> str:
    """Get fallback fill color from palette.

    Args:
        recipe: Color recipe with theme slot and tint/shade
        palette: Pre-computed color palette, or None to use "auto"

    Returns:
        Hex color without # (e.g., "4472C4") or "auto" if no palette
    """
    if palette is None:
        return "auto"

    palette_slot = recipe.slot

    # Determine transform type and strength
    if recipe.tint is not None and recipe.tint > 0:
        transform = TransformType.TINT
        # Round to nearest standard level (20, 40, 60, 80)
        strength = round(recipe.tint / 20) * 20
    elif recipe.shade is not None and recipe.shade > 0:
        transform = TransformType.SHADE
        # Round to nearest standard level (20, 40)
        strength = round(recipe.shade / 20) * 20
    else:
        transform = TransformType.NONE
        strength = 0

    # Look up variant in palette
    variant = palette.get_variant(palette_slot, transform, strength)
    if variant:
        return variant.hex.lstrip("#")

    # Fallback to base color
    base = palette.get_base(palette_slot)
    if base:
        return base.hex.lstrip("#")

    return "auto"


def _compute_tint_hex(tint: float | None) -> str | None:
    """Compute Word tint hex value.

    Args:
        tint: Tint percentage (0-100), or None

    Returns:
        Hex value (e.g., "66") or None if no tint
    """
    if tint is None or tint <= 0:
        return None
    # Word tint: smaller hex = more tint (lighter)
    hex_val = int(255 * (1 - tint / 100))
    return f"{hex_val:02X}"


def _compute_shade_hex(shade: float | None) -> str | None:
    """Compute Word shade hex value.

    Args:
        shade: Shade percentage (0-100), or None

    Returns:
        Hex value (e.g., "66") or None if no shade
    """
    if shade is None or shade <= 0:
        return None
    # Word shade: smaller hex = more shade (darker)
    hex_val = int(255 * (1 - shade / 100))
    return f"{hex_val:02X}"


def _apply_shading_to_element(
    shd: etree._Element,
    recipe: ColorRecipe,
    palette: Optional[ThemePalette] = None,
) -> None:
    """Apply shading attributes to an existing w:shd element (SSOT pattern).

    Args:
        shd: Existing w:shd element from template
        recipe: Color recipe with theme slot and tint/shade
        palette: Pre-computed color palette for fallback colors
    """
    fallback_fill = _get_fallback_fill(recipe, palette)

    shd.set(f"{W}val", "clear")
    shd.set(f"{W}color", "auto")
    shd.set(f"{W}fill", fallback_fill)
    shd.set(f"{W}themeFill", recipe.slot.value)

    # Add tint if specified
    tint_hex = _compute_tint_hex(recipe.tint)
    if tint_hex:
        shd.set(f"{W}themeFillTint", tint_hex)
    elif f"{W}themeFillTint" in shd.attrib:
        del shd.attrib[f"{W}themeFillTint"]

    # Add shade if specified
    shade_hex = _compute_shade_hex(recipe.shade)
    if shade_hex:
        shd.set(f"{W}themeFillShade", shade_hex)
    elif f"{W}themeFillShade" in shd.attrib:
        del shd.attrib[f"{W}themeFillShade"]


def _apply_border_to_element(
    border: etree._Element,
    recipe: ColorRecipe,
    size: int = 4,
    palette: Optional[ThemePalette] = None,
) -> None:
    """Apply border attributes to an existing border element (SSOT pattern).

    Args:
        border: Existing border element from template (w:top, w:bottom, etc.)
        recipe: Color recipe with theme slot and tint/shade
        size: Border width in eighths of a point
        palette: Pre-computed color palette for fallback colors
    """
    fallback_color = _get_fallback_fill(recipe, palette)

    border.set(f"{W}val", "single")
    border.set(f"{W}sz", str(size))
    border.set(f"{W}space", "0")
    border.set(f"{W}color", fallback_color)
    border.set(f"{W}themeColor", _wml_theme_color(recipe.slot.value))

    # Add tint if specified
    tint_hex = _compute_tint_hex(recipe.tint)
    if tint_hex:
        border.set(f"{W}themeTint", tint_hex)
    elif f"{W}themeTint" in border.attrib:
        del border.attrib[f"{W}themeTint"]

    # Add shade if specified
    shade_hex = _compute_shade_hex(recipe.shade)
    if shade_hex:
        border.set(f"{W}themeShade", shade_hex)
    elif f"{W}themeShade" in border.attrib:
        del border.attrib[f"{W}themeShade"]


def generate_table_style(
    recipe: TableStyleRecipe,
    style_id: str = "BrandTable",
    palette: Optional[ThemePalette] = None,
) -> etree._Element:
    """Generate a Word table style element from a recipe (SSOT pattern).

    Uses template cloning: loads table_style.xml template, clones the
    table_style element, and populates attributes via DOM manipulation.

    Creates a complete w:style element with:
    - Base table properties
    - Header row styling (firstRow)
    - Row banding (band1Horz, band2Horz)
    - Typographic borders (horizontal rules only)

    Args:
        recipe: Table style recipe with header, banding, border settings
        style_id: Unique style ID for Word
        palette: Pre-computed color palette for fallback colors

    Returns:
        Complete w:style element ready for insertion into styles.xml
    """
    # Load template and clone the table style
    root = _load_template()
    style = clone_template_element(root, "table_style")

    # Replace basic placeholders
    replace_placeholders(
        style,
        {
            "STYLE_ID": style_id,
            "STYLE_NAME": recipe.name,
        },
    )

    # Apply table borders (top and bottom)
    top_border = style.find(".//w:top[@data-template='border_top']", NSMAP)
    if top_border is not None and recipe.borders.top:
        _apply_border_to_element(
            top_border, recipe.borders.top, size=8, palette=palette
        )
        del top_border.attrib["data-template"]
    elif top_border is not None:
        parent = top_border.getparent()
        if parent is not None:
            parent.remove(top_border)

    bottom_border = style.find(".//w:bottom[@data-template='border_bottom']", NSMAP)
    if bottom_border is not None and recipe.borders.bottom:
        _apply_border_to_element(
            bottom_border, recipe.borders.bottom, size=8, palette=palette
        )
        del bottom_border.attrib["data-template"]
    elif bottom_border is not None:
        parent = bottom_border.getparent()
        if parent is not None:
            parent.remove(bottom_border)

    # Apply header (firstRow) styling
    first_row = style.find(".//w:tblStylePr[@data-template='first_row']", NSMAP)
    if first_row is not None:
        del first_row.attrib["data-template"]

        # Header text color
        color_elem = first_row.find(".//w:color", NSMAP)
        if color_elem is not None:
            text_fallback = _get_fallback_fill(recipe.header.text, palette)
            color_elem.set(f"{W}val", text_fallback)
            color_elem.set(
                f"{W}themeColor", _wml_theme_color(recipe.header.text.slot.value)
            )

        # Header fill shading
        header_shd = first_row.find(".//w:shd", NSMAP)
        if header_shd is not None:
            _apply_shading_to_element(header_shd, recipe.header.fill, palette=palette)

        # Header separator border
        header_sep = first_row.find(
            ".//w:bottom[@data-template='header_separator']", NSMAP
        )
        if header_sep is not None and recipe.borders.header_separator:
            _apply_border_to_element(
                header_sep, recipe.borders.header_separator, size=12, palette=palette
            )
            del header_sep.attrib["data-template"]
        elif header_sep is not None:
            parent = header_sep.getparent()
            if parent is not None:
                parent.remove(header_sep)

    # Apply band1Horz (odd rows) styling
    band1 = style.find(".//w:tblStylePr[@data-template='band1_horz']", NSMAP)
    if band1 is not None:
        del band1.attrib["data-template"]
        band1_shd = band1.find(".//w:shd", NSMAP)
        if band1_shd is not None:
            _apply_shading_to_element(band1_shd, recipe.banding.band1, palette=palette)

    # Apply band2Horz (even rows) styling
    band2 = style.find(".//w:tblStylePr[@data-template='band2_horz']", NSMAP)
    if band2 is not None:
        del band2.attrib["data-template"]
        band2_shd = band2.find(".//w:shd", NSMAP)
        if band2_shd is not None:
            _apply_shading_to_element(band2_shd, recipe.banding.band2, palette=palette)

    # Clean up the template marker on style itself
    if "data-template" in style.attrib:
        del style.attrib["data-template"]

    return style


def generate_table_style_xml(
    recipe: TableStyleRecipe,
    style_id: str = "BrandTable",
    palette: Optional[ThemePalette] = None,
) -> bytes:
    """Generate table style as XML bytes.

    Args:
        recipe: Table style recipe
        style_id: Unique style ID
        palette: Pre-computed color palette for fallback colors

    Returns:
        XML bytes ready for insertion
    """
    style = generate_table_style(recipe, style_id, palette=palette)
    return etree.tostring(style, encoding="unicode").encode("utf-8")


__all__ = [
    "generate_table_style",
    "generate_table_style_xml",
]
