"""LibreOffice Writer styles emitter based on Document IR.

SSOT pattern: XML template defines structure, this emitter updates
attribute values from the Document IR. OpenType features (liga, calt,
kern, tnum) supported via style:font-feature-settings (LibreOffice 7.0+).
"""

from __future__ import annotations

from pathlib import Path

from lxml import etree

from tokenmoulds.colors.parser import parse_color
from tokenmoulds.design.color_defaults import PURE_WHITE
from tokenmoulds.design.layout_defaults import (
    A4_HEIGHT_EMU,
    A4_WIDTH_EMU,
    DEFAULT_BASELINE_PT,
    table_cell_padding_pt,
    word_fallback_header_footer_twips,
)
from tokenmoulds.design.page_geometry import PageGeometry
from tokenmoulds.design.units import EMU_PER_POINT, twips_to_pt
from tokenmoulds.emitters import TemplateResolver
from tokenmoulds.emitters.odf_helpers import (
    DEFAULT_TABLE_BORDER_COLOR,
    DEFAULT_TABLE_BORDER_WIDTH_PT,
    ODF_NSMAP,
    find_default_style,
    find_font_face,
    q,
)
from tokenmoulds.ir import DocumentIR
from tokenmoulds.styles.writer_builtin import (
    WRITER_BUILTIN_STYLES,
    OdfBuiltinStyleDef,
    OdfStyleFamily,
)

# Extend the shared ODF namespace map with LibreOffice-specific namespaces
_LOEXT_NS = "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"
ODF_NSMAP.setdefault("loext", _LOEXT_NS)

# Default 2cm page margin in points (used when no PageGeometry is provided)
_DEFAULT_MARGIN_PT: float = 2.0 * 72.0 / 2.54  # 56.6929...

# Numbered list format cycle: decimal → lowercase → roman, repeating
_NUM_FORMATS: tuple[str, ...] = ("1", "a", "i", "1", "a", "i", "1", "a", "i", "1")

# Frame stroke width in pt (thin hairline border, ~0.02cm ≈ 0.57pt)
_FRAME_STROKE_WIDTH_PT: float = 0.57


class WriterStylesEmitter:
    """Generate ODF styles.xml for LibreOffice Writer."""

    def __init__(
        self,
        document: DocumentIR,
        template_root: Path | str | None = None,
        enable_opentype_features: bool = True,
    ) -> None:
        self.document = document
        self.templates = TemplateResolver("writer", template_root)
        self.enable_opentype_features = enable_opentype_features

    def build_xml(self) -> str:
        """Build styles.xml by modifying the template with Document IR values."""
        root = self.templates.read_tree(
            "styles.xml", spec_rel_path="templates/styles.xml"
        )

        # Build name→element index for O(1) style lookups
        style_index = self._build_style_index(root)

        # Modify existing elements with Document IR values
        self._apply_fonts(root)
        self._apply_builtin_styles(root, style_index)
        self._apply_default_styles(root)
        self._apply_heading_styles(style_index)
        self._apply_outline_style(root)
        self._apply_quote_style(style_index)
        self._apply_hyperlink_styles(style_index)
        self._apply_table_styles(style_index)
        self._apply_list_styles(root)
        self._apply_frame_styles(style_index)

        # Apply extended typography settings
        self._apply_text_direction(style_index)
        self._apply_text_alignment(style_index)
        self._apply_letter_spacing(style_index)

        # Apply page geometry (size, margins, orientation)
        self._apply_page_geometry(root)

        # Remove OpenType features if disabled
        if not self.enable_opentype_features:
            self._remove_opentype_features(root)

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

    @staticmethod
    def _build_style_index(
        root: etree._Element,
    ) -> dict[str, etree._Element]:
        """Build a name-keyed index of all style elements for O(1) lookup."""
        name_attr = q("style", "name")
        return {el.get(name_attr, ""): el for el in root.iter(q("style", "style"))}

    def _apply_fonts(self, root: etree._Element) -> None:
        """Update font declarations with Document IR fonts."""
        body_font = find_font_face(root, "BodyFont")
        if body_font is not None:
            body_font.set(q("svg", "font-family"), self.document.body_style.font_family)

        heading_font = find_font_face(root, "HeadingFont")
        if heading_font is not None:
            heading_font.set(
                q("svg", "font-family"), self.document.typography.heading_font
            )

        table_font = find_font_face(root, "TableFont")
        if table_font is not None:
            # Use body font for tables (table_style was removed from IR)
            table_font.set(
                q("svg", "font-family"), self.document.body_style.font_family
            )

        mono_font = find_font_face(root, "MonoFont")
        if mono_font is not None:
            mono_font.set(
                q("svg", "font-family"), self.document.typography.monospace_font
            )

    def _apply_builtin_styles(
        self,
        root: etree._Element,
        style_index: dict[str, etree._Element],
    ) -> None:
        """Apply all built-in styles from the writer_builtin registry."""
        body = self.document.body_style
        body_size_pt = body.font_size_pt
        body_before_pt = twips_to_pt(body.spacing.before_twips)
        body_after_pt = twips_to_pt(body.spacing.after_twips)
        body_font = body.font_family
        heading_font = self.document.typography.heading_font
        mono_font = self.document.typography.monospace_font
        text_color = body.color
        muted_color = self.document.color_theme.dk2

        office_styles = root.find("office:styles", ODF_NSMAP)
        if office_styles is None:
            return

        for defn in WRITER_BUILTIN_STYLES:
            if defn.family == OdfStyleFamily.TABLE_CELL:
                continue  # table-cell styles handled by _apply_table_styles

            style = style_index.get(defn.style_name)
            if style is None:
                style = self._create_builtin_style(office_styles, defn)
                style_index[defn.style_name] = style

            # Resolve computed values
            if defn.use_monospace:
                font = mono_font
            elif defn.use_heading_font:
                font = heading_font
            else:
                font = body_font
            size_pt = body_size_pt * defn.size_scale
            color = muted_color if defn.use_muted_color else text_color
            space_before_pt = body_before_pt * defn.space_before_scale
            space_after_pt = body_after_pt * defn.space_after_scale

            # Apply text properties
            if defn.family == OdfStyleFamily.TEXT:
                self._apply_character_style_props(style, defn, font, size_pt, color)
            else:
                self._apply_paragraph_style_props(
                    style,
                    defn,
                    font,
                    size_pt,
                    color,
                    space_before_pt,
                    space_after_pt,
                    body_size_pt,
                )

    def _create_builtin_style(
        self, parent: etree._Element, defn: OdfBuiltinStyleDef
    ) -> etree._Element:
        """Create a new style element from a builtin definition."""
        style = etree.SubElement(parent, q("style", "style"))
        style.set(q("style", "name"), defn.style_name)
        style.set(q("style", "family"), defn.family.value)
        style.set(q("style", "display-name"), defn.display_name)
        if defn.parent_style:
            style.set(q("style", "parent-style-name"), defn.parent_style)
        if defn.next_style:
            style.set(q("style", "next-style-name"), defn.next_style)
        return style

    def _apply_default_styles(self, root: etree._Element) -> None:
        """Add style:default-style elements for paragraph and text families."""
        office_styles = root.find("office:styles", ODF_NSMAP)
        if office_styles is None:
            return

        body = self.document.body_style
        body_font = body.font_family
        body_size_pt = body.font_size_pt
        body_color = body.color

        # Compute line height from baseline grid
        baseline_emu = self.document.typography.baseline_emu
        line_height_pt = baseline_emu / EMU_PER_POINT if baseline_emu > 0 else 0.0

        # style:default-style family="paragraph"
        para_default = find_default_style(root, "paragraph")
        if para_default is None:
            para_default = etree.SubElement(office_styles, q("style", "default-style"))
            para_default.set(q("style", "family"), "paragraph")
            # Insert before first style:style to keep document order clean
            first_style = office_styles.find(q("style", "style"))
            if first_style is not None:
                office_styles.remove(para_default)
                office_styles.insert(office_styles.index(first_style), para_default)

        text_props = para_default.find(q("style", "text-properties"))
        if text_props is None:
            text_props = etree.SubElement(para_default, q("style", "text-properties"))
        text_props.set(q("fo", "font-family"), body_font)
        text_props.set(q("fo", "font-size"), f"{body_size_pt:.2f}pt")
        text_props.set(q("fo", "color"), body_color)

        para_props = para_default.find(q("style", "paragraph-properties"))
        if para_props is None:
            para_props = etree.SubElement(
                para_default, q("style", "paragraph-properties")
            )
        if line_height_pt > 0:
            para_props.set(q("fo", "line-height"), f"{line_height_pt:.2f}pt")

        # style:default-style family="text"
        text_default = find_default_style(root, "text")
        if text_default is None:
            text_default = etree.SubElement(office_styles, q("style", "default-style"))
            text_default.set(q("style", "family"), "text")
            # Insert after paragraph default-style
            idx = office_styles.index(para_default)
            office_styles.remove(text_default)
            office_styles.insert(idx + 1, text_default)

        text_text_props = text_default.find(q("style", "text-properties"))
        if text_text_props is None:
            text_text_props = etree.SubElement(
                text_default, q("style", "text-properties")
            )
        text_text_props.set(q("fo", "font-family"), body_font)
        text_text_props.set(q("fo", "font-size"), f"{body_size_pt:.2f}pt")
        text_text_props.set(q("fo", "color"), body_color)

    def _apply_outline_style(self, root: etree._Element) -> None:
        """Add text:outline-style for TOC generation (heading levels 1-10)."""
        office_styles = root.find("office:styles", ODF_NSMAP)
        if office_styles is None:
            return

        # Remove existing outline-style if present
        existing = office_styles.find(q("text", "outline-style"))
        if existing is not None:
            office_styles.remove(existing)

        outline_style = etree.SubElement(office_styles, q("text", "outline-style"))
        outline_style.set(q("style", "name"), "Outline")

        for level in range(1, 11):
            level_style = etree.SubElement(
                outline_style, q("text", "outline-level-style")
            )
            level_style.set(q("text", "level"), str(level))
            level_style.set(q("text", "style-name"), f"Heading_20_{level}")
            level_style.set(q("style", "num-format"), "")

    def _apply_list_styles(self, root: etree._Element) -> None:
        """Add BrandBullet and BrandNumber list-style definitions."""
        office_styles = root.find("office:styles", ODF_NSMAP)
        if office_styles is None:
            return

        ls = self.document.list_style
        body_font = self.document.body_style.font_family
        body_size = self.document.body_style.font_size_pt
        body_color = self.document.body_style.color
        indent_pt = twips_to_pt(ls.indent_twips)
        hanging_pt = twips_to_pt(ls.hanging_twips)
        indent_per_level_pt = twips_to_pt(ls.effective_indent_per_level)
        bullets = ls.nested_bullet_chars  # e.g. ("•", "–", "·")

        # Remove existing list styles if present (idempotent rebuild)
        for name in ("BrandBullet", "BrandNumber"):
            existing = office_styles.find(
                f"text:list-style[@style:name='{name}']", ODF_NSMAP
            )
            if existing is not None:
                office_styles.remove(existing)

        # -- BrandBullet (bullet list) --
        bullet_list = etree.SubElement(office_styles, q("text", "list-style"))
        bullet_list.set(q("style", "name"), "BrandBullet")
        for level in range(1, 11):
            char = bullets[min(level - 1, len(bullets) - 1)]
            margin_left = indent_pt + (level - 1) * indent_per_level_pt

            lvl = etree.SubElement(bullet_list, q("text", "list-level-style-bullet"))
            lvl.set(q("text", "level"), str(level))
            lvl.set(q("text", "bullet-char"), char)

            props = etree.SubElement(lvl, q("style", "list-level-properties"))
            props.set(q("text", "space-before"), f"{margin_left:.2f}pt")
            props.set(q("text", "min-label-width"), f"{hanging_pt:.2f}pt")

            tprops = etree.SubElement(lvl, q("style", "text-properties"))
            tprops.set(q("fo", "font-family"), body_font)
            tprops.set(q("fo", "font-size"), f"{body_size:.2f}pt")
            tprops.set(q("fo", "color"), body_color)

        # -- BrandNumber (numbered list) --
        number_list = etree.SubElement(office_styles, q("text", "list-style"))
        number_list.set(q("style", "name"), "BrandNumber")
        for level in range(1, 11):
            margin_left = indent_pt + (level - 1) * indent_per_level_pt

            lvl = etree.SubElement(number_list, q("text", "list-level-style-number"))
            lvl.set(q("text", "level"), str(level))
            lvl.set(q("style", "num-format"), _NUM_FORMATS[level - 1])
            lvl.set(q("text", "display-levels"), str(min(level, 3)))
            lvl.set(q("style", "num-suffix"), ".")

            props = etree.SubElement(lvl, q("style", "list-level-properties"))
            props.set(q("text", "space-before"), f"{margin_left:.2f}pt")
            props.set(q("text", "min-label-width"), f"{hanging_pt:.2f}pt")
            props.set(
                q("text", "list-level-position-and-space-mode"),
                "label-alignment",
            )

            tprops = etree.SubElement(lvl, q("style", "text-properties"))
            tprops.set(q("fo", "font-family"), body_font)
            tprops.set(q("fo", "font-size"), f"{body_size:.2f}pt")
            tprops.set(q("fo", "color"), body_color)

    def _apply_frame_styles(
        self,
        style_index: dict[str, etree._Element],
    ) -> None:
        """Update graphic-family frame styles with IR-derived values.

        Sets stroke color from DEFAULT_TABLE_BORDER_COLOR, stroke width from
        _FRAME_STROKE_WIDTH_CM, and padding from grid-derived table cell padding.
        Also updates Horizontal_20_Line border to use the shared border color.
        """
        border_color = DEFAULT_TABLE_BORDER_COLOR
        padding = table_cell_padding_pt()
        padding_pt = padding["left"]
        margin_pt = padding["top"]
        stroke_pt = _FRAME_STROKE_WIDTH_PT

        frame = style_index.get("Frame")
        if frame is not None:
            gprops = frame.find(q("style", "graphic-properties"))
            if gprops is not None:
                gprops.set(q("svg", "stroke-color"), border_color)
                gprops.set(q("svg", "stroke-width"), f"{stroke_pt:.2f}pt")
                gprops.set(q("fo", "padding"), f"{padding_pt:.2f}pt")
                gprops.set(q("fo", "margin"), f"{margin_pt:.2f}pt")

        # Graphics and OLE inherit from Frame, but override stroke/padding
        for name in ("Graphics", "OLE"):
            style = style_index.get(name)
            if style is None:
                continue
            gprops = style.find(q("style", "graphic-properties"))
            if gprops is not None:
                gprops.set(q("fo", "margin"), f"{margin_pt:.2f}pt")

        # Horizontal_20_Line: derive border from shared constants
        hr_style = style_index.get("Horizontal_20_Line")
        if hr_style is not None:
            para_props = hr_style.find(q("style", "paragraph-properties"))
            if para_props is not None:
                border_spec = (
                    f"{DEFAULT_TABLE_BORDER_WIDTH_PT:.2f}pt solid {border_color}"
                )
                para_props.set(q("fo", "border-bottom"), border_spec)

    def _apply_paragraph_style_props(
        self,
        style: etree._Element,
        defn: OdfBuiltinStyleDef,
        font: str,
        size_pt: float,
        color: str,
        space_before_pt: float,
        space_after_pt: float,
        body_size_pt: float = 11.0,
    ) -> None:
        """Apply text and paragraph properties to a paragraph-family style."""
        # Text properties
        text_props = style.find(q("style", "text-properties"))
        if text_props is None:
            text_props = etree.SubElement(style, q("style", "text-properties"))

        text_props.set(q("fo", "font-family"), font)
        text_props.set(q("fo", "font-size"), f"{size_pt:.2f}pt")
        text_props.set(q("fo", "color"), color)
        if defn.bold:
            text_props.set(q("fo", "font-weight"), "bold")
        elif q("fo", "font-weight") in text_props.attrib:
            del text_props.attrib[q("fo", "font-weight")]
        if defn.italic:
            text_props.set(q("fo", "font-style"), "italic")
        elif q("fo", "font-style") in text_props.attrib:
            del text_props.attrib[q("fo", "font-style")]
        if defn.small_caps:
            text_props.set(q("fo", "font-variant"), "small-caps")

        # Paragraph properties
        para_props = style.find(q("style", "paragraph-properties"))
        if para_props is None:
            para_props = etree.SubElement(style, q("style", "paragraph-properties"))

        para_props.set(q("fo", "margin-top"), f"{space_before_pt:.2f}pt")
        para_props.set(q("fo", "margin-bottom"), f"{space_after_pt:.2f}pt")

        # Line height from baseline grid
        baseline_emu = self.document.typography.baseline_emu
        if baseline_emu > 0:
            line_height_pt = baseline_emu / EMU_PER_POINT
            para_props.set(q("fo", "line-height"), f"{line_height_pt:.2f}pt")

        if defn.indent_em > 0:
            indent_pt = defn.indent_em * body_size_pt
            para_props.set(q("fo", "margin-left"), f"{indent_pt:.2f}pt")
        if defn.right_indent_em > 0:
            right_pt = defn.right_indent_em * body_size_pt
            para_props.set(q("fo", "margin-right"), f"{right_pt:.2f}pt")
        if defn.text_indent_em != 0:
            text_indent_pt = defn.text_indent_em * body_size_pt
            para_props.set(q("fo", "text-indent"), f"{text_indent_pt:.2f}pt")
        if defn.text_align:
            para_props.set(q("fo", "text-align"), defn.text_align)

    def _apply_character_style_props(
        self,
        style: etree._Element,
        defn: OdfBuiltinStyleDef,
        font: str,
        size_pt: float,
        color: str,
    ) -> None:
        """Apply text properties to a character-family style."""
        text_props = style.find(q("style", "text-properties"))
        if text_props is None:
            text_props = etree.SubElement(style, q("style", "text-properties"))

        # Set font for monospace styles
        if defn.use_monospace:
            text_props.set(q("fo", "font-family"), font)

        # Only set size/color when the style makes explicit decisions
        if defn.size_scale != 1.0:
            text_props.set(q("fo", "font-size"), f"{size_pt:.2f}pt")
        if defn.use_muted_color:
            text_props.set(q("fo", "color"), color)

        if defn.bold:
            text_props.set(q("fo", "font-weight"), "bold")
        elif q("fo", "font-weight") in text_props.attrib:
            del text_props.attrib[q("fo", "font-weight")]
        if defn.italic:
            text_props.set(q("fo", "font-style"), "italic")
        elif q("fo", "font-style") in text_props.attrib:
            del text_props.attrib[q("fo", "font-style")]
        if defn.small_caps:
            text_props.set(q("fo", "font-variant"), "small-caps")
        if defn.superscript:
            text_props.set(q("style", "text-position"), "super 58%")

    def _apply_heading_styles(
        self,
        style_index: dict[str, etree._Element],
    ) -> None:
        """Override heading styles with design-engine-computed IR values."""
        for heading in self.document.heading_styles:
            style_name = f"Heading_20_{heading.level}"
            style = style_index.get(style_name)
            if style is None:
                continue

            text_props = style.find(q("style", "text-properties"))
            if text_props is not None:
                text_props.set(q("fo", "font-family"), heading.font_family)
                text_props.set(q("fo", "font-size"), f"{heading.font_size_pt:.2f}pt")
                text_props.set(q("fo", "color"), heading.color)
                if heading.bold:
                    text_props.set(q("fo", "font-weight"), "bold")
                elif q("fo", "font-weight") in text_props.attrib:
                    del text_props.attrib[q("fo", "font-weight")]
                if heading.italic:
                    text_props.set(q("fo", "font-style"), "italic")
                elif q("fo", "font-style") in text_props.attrib:
                    del text_props.attrib[q("fo", "font-style")]

            para_props = style.find(q("style", "paragraph-properties"))
            if para_props is not None:
                para_props.set(
                    q("fo", "margin-top"),
                    f"{twips_to_pt(heading.spacing.before_twips):.2f}pt",
                )
                para_props.set(
                    q("fo", "margin-bottom"),
                    f"{twips_to_pt(heading.spacing.after_twips):.2f}pt",
                )
                # Line height from baseline grid
                baseline_emu = self.document.typography.baseline_emu
                if baseline_emu > 0:
                    line_height_pt = baseline_emu / EMU_PER_POINT
                    para_props.set(q("fo", "line-height"), f"{line_height_pt:.2f}pt")

    def _apply_quote_style(
        self,
        style_index: dict[str, etree._Element],
    ) -> None:
        """Update Quotations style with Document IR values."""
        quote = self.document.quote_style
        style = style_index.get("Quotations")
        if style is None:
            return

        text_props = style.find(q("style", "text-properties"))
        if text_props is not None:
            text_props.set(q("fo", "font-family"), quote.font_family)
            text_props.set(q("fo", "font-size"), f"{quote.font_size_pt:.2f}pt")
            text_props.set(q("fo", "color"), quote.color)
            if quote.italic:
                text_props.set(q("fo", "font-style"), "italic")

        para_props = style.find(q("style", "paragraph-properties"))
        if para_props is not None:
            para_props.set(
                q("fo", "margin-top"),
                f"{twips_to_pt(quote.spacing.before_twips):.2f}pt",
            )
            para_props.set(
                q("fo", "margin-bottom"),
                f"{twips_to_pt(quote.spacing.after_twips):.2f}pt",
            )

    def _apply_hyperlink_styles(
        self,
        style_index: dict[str, etree._Element],
    ) -> None:
        """Update hyperlink styles with Document IR values."""
        hyperlink = self.document.hyperlink_style

        internet_link = style_index.get("Internet_20_link")
        if internet_link is not None:
            text_props = internet_link.find(q("style", "text-properties"))
            if text_props is not None:
                text_props.set(q("fo", "color"), hyperlink.link_color)

        visited_link = style_index.get("Visited_20_link")
        if visited_link is not None:
            text_props = visited_link.find(q("style", "text-properties"))
            if text_props is not None:
                text_props.set(q("fo", "color"), hyperlink.visited_color)

    def _apply_table_styles(
        self,
        style_index: dict[str, etree._Element],
    ) -> None:
        """Update table styles with IR-derived values."""
        # Derive table properties from body style
        font_family = self.document.body_style.font_family
        font_size_pt = self.document.body_style.font_size_pt
        text_color = self.document.body_style.color

        border_spec = (
            f"{DEFAULT_TABLE_BORDER_WIDTH_PT:.2f}pt solid {DEFAULT_TABLE_BORDER_COLOR}"
        )

        # Grid-derived padding
        padding = table_cell_padding_pt()
        padding_left = f"{padding['left']:.2f}pt"
        padding_right = f"{padding['right']:.2f}pt"
        padding_top = f"{padding['top']:.2f}pt"
        padding_bottom = f"{padding['bottom']:.2f}pt"

        # Header uses lt2 color (light gray) from color theme
        header_fill = self.document.color_theme.lt2

        # Body uses lt1 color (white/lightest) from color theme
        body_fill = self.document.color_theme.lt1

        # TableHeader
        header_style = style_index.get("TableHeader")
        if header_style is not None:
            self._apply_table_cell_props(
                header_style,
                font_family,
                font_size_pt,
                header_fill,
                text_color,
                border_spec,
                padding_left,
                padding_right,
                padding_top,
                padding_bottom,
                bold=True,
            )

        # TableBody
        table_body_style = style_index.get("TableBody")
        if table_body_style is not None:
            self._apply_table_cell_props(
                table_body_style,
                font_family,
                font_size_pt,
                body_fill,
                text_color,
                border_spec,
                padding_left,
                padding_right,
                padding_top,
                padding_bottom,
            )

        # Banding styles - use pre-computed palette if available
        if self.document.color_palette:
            band1_variant = self.document.color_palette.get("Accent1.Tint60")
            band1_color = (
                band1_variant.hex
                if band1_variant
                else self._apply_tint(self.document.color_theme.accent1, 60)
            )
        else:
            band1_color = self._apply_tint(self.document.color_theme.accent1, 60)
        band1_style = style_index.get("TableBand1")
        if band1_style is not None:
            self._apply_table_cell_props(
                band1_style,
                font_family,
                font_size_pt,
                band1_color,
                text_color,
                border_spec,
                padding_left,
                padding_right,
                padding_top,
                padding_bottom,
            )

        band2_style = style_index.get("TableBand2")
        if band2_style is not None:
            self._apply_table_cell_props(
                band2_style,
                font_family,
                font_size_pt,
                body_fill,
                text_color,
                border_spec,
                padding_left,
                padding_right,
                padding_top,
                padding_bottom,
            )

        # Accent table headers
        accent_colors = [
            self.document.color_theme.accent1,
            self.document.color_theme.accent2,
            self.document.color_theme.accent3,
            self.document.color_theme.accent4,
            self.document.color_theme.accent5,
            self.document.color_theme.accent6,
        ]

        for i, accent in enumerate(accent_colors, 1):
            accent_style = style_index.get(f"TableAccent{i}Header")
            if accent_style is not None:
                self._apply_table_cell_props(
                    accent_style,
                    font_family,
                    font_size_pt,
                    accent,
                    PURE_WHITE,
                    border_spec,
                    padding_left,
                    padding_right,
                    padding_top,
                    padding_bottom,
                    bold=True,
                )

    def _apply_table_cell_props(
        self,
        style: etree._Element,
        font_family: str,
        font_size_pt: float,
        bg_color: str,
        text_color: str,
        border_spec: str,
        padding_left: str,
        padding_right: str,
        padding_top: str,
        padding_bottom: str,
        bold: bool = False,
    ) -> None:
        """Apply table cell properties to a style element."""
        cell_props = style.find(q("style", "table-cell-properties"))
        if cell_props is not None:
            if bg_color.startswith("#"):
                cell_props.set(q("fo", "background-color"), bg_color)
            cell_props.set(q("fo", "border"), border_spec)
            cell_props.set(q("fo", "padding-left"), padding_left)
            cell_props.set(q("fo", "padding-right"), padding_right)
            cell_props.set(q("fo", "padding-top"), padding_top)
            cell_props.set(q("fo", "padding-bottom"), padding_bottom)

        text_props = style.find(q("style", "text-properties"))
        if text_props is not None:
            text_props.set(q("fo", "font-family"), font_family)
            text_props.set(q("fo", "font-size"), f"{font_size_pt:.1f}pt")
            if text_color.startswith("#"):
                text_props.set(q("fo", "color"), text_color)
            if bold:
                text_props.set(q("fo", "font-weight"), "bold")

    def _apply_tint(self, hex_color: str, tint_percent: int) -> str:
        """Apply a tint (lighten) to a hex color using OKLab perceptual color space."""
        color = parse_color(hex_color)
        if color is None:
            return hex_color

        lightness_delta = (tint_percent / 100.0) * 0.5
        tinted = color.lighten(lightness_delta)
        return tinted.to_hex()

    def _apply_page_geometry(self, root: etree._Element) -> None:
        """Apply page size, margins, orientation, and header/footer from IR."""
        page_layout = root.find(".//style:page-layout[@style:name='pm1']", ODF_NSMAP)
        if page_layout is None:
            return

        props = page_layout.find("style:page-layout-properties", ODF_NSMAP)
        if props is None:
            return

        sections = self.document.sections

        if sections:
            # Multi-section: first section uses pm1, additional sections
            # get pm2, pm3, ... with corresponding master pages.
            first_geom = sections[0].page_geometry or self.document.page_geometry
            self._set_page_layout_props(props, first_geom)

            auto_styles = root.find("office:automatic-styles", ODF_NSMAP)
            master_styles = root.find("office:master-styles", ODF_NSMAP)
            if auto_styles is not None and master_styles is not None:
                for i, section in enumerate(sections[1:], 2):
                    sec_geom = section.page_geometry or self.document.page_geometry
                    layout_name = f"pm{i}"
                    master_name = f"Section{i}"

                    # Create new page-layout
                    new_layout = etree.SubElement(
                        auto_styles, q("style", "page-layout")
                    )
                    new_layout.set(q("style", "name"), layout_name)
                    new_props = etree.SubElement(
                        new_layout, q("style", "page-layout-properties")
                    )
                    self._set_page_layout_props(new_props, sec_geom)

                    # Create new master-page
                    new_master = etree.SubElement(
                        master_styles, q("style", "master-page")
                    )
                    new_master.set(q("style", "name"), master_name)
                    new_master.set(q("style", "page-layout-name"), layout_name)
        else:
            # Single section
            self._set_page_layout_props(props, self.document.page_geometry)

        # Add header/footer to the Standard master page
        master_page = root.find(
            ".//style:master-page[@style:name='Standard']", ODF_NSMAP
        )
        if master_page is not None:
            # Add header with org name
            header = master_page.find(q("style", "header"))
            if header is None:
                header = etree.SubElement(master_page, q("style", "header"))
                header_p = etree.SubElement(header, q("text", "p"))
                header_p.text = self.document.org_id

            # Add footer with page number
            footer = master_page.find(q("style", "footer"))
            if footer is None:
                footer = etree.SubElement(master_page, q("style", "footer"))
                footer_p = etree.SubElement(footer, q("text", "p"))
                page_num = etree.SubElement(footer_p, q("text", "page-number"))
                page_num.text = "1"

    @staticmethod
    def _set_page_layout_props(
        props: etree._Element,
        geom: object | None,
    ) -> None:
        """Set page-layout-properties attributes from a PageGeometry."""
        if isinstance(geom, PageGeometry):
            w_pt = geom.width_emu / EMU_PER_POINT
            h_pt = geom.height_emu / EMU_PER_POINT
            mt_pt = geom.margins.top / EMU_PER_POINT
            mb_pt = geom.margins.bottom / EMU_PER_POINT
            ml_pt = geom.margins.left / EMU_PER_POINT
            mr_pt = geom.margins.right / EMU_PER_POINT
            orient = "landscape" if geom.is_landscape else "portrait"
        else:
            w_pt = A4_WIDTH_EMU / EMU_PER_POINT
            h_pt = A4_HEIGHT_EMU / EMU_PER_POINT
            mt_pt = mb_pt = ml_pt = mr_pt = _DEFAULT_MARGIN_PT
            orient = "portrait"

        # Header/footer heights: 2× baseline grid
        hf_pt = twips_to_pt(word_fallback_header_footer_twips(DEFAULT_BASELINE_PT))

        props.set(q("fo", "page-width"), f"{w_pt:.2f}pt")
        props.set(q("fo", "page-height"), f"{h_pt:.2f}pt")
        props.set(q("style", "print-orientation"), orient)
        props.set(q("fo", "margin-top"), f"{mt_pt:.2f}pt")
        props.set(q("fo", "margin-bottom"), f"{mb_pt:.2f}pt")
        props.set(q("fo", "margin-left"), f"{ml_pt:.2f}pt")
        props.set(q("fo", "margin-right"), f"{mr_pt:.2f}pt")
        props.set(q("style", "header-height"), f"{hf_pt:.2f}pt")
        props.set(q("style", "footer-height"), f"{hf_pt:.2f}pt")

    def _remove_opentype_features(self, root: etree._Element) -> None:
        """Remove style:font-feature-settings attributes for older LibreOffice."""
        font_feature_attr = q("style", "font-feature-settings")
        for text_props in root.iter(q("style", "text-properties")):
            if font_feature_attr in text_props.attrib:
                del text_props.attrib[font_feature_attr]

    def _apply_text_direction(
        self,
        style_index: dict[str, etree._Element],
    ) -> None:
        """Apply RTL/LTR writing mode based on locale settings."""
        if not self.document.locale_settings.rtl:
            return  # LTR is the default, no changes needed

        writing_mode = "rl-tb"  # Right-to-left, top-to-bottom

        # Apply to body and heading styles
        style_names = ["Standard", "Text_20_body"] + [
            f"Heading_20_{h.level}" for h in self.document.heading_styles
        ]

        for style_name in style_names:
            style = style_index.get(style_name)
            if style is None:
                continue

            para_props = style.find(q("style", "paragraph-properties"))
            if para_props is not None:
                para_props.set(q("style", "writing-mode"), writing_mode)

    def _apply_text_alignment(
        self,
        style_index: dict[str, etree._Element],
    ) -> None:
        """Apply text alignment from Document IR (l/r/ctr/just → ODF)."""
        alignment_map = {
            "l": "start",
            "r": "end",
            "ctr": "center",
            "just": "justify",
        }

        text_align = self.document.text_alignment
        body_align = alignment_map.get(text_align.body_horizontal, "start")

        # Apply to body text styles
        for style_name in ("Standard", "Text_20_body"):
            style = style_index.get(style_name)
            if style is None:
                continue

            para_props = style.find(q("style", "paragraph-properties"))
            if para_props is not None:
                para_props.set(q("fo", "text-align"), body_align)

    def _apply_letter_spacing(
        self,
        style_index: dict[str, etree._Element],
    ) -> None:
        """Apply letter spacing (tracking) from Document IR."""
        tracking = self.document.tracking
        body_font_size = self.document.body_style.font_size_pt

        # Convert tracking from 1/100th em to pt
        # tracking.body is in hundredths of em
        body_spacing_pt = (tracking.body / 100.0) * body_font_size
        heading_spacing_pt = (tracking.heading / 100.0) * body_font_size

        # Only apply if non-zero
        if abs(body_spacing_pt) > 0.01:
            for style_name in ("Standard", "Text_20_body"):
                style = style_index.get(style_name)
                if style is None:
                    continue

                text_props = style.find(q("style", "text-properties"))
                if text_props is not None:
                    text_props.set(
                        q("fo", "letter-spacing"), f"{body_spacing_pt:.2f}pt"
                    )

        if abs(heading_spacing_pt) > 0.01:
            for heading in self.document.heading_styles:
                style_name = f"Heading_20_{heading.level}"
                style = style_index.get(style_name)
                if style is None:
                    continue

                text_props = style.find(q("style", "text-properties"))
                if text_props is not None:
                    text_props.set(
                        q("fo", "letter-spacing"), f"{heading_spacing_pt:.2f}pt"
                    )
