"""PowerPoint template emitter built from the Document IR.

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

Supports:
- Theme customization (colors, fonts) via lxml DOM manipulation
- Built-in table styles controlled via theme colors (Brandwares method)
- Table text defaults via slide master otherStyle
- Multiple color themes via extraClrSchemeLst
- Font embedding (EOT format) via template fragments

Note on table styles: Built-in Office table styles (like "Medium Style 2 - Accent 1")
reference theme colors. By setting clrScheme colors, we control how these styles look.
We use a minimal tableStyles.xml that just specifies the default style GUID.
"""

from __future__ import annotations

import logging
from copy import deepcopy
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Literal

from lxml import etree

from tokenmoulds.emitters.ooxml_base import A_NS, EmbeddedFont, OOXMLBaseEmitter
from tokenmoulds.emitters.xml_helpers import (
    R_NS,
    clone_template_element,
    remove_template_elements,
    replace_placeholders,
)
from tokenmoulds.ir import ColorTheme, DocumentIR
from tokenmoulds.themes.tables import (
    TableStyleGenerator,
    TableTextTokens,
    derive_table_style_from_brand_tone,
)

logger = logging.getLogger(__name__)

PRESENTATIONML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"
PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
SLIDE_LAYOUT_REL_TYPE = (
    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"
)
THEME_REL_TYPE = (
    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
)
DEFAULT_SLIDE_LAYOUT_NUMBERS = tuple(range(1, 41))


def _build_default_slide_layout_structure() -> Dict[str, str]:
    """Return the shipped PowerPoint default slide layout parts."""
    structure: Dict[str, str] = {}
    for layout_number in DEFAULT_SLIDE_LAYOUT_NUMBERS:
        structure[f"ppt/slideLayouts/slideLayout{layout_number}.xml"] = (
            f"ssot/slide_layouts/slideLayout{layout_number}.xml"
        )
        structure[f"ppt/slideLayouts/_rels/slideLayout{layout_number}.xml.rels"] = (
            "ssot/slide_layout_rels.xml"
        )
    return structure


@dataclass
class ExtraColorTheme:
    """Additional color theme for extraClrSchemeLst."""

    name: str
    theme: ColorTheme


class PowerPointEmitter(OOXMLBaseEmitter):
    """Build PowerPoint templates from the canonical Document IR.

    Uses SSOT pattern: XML structure comes from xml-structures/ecma-376/presentationml/,
    Python only substitutes template variables. Never creates elements with etree.

    PowerPoint-specific features:
    - Built-in table styles controlled via theme colors (Brandwares method)
    - Table text defaults via slide master otherStyle
    - Multiple color themes via extraClrSchemeLst (light/dark mode)
    - Font embedding (EOT format) via template fragments
    """

    DEFAULT_SLIDE_LAYOUT_NUMBERS = DEFAULT_SLIDE_LAYOUT_NUMBERS

    # PowerPoint package structure: archive path -> template name
    # Template names are relative to the spec templates folder
    PACKAGE_STRUCTURE: Dict[str, str] = {
        "[Content_Types].xml": "ssot/content_types.xml",
        "_rels/.rels": "ssot/package_rels.xml",
        "ppt/presentation.xml": "ssot/presentation.xml",
        "ppt/_rels/presentation.xml.rels": "ssot/presentation_rels.xml",
        "ppt/theme/theme1.xml": "ssot/theme.xml",
        "ppt/slideMasters/slideMaster1.xml": "ssot/slide_master.xml",
        "ppt/slideMasters/_rels/slideMaster1.xml.rels": "ssot/slide_master_rels.xml",
        **_build_default_slide_layout_structure(),
        "ppt/slides/slide1.xml": "ssot/slide.xml",
        "ppt/slides/_rels/slide1.xml.rels": "ssot/slide_rels.xml",
        "ppt/tableStyles.xml": "ssot/table_styles.xml",
        "docProps/core.xml": "ssot/docprops_core.xml",
        "docProps/app.xml": "ssot/docprops_app.xml",
    }

    def __init__(
        self,
        document: DocumentIR,
        template_root: Path | str | None = None,
        embed_fonts: bool = False,
        fonts_dir: Path | str | None = None,
        extra_color_themes: List[ExtraColorTheme] | 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.extra_color_themes = extra_color_themes or []

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

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

    def _get_base_template_path(self) -> Path:
        """Not used - SSOT builds from xml-structures."""
        return Path()

    def _get_package_structure(self) -> Dict[str, str]:
        """Return the package structure mapping archive paths to template names."""
        return self.PACKAGE_STRUCTURE

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

    def _get_font_embedding_format(self) -> Literal["odttf", "eot", "none"]:
        """Return 'eot' - PowerPoint uses EOT format fonts."""
        return "eot"

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

    def _get_initial_rel_id(self) -> int:
        """Return 20 to avoid conflicts with existing PowerPoint relationships."""
        return 20

    def _should_customize_entry(self, entry_path: str) -> bool:
        """Check if entry needs customization."""
        return entry_path in (
            "[Content_Types].xml",
            "ppt/presentation.xml",
            "ppt/_rels/presentation.xml.rels",
            "ppt/theme/theme1.xml",
            "ppt/slideMasters/slideMaster1.xml",
            "ppt/slideMasters/_rels/slideMaster1.xml.rels",
            "ppt/slides/slide1.xml",
            "ppt/tableStyles.xml",
            "docProps/core.xml",
            "docProps/app.xml",
        )

    def _customize_entry(self, entry_path: str, data: bytes) -> bytes | None:
        """Customize specific ZIP entry via template substitution."""
        if entry_path == "[Content_Types].xml":
            return self._customize_content_types_ssot(data)
        elif entry_path == "ppt/presentation.xml":
            return self._customize_presentation_ssot(data)
        elif entry_path == "ppt/_rels/presentation.xml.rels":
            return self._customize_presentation_rels_ssot(data)
        elif entry_path == "ppt/theme/theme1.xml":
            return self._customize_theme_ssot(data)
        elif entry_path == "ppt/slideMasters/slideMaster1.xml":
            return self._customize_slide_master_ssot(data)
        elif entry_path == "ppt/slideMasters/_rels/slideMaster1.xml.rels":
            return self._customize_slide_master_rels_ssot(data)
        elif entry_path == "ppt/slides/slide1.xml":
            return self._customize_slide_ssot(data)
        elif entry_path == "ppt/tableStyles.xml":
            return self._customize_table_styles_ssot(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 _get_font_metadata_entries(self) -> list:
        """PowerPoint handles font metadata in presentation.xml, not separately."""
        return []  # Handled in _customize_presentation_ssot and _customize_presentation_rels_ssot

    # =========================================================================
    # SSOT CUSTOMIZATIONS (DOM-based with cached templates)
    # =========================================================================

    def _customize_content_types_ssot(self, data: bytes) -> bytes:
        """Customize [Content_Types].xml via DOM-based placeholder replacement."""
        extra_types = [
            (
                f'  <Override PartName="/ppt/slideLayouts/slideLayout{layout_number}.xml" '
                'ContentType="application/vnd.openxmlformats-officedocument.'
                'presentationml.slideLayout+xml"/>'
            )
            for layout_number in self.DEFAULT_SLIDE_LAYOUT_NUMBERS[1:]
        ]

        if self.embed_fonts and self.embedded_fonts:
            extra_types.append(
                '  <Default Extension="fntdata" ContentType="application/x-fontdata"/>'
            )

        context = {"EXTRA_CONTENT_TYPES": "\n".join(extra_types)}
        return self._render_tree("ssot/content_types.xml", context)

    def _customize_presentation_ssot(self, data: bytes) -> bytes:
        """Customize ppt/presentation.xml via SSOT clone pattern.

        Uses the SSOT pattern: presentation.xml contains example elements with
        data-template markers. We clone these examples for dynamic content,
        then remove the original templates.
        """
        # PresentationML namespace
        P_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"
        R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"

        # Load the presentation template (contains example font elements)
        root = self.templates.read_tree("ssot/presentation.xml")

        # Populate slide master ID list (required for valid PPTX)
        # Clone the template element and set proper values
        sld_master_lst = root.find(f"{{{P_NS}}}sldMasterIdLst")
        if sld_master_lst is not None:
            master_clone = clone_template_element(root, "sld_master_id")
            if master_clone is not None:
                master_clone.set("id", "2147483648")  # Standard starting ID
                master_clone.set(f"{{{R_NS}}}id", "rId1")
                sld_master_lst.append(master_clone)

        # Populate slide ID list (required for valid PPTX)
        sld_id_lst = root.find(f"{{{P_NS}}}sldIdLst")
        if sld_id_lst is not None:
            slide_clone = clone_template_element(root, "sld_id")
            if slide_clone is not None:
                slide_clone.set("id", "256")  # Standard starting ID for slides
                slide_clone.set(f"{{{R_NS}}}id", "rId2")
                sld_id_lst.append(slide_clone)

        # Remove optional empty lists (notes master, handout master, custom shows)
        for list_name in ["notesMasterIdLst", "handoutMasterIdLst", "custShowLst"]:
            lst_el = root.find(f"{{{P_NS}}}{list_name}")
            if lst_el is not None:
                # Remove if empty (only has template children that will be removed)
                has_non_template_children = any(
                    "data-template" not in child.attrib for child in lst_el
                )
                if not has_non_template_children:
                    root.remove(lst_el)

        # Remove optional elements that have unresolved placeholders
        for el_name in ["smartTags", "photoAlbum", "kinsoku", "custDataLst"]:
            el = root.find(f"{{{P_NS}}}{el_name}")
            if el is not None:
                root.remove(el)

        # Build embedded font list using clone pattern
        if self.embed_fonts and self.embedded_fonts:
            self._build_embedded_fonts_from_template(root)
        else:
            # No fonts to embed - remove the entire embeddedFontLst element
            font_lst = root.find(f"{{{P_NS}}}embeddedFontLst")
            if font_lst is not None:
                root.remove(font_lst)

        # Remove all template example elements from final output
        remove_template_elements(root)

        return self._serialize_tree(root)

    def _build_embedded_fonts_from_template(self, root: etree._Element) -> None:
        """Build embeddedFontLst using SSOT clone pattern.

        Clones example elements from the template to create font entries.
        """
        # PresentationML namespace
        P_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"

        # Find the embeddedFontLst element
        font_lst = root.find(f"{{{P_NS}}}embeddedFontLst")
        if font_lst is None:
            return

        # Remove data-template attribute from the list itself (we keep it)
        if "data-template" in font_lst.attrib:
            del font_lst.attrib["data-template"]

        # Group fonts by family name
        fonts_by_family: Dict[str, List[EmbeddedFont]] = {}
        for font in self.embedded_fonts:
            if font.family_name not in fonts_by_family:
                fonts_by_family[font.family_name] = []
            fonts_by_family[font.family_name].append(font)

        # Clone embedded font entries for each family
        for family_name, fonts in fonts_by_family.items():
            # Clone the embedded font template
            embed_clone = clone_template_element(root, "embedded_font")

            # Set font name
            font_el = embed_clone.find(f"{{{P_NS}}}font")
            if font_el is not None:
                font_el.set("typeface", family_name)

            # Remove template style reference children (we'll add the ones we need)
            for child in list(embed_clone):
                if child.get("data-template") in (
                    "regular",
                    "bold",
                    "italic",
                    "bold_italic",
                ):
                    embed_clone.remove(child)

            # Add font style references for each font variant
            for font in fonts:
                if font.is_bold and font.is_italic:
                    ref_clone = clone_template_element(root, "bold_italic")
                elif font.is_bold:
                    ref_clone = clone_template_element(root, "bold")
                elif font.is_italic:
                    ref_clone = clone_template_element(root, "italic")
                else:
                    ref_clone = clone_template_element(root, "regular")

                replace_placeholders(ref_clone, {"REL_ID": font.relationship_id})
                embed_clone.append(ref_clone)

            font_lst.append(embed_clone)

    def _customize_presentation_rels_ssot(self, data: bytes) -> bytes:
        """Customize ppt/_rels/presentation.xml.rels via SSOT clone pattern."""
        # Load the relationships template
        root = self.templates.read_tree("ssot/presentation_rels.xml")

        # Clone and add font relationships
        if self.embed_fonts and self.embedded_fonts:
            for font in self.embedded_fonts:
                rel_clone = clone_template_element(root, "font_relationship")
                replace_placeholders(
                    rel_clone,
                    {
                        "REL_ID": font.relationship_id,
                        "FILENAME": font.fntdata_filename,
                    },
                )
                root.append(rel_clone)

        # Remove template elements
        remove_template_elements(root)

        return self._serialize_tree(root)

    def _customize_slide_master_rels_ssot(self, data: bytes) -> bytes:
        """Customize the slide master relationships for the full layout set."""
        root = self.templates.read_tree("ssot/slide_master_rels.xml")
        template_relationships = root.findall(f"{{{PACKAGE_REL_NS}}}Relationship")
        layout_template = next(
            rel
            for rel in template_relationships
            if rel.get("Type") == SLIDE_LAYOUT_REL_TYPE
        )
        theme_relationship = next(
            rel for rel in template_relationships if rel.get("Type") == THEME_REL_TYPE
        )

        for rel in template_relationships:
            root.remove(rel)

        for layout_number in self.DEFAULT_SLIDE_LAYOUT_NUMBERS:
            layout_relationship = deepcopy(layout_template)
            layout_relationship.set("Id", f"rId{layout_number}")
            layout_relationship.set(
                "Target", f"../slideLayouts/slideLayout{layout_number}.xml"
            )
            root.append(layout_relationship)

        theme_relationship = deepcopy(theme_relationship)
        theme_relationship.set("Id", f"rId{len(self.DEFAULT_SLIDE_LAYOUT_NUMBERS) + 1}")
        root.append(theme_relationship)

        return self._serialize_tree(root)

    def _customize_theme_ssot(self, data: bytes) -> bytes:
        """Customize theme via lxml DOM manipulation with cached template.

        Uses read_tree() for cached parsing, then applies color and font
        schemes via XPath.
        """
        root = self.templates.read_tree("ssot/theme.xml")
        theme = self.document.color_theme
        typo = self.document.typography

        # Use pure black/white for dk1/lt1 if configured
        if self.use_pure_text_colors:
            self._set_color_value(root, "dk1", "000000")
            self._set_color_value(root, "lt1", "FFFFFF")
        else:
            self._set_color_value(root, "dk1", theme.dk1)
            self._set_color_value(root, "lt1", theme.lt1)

        # Set accent colors
        self._set_color_value(root, "dk2", theme.dk2)
        self._set_color_value(root, "lt2", theme.lt2)
        self._set_color_value(root, "accent1", theme.accent1)
        self._set_color_value(root, "accent2", theme.accent2)
        self._set_color_value(root, "accent3", theme.accent3)
        self._set_color_value(root, "accent4", theme.accent4)
        self._set_color_value(root, "accent5", theme.accent5)
        self._set_color_value(root, "accent6", theme.accent6)
        self._set_color_value(root, "hlink", theme.hyperlink)
        self._set_color_value(root, "folHlink", theme.hyperlink_followed)

        # Set font schemes
        self._set_font_typeface(root, "majorFont", "latin", typo.heading_font)
        self._set_font_typeface(root, "minorFont", "latin", typo.body_font)

        # Add extra color schemes if provided
        if self.extra_color_themes:
            self._add_extra_color_schemes(root)
            logger.info(f"Added {len(self.extra_color_themes)} extra color themes")

        # Remove template markers from final output
        remove_template_elements(root)

        return self._serialize_tree(root)

    def _add_extra_color_schemes(self, root: etree._Element) -> None:
        """Add extra color schemes using SSOT clone pattern.

        Uses the clone_template_element pattern to create extra color schemes
        from the template in ssot/theme.xml rather than programmatic element
        creation. This maintains SSOT consistency across all emitters.
        """
        ns = {"a": A_NS}

        # Find the extraClrSchemeLst element
        extra_lst = self._find_element(root, ".//a:extraClrSchemeLst", ns)
        if extra_lst is None:
            return

        for extra_theme in self.extra_color_themes:
            theme = extra_theme.theme

            # Clone the extra color scheme template
            extra_scheme = clone_template_element(root, "extra_color_scheme")

            # Replace all placeholders with theme colors
            replace_placeholders(
                extra_scheme,
                {
                    "SCHEME_NAME": extra_theme.name,
                    "DK1": theme.dk1.lstrip("#").upper(),
                    "LT1": theme.lt1.lstrip("#").upper(),
                    "DK2": theme.dk2.lstrip("#").upper(),
                    "LT2": theme.lt2.lstrip("#").upper(),
                    "ACCENT1": theme.accent1.lstrip("#").upper(),
                    "ACCENT2": theme.accent2.lstrip("#").upper(),
                    "ACCENT3": theme.accent3.lstrip("#").upper(),
                    "ACCENT4": theme.accent4.lstrip("#").upper(),
                    "ACCENT5": theme.accent5.lstrip("#").upper(),
                    "ACCENT6": theme.accent6.lstrip("#").upper(),
                    "HLINK": theme.hyperlink.lstrip("#").upper(),
                    "FOLHLINK": theme.hyperlink_followed.lstrip("#").upper(),
                },
            )

            extra_lst.append(extra_scheme)

    def _customize_slide_master_ssot(self, data: bytes) -> bytes:
        """Customize slide master via DOM-based placeholder replacement.

        All values come from the Document IR (SSOT pattern).
        Trivial unit conversions: pt → hundredths, twips → EMU.
        """

        # Helper: points to hundredths
        def pt_to_hundredths(pt: float) -> int:
            return int(pt * 100)

        # Helper: twips to EMU
        def twips_to_emu(twips: int) -> int:
            return twips * 635

        # =================================================================
        # Extract IR values
        # =================================================================
        typo = self.document.typography
        layout = self.document.presentation_layout
        list_style = self.document.list_style
        body_style = self.document.body_style

        # =================================================================
        # Build substitution dictionary
        # =================================================================
        subs: Dict[str, str] = {}

        # --- Locale ---
        subs["LANG"] = self.document.locale

        # --- Kerning (used across all text styles) ---
        # Half-points * 50 = hundredths (8hp = 400 hundredths)
        subs["KERN_THRESHOLD"] = str(body_style.character.kern_threshold_half_pt * 50)

        # --- Tab size (EMU) ---
        subs["TAB_SIZE"] = str(
            self.document.document_settings.default_tab_stop_twips * 635
        )

        # --- Text insets (EMU) ---
        # Default PowerPoint insets: 91440 EMU (0.1") left/right, 45720 EMU (0.05") top/bottom
        subs["TEXT_INSET_LEFT"] = str(91440)
        subs["TEXT_INSET_TOP"] = str(45720)
        subs["TEXT_INSET_RIGHT"] = str(91440)
        subs["TEXT_INSET_BOTTOM"] = str(45720)

        # --- Title placeholder geometry (EMU) ---
        title_ph = next((p for p in layout.placeholders if p.kind == "title"), None)
        if title_ph:
            subs["TITLE_X"] = str(title_ph.x)
            subs["TITLE_Y"] = str(title_ph.y)
            subs["TITLE_WIDTH"] = str(title_ph.cx)
            subs["TITLE_HEIGHT"] = str(title_ph.cy)
        else:
            # Fallback to standard dimensions
            subs["TITLE_X"] = "457200"
            subs["TITLE_Y"] = "274638"
            subs["TITLE_WIDTH"] = "8229600"
            subs["TITLE_HEIGHT"] = "1143000"

        # --- Body placeholder geometry (EMU) ---
        body_ph = next((p for p in layout.placeholders if p.kind == "body"), None)
        if body_ph:
            subs["BODY_X"] = str(body_ph.x)
            subs["BODY_Y"] = str(body_ph.y)
            subs["BODY_WIDTH"] = str(body_ph.cx)
            subs["BODY_HEIGHT"] = str(body_ph.cy)
        else:
            # Fallback to standard dimensions
            subs["BODY_X"] = "457200"
            subs["BODY_Y"] = "1600200"
            subs["BODY_WIDTH"] = "8229600"
            subs["BODY_HEIGHT"] = "4525963"

        # --- Title font size (hundredths of a point) ---
        title_pt = typo.size_map_pt.get("h1", 44.0)
        subs["TITLE_FONT_SIZE"] = str(pt_to_hundredths(title_pt))

        # --- Body font sizes for levels (hundredths of a point) ---
        # Use modular scale from typography scheme, fallback to standard sizes
        subs["BODY_L1_FONT_SIZE"] = str(
            pt_to_hundredths(typo.size_map_pt.get("h3", 32.0))
        )
        subs["BODY_L2_FONT_SIZE"] = str(
            pt_to_hundredths(typo.size_map_pt.get("h4", 28.0))
        )
        subs["BODY_L3_FONT_SIZE"] = str(
            pt_to_hundredths(typo.size_map_pt.get("h5", 24.0))
        )

        # --- Body list margins (EMU) ---
        # Convert list indentation from twips to EMU
        base_indent = list_style.indent_twips
        subs["BODY_L1_MAR_LEFT"] = str(twips_to_emu(base_indent))
        subs["BODY_L2_MAR_LEFT"] = str(
            twips_to_emu(base_indent + list_style.effective_indent_per_level)
        )
        subs["BODY_L3_MAR_LEFT"] = str(
            twips_to_emu(base_indent + 2 * list_style.effective_indent_per_level)
        )

        # --- Body list hanging indents (EMU) ---
        hanging = list_style.hanging_twips
        subs["BODY_L1_INDENT"] = str(twips_to_emu(hanging))
        subs["BODY_L2_INDENT"] = str(twips_to_emu(hanging))
        subs["BODY_L3_INDENT"] = str(twips_to_emu(hanging))

        # --- Body spacing (percentage × 1000) ---
        # 20000 = 20% of line height
        subs["BODY_SPACE_BEFORE"] = "20000"

        # --- Bullet characters ---
        bullets = list_style.nested_bullet_chars
        subs["BULLET_L1_CHAR"] = bullets[0] if len(bullets) > 0 else "•"
        subs["BULLET_L2_CHAR"] = bullets[1] if len(bullets) > 1 else "–"
        subs["BULLET_L3_CHAR"] = bullets[2] if len(bullets) > 2 else "•"

        # --- Bullet font ---
        subs["BULLET_FONT"] = list_style.bullet_font or "Arial"
        subs["BULLET_PITCH_FAMILY"] = "34"  # Variable pitch, Swiss family
        subs["BULLET_CHARSET"] = "0"  # ANSI

        # --- Table defaults (otherStyle) ---
        # Use body style for tables (table_style was removed from IR)
        subs["TABLE_FONT_SIZE"] = str(pt_to_hundredths(body_style.font_size_pt))
        subs["TABLE_FONT"] = body_style.font_family

        # =================================================================
        # Extended typography settings (from Document IR)
        # =================================================================

        # --- RTL and locale settings ---
        locale_settings = self.document.locale_settings
        subs["RTL"] = "1" if locale_settings.rtl else "0"
        subs["EA_LN_BRK"] = "1" if locale_settings.ea_ln_brk else "0"
        subs["LATIN_LN_BRK"] = "1" if locale_settings.latin_ln_brk else "0"
        subs["HANGING_PUNCT"] = "1" if locale_settings.hanging_punct else "0"

        # --- Text alignment ---
        alignment = self.document.text_alignment
        subs["TITLE_ALIGN"] = alignment.title_horizontal
        subs["BODY_ALIGN"] = alignment.body_horizontal
        subs["TITLE_ANCHOR"] = alignment.title_vertical
        subs["BODY_ANCHOR"] = alignment.body_vertical

        # --- Tracking / letter spacing (hundredths of EMU) ---
        # Convert from 1/100 em to EMU (at body size: 1em = body_size_pt * 12700 EMU)
        tracking = self.document.tracking
        # For PowerPoint, tracking is in hundredths of a point (like font size)
        # Title uses display tracking, body uses body tracking
        subs["TITLE_TRACKING"] = str(tracking.display * 10)  # Approximate conversion
        subs["BODY_TRACKING"] = str(tracking.body * 10)

        # --- Font fallbacks for international text ---
        fallback = self.document.fallback_fonts
        subs["EA_FONT"] = fallback.ea_font
        subs["CS_FONT"] = fallback.cs_font

        # =================================================================
        root = self.templates.read_tree("ssot/slide_master.xml")
        replace_placeholders(root, subs)

        layout_id_list = root.find(f"{{{PRESENTATIONML_NS}}}sldLayoutIdLst")
        if layout_id_list is not None:
            existing_layouts = layout_id_list.findall(
                f"{{{PRESENTATIONML_NS}}}sldLayoutId"
            )
            layout_template = existing_layouts[0] if existing_layouts else None
            for layout in existing_layouts:
                layout_id_list.remove(layout)

            if layout_template is not None:
                base_layout_id = 2147483649
                for offset, layout_number in enumerate(
                    self.DEFAULT_SLIDE_LAYOUT_NUMBERS
                ):
                    layout_ref = deepcopy(layout_template)
                    layout_ref.set("id", str(base_layout_id + offset))
                    layout_ref.set(f"{{{R_NS}}}id", f"rId{layout_number}")
                    layout_id_list.append(layout_ref)

        return self._serialize_tree(root)

    def _customize_slide_ssot(self, data: bytes) -> bytes:
        """Customize slide via DOM-based placeholder replacement."""
        context = {
            "SLIDE_TITLE": f"{self.document.org_id} Template",
            "SLIDE_CONTENT": "Sample slide content",
        }
        return self._render_tree("ssot/slide.xml", context)

    def _customize_table_styles_ssot(self, data: bytes) -> bytes:
        """Generate typographically correct table styles derived from brand tone.

        Creates table styles following typographic principles:
        - No outer vertical borders
        - Horizontal rules only (top, header separator, bottom)
        - Header and banding intensity based on brand_tone

        All colors reference theme tokens so styles adapt to brand colors.
        """
        # Create text tokens from document typography
        text_tokens = TableTextTokens(
            font_family=self.document.typography.body_font,
            body_size_pt=self.document.body_style.font_size_pt,
            header_size_pt=self.document.body_style.font_size_pt + 1.0,
            header_bold=True,
        )
        generator = TableStyleGenerator(text_tokens)

        # Derive table style from brand tone
        table_style = derive_table_style_from_brand_tone(
            brand_tone=self.document.brand_tone,
            border_width_pt=0.75,
        )

        # Generate tableStyles.xml
        table_styles_bytes = generator.generate_table_styles_xml(
            [table_style],
            table_style.style_id,
        )

        logger.info(
            f"Generated typographically correct table style "
            f"(brand_tone={self.document.brand_tone:.2f})"
        )
        return table_styles_bytes

    def _customize_core_props_ssot(self, data: bytes) -> bytes:
        """Customize core properties via DOM-based placeholder replacement."""
        timestamp = self._utc_timestamp()
        context = {
            "TITLE": self._get_document_title(),
            "CREATOR": "TokenMoulds",
            "MODIFIER": "TokenMoulds",
            "TIMESTAMP": timestamp,
        }
        return self._render_tree("ssot/docprops_core.xml", context)

    def _customize_app_props_ssot(self, data: bytes) -> bytes:
        """Customize app properties via DOM-based placeholder replacement."""
        context = {
            "APPLICATION": "TokenMoulds",
            "COMPANY": self.document.org_id,
        }
        return self._render_tree("ssot/docprops_app.xml", context)
