"""Word emitter mixin for settings, document layout, and numbering customization."""

from __future__ import annotations

from typing import TYPE_CHECKING, Dict, List

from lxml import etree

from tokenmoulds.design.layout_defaults import (
    word_fallback_header_footer_twips,
    word_fallback_margin_twips,
)
from tokenmoulds.design.page_geometry import PageGeometry
from tokenmoulds.design.units import TWIPS_PER_INCH, emu_to_twips
from tokenmoulds.emitters.ooxml_base import EmbeddedFont
from tokenmoulds.emitters.word_shared import NSMAP, W
from tokenmoulds.emitters.xml_helpers import (
    clone_template_element,
    remove_template_elements,
    replace_placeholders,
)
from tokenmoulds.ir import (
    DocumentGrid,
    DocumentIR,
    DocumentSettings,
    SectionColumns,
    SectionSettings,
)

if TYPE_CHECKING:
    from tokenmoulds.emitters import TemplateResolver


class WordSectionsMixin:
    # Declare attributes provided by the host class (OOXMLBaseEmitter / WordDocumentEmitter)
    document: DocumentIR
    allow_opentype_features: bool
    templates: TemplateResolver
    embed_fonts: bool
    embedded_fonts: list[EmbeddedFont]

    def _customize_settings(self, data: bytes) -> bytes:
        """Customize settings.xml with document-wide typography settings.

        SSOT: The template already contains all settings elements. This method
        only modifies attribute values.
        """
        root = etree.fromstring(data)
        settings = self.document.document_settings

        self._apply_hyphenation_settings_ssot(root, settings)
        self._apply_tab_settings_ssot(root, settings)
        self._apply_compatibility_settings_ssot(root, settings)

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

    def _apply_hyphenation_settings_ssot(
        self, root: etree._Element, settings: DocumentSettings
    ) -> None:
        """Apply hyphenation settings to settings.xml.

        SSOT: The template already has hyphenation elements. Only modify values.
        """
        # Auto hyphenation - template has <w:autoHyphenation w:val="1"/>
        auto_hyph = root.find("w:autoHyphenation", NSMAP)
        if auto_hyph is not None:
            auto_hyph.set(f"{W}val", "1" if settings.auto_hyphenation else "0")

        # Hyphenation zone - template has <w:hyphenationZone w:val="360"/>
        hyph_zone = root.find("w:hyphenationZone", NSMAP)
        if hyph_zone is not None:
            hyph_zone.set(f"{W}val", str(settings.hyphenation_zone_twips))

        # Consecutive hyphen limit - template has <w:consecutiveHyphenLimit w:val="0"/>
        consec = root.find("w:consecutiveHyphenLimit", NSMAP)
        if consec is not None:
            consec.set(f"{W}val", str(settings.consecutive_hyphen_limit))

        # Don't hyphenate caps - template has <w:doNotHyphenateCaps w:val="1"/>
        no_caps = root.find("w:doNotHyphenateCaps", NSMAP)
        if no_caps is not None:
            no_caps.set(f"{W}val", "1" if not settings.hyphenate_caps else "0")

    def _apply_tab_settings_ssot(
        self, root: etree._Element, settings: DocumentSettings
    ) -> None:
        """Apply default tab stop setting.

        SSOT: The template already has <w:defaultTabStop w:val="540"/>.
        """
        tab_stop = root.find("w:defaultTabStop", NSMAP)
        if tab_stop is not None:
            tab_stop.set(f"{W}val", str(settings.default_tab_stop_twips))

    def _apply_compatibility_settings_ssot(
        self, root: etree._Element, settings: DocumentSettings
    ) -> None:
        """Apply compatibility settings.

        SSOT: The template already has compat/compatSetting elements.
        """
        compat = root.find("w:compat", NSMAP)
        if compat is None:
            return

        # Find and update the enableOpenTypeFeatures setting
        for cs in compat.findall("w:compatSetting", NSMAP):
            if cs.get(f"{W}name") == "enableOpenTypeFeatures":
                # Update existing element's value
                if self.allow_opentype_features and settings.enable_opentype_features:
                    cs.set(f"{W}val", "1")
                else:
                    cs.set(f"{W}val", "0")
                break

    # =========================================================================
    # DOCUMENT.XML - Content and Layout
    # =========================================================================

    def _customize_document(self, data: bytes) -> bytes:
        """Customize document.xml with sample content and layout."""
        root = etree.fromstring(data)

        # Add sample content to demonstrate the template
        self._add_sample_content(root)

        # Apply section properties (margins, columns, grid)
        self._apply_section_properties(root)

        # Strip any remaining data-template markers from the output
        for el in root.iter():
            if "data-template" in el.attrib:
                del el.attrib["data-template"]

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

    def _add_sample_content(self, root: etree._Element) -> None:
        """Add sample content to document body for template preview.

        Uses SSOT clone pattern: loads document.xml template containing
        example paragraph elements with data-template markers.
        """
        body = root.find(".//w:body", NSMAP)
        if body is None:
            return

        # Load SSOT template for cloning paragraph elements
        ssot_template = self.templates.read_tree("document.xml")

        # Remove existing paragraphs but preserve sectPr
        sect_pr = body.find("w:sectPr", NSMAP)
        for p in body.findall("w:p", NSMAP):
            body.remove(p)

        # Add heading from SSOT template
        heading_el = clone_template_element(ssot_template, "paragraph_heading")
        replace_placeholders(
            heading_el,
            {
                "STYLE_ID": "Heading1",
                "TEXT": f"{self.document.org_id} Template",
                "OUTLINE_LEVEL": "0",
                "BOOKMARK_ID": "1",
            },
        )
        # Remove template child elements we don't need
        remove_template_elements(heading_el)
        body.insert(0, heading_el)

        # Add body paragraph from SSOT template
        body_el = clone_template_element(ssot_template, "paragraph_body")
        replace_placeholders(
            body_el,
            {
                "STYLE_ID": "BodyText",
                "TEXT": "TokenMoulds Document IR powers this body copy.",
            },
        )
        remove_template_elements(body_el)
        body.insert(1, body_el)

        # Ensure sectPr is at the end
        if sect_pr is not None:
            body.remove(sect_pr)
            body.append(sect_pr)

    def _apply_section_properties(self, root: etree._Element) -> None:
        """Apply section properties (page size, margins, columns, grid).

        When ``DocumentIR.sections`` is non-empty, intermediate sections are
        inserted as section-break paragraphs before the body's final
        ``w:sectPr``.  The last entry in ``sections`` drives the final
        section; preceding entries become ``w:sectPr`` inside ``w:pPr``.

        When ``sections`` is empty the top-level ``page_geometry``,
        ``section_columns``, and ``document_grid`` are applied to the
        single final section as before.
        """
        body = root.find(".//w:body", NSMAP)
        if body is None:
            return
        final_sect_pr = body.find("w:sectPr", NSMAP)
        if final_sect_pr is None:
            return

        sections = self.document.sections

        if sections:
            # Multi-section: insert intermediate section breaks
            ssot_template = self.templates.read_tree("document.xml")

            # All sections except the last produce a section-break paragraph
            for section in sections[:-1]:
                self._insert_section_break(body, final_sect_pr, ssot_template, section)

            # Last section drives the final w:sectPr
            last = sections[-1]
            self._apply_section_settings_to_sect_pr(
                final_sect_pr,
                page_geometry=last.page_geometry or self.document.page_geometry,
                columns=last.columns or self.document.section_columns,
                grid=last.grid or self.document.document_grid,
            )
        else:
            # Single section — use top-level settings
            self._apply_section_settings_to_sect_pr(
                final_sect_pr,
                page_geometry=self.document.page_geometry,
                columns=self.document.section_columns,
                grid=self.document.document_grid,
            )

    def _apply_section_settings_to_sect_pr(
        self,
        sect_pr: etree._Element,
        *,
        page_geometry: object | None,
        columns: SectionColumns,
        grid: DocumentGrid,
    ) -> None:
        """Apply page size, margins, columns, and grid to a w:sectPr."""
        geom = page_geometry if isinstance(page_geometry, PageGeometry) else None
        self._apply_page_size(sect_pr, geom)
        self._apply_page_margins(sect_pr, geom)

        if not columns.is_single_column:
            self._apply_section_columns(sect_pr, columns)

        if not grid.is_default:
            self._apply_document_grid(sect_pr, grid)

    def _insert_section_break(
        self,
        body: etree._Element,
        final_sect_pr: etree._Element,
        ssot_template: etree._Element,
        section: SectionSettings,
    ) -> None:
        """Insert a section-break paragraph before the final sectPr."""
        geom = section.page_geometry or self.document.page_geometry

        if isinstance(geom, PageGeometry):
            page_w = str(emu_to_twips(geom.width_emu))
            page_h = str(emu_to_twips(geom.height_emu))
            orient = "landscape" if geom.is_landscape else "portrait"
            m_top = str(emu_to_twips(geom.margins.top))
            m_bot = str(emu_to_twips(geom.margins.bottom))
            m_left = str(emu_to_twips(geom.margins.left))
            m_right = str(emu_to_twips(geom.margins.right))
            if geom.baseline_grid_emu > 0:
                hf = str(emu_to_twips(geom.baseline_grid_emu * 2))
            else:
                hf = str(emu_to_twips(geom.margins.top) // 3)
        else:
            # Fallback: Letter portrait with grid-derived margins
            page_w = str(int(8.5 * TWIPS_PER_INCH))  # 12240
            page_h = str(int(11.0 * TWIPS_PER_INCH))  # 15840
            orient = "portrait"
            m_val = str(word_fallback_margin_twips())
            m_top = m_bot = m_left = m_right = m_val
            hf = str(word_fallback_header_footer_twips())

        break_para = clone_template_element(ssot_template, "section_break")
        replace_placeholders(
            break_para,
            {
                "SECTION_TYPE": section.break_type,
                "PAGE_WIDTH": page_w,
                "PAGE_HEIGHT": page_h,
                "ORIENTATION": orient,
                "MARGIN_TOP": m_top,
                "MARGIN_RIGHT": m_right,
                "MARGIN_BOTTOM": m_bot,
                "MARGIN_LEFT": m_left,
                "HEADER_DIST": hf,
                "FOOTER_DIST": hf,
                "GUTTER": "0",
            },
        )

        # Insert before the final sectPr
        body.insert(list(body).index(final_sect_pr), break_para)

    def _apply_page_size(
        self,
        sect_pr: etree._Element,
        geom: object | None = None,
    ) -> None:
        """Apply page size and orientation from page geometry.

        Sets ``w:pgSz`` width, height, and orientation.  When no geometry
        is available the template defaults (Letter portrait) are kept.
        """
        if not isinstance(geom, PageGeometry):
            return

        pg_sz = sect_pr.find("w:pgSz", NSMAP)
        if pg_sz is None:
            return

        pg_sz.set(f"{W}w", str(emu_to_twips(geom.width_emu)))
        pg_sz.set(f"{W}h", str(emu_to_twips(geom.height_emu)))
        pg_sz.set(f"{W}orient", "landscape" if geom.is_landscape else "portrait")

    def _apply_page_margins(
        self,
        sect_pr: etree._Element,
        geom: object | None = None,
    ) -> None:
        """Apply page margins from page geometry.

        Uses grid-computed margins from ``PageGeometry`` when available,
        with a 2-grid-line header/footer distance.  Falls back to
        baseline × 4 margins when no geometry is provided.
        """
        pg_mar = sect_pr.find("w:pgMar", NSMAP)
        if pg_mar is None:
            return

        if isinstance(geom, PageGeometry):
            top = emu_to_twips(geom.margins.top)
            bottom = emu_to_twips(geom.margins.bottom)
            left = emu_to_twips(geom.margins.left)
            right = emu_to_twips(geom.margins.right)
            # Header/footer distance: 2 baseline grid lines, or 1/3 margin
            if geom.baseline_grid_emu > 0:
                hf = emu_to_twips(geom.baseline_grid_emu * 2)
            else:
                hf = top // 3
        else:
            # Fallback: grid-derived professional margins
            top = bottom = left = right = word_fallback_margin_twips()
            hf = word_fallback_header_footer_twips()

        pg_mar.set(f"{W}top", str(top))
        pg_mar.set(f"{W}bottom", str(bottom))
        pg_mar.set(f"{W}left", str(left))
        pg_mar.set(f"{W}right", str(right))
        pg_mar.set(f"{W}header", str(hf))
        pg_mar.set(f"{W}footer", str(hf))

    def _apply_section_columns(
        self, sect_pr: etree._Element, columns: SectionColumns
    ) -> None:
        """Apply column layout to section."""
        cols = sect_pr.find("w:cols", NSMAP)
        if cols is None:
            cols = etree.SubElement(sect_pr, f"{W}cols")

        cols.set(f"{W}num", str(columns.num_columns))
        cols.set(f"{W}space", str(columns.space_twips))

        if columns.equal_width:
            cols.set(f"{W}equalWidth", "1")
        else:
            cols.set(f"{W}equalWidth", "0")
            # Add individual column definitions
            for col_def in columns.columns:
                col = etree.SubElement(cols, f"{W}col")
                col.set(f"{W}w", str(col_def.width_twips))
                if col_def.space_twips > 0:
                    col.set(f"{W}space", str(col_def.space_twips))

        if columns.separator:
            cols.set(f"{W}sep", "1")

    def _apply_document_grid(self, sect_pr: etree._Element, grid: DocumentGrid) -> None:
        """Apply document grid settings for baseline alignment."""
        doc_grid = sect_pr.find("w:docGrid", NSMAP)
        if doc_grid is None:
            doc_grid = etree.SubElement(sect_pr, f"{W}docGrid")

        doc_grid.set(f"{W}type", grid.grid_type)
        doc_grid.set(f"{W}linePitch", str(grid.line_pitch_twips))

        if grid.char_space_twips > 0:
            doc_grid.set(f"{W}charSpace", str(grid.char_space_twips))

    # =========================================================================
    # WORD-SPECIFIC CUSTOMIZATIONS
    # =========================================================================

    def _customize_content_types(self, data: bytes) -> bytes:
        """Customize [Content_Types].xml."""
        # Change to template content type
        data = data.replace(
            b"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
            b"application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml",
        )

        # Add odttf content type if embedding fonts
        if self.embed_fonts and self.embedded_fonts:
            root = etree.fromstring(data)
            ns = "http://schemas.openxmlformats.org/package/2006/content-types"

            # Check if odttf extension already defined
            existing = root.find(f'.//{{{ns}}}Default[@Extension="odttf"]')
            if existing is None:
                # Add Default element for odttf
                default = etree.Element(f"{{{ns}}}Default")
                default.set("Extension", "odttf")
                default.set(
                    "ContentType",
                    "application/vnd.openxmlformats-officedocument.obfuscatedFont",
                )
                root.insert(0, default)

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

        return data

    def _customize_font_table(self, data: bytes) -> bytes:
        """Customize fontTable.xml with embedded font references.

        Uses SSOT clone pattern: loads fontTable.xml template containing
        example elements with data-template markers, clones them for each
        embedded font.
        """
        if not self.embedded_fonts:
            return data

        root = etree.fromstring(data)

        # Load SSOT template for cloning font entries and embed elements
        ssot_template = self.templates.read_tree("fontTable.xml")

        # Group embedded fonts by family
        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)

        for family_name, fonts in fonts_by_family.items():
            # Find or create font element
            font_el = root.find(f'.//w:font[@w:name="{family_name}"]', NSMAP)
            if font_el is None:
                # Clone font entry from SSOT template
                font_el = clone_template_element(ssot_template, "font_entry")
                replace_placeholders(
                    font_el,
                    {
                        "FONT_NAME": family_name,
                        "FAMILY": "swiss",
                        "PITCH": "variable",
                        "CHARSET": "00",
                        "PANOSE": "00000000000000000000",
                        "USB0": "00000000",
                        "USB1": "00000000",
                        "USB2": "00000000",
                        "USB3": "00000000",
                        "CSB0": "00000000",
                        "CSB1": "00000000",
                    },
                )
                # Remove template embed elements (we'll add the ones we need)
                for child in list(font_el):
                    if child.get("data-template") in (
                        "embed_regular",
                        "embed_bold",
                        "embed_italic",
                        "embed_bold_italic",
                    ):
                        font_el.remove(child)
                # Also remove other template placeholder children
                for child in list(font_el):
                    if "data-template" in child.attrib:
                        font_el.remove(child)
                root.append(font_el)

            # Add embedded font references by cloning from SSOT template
            for font in fonts:
                if font.is_bold and font.is_italic:
                    embed_el = clone_template_element(
                        ssot_template, "embed_bold_italic"
                    )
                elif font.is_bold:
                    embed_el = clone_template_element(ssot_template, "embed_bold")
                elif font.is_italic:
                    embed_el = clone_template_element(ssot_template, "embed_italic")
                else:
                    embed_el = clone_template_element(ssot_template, "embed_regular")

                replace_placeholders(
                    embed_el,
                    {
                        "REL_ID": font.relationship_id,
                        "FONT_KEY": "{" + font.font_key + "}",
                        "SUBSETTED": "0",
                    },
                )
                font_el.append(embed_el)

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

    def _build_font_relationships(self) -> bytes:
        """Build word/_rels/fontTable.xml.rels file.

        Uses SSOT clone pattern: loads font_table_rels.xml template with
        data-template markers, clones relationship entries for each font.
        """
        # Load SSOT template
        root = self.templates.read_tree("font_table_rels.xml")

        # Clone relationship for each embedded font
        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.odttf_filename,
                },
            )
            root.append(rel_clone)

        # Remove template elements
        remove_template_elements(root)

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

    # =========================================================================
    # NUMBERING.XML - List Styles
    # =========================================================================

    def _customize_numbering(self, data: bytes) -> bytes:
        """Customize numbering.xml with typographically correct list styles.

        Implements best practices:
        - Hanging indent: bullet/number hangs, text aligns cleanly
        - Bullet character: small bullet (•), not heavy circle
        - Number alignment: right-align for proper digit alignment
        - Nested bullets: differentiated by character (•, –, ·)
        - Spacing: tighter between items than paragraphs
        """
        root = etree.fromstring(data)
        list_style = self.document.list_style

        if list_style is None:
            return data

        # Find or create abstract numbering definitions
        # abstractNumId 0 = bullet list, abstractNumId 1 = numbered list

        self._customize_bullet_list(root, list_style)
        self._customize_numbered_list(root, list_style)

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

    def _customize_bullet_list(self, root: etree._Element, list_style) -> None:
        """Customize bullet list definition (abstractNumId typically 1)."""
        # Find bullet list abstract num (look for one with bullet format)
        for abstract_num in root.findall("w:abstractNum", NSMAP):
            # Check first level for bullet
            lvl = abstract_num.find('w:lvl[@w:ilvl="0"]', NSMAP)
            if lvl is not None:
                num_fmt = lvl.find("w:numFmt", NSMAP)
                if num_fmt is not None and num_fmt.get(f"{W}val") == "bullet":
                    self._apply_bullet_levels(abstract_num, list_style)
                    break

    def _customize_numbered_list(self, root: etree._Element, list_style) -> None:
        """Customize numbered list definition."""
        for abstract_num in root.findall("w:abstractNum", NSMAP):
            lvl = abstract_num.find('w:lvl[@w:ilvl="0"]', NSMAP)
            if lvl is not None:
                num_fmt = lvl.find("w:numFmt", NSMAP)
                if num_fmt is not None and num_fmt.get(f"{W}val") == "decimal":
                    self._apply_numbered_levels(abstract_num, list_style)
                    break

    def _apply_bullet_levels(self, abstract_num: etree._Element, list_style) -> None:
        """Apply bullet list styling to all levels."""
        bullet_chars = (
            list_style.nested_bullet_chars
            if hasattr(list_style, "nested_bullet_chars")
            else ("•", "–", "·")
        )

        for lvl in abstract_num.findall("w:lvl", NSMAP):
            ilvl = int(lvl.get(f"{W}ilvl", "0"))

            # Set bullet character based on level
            bullet_char = bullet_chars[min(ilvl, len(bullet_chars) - 1)]
            lvl_text = lvl.find("w:lvlText", NSMAP)
            if lvl_text is not None:
                lvl_text.set(f"{W}val", bullet_char)

            # Set bullet font
            rpr = lvl.find("w:rPr", NSMAP)
            if rpr is None:
                rpr = etree.SubElement(lvl, f"{W}rPr")
            rfonts = rpr.find("w:rFonts", NSMAP)
            if rfonts is None:
                rfonts = etree.SubElement(rpr, f"{W}rFonts")

            # Use Symbol font for bullet, or body font for dashes/dots
            bullet_font = (
                list_style.bullet_font
                if hasattr(list_style, "bullet_font")
                else "Symbol"
            )
            if bullet_char in ("–", "·", "-"):
                bullet_font = self.document.body_style.font_family
            rfonts.set(f"{W}ascii", bullet_font)
            rfonts.set(f"{W}hAnsi", bullet_font)

            # Apply indentation
            self._apply_list_indent(lvl, list_style, ilvl)

    def _apply_numbered_levels(self, abstract_num: etree._Element, list_style) -> None:
        """Apply numbered list styling to all levels."""
        for lvl in abstract_num.findall("w:lvl", NSMAP):
            ilvl = int(lvl.get(f"{W}ilvl", "0"))

            # Right-align numbers for proper digit alignment
            lvl_jc = lvl.find("w:lvlJc", NSMAP)
            if lvl_jc is None:
                lvl_jc = etree.SubElement(lvl, f"{W}lvlJc")

            number_alignment = (
                list_style.number_alignment
                if hasattr(list_style, "number_alignment")
                else "right"
            )
            lvl_jc.set(f"{W}val", number_alignment)

            # Use tabular figures if available (via run properties)
            rpr = lvl.find("w:rPr", NSMAP)
            if rpr is None:
                rpr = etree.SubElement(lvl, f"{W}rPr")

            # Set font to body font for consistent appearance
            rfonts = rpr.find("w:rFonts", NSMAP)
            if rfonts is None:
                rfonts = etree.SubElement(rpr, f"{W}rFonts")
            rfonts.set(f"{W}ascii", self.document.body_style.font_family)
            rfonts.set(f"{W}hAnsi", self.document.body_style.font_family)

            # Apply indentation
            self._apply_list_indent(lvl, list_style, ilvl)

    def _apply_list_indent(self, lvl: etree._Element, list_style, ilvl: int) -> None:
        """Apply proper indentation for list level."""
        ppr = lvl.find("w:pPr", NSMAP)
        if ppr is None:
            ppr = etree.SubElement(lvl, f"{W}pPr")

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

        # Calculate indent based on level
        base_indent = list_style.indent_twips
        indent_per_level = (
            list_style.effective_indent_per_level
            if hasattr(list_style, "effective_indent_per_level")
            else base_indent
        )
        hanging = list_style.hanging_twips

        # Left indent increases with each level
        left_indent = base_indent + (ilvl * indent_per_level)

        ind.set(f"{W}left", str(left_indent))
        ind.set(f"{W}hanging", str(hanging))

        # Apply spacing between items
        if (
            hasattr(list_style, "space_between_items_twips")
            and list_style.space_between_items_twips > 0
        ):
            spacing = ppr.find("w:spacing", NSMAP)
            if spacing is None:
                spacing = etree.SubElement(ppr, f"{W}spacing")
            # Contextual spacing with reduced after-space
            spacing.set(f"{W}after", str(list_style.space_between_items_twips))
