"""ODF template emitters for Writer (.ott) and Impress (.otp).

LibreOffice/OpenDocument Format (ODF) emitters that generate templates with
full OpenType feature support. Unlike Google Docs, LibreOffice preserves
advanced typography features.

LibreOffice Typography Capabilities:
====================================

OPENTYPE FEATURES (SUPPORTED):
- Ligatures (liga, dlig) - standard and discretionary
- Contextual alternates (calt) - context-aware glyph substitution
- Stylistic sets (ss01-ss20) - designer-defined alternate glyphs
- Small caps (smcp) - true small capitals
- Old-style numerals (onum) - text figures
- Tabular numerals (tnum) - monospace-width numbers
- Kerning (kern) - precise letter spacing
- Character variants (cv01-cv99) - individual glyph alternates

FONT EMBEDDING (SUPPORTED):
- OTF and TTF fonts can be embedded directly
- No obfuscation required (unlike OOXML)
- Font licensing restrictions still apply

COLOR THEMES:
- ODF doesn't have native theme color support like OOXML
- Colors are stored as direct RGB values
- Theme changes require regenerating the document

Known Limitations:
==================
- No native "theme color" concept - all colors are literal
- Variable fonts partially supported (depends on LibreOffice version)
- Some OpenType features require LibreOffice 7.0+
"""

from __future__ import annotations

import datetime
import io
import zipfile
from pathlib import Path
from string import Template
from xml.sax.saxutils import escape

from tokenmoulds.design.units import emu_to_pt, twips_to_pt
from tokenmoulds.emitters import TemplateResolver
from tokenmoulds.emitters.base import BaseEmitter
from tokenmoulds.emitters.odf_helpers import (
    DEFAULT_TABLE_BORDER_COLOR,
    DEFAULT_TABLE_BORDER_WIDTH_PT,
    DEFAULT_TABLE_PADDING_X_PT,
    DEFAULT_TABLE_PADDING_Y_PT,
)
from tokenmoulds.emitters.writer import WriterStylesEmitter
from tokenmoulds.ir import DocumentIR


# BCP-47 language tag lookup from 2-letter locale code
_LOCALE_TO_BCP47: dict[str, str] = {
    "US": "en-US",
    "GB": "en-GB",
    "UK": "en-GB",
    "AU": "en-AU",
    "NZ": "en-NZ",
    "IE": "en-IE",
    "ZA": "en-ZA",
    "IN": "en-IN",
    "SG": "en-SG",
    "HK": "en-HK",
    "CA": "en-CA",
    "DE": "de-DE",
    "AT": "de-AT",
    "FR": "fr-FR",
    "BE": "fr-BE",
    "ES": "es-ES",
    "IT": "it-IT",
    "PT": "pt-PT",
    "NL": "nl-NL",
    "JP": "ja-JP",
    "CN": "zh-CN",
    "BR": "pt-BR",
    "MX": "es-MX",
}

# Currency symbol lookup from 2-letter locale code
_LOCALE_CURRENCY: dict[str, str] = {
    "US": "$",
    "GB": "\u00a3",
    "UK": "\u00a3",
    "AU": "$",
    "NZ": "$",
    "CA": "$",
    "SG": "$",
    "HK": "$",
    "EU": "\u20ac",
    "DE": "\u20ac",
    "AT": "\u20ac",
    "FR": "\u20ac",
    "ES": "\u20ac",
    "IT": "\u20ac",
    "PT": "\u20ac",
    "NL": "\u20ac",
    "BE": "\u20ac",
    "IE": "\u20ac",
    "JP": "\u00a5",
    "CN": "\u00a5",
    "IN": "\u20b9",
    "ZA": "R",
    "BR": "R$",
}


class OdfBaseEmitter(BaseEmitter):
    """Base class for ODF (LibreOffice) document emitters.

    Inherits from BaseEmitter and provides ODF-specific functionality:
    - ZIP package building with ODF structure
    - Template rendering with variable substitution
    - OpenType feature support
    - Direct font embedding (OTF/TTF)

    Unlike OOXML emitters, ODF doesn't have native theme colors,
    so use_pure_text_colors has less impact (colors are always literal).
    """

    def __init__(
        self,
        format_key: str,
        document: DocumentIR,
        template_root: Path | str | None = None,
        use_pure_text_colors: bool = True,
        enable_opentype_features: bool = True,
    ) -> None:
        """Initialize ODF emitter.

        Args:
            format_key: Format identifier for template resolution ('writer', 'impress')
            document: Document IR with style information
            template_root: Optional override for template resolution
            use_pure_text_colors: If True, use pure black/white for text colors
                (less relevant for ODF since colors are always literal)
            enable_opentype_features: If True, enable OpenType features like
                ligatures, stylistic sets, etc. (LibreOffice 7.0+)
        """
        super().__init__(document, template_root)
        self.templates = TemplateResolver(format_key, template_root)
        self.use_pure_text_colors = use_pure_text_colors
        self.enable_opentype_features = enable_opentype_features

    def _render(self, name: str, *, context: dict[str, str] | None = None) -> str:
        """Render an XML template with optional variable substitution."""
        template = self.templates.read_text(name, spec_rel_path=f"templates/{name}")
        if context:
            return Template(template).safe_substitute(context)
        return template

    def _write_common_entries(self, archive: zipfile.ZipFile, *, title: str) -> None:
        """Write common ODF package entries (manifest, metadata, settings)."""
        if "META-INF/manifest.xml" not in archive.NameToInfo:
            archive.writestr(
                "META-INF/manifest.xml",
                self._render("META-INF/manifest.xml"),
                compress_type=zipfile.ZIP_DEFLATED,
            )
        locale_upper = (self.document.locale or "US").upper()
        language = _LOCALE_TO_BCP47.get(locale_upper, f"en-{locale_upper}")
        if "meta.xml" not in archive.NameToInfo:
            meta_context = {
                "TITLE": title,
                "DATE": datetime.datetime.now(datetime.timezone.utc).isoformat(),
                "LANGUAGE": language,
            }
            archive.writestr(
                "meta.xml",
                self._render("meta.xml", context=meta_context),
                compress_type=zipfile.ZIP_DEFLATED,
            )
        # Write settings.xml if not already written by a subclass
        if "settings.xml" not in archive.NameToInfo:
            try:
                settings_xml = self._render("settings.xml")
            except FileNotFoundError:
                pass
            else:
                archive.writestr(
                    "settings.xml",
                    settings_xml,
                    compress_type=zipfile.ZIP_DEFLATED,
                )


class WriterTemplateEmitter(OdfBaseEmitter):
    """LibreOffice Writer template emitter (.ott).

    Generates ODF text document templates with:
    - Paragraph styles (Normal, Heading1-6, Quote)
    - Character styles (Hyperlinks)
    - Table styles with banding and accent variants
    - OpenType features (ligatures, stylistic sets, etc.)

    Inherits from OdfBaseEmitter (→ BaseEmitter).
    """

    MIMETYPE = "application/vnd.oasis.opendocument.text-template"

    def __init__(
        self,
        document: DocumentIR,
        template_root: Path | str | None = None,
        use_pure_text_colors: bool = True,
        enable_opentype_features: bool = True,
    ) -> None:
        """Initialize Writer template emitter.

        Args:
            document: Document IR with style information
            template_root: Optional override for template resolution
            use_pure_text_colors: If True, use pure black/white for text
            enable_opentype_features: If True, enable OpenType features
        """
        super().__init__(
            "writer",
            document,
            template_root,
            use_pure_text_colors=use_pure_text_colors,
            enable_opentype_features=enable_opentype_features,
        )

    def build_package(self) -> bytes:
        """Build the Writer template package."""
        buffer = io.BytesIO()
        title_text = f"{self.document.org_id} Narrative Template"
        body_style = self.document.body_style
        color_theme = self.document.color_theme
        body_text = (
            f"Our body copy uses {body_style.font_family} {body_style.font_size_pt:.1f}pt "
            f"in {body_style.color}, paired with accent {color_theme.accent1}."
        )
        # Prepare settings.xml context from document_settings
        ds = self.document.document_settings
        auto_hyph = "true" if ds.auto_hyphenation else "false"
        # Convert hyphenation zone from twips to hundredths of mm
        hyph_zone_hmm = int(ds.hyphenation_zone_twips * 25.4 / 1440 * 100)
        settings_context = {
            "AUTO_HYPHENATION": auto_hyph,
            "HYPHENATION_ZONE": str(hyph_zone_hmm),
        }

        with zipfile.ZipFile(
            buffer, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
        ) as archive:
            archive.writestr(
                "mimetype", self.MIMETYPE, compress_type=zipfile.ZIP_STORED
            )
            archive.writestr(
                "content.xml",
                self._render(
                    "content.xml",
                    context={
                        "TITLE": title_text,
                        "BODY": body_text,
                    },
                ),
            )
            styles_xml = WriterStylesEmitter(
                self.document,
                enable_opentype_features=self.enable_opentype_features,
            ).build_xml()
            archive.writestr(
                "styles.xml", styles_xml, compress_type=zipfile.ZIP_DEFLATED
            )
            # Write settings.xml with hyphenation context
            archive.writestr(
                "settings.xml",
                self._render("settings.xml", context=settings_context),
                compress_type=zipfile.ZIP_DEFLATED,
            )
            self._write_common_entries(archive, title=title_text)
        return buffer.getvalue()


class ImpressTemplateEmitter(OdfBaseEmitter):
    """LibreOffice Impress template emitter (.otp).

    Generates ODF presentation templates with:
    - Slide layouts with placeholder positioning
    - Text styles (title, body, lists)
    - Table styles with accent variants
    - OpenType features (ligatures, stylistic sets, etc.)

    Inherits from OdfBaseEmitter (→ BaseEmitter).
    """

    MIMETYPE = "application/vnd.oasis.opendocument.presentation-template"

    def __init__(
        self,
        document: DocumentIR,
        template_root: Path | str | None = None,
        use_pure_text_colors: bool = True,
        enable_opentype_features: bool = True,
    ) -> None:
        """Initialize Impress template emitter.

        Args:
            document: Document IR with style information
            template_root: Optional override for template resolution
            use_pure_text_colors: If True, use pure black/white for text
            enable_opentype_features: If True, enable OpenType features
        """
        super().__init__(
            "impress",
            document,
            template_root,
            use_pure_text_colors=use_pure_text_colors,
            enable_opentype_features=enable_opentype_features,
        )

    def build_package(self) -> bytes:
        buffer = io.BytesIO()
        title_text = f"{self.document.org_id} Presentation"
        body_text = (
            f"Body text uses {self.document.body_style.font_family} "
            f"{self.document.body_style.font_size_pt:.1f}pt with accent {self.document.color_theme.accent2}."
        )
        title_rect = self._placeholder_rect("title", prefix="TITLE")
        body_rect = self._placeholder_rect("body", prefix="BODY")
        table_rect = self._placeholder_rect("table", prefix="TABLE")
        list_items = self._body_list_items()
        table_rows = self._table_rows()
        hyperlink_label = (
            f"{self.document.org_id.lower()}.brand/{self.document.locale.lower()}"
        )
        styles_context = self._styles_context()
        content_context = {
            "TITLE": title_text,
            "BODY": body_text,
            "HYPERLINK_LABEL": hyperlink_label,
            **title_rect,
            **body_rect,
            **table_rect,
        }
        for idx, value in enumerate(list_items, start=1):
            content_context[f"LIST_ITEM_{idx}"] = value
        for row_idx, row in enumerate(table_rows, start=1):
            for col_idx, value in enumerate(row, start=1):
                content_context[f"TABLE_ROW{row_idx}_COL{col_idx}"] = value
        with zipfile.ZipFile(
            buffer, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
        ) as archive:
            archive.writestr(
                "mimetype", self.MIMETYPE, compress_type=zipfile.ZIP_STORED
            )
            archive.writestr(
                "content.xml",
                self._render("content.xml", context=content_context),
            )
            archive.writestr(
                "styles.xml",
                self._render("styles.xml", context=styles_context),
            )
            self._write_common_entries(archive, title=title_text)
        return buffer.getvalue()

    def _placeholder_rect(self, kind: str, *, prefix: str) -> dict[str, str]:
        layout = self.document.presentation_layout
        fallback = layout.placeholders[0] if layout.placeholders else None
        placeholder = next((p for p in layout.placeholders if p.kind == kind), fallback)
        if placeholder is None:
            return {
                f"{prefix}_X": "0.00",
                f"{prefix}_Y": "0.00",
                f"{prefix}_WIDTH": "0.00",
                f"{prefix}_HEIGHT": "0.00",
            }
        return {
            f"{prefix}_X": f"{emu_to_pt(placeholder.x):.2f}",
            f"{prefix}_Y": f"{emu_to_pt(placeholder.y):.2f}",
            f"{prefix}_WIDTH": f"{emu_to_pt(placeholder.cx):.2f}",
            f"{prefix}_HEIGHT": f"{emu_to_pt(placeholder.cy):.2f}",
        }

    def _body_list_items(self) -> list[str]:
        items = [
            f"Heading font: {self.document.typography.heading_font}",
            f"Body font: {self.document.body_style.font_family}",
            f"Accent palette: {self.document.color_theme.accent1} / {self.document.color_theme.accent2}",
            f"Locale: {self.document.locale}",
        ]
        return [escape(item) for item in items]

    def _table_rows(self) -> list[tuple[str, ...]]:
        # Derive table font size from body style (table_style was removed from IR)
        table_font_size = self.document.body_style.font_size_pt
        rows = [
            ("Org", self.document.org_id, self.document.locale),
            ("Primary Accent", self.document.color_theme.accent1, "Theme color"),
            (
                "Heading Font",
                self.document.typography.heading_font,
                f"{table_font_size:.1f}pt tables",
            ),
        ]
        return [tuple(escape(value) for value in row) for row in rows]

    def _styles_context(self) -> dict[str, str]:
        heading_style = (
            self.document.heading_styles[0]
            if self.document.heading_styles
            else self.document.body_style
        )
        list_margin = f"{twips_to_pt(self.document.list_style.indent_twips):.2f}pt"
        list_indent = f"{twips_to_pt(self.document.list_style.hanging_twips):.2f}pt"

        # Derive table properties from body style and color theme
        # (table_style was removed from DocumentIR, now uses DTCG tokens)
        table_font_family = self.document.body_style.font_family
        table_font_size = self.document.body_style.font_size_pt

        table_border = (
            f"{DEFAULT_TABLE_BORDER_WIDTH_PT:.2f}pt solid {DEFAULT_TABLE_BORDER_COLOR}"
        )
        padding_x_pt = DEFAULT_TABLE_PADDING_X_PT
        padding_y_pt = DEFAULT_TABLE_PADDING_Y_PT

        # Header uses lt2 color, body uses lt1 color from color theme
        header_fill = self.document.color_theme.lt2
        body_fill = self.document.color_theme.lt1

        layout = self.document.presentation_layout
        slide_width = f"{emu_to_pt(layout.slide_width):.2f}"
        slide_height = f"{emu_to_pt(layout.slide_height):.2f}"

        return {
            "SLIDE_WIDTH": slide_width,
            "SLIDE_HEIGHT": slide_height,
            "HEADING_FONT": self.document.typography.heading_font,
            "BODY_FONT": self.document.body_style.font_family,
            "TITLE_COLOR": self.document.color_theme.accent1,
            "BODY_COLOR": self.document.body_style.color,
            "TITLE_FONT": self.document.typography.heading_font,
            "TITLE_SIZE": f"{heading_style.font_size_pt:.1f}",
            "BODY_SIZE": f"{self.document.body_style.font_size_pt:.1f}",
            "LINK_COLOR": self.document.hyperlink_style.link_color,
            "VISITED_LINK_COLOR": self.document.hyperlink_style.visited_color,
            "LIST_MARGIN": list_margin,
            "LIST_INDENT": list_indent,
            "TABLE_HEADER_FILL": header_fill,
            "TABLE_BODY_FILL": body_fill,
            "TABLE_BORDER": table_border,
            "TABLE_PADDING_X": padding_x_pt,
            "TABLE_PADDING_Y": padding_y_pt,
            "TABLE_FONT": table_font_family,
            "TABLE_FONT_SIZE": f"{table_font_size:.1f}",
            "TABLE_HEADER_TEXT_COLOR": heading_style.color,
            "TABLE_TEXT_COLOR": self.document.body_style.color,
            # Presentation frame padding from grid (≈ 0.5× list hanging indent)
            "FRAME_PADDING": f"{twips_to_pt(self.document.list_style.hanging_twips):.2f}",
            **self._layout_position_vars(),
        }

    def _layout_position_vars(self) -> dict[str, str]:
        """Compute per-layout placeholder position template variables.

        Returns variables like ``AL1T_TITLE_X``, ``AL2T_BODY_WIDTH`` etc.
        for all defined ODF layout types.
        """
        from tokenmoulds.design.page_geometry import create_slide_grid
        from tokenmoulds.dtcg.layout_recipe_resolver import (
            LAYOUT_TYPE_TO_ODF,
            compute_default_slide_regions,
            resolve_layout_by_name,
        )

        from tokenmoulds.design.layout_defaults import DEFAULT_BASELINE_EMU

        layout = self.document.presentation_layout
        grid = create_slide_grid(
            layout.slide_width, layout.slide_height, DEFAULT_BASELINE_EMU
        )
        regions = compute_default_slide_regions(grid)
        result: dict[str, str] = {}

        for layout_type, (odf_name, _display) in LAYOUT_TYPE_TO_ODF.items():
            layout_regions = regions.get(layout_type, {})
            positions = resolve_layout_by_name(layout_type, layout_regions, grid)

            for region_name, (x, y, cx, cy) in positions.items():
                prefix = f"{odf_name}_{region_name.upper()}"
                result[f"{prefix}_X"] = f"{emu_to_pt(x):.2f}"
                result[f"{prefix}_Y"] = f"{emu_to_pt(y):.2f}"
                result[f"{prefix}_WIDTH"] = f"{emu_to_pt(cx):.2f}"
                result[f"{prefix}_HEIGHT"] = f"{emu_to_pt(cy):.2f}"

        return result


class CalcTemplateEmitter(OdfBaseEmitter):
    """LibreOffice Calc template emitter (.ots).

    Generates ODF spreadsheet templates with:
    - 27 named cell styles (Default, Heading, Good, Bad, Neutral, Note,
      Result, Accent1-6, Accent1Light-Accent6Light)
    - Number/date/currency/percentage formats
    - Locale-aware page setup with baseline-grid row heights
    - OpenType features (ligatures, stylistic sets, etc.)

    Inherits from OdfBaseEmitter (-> BaseEmitter).
    """

    MIMETYPE = "application/vnd.oasis.opendocument.spreadsheet-template"

    # Semantic background tint factor (80% toward white for pastels)
    _SEMANTIC_TINT_FACTOR = 0.8

    def __init__(
        self,
        document: DocumentIR,
        template_root: Path | str | None = None,
        use_pure_text_colors: bool = True,
        enable_opentype_features: bool = True,
    ) -> None:
        """Initialize Calc template emitter.

        Args:
            document: Document IR with style information
            template_root: Optional override for template resolution
            use_pure_text_colors: If True, use pure black/white for text
            enable_opentype_features: If True, enable OpenType features
        """
        super().__init__(
            "calc",
            document,
            template_root,
            use_pure_text_colors=use_pure_text_colors,
            enable_opentype_features=enable_opentype_features,
        )

    # ------------------------------------------------------------------
    # Light-tint computation
    # ------------------------------------------------------------------

    @staticmethod
    def _tint_toward_white(hex_color: str, factor: float = 0.8) -> str:
        """Compute a light tint of *hex_color* toward white.

        ``factor`` of 0.8 means 80 % of the way to white, producing a
        pastel variant suitable for AccentNLight cell backgrounds.
        """
        h = hex_color.lstrip("#")
        r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
        r2 = int(r + (255 - r) * factor)
        g2 = int(g + (255 - g) * factor)
        b2 = int(b + (255 - b) * factor)
        return f"#{r2:02X}{g2:02X}{b2:02X}"

    # ------------------------------------------------------------------
    # Page geometry helpers
    # ------------------------------------------------------------------

    def _page_dimensions_pt(self) -> dict[str, str]:
        """Return page size, margins and orientation from the IR (in pt)."""
        geom = self.document.page_geometry
        if geom is not None:
            width_pt = emu_to_pt(geom.width_emu)
            height_pt = emu_to_pt(geom.height_emu)
            margin_top = emu_to_pt(geom.margins.top)
            margin_bottom = emu_to_pt(geom.margins.bottom)
            margin_left = emu_to_pt(geom.margins.left)
            margin_right = emu_to_pt(geom.margins.right)
            orientation = "landscape" if geom.is_landscape else "portrait"
        else:
            # A4 defaults in pt (210mm × 297mm)
            width_pt = 210.0 * 72.0 / 25.4
            height_pt = 297.0 * 72.0 / 25.4
            margin_top = margin_bottom = margin_left = margin_right = 2.0 * 72.0 / 2.54
            orientation = "portrait"
        return {
            "PAGE_WIDTH": f"{width_pt:.2f}",
            "PAGE_HEIGHT": f"{height_pt:.2f}",
            "MARGIN_TOP": f"{margin_top:.2f}",
            "MARGIN_BOTTOM": f"{margin_bottom:.2f}",
            "MARGIN_LEFT": f"{margin_left:.2f}",
            "MARGIN_RIGHT": f"{margin_right:.2f}",
            "ORIENTATION": orientation,
        }

    # ------------------------------------------------------------------
    # Styles context
    # ------------------------------------------------------------------

    def _styles_context(self) -> dict[str, str]:
        """Build the full substitution context for styles.xml."""
        body = self.document.body_style
        typo = self.document.typography
        ct = self.document.color_theme

        heading_style = (
            self.document.heading_styles[0] if self.document.heading_styles else body
        )
        heading1_size = heading_style.font_size_pt

        # Row height: from baseline grid or 1.4x body size
        row_height_pt = round(body.font_size_pt * 1.4, 1)
        if (
            self.document.page_geometry is not None
            and self.document.page_geometry.baseline_grid_emu > 0
        ):
            row_height_pt = round(
                emu_to_pt(self.document.page_geometry.baseline_grid_emu), 1
            )
        # Default column width: 5× body font size
        col_width_pt = round(body.font_size_pt * 5, 1)

        # Currency symbol based on locale
        locale_upper = self.document.locale.upper()
        currency_symbol = _LOCALE_CURRENCY.get(locale_upper, "$")

        # Light accent tints (80 % toward white)
        accents = [
            ct.accent1,
            ct.accent2,
            ct.accent3,
            ct.accent4,
            ct.accent5,
            ct.accent6,
        ]
        light_accents = {
            f"ACCENT{i}_LIGHT": self._tint_toward_white(c)
            for i, c in enumerate(accents, 1)
        }

        ctx: dict[str, str] = {
            "BODY_FONT": body.font_family,
            "HEADING_FONT": typo.heading_font,
            "MONO_FONT": typo.monospace_font,
            "BODY_SIZE": f"{body.font_size_pt:.1f}",
            "HEADING_SIZE": f"{heading_style.font_size_pt:.1f}",
            "HEADING1_SIZE": f"{heading1_size:.1f}",
            "BODY_COLOR": body.color,
            "HEADING_COLOR": heading_style.color,
            "ACCENT1": ct.accent1,
            "ACCENT2": ct.accent2,
            "ACCENT3": ct.accent3,
            "ACCENT4": ct.accent4,
            "ACCENT5": ct.accent5,
            "ACCENT6": ct.accent6,
            "DK1": ct.dk1,
            "DK2": ct.dk2,
            "LT1": ct.lt1,
            "LT2": ct.lt2,
            "LINK_COLOR": self.document.hyperlink_style.link_color,
            # Semantic backgrounds: tint accent3 (green-ish), accent2 (red), accent4 (warm)
            "GOOD_BG": self._tint_toward_white(ct.accent3, self._SEMANTIC_TINT_FACTOR),
            "BAD_BG": self._tint_toward_white(ct.accent2, self._SEMANTIC_TINT_FACTOR),
            "NEUTRAL_BG": self._tint_toward_white(
                ct.accent4, self._SEMANTIC_TINT_FACTOR
            ),
            "ROW_HEIGHT": f"{row_height_pt:.2f}",
            "COL_WIDTH": f"{col_width_pt:.2f}",
            "CURRENCY_SYMBOL": currency_symbol,
            **light_accents,
        }
        ctx.update(self._page_dimensions_pt())
        return ctx

    # ------------------------------------------------------------------
    # Package builder
    # ------------------------------------------------------------------

    def build_package(self) -> bytes:
        """Build the Calc template package."""
        buffer = io.BytesIO()
        title_text = f"{self.document.org_id} Spreadsheet Template"
        body_text = (
            f"Body font: {self.document.body_style.font_family} "
            f"{self.document.body_style.font_size_pt:.1f}pt"
        )

        styles_context = self._styles_context()

        with zipfile.ZipFile(
            buffer, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
        ) as archive:
            archive.writestr(
                "mimetype", self.MIMETYPE, compress_type=zipfile.ZIP_STORED
            )
            archive.writestr(
                "content.xml",
                self._render(
                    "content.xml", context={"TITLE": title_text, "BODY": body_text}
                ),
            )
            archive.writestr(
                "styles.xml",
                self._render("styles.xml", context=styles_context),
            )
            # settings.xml (static, no substitutions needed)
            archive.writestr(
                "settings.xml",
                self._render("settings.xml"),
                compress_type=zipfile.ZIP_DEFLATED,
            )
            self._write_common_entries(archive, title=title_text)
        return buffer.getvalue()


class DrawTemplateEmitter(OdfBaseEmitter):
    """LibreOffice Draw template emitter (.otg).

    Generates ODF graphics templates with:
    - Drawing page styles
    - Graphic object styles
    - Text styles for shapes
    - OpenType features (ligatures, stylistic sets, etc.)

    Inherits from OdfBaseEmitter (→ BaseEmitter).
    """

    MIMETYPE = "application/vnd.oasis.opendocument.graphics-template"

    def __init__(
        self,
        document: DocumentIR,
        template_root: Path | str | None = None,
        use_pure_text_colors: bool = True,
        enable_opentype_features: bool = True,
    ) -> None:
        """Initialize Draw template emitter.

        Args:
            document: Document IR with style information
            template_root: Optional override for template resolution
            use_pure_text_colors: If True, use pure black/white for text
            enable_opentype_features: If True, enable OpenType features
        """
        super().__init__(
            "draw",
            document,
            template_root,
            use_pure_text_colors=use_pure_text_colors,
            enable_opentype_features=enable_opentype_features,
        )

    def build_package(self) -> bytes:
        """Build the Draw template package."""
        buffer = io.BytesIO()
        title_text = f"{self.document.org_id} Graphics Template"
        body_text = (
            f"Body font: {self.document.body_style.font_family} "
            f"{self.document.body_style.font_size_pt:.1f}pt with accent {self.document.color_theme.accent1}."
        )
        heading_style = (
            self.document.heading_styles[0]
            if self.document.heading_styles
            else self.document.body_style
        )

        # Compute page dimensions from page_geometry or presentation_layout
        if self.document.page_geometry is not None:
            pg = self.document.page_geometry
            page_width_pt = emu_to_pt(pg.width_emu)
            page_height_pt = emu_to_pt(pg.height_emu)
            margin_pt = emu_to_pt(pg.margins.left)
            orientation = "landscape" if pg.width_emu > pg.height_emu else "portrait"
        else:
            layout = self.document.presentation_layout
            page_width_pt = emu_to_pt(layout.slide_width)
            page_height_pt = emu_to_pt(layout.slide_height)
            margin_pt = 72.0  # 1 inch fallback
            orientation = (
                "landscape" if layout.slide_width > layout.slide_height else "portrait"
            )

        frame_width_pt = page_width_pt - 2 * margin_pt
        frame_height_pt = page_height_pt * 0.2  # 20% of page for text frame

        styles_context = {
            "BODY_FONT": self.document.body_style.font_family,
            "HEADING_FONT": self.document.typography.heading_font,
            "BODY_SIZE": f"{self.document.body_style.font_size_pt:.1f}",
            "HEADING_SIZE": f"{heading_style.font_size_pt:.1f}",
            "BODY_COLOR": self.document.body_style.color,
            "HEADING_COLOR": heading_style.color,
            "ACCENT1": self.document.color_theme.accent1,
            "ACCENT2": self.document.color_theme.accent2,
            "PAGE_WIDTH": f"{page_width_pt:.2f}",
            "PAGE_HEIGHT": f"{page_height_pt:.2f}",
            "MARGIN": f"{margin_pt:.2f}",
            "ORIENTATION": orientation,
        }

        content_context = {
            "TITLE": title_text,
            "BODY": body_text,
            "WIDTH": f"{frame_width_pt:.2f}",
            "HEIGHT": f"{frame_height_pt:.2f}",
            "MARGIN": f"{margin_pt:.2f}",
        }

        with zipfile.ZipFile(
            buffer, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
        ) as archive:
            archive.writestr(
                "mimetype", self.MIMETYPE, compress_type=zipfile.ZIP_STORED
            )
            archive.writestr(
                "content.xml",
                self._render("content.xml", context=content_context),
            )
            archive.writestr(
                "styles.xml",
                self._render("styles.xml", context=styles_context),
            )
            self._write_common_entries(archive, title=title_text)
        return buffer.getvalue()
