"""Word document emitter that renders the Document IR into a DOTX template.

SSOT Architecture:
==================
All XML structure comes from xml-structures/ecma-376/wordprocessingml/ as the
Single Source of Truth. The emitter loads templates and modifies attribute
values - it never constructs new XML elements programmatically.

For dynamic content (e.g., paragraphs, font entries), template fragments are
loaded, values substituted, parsed, and inserted into the document.
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Dict, Literal

from lxml import etree

from tokenmoulds.design.style_recipes import TableStyleRecipe
from tokenmoulds.emitters.ooxml_base import OOXMLBaseEmitter
from tokenmoulds.emitters.word_sections_mixin import WordSectionsMixin
from tokenmoulds.emitters.word_shared import NSMAP, W14, W
from tokenmoulds.emitters.word_tables import generate_table_style
from tokenmoulds.ir import (
    CharacterTypography,
    DocumentIR,
    LineSpacing,
    OpenTypeFeatures,
    ParagraphControl,
    ParagraphIndent,
)
from tokenmoulds.styles.resolver import ResolvedStyle, StyleResolver
from tokenmoulds.styles.word_builtin import BUILTIN_STYLES, StyleCategory, StyleType

logger = logging.getLogger(__name__)


# =============================================================================
# WORD DOCUMENT EMITTER
# =============================================================================


class WordDocumentEmitter(WordSectionsMixin, OOXMLBaseEmitter):
    """Build Word templates from the canonical Document IR.

    Uses a known-good base template and modifies specific style values
    based on the Document IR, preserving Word-required internal structures.

    Inherits from OOXMLBaseEmitter which provides:
    - ZIP package building pattern
    - Font embedding infrastructure (ODTTF format)
    - Theme customization (color scheme, font scheme)
    - Core properties metadata

    Word-specific features:
    - Basic typography (fonts, sizes, colors, spacing)
    - Advanced typography (kerning, tracking; OpenType features suppressed for OOXML)
    - Paragraph control (justification, indentation, keep-with-next)
    - Document settings (hyphenation, tab stops)
    - Numbering definitions (bullets, numbered lists)
    """

    def __init__(
        self,
        document: DocumentIR,
        template_root: Path | str | None = None,
        embed_fonts: bool = False,
        fonts_dir: Path | str | None = None,
        style_resolver: StyleResolver | None = None,
        use_pure_text_colors: bool = True,
    ) -> None:
        super().__init__(
            document=document,
            template_root=template_root,
            embed_fonts=embed_fonts,
            fonts_dir=fonts_dir,
            use_pure_text_colors=use_pure_text_colors,
        )
        self.style_resolver = style_resolver
        self.allow_opentype_features = False

    # =========================================================================
    # SSOT PACKAGE STRUCTURE
    # =========================================================================

    # Word package structure: archive path -> template name
    PACKAGE_STRUCTURE = {
        "[Content_Types].xml": "content_types.xml",
        "_rels/.rels": "package_rels.xml",
        "word/document.xml": "document.xml",
        "word/styles.xml": "styles.xml",
        "word/stylesWithEffects.xml": "styles.xml",
        "word/settings.xml": "settings.xml",
        "word/numbering.xml": "numbering.xml",
        "word/fontTable.xml": "fontTable.xml",
        "word/webSettings.xml": "webSettings.xml",
        "word/theme/theme1.xml": "theme.xml",
        "word/_rels/document.xml.rels": "document_rels.xml",
        "docProps/core.xml": "docprops_core.xml",
        "docProps/app.xml": "docprops_app.xml",
    }

    # =========================================================================
    # ABSTRACT METHOD IMPLEMENTATIONS (from OOXMLBaseEmitter)
    # =========================================================================

    def _get_format_key(self) -> str:
        """Return 'word' for template resolution."""
        return "word"

    def _get_package_structure(self) -> Dict[str, str]:
        """Return the Word package structure for SSOT building."""
        return self.PACKAGE_STRUCTURE

    def _get_document_title(self) -> str:
        """Return document title for metadata."""
        return f"{self.document.org_id} Template"

    def _get_font_embedding_format(self) -> Literal["odttf", "eot", "none"]:
        """Return 'odttf' - Word uses GUID-obfuscated fonts."""
        return "odttf"

    def _get_font_entries_path(self) -> str:
        """Return path prefix for embedded fonts."""
        return "word/fonts/"

    def _should_customize_entry(self, entry_path: str) -> bool:
        """Check if entry needs customization."""
        # All entries that need IR values applied
        customizable = {
            "[Content_Types].xml",
            "word/styles.xml",
            "word/stylesWithEffects.xml",
            "word/settings.xml",
            "word/theme/theme1.xml",
            "word/document.xml",
            "word/numbering.xml",
            "word/fontTable.xml",
            "docProps/core.xml",
            "docProps/app.xml",
        }
        return entry_path in customizable

    def _customize_entry(self, entry_path: str, data: bytes) -> bytes | None:
        """Customize specific ZIP entry with Document IR values.

        For SSOT approach, this modifies attribute values in the template XML.
        Templates with ${PLACEHOLDER} syntax are substituted before parsing.
        """
        if entry_path == "[Content_Types].xml":
            return self._customize_content_types(data)
        elif entry_path in ("word/styles.xml", "word/stylesWithEffects.xml"):
            return self._customize_styles(data)
        elif entry_path == "word/settings.xml":
            return self._customize_settings(data)
        elif entry_path == "word/theme/theme1.xml":
            return self._customize_theme(data)
        elif entry_path == "word/document.xml":
            return self._customize_document(data)
        elif entry_path == "word/numbering.xml":
            return self._customize_numbering(data)
        elif entry_path == "word/fontTable.xml":
            return self._customize_font_table(data)
        elif entry_path == "docProps/core.xml":
            return self._customize_core_props_ssot(data)
        elif entry_path == "docProps/app.xml":
            return self._customize_app_props_ssot(data)
        return data

    def _customize_core_props_ssot(self, data: bytes) -> bytes:
        """Customize core properties using SSOT template substitution."""
        timestamp = self._utc_timestamp()
        content = data.decode("utf-8")
        content = content.replace("${TITLE}", self._get_document_title())
        content = content.replace("${CREATOR}", "TokenMoulds")
        content = content.replace("${MODIFIER}", "TokenMoulds")
        content = content.replace("${TIMESTAMP}", timestamp)
        return content.encode("utf-8")

    def _customize_app_props_ssot(self, data: bytes) -> bytes:
        """Customize app properties using SSOT template substitution."""
        content = data.decode("utf-8")
        content = content.replace("${APPLICATION}", "TokenMoulds")
        content = content.replace("${COMPANY}", self.document.org_id)
        return content.encode("utf-8")

    def _get_font_metadata_entries(self) -> list:
        """Return Word font relationships as PackagedEntry list."""
        from tokenmoulds.packaging.types import PackagedEntry

        if self.embedded_fonts:
            rels_data = self._build_font_relationships()
            return [
                PackagedEntry(
                    archive_path="word/_rels/fontTable.xml.rels",
                    data=rels_data,
                )
            ]
        return []

    # =========================================================================
    # STYLES.XML - Style Definitions (SSOT approach)
    # =========================================================================

    def _customize_styles(self, data: bytes) -> bytes:
        """Customize styles.xml with Document IR values.

        SSOT approach: The template styles.xml already contains all built-in
        styles with correct structure. This method only modifies attribute
        values - it never creates new elements.

        The template uses TokenMoulds base styles (TMBodyBase, TMHeadingBase, etc.)
        which define the inheritance hierarchy. This method updates font names,
        sizes, colors, and spacing from the Document IR.
        """
        root = etree.fromstring(data)

        # Update docDefaults with body font from Document IR
        self._update_doc_defaults_ssot(root)

        # Update built-in styles with Document IR values
        # The template already has all these styles defined
        self._update_style_ssot(root, "Normal", self.document.body_style)
        self._update_style_ssot(root, "BodyText", self.document.body_style)

        # Update heading styles
        for heading in self.document.heading_styles:
            style_id = f"Heading{heading.level}"
            self._update_style_ssot(root, style_id, heading)
            # Also update the linked character style
            self._update_style_ssot(root, f"{style_id}Char", heading)

        # Update Quote style
        self._update_style_ssot(root, "Quote", self.document.quote_style)

        # Update Hyperlink styles
        self._update_hyperlink_style_ssot(root)

        # Update base styles (TokenMoulds foundation styles)
        self._update_base_styles_ssot(root)

        # Add table styles with theme-aware colors
        self._add_table_styles_ssot(root)

        # Add missing built-in styles so Word doesn't inject its own defaults
        self._add_missing_builtin_styles(root)

        return etree.tostring(root, xml_declaration=True, encoding="UTF-8")

    def _update_doc_defaults_ssot(self, root: etree._Element) -> None:
        """Update docDefaults with body font settings.

        SSOT: Only modifies existing attribute values, never creates elements.
        """
        doc_defaults = root.find(".//w:docDefaults", NSMAP)
        if doc_defaults is None:
            return

        rpr = doc_defaults.find(".//w:rPrDefault/w:rPr", NSMAP)
        if rpr is not None:
            # Update fonts - find existing rFonts element
            rfonts = rpr.find("w:rFonts", NSMAP)
            if rfonts is not None:
                font = self.document.body_style.font_family
                rfonts.set(f"{W}ascii", font)
                rfonts.set(f"{W}hAnsi", font)
                rfonts.set(f"{W}eastAsia", font)
                rfonts.set(f"{W}cs", font)

            # Update font size
            sz = rpr.find("w:sz", NSMAP)
            if sz is not None:
                sz.set(f"{W}val", str(int(self.document.body_style.font_size_pt * 2)))
            szCs = rpr.find("w:szCs", NSMAP)
            if szCs is not None:
                szCs.set(f"{W}val", str(int(self.document.body_style.font_size_pt * 2)))

    def _update_style_ssot(
        self, root: etree._Element, style_id: str, style_data
    ) -> None:
        """Update a style with Document IR values.

        SSOT: Only modifies existing attribute values in the template.
        """
        style_el = root.find(f'.//w:style[@w:styleId="{style_id}"]', NSMAP)
        if style_el is None:
            return

        # Update run properties (character-level)
        rpr = style_el.find("w:rPr", NSMAP)
        if rpr is not None:
            self._update_rpr_ssot(rpr, style_data)

        # Update paragraph properties
        ppr = style_el.find("w:pPr", NSMAP)
        if ppr is not None:
            self._update_ppr_ssot(ppr, style_data)

    def _update_rpr_ssot(self, rpr: etree._Element, style_data) -> None:
        """Update run properties with style data.

        SSOT: Only modifies existing elements.
        """
        # Font family
        rfonts = rpr.find("w:rFonts", NSMAP)
        if rfonts is not None:
            font = style_data.font_family
            rfonts.set(f"{W}ascii", font)
            rfonts.set(f"{W}hAnsi", font)
            if rfonts.get(f"{W}eastAsia") is not None:
                rfonts.set(f"{W}eastAsia", font)
            if rfonts.get(f"{W}cs") is not None:
                rfonts.set(f"{W}cs", font)

        # Font size (half-points)
        sz = rpr.find("w:sz", NSMAP)
        if sz is not None:
            sz.set(f"{W}val", str(int(style_data.font_size_pt * 2)))
        szCs = rpr.find("w:szCs", NSMAP)
        if szCs is not None:
            szCs.set(f"{W}val", str(int(style_data.font_size_pt * 2)))

        # Color
        color = rpr.find("w:color", NSMAP)
        if color is not None:
            color.set(f"{W}val", style_data.color.lstrip("#").upper())

    def _update_ppr_ssot(self, ppr: etree._Element, style_data) -> None:
        """Update paragraph properties with style data.

        SSOT: Only modifies existing elements.
        """
        # Spacing
        spacing = ppr.find("w:spacing", NSMAP)
        if spacing is not None and hasattr(style_data, "spacing"):
            spacing.set(f"{W}before", str(style_data.spacing.before_twips))
            spacing.set(f"{W}after", str(style_data.spacing.after_twips))

    def _update_hyperlink_style_ssot(self, root: etree._Element) -> None:
        """Update Hyperlink styles with Document IR colors.

        SSOT: Only modifies existing elements.
        """
        # Hyperlink style
        hyperlink = root.find('.//w:style[@w:styleId="Hyperlink"]', NSMAP)
        if hyperlink is not None:
            rpr = hyperlink.find("w:rPr", NSMAP)
            if rpr is not None:
                color = rpr.find("w:color", NSMAP)
                if color is not None:
                    color.set(
                        f"{W}val",
                        self.document.hyperlink_style.link_color.lstrip("#").upper(),
                    )

        # Followed Hyperlink style
        followed = root.find('.//w:style[@w:styleId="FollowedHyperlink"]', NSMAP)
        if followed is not None:
            rpr = followed.find("w:rPr", NSMAP)
            if rpr is not None:
                color = rpr.find("w:color", NSMAP)
                if color is not None:
                    color.set(
                        f"{W}val",
                        self.document.hyperlink_style.visited_color.lstrip("#").upper(),
                    )

    def _update_base_styles_ssot(self, root: etree._Element) -> None:
        """Update TokenMoulds base styles with Document IR values.

        SSOT: The template has TMBodyBase, TMHeadingBase, etc. as hidden base
        styles. Updating these propagates changes to all derived styles.
        """
        # Update TMBodyBase
        body_base = root.find('.//w:style[@w:styleId="TMBodyBase"]', NSMAP)
        if body_base is not None:
            rpr = body_base.find("w:rPr", NSMAP)
            if rpr is not None:
                self._update_rpr_ssot(rpr, self.document.body_style)
            ppr = body_base.find("w:pPr", NSMAP)
            if ppr is not None:
                self._update_ppr_ssot(ppr, self.document.body_style)

        # Update TMHeadingBase with heading font
        heading_base = root.find('.//w:style[@w:styleId="TMHeadingBase"]', NSMAP)
        if heading_base is not None:
            rpr = heading_base.find("w:rPr", NSMAP)
            if rpr is not None:
                rfonts = rpr.find("w:rFonts", NSMAP)
                if rfonts is not None:
                    font = self.document.typography.heading_font
                    rfonts.set(f"{W}ascii", font)
                    rfonts.set(f"{W}hAnsi", font)
                    if rfonts.get(f"{W}eastAsia") is not None:
                        rfonts.set(f"{W}eastAsia", font)
                    if rfonts.get(f"{W}cs") is not None:
                        rfonts.set(f"{W}cs", font)
                # Update heading color
                color = rpr.find("w:color", NSMAP)
                if color is not None and self.document.heading_styles:
                    color.set(
                        f"{W}val",
                        self.document.heading_styles[0].color.lstrip("#").upper(),
                    )

    def _add_table_styles_ssot(self, root: etree._Element) -> None:
        """Add table styles with theme-aware colors.

        Creates table styles using the palette system:
        - Theme references (themeFill, themeFillTint, themeFillShade) for Word
        - Pre-computed fallback colors from the palette
        """
        # Create table recipe based on brand tone
        recipe = TableStyleRecipe.from_brand_tone(
            self.document.brand_tone,
            name="Brand Table",
        )

        # Generate table style with palette for fallback colors
        table_style = generate_table_style(
            recipe,
            style_id="BrandTable",
            palette=self.document.color_palette,
        )

        # Insert before latentStyles or at end
        latent = root.find("w:latentStyles", NSMAP)
        if latent is not None:
            latent.addprevious(table_style)
        else:
            root.append(table_style)

    def _add_missing_builtin_styles(self, root: etree._Element) -> None:
        """Add stub definitions for built-in styles not already in the template.

        Word auto-generates missing built-in styles using its own defaults,
        which conflict with our design system. By defining them explicitly
        (inheriting from our base styles), we ensure consistent typography.

        Table accent styles (98 variants) are excluded — they use theme
        references and auto-brand correctly without explicit definitions.
        """
        # Collect IDs already present
        existing = {
            style.get(f"{W}styleId") for style in root.findall(".//w:style", NSMAP)
        }

        for defn in BUILTIN_STYLES:
            if defn.style_id in existing:
                continue
            # Skip table accent styles — they're complex and theme-aware
            if defn.category == StyleCategory.TABLE:
                continue

            style_el = etree.SubElement(root, f"{W}style")
            style_el.set(f"{W}type", defn.style_type.value)
            style_el.set(f"{W}styleId", defn.style_id)

            name_el = etree.SubElement(style_el, f"{W}name")
            name_el.set(f"{W}val", defn.name)

            if defn.based_on:
                based = etree.SubElement(style_el, f"{W}basedOn")
                based.set(f"{W}val", defn.based_on)

            if defn.next_style:
                next_el = etree.SubElement(style_el, f"{W}next")
                next_el.set(f"{W}val", defn.next_style)

            ui = etree.SubElement(style_el, f"{W}uiPriority")
            ui.set(f"{W}val", str(defn.ui_priority))

            if defn.semi_hidden:
                etree.SubElement(style_el, f"{W}semiHidden")
            if defn.unhide_when_used:
                etree.SubElement(style_el, f"{W}unhideWhenUsed")
            if defn.qformat:
                etree.SubElement(style_el, f"{W}qFormat")

            # Paragraph properties
            if defn.style_type == StyleType.PARAGRAPH:
                has_ppr = (
                    defn.outline_level is not None
                    or defn.indent_scale > 0
                    or defn.space_before_scale > 0
                    or defn.space_after_scale > 0
                )
                if has_ppr:
                    ppr = etree.SubElement(style_el, f"{W}pPr")
                    if defn.space_before_scale > 0 or defn.space_after_scale > 0:
                        spacing = etree.SubElement(ppr, f"{W}spacing")
                        if defn.space_before_scale > 0:
                            twips = int(defn.space_before_scale * 240)
                            spacing.set(f"{W}before", str(twips))
                        if defn.space_after_scale > 0:
                            twips = int(defn.space_after_scale * 240)
                            spacing.set(f"{W}after", str(twips))
                    if defn.indent_scale > 0:
                        ind = etree.SubElement(ppr, f"{W}ind")
                        twips = int(defn.indent_scale * 360)
                        ind.set(f"{W}left", str(twips))
                    if defn.outline_level is not None:
                        ol = etree.SubElement(ppr, f"{W}outlineLvl")
                        ol.set(f"{W}val", str(defn.outline_level))

            # Run properties (character formatting)
            has_rpr = defn.bold or defn.italic or defn.small_caps or defn.all_caps
            if has_rpr:
                rpr = etree.SubElement(style_el, f"{W}rPr")
                if defn.bold:
                    etree.SubElement(rpr, f"{W}b")
                    etree.SubElement(rpr, f"{W}bCs")
                if defn.italic:
                    etree.SubElement(rpr, f"{W}i")
                    etree.SubElement(rpr, f"{W}iCs")
                if defn.small_caps:
                    etree.SubElement(rpr, f"{W}smallCaps")
                if defn.all_caps:
                    etree.SubElement(rpr, f"{W}caps")

    def _create_style_element(self, resolved: ResolvedStyle) -> etree._Element:
        """Create a new style element from a ResolvedStyle."""
        style_el = etree.Element(f"{W}style")
        style_el.set(f"{W}type", resolved.style_type)
        style_el.set(f"{W}styleId", resolved.style_id)

        # Style name
        name_el = etree.SubElement(style_el, f"{W}name")
        name_el.set(f"{W}val", resolved.name)

        # Based on
        if resolved.based_on:
            based_on = etree.SubElement(style_el, f"{W}basedOn")
            based_on.set(f"{W}val", resolved.based_on)

        # Next style
        if resolved.next_style:
            next_el = etree.SubElement(style_el, f"{W}next")
            next_el.set(f"{W}val", resolved.next_style)

        # Linked style (for paragraph styles with character link)
        if resolved.linked_style_id:
            link = etree.SubElement(style_el, f"{W}link")
            link.set(f"{W}val", resolved.linked_style_id)

        # UI Priority
        ui_priority = etree.SubElement(style_el, f"{W}uiPriority")
        ui_priority.set(f"{W}val", str(resolved.ui_priority))

        # Quick format (show in gallery)
        if resolved.qformat:
            etree.SubElement(style_el, f"{W}qFormat")

        # Semi-hidden
        if resolved.semi_hidden:
            etree.SubElement(style_el, f"{W}semiHidden")

        # Unhide when used
        if resolved.unhide_when_used:
            etree.SubElement(style_el, f"{W}unhideWhenUsed")

        # Apply properties based on style type
        if resolved.style_type == "paragraph":
            self._apply_resolved_paragraph_style(style_el, resolved)
        elif resolved.style_type == "character":
            self._apply_resolved_character_style(style_el, resolved)
        # Table and numbering styles: just the metadata above for now

        return style_el

    def _update_style_from_resolved(
        self, style_el: etree._Element, resolved: ResolvedStyle
    ) -> None:
        """Update an existing style element with resolved values."""
        # Update UI metadata
        ui_priority = style_el.find("w:uiPriority", NSMAP)
        if ui_priority is not None:
            ui_priority.set(f"{W}val", str(resolved.ui_priority))

        # Apply properties based on style type
        if resolved.style_type == "paragraph":
            self._apply_resolved_paragraph_style(style_el, resolved)
        elif resolved.style_type == "character":
            self._apply_resolved_character_style(style_el, resolved)

    def _apply_resolved_paragraph_style(
        self, style_el: etree._Element, resolved: ResolvedStyle
    ) -> None:
        """Apply paragraph style properties from a ResolvedStyle."""
        # Run properties (character-level)
        rpr = style_el.find("w:rPr", NSMAP)
        if rpr is None:
            rpr = etree.SubElement(style_el, f"{W}rPr")

        self._apply_resolved_run_properties(rpr, resolved)

        # Paragraph properties
        ppr = style_el.find("w:pPr", NSMAP)
        if ppr is None:
            ppr = etree.SubElement(style_el, f"{W}pPr")

        self._apply_resolved_paragraph_properties(ppr, resolved)

    def _apply_resolved_character_style(
        self, style_el: etree._Element, resolved: ResolvedStyle
    ) -> None:
        """Apply character style properties from a ResolvedStyle."""
        rpr = style_el.find("w:rPr", NSMAP)
        if rpr is None:
            rpr = etree.SubElement(style_el, f"{W}rPr")

        self._apply_resolved_run_properties(rpr, resolved)

    def _apply_resolved_run_properties(
        self, rpr: etree._Element, resolved: ResolvedStyle
    ) -> None:
        """Apply run (character-level) properties from a ResolvedStyle."""
        # Font family
        if resolved.font_family:
            rfonts = rpr.find("w:rFonts", NSMAP)
            if rfonts is None:
                rfonts = etree.SubElement(rpr, f"{W}rFonts")
            rfonts.set(f"{W}ascii", resolved.font_family)
            rfonts.set(f"{W}hAnsi", resolved.font_family)

        # Font size (half-points)
        if resolved.font_size_pt is not None:
            sz = rpr.find("w:sz", NSMAP)
            if sz is None:
                sz = etree.SubElement(rpr, f"{W}sz")
            sz.set(f"{W}val", str(int(resolved.font_size_pt * 2)))

            szCs = rpr.find("w:szCs", NSMAP)
            if szCs is None:
                szCs = etree.SubElement(rpr, f"{W}szCs")
            szCs.set(f"{W}val", str(int(resolved.font_size_pt * 2)))

        # Color
        if resolved.color:
            color = rpr.find("w:color", NSMAP)
            if color is None:
                color = etree.SubElement(rpr, f"{W}color")
            color.set(f"{W}val", resolved.color.lstrip("#").upper())

        # Bold
        if resolved.bold:
            if rpr.find("w:b", NSMAP) is None:
                etree.SubElement(rpr, f"{W}b")

        # Italic
        if resolved.italic:
            if rpr.find("w:i", NSMAP) is None:
                etree.SubElement(rpr, f"{W}i")

        # Small caps
        if resolved.small_caps:
            if rpr.find("w:smallCaps", NSMAP) is None:
                etree.SubElement(rpr, f"{W}smallCaps")

        # All caps
        if resolved.all_caps:
            if rpr.find("w:caps", NSMAP) is None:
                etree.SubElement(rpr, f"{W}caps")

        # Tracking (character spacing)
        if resolved.tracking_twips != 0:
            spacing = rpr.find("w:spacing", NSMAP)
            if spacing is None:
                spacing = etree.SubElement(rpr, f"{W}spacing")
            spacing.set(f"{W}val", str(resolved.tracking_twips))

        # OpenType features
        if resolved.opentype:
            self._apply_opentype_features(rpr, resolved.opentype)

    def _apply_resolved_paragraph_properties(
        self, ppr: etree._Element, resolved: ResolvedStyle
    ) -> None:
        """Apply paragraph properties from a ResolvedStyle."""
        # Spacing
        if (
            resolved.space_before_twips is not None
            or resolved.space_after_twips is not None
        ):
            spacing = ppr.find("w:spacing", NSMAP)
            if spacing is None:
                spacing = etree.SubElement(ppr, f"{W}spacing")
            if resolved.space_before_twips is not None:
                spacing.set(f"{W}before", str(resolved.space_before_twips))
            if resolved.space_after_twips is not None:
                spacing.set(f"{W}after", str(resolved.space_after_twips))

        # Indentation
        if resolved.left_indent_twips > 0 or resolved.first_line_indent_twips > 0:
            ind = ppr.find("w:ind", NSMAP)
            if ind is None:
                ind = etree.SubElement(ppr, f"{W}ind")
            if resolved.left_indent_twips > 0:
                ind.set(f"{W}left", str(resolved.left_indent_twips))
            if resolved.first_line_indent_twips > 0:
                ind.set(f"{W}firstLine", str(resolved.first_line_indent_twips))

        # Outline level (for headings)
        if resolved.outline_level is not None:
            outline = ppr.find("w:outlineLvl", NSMAP)
            if outline is None:
                outline = etree.SubElement(ppr, f"{W}outlineLvl")
            outline.set(f"{W}val", str(resolved.outline_level))

        # Keep with next
        if resolved.keep_with_next:
            if ppr.find("w:keepNext", NSMAP) is None:
                etree.SubElement(ppr, f"{W}keepNext")

        # Keep lines together
        if resolved.keep_lines_together:
            if ppr.find("w:keepLines", NSMAP) is None:
                etree.SubElement(ppr, f"{W}keepLines")

    def _update_style(self, root: etree._Element, style_id: str, style_data) -> None:
        """Update a paragraph style with new values including advanced typography."""
        # Find the style element
        style_el = root.find(f'.//w:style[@w:styleId="{style_id}"]', NSMAP)
        if style_el is None:
            return

        # Update run properties (character-level)
        rpr = style_el.find("w:rPr", NSMAP)
        if rpr is None:
            rpr = etree.SubElement(style_el, f"{W}rPr")

        self._apply_run_properties(rpr, style_data)

        # Update paragraph properties
        ppr = style_el.find("w:pPr", NSMAP)
        if ppr is None:
            ppr = etree.SubElement(style_el, f"{W}pPr")

        self._apply_paragraph_properties(ppr, style_data)

    def _apply_run_properties(self, rpr: etree._Element, style_data) -> None:
        """Apply all run (character-level) properties to an rPr element."""
        # Basic font properties
        self._apply_font_properties(rpr, style_data)

        # Bold
        if hasattr(style_data, "bold") and style_data.bold:
            if rpr.find("w:b", NSMAP) is None:
                etree.SubElement(rpr, f"{W}b")

        # Italic
        if hasattr(style_data, "italic") and style_data.italic:
            if rpr.find("w:i", NSMAP) is None:
                etree.SubElement(rpr, f"{W}i")

        # Advanced character typography
        if hasattr(style_data, "character"):
            self._apply_character_typography(rpr, style_data.character)

    def _apply_font_properties(self, rpr: etree._Element, style_data) -> None:
        """Apply basic font properties (font family, size, color)."""
        # Font family
        rfonts = rpr.find("w:rFonts", NSMAP)
        if rfonts is None:
            rfonts = etree.SubElement(rpr, f"{W}rFonts")
        font = style_data.font_family
        rfonts.set(f"{W}ascii", font)
        rfonts.set(f"{W}hAnsi", font)

        # Size (half-points)
        sz = rpr.find("w:sz", NSMAP)
        if sz is None:
            sz = etree.SubElement(rpr, f"{W}sz")
        sz.set(f"{W}val", str(int(style_data.font_size_pt * 2)))

        # Complex script size (same as regular)
        szCs = rpr.find("w:szCs", NSMAP)
        if szCs is None:
            szCs = etree.SubElement(rpr, f"{W}szCs")
        szCs.set(f"{W}val", str(int(style_data.font_size_pt * 2)))

        # Color
        color = rpr.find("w:color", NSMAP)
        if color is None:
            color = etree.SubElement(rpr, f"{W}color")
        color.set(f"{W}val", style_data.color.lstrip("#").upper())

    def _apply_character_typography(
        self, rpr: etree._Element, char: CharacterTypography
    ) -> None:
        """Apply advanced character-level typography settings."""
        # Character spacing (tracking)
        if char.tracking_twips != 0:
            spacing = rpr.find("w:spacing", NSMAP)
            if spacing is None:
                spacing = etree.SubElement(rpr, f"{W}spacing")
            spacing.set(f"{W}val", str(char.tracking_twips))

        # Kerning threshold (0 = always kern, which is modern best practice)
        self._apply_kerning(rpr, char.kern_threshold_half_pt)

        # Character scaling (condensed/expanded)
        if char.character_scale_pct != 100:
            w_elem = rpr.find("w:w", NSMAP)
            if w_elem is None:
                w_elem = etree.SubElement(rpr, f"{W}w")
            w_elem.set(f"{W}val", str(char.character_scale_pct))

        # Small caps
        if char.small_caps:
            if rpr.find("w:smallCaps", NSMAP) is None:
                etree.SubElement(rpr, f"{W}smallCaps")

        # All caps
        if char.all_caps:
            if rpr.find("w:caps", NSMAP) is None:
                etree.SubElement(rpr, f"{W}caps")

        # OpenType features
        self._apply_opentype_features(rpr, char.opentype)

    def _apply_kerning(self, rpr: etree._Element, threshold_half_pt: int) -> None:
        """Apply kerning with specified threshold."""
        kern = rpr.find("w:kern", NSMAP)
        if kern is None:
            kern = etree.SubElement(rpr, f"{W}kern")
        kern.set(f"{W}val", str(threshold_half_pt))

    def _apply_opentype_features(
        self, rpr: etree._Element, opentype: OpenTypeFeatures
    ) -> None:
        """Apply Word 2010+ OpenType feature elements (w14 namespace)."""
        if not self.allow_opentype_features:
            for tag in (
                "ligatures",
                "numForm",
                "numSpacing",
                "stylisticSets",
                "cntxtAlts",
            ):
                existing = rpr.find(f"w14:{tag}", NSMAP)
                if existing is not None:
                    rpr.remove(existing)
            return
        # Ligatures
        if opentype.ligatures != "none":
            lig = rpr.find("w14:ligatures", NSMAP)
            if lig is None:
                lig = etree.SubElement(rpr, f"{W14}ligatures")
            lig.set(f"{W14}val", opentype.ligatures)

        # Number form (lining vs oldstyle figures)
        if opentype.number_form != "default":
            numForm = rpr.find("w14:numForm", NSMAP)
            if numForm is None:
                numForm = etree.SubElement(rpr, f"{W14}numForm")
            numForm.set(f"{W14}val", opentype.number_form)

        # Number spacing (proportional vs tabular)
        if opentype.number_spacing != "default":
            numSpacing = rpr.find("w14:numSpacing", NSMAP)
            if numSpacing is None:
                numSpacing = etree.SubElement(rpr, f"{W14}numSpacing")
            numSpacing.set(f"{W14}val", opentype.number_spacing)

        # Stylistic sets (ss01-ss20)
        if opentype.stylistic_sets:
            stylSets = rpr.find("w14:stylisticSets", NSMAP)
            if stylSets is None:
                stylSets = etree.SubElement(rpr, f"{W14}stylisticSets")
            for ss_num in opentype.stylistic_sets:
                if 1 <= ss_num <= 20:
                    styleSet = etree.SubElement(stylSets, f"{W14}styleSet")
                    styleSet.set(f"{W14}id", str(ss_num))

        # Contextual alternates
        if opentype.contextual_alternates:
            cntxtAlts = rpr.find("w14:cntxtAlts", NSMAP)
            if cntxtAlts is None:
                cntxtAlts = etree.SubElement(rpr, f"{W14}cntxtAlts")
            cntxtAlts.set(f"{W14}val", "1")

    def _apply_paragraph_properties(self, ppr: etree._Element, style_data) -> None:
        """Apply all paragraph-level properties to a pPr element."""
        # Basic spacing (before/after)
        self._apply_paragraph_spacing(
            ppr, style_data.spacing, getattr(style_data, "line_spacing", None)
        )

        # Indentation
        if hasattr(style_data, "indent"):
            self._apply_paragraph_indent(ppr, style_data.indent)

        # Paragraph control (justification, keep-with-next, etc.)
        if hasattr(style_data, "control"):
            self._apply_paragraph_control(ppr, style_data.control)

    def _apply_paragraph_spacing(
        self, ppr: etree._Element, spacing, line_spacing: LineSpacing | None
    ) -> None:
        """Apply paragraph spacing (before, after, line)."""
        spacing_el = ppr.find("w:spacing", NSMAP)
        if spacing_el is None:
            spacing_el = etree.SubElement(ppr, f"{W}spacing")

        spacing_el.set(f"{W}before", str(spacing.before_twips))
        spacing_el.set(f"{W}after", str(spacing.after_twips))

        # Line spacing
        if line_spacing and not line_spacing.is_default:
            spacing_el.set(f"{W}line", str(line_spacing.value_twips))
            spacing_el.set(f"{W}lineRule", line_spacing.rule)

    def _apply_paragraph_indent(
        self, ppr: etree._Element, indent: ParagraphIndent
    ) -> None:
        """Apply paragraph indentation."""
        if indent.is_default:
            return

        ind = ppr.find("w:ind", NSMAP)
        if ind is None:
            ind = etree.SubElement(ppr, f"{W}ind")

        if indent.left_twips != 0:
            ind.set(f"{W}left", str(indent.left_twips))
        if indent.right_twips != 0:
            ind.set(f"{W}right", str(indent.right_twips))
        if indent.first_line_twips != 0:
            ind.set(f"{W}firstLine", str(indent.first_line_twips))
        elif indent.hanging_twips != 0:
            ind.set(f"{W}hanging", str(indent.hanging_twips))

    def _apply_paragraph_control(
        self, ppr: etree._Element, control: ParagraphControl
    ) -> None:
        """Apply paragraph control settings (justification, keep-with-next, etc.)."""
        # Justification
        if control.justification != "left":
            jc = ppr.find("w:jc", NSMAP)
            if jc is None:
                jc = etree.SubElement(ppr, f"{W}jc")
            jc.set(f"{W}val", control.justification)

        # Contextual spacing
        if control.contextual_spacing:
            if ppr.find("w:contextualSpacing", NSMAP) is None:
                etree.SubElement(ppr, f"{W}contextualSpacing")

        # Keep with next
        if control.keep_with_next:
            if ppr.find("w:keepNext", NSMAP) is None:
                etree.SubElement(ppr, f"{W}keepNext")

        # Keep lines together
        if control.keep_lines_together:
            if ppr.find("w:keepLines", NSMAP) is None:
                etree.SubElement(ppr, f"{W}keepLines")

        # Widow/orphan control (default is on, so only add if off)
        if not control.widow_orphan_control:
            widowControl = ppr.find("w:widowControl", NSMAP)
            if widowControl is None:
                widowControl = etree.SubElement(ppr, f"{W}widowControl")
            widowControl.set(f"{W}val", "0")

        # Suppress auto hyphens
        if control.suppress_auto_hyphens:
            if ppr.find("w:suppressAutoHyphens", NSMAP) is None:
                etree.SubElement(ppr, f"{W}suppressAutoHyphens")

    # =========================================================================
