"""Template cloning, placeholder replacement, and XML serialization helpers."""

from __future__ import annotations

import copy
import re
from typing import Dict

from lxml import etree

_PLACEHOLDER_RE = re.compile(r"\$\{([^}]+)\}")


def replace_placeholders(
    root: etree._Element,
    context: Dict[str, str],
) -> int:
    """Replace ${PLACEHOLDER} patterns in element text and attributes.

    Walks the entire XML tree and replaces all ${...} patterns with values
    from the context dictionary. This is a DOM-based alternative to string
    substitution that benefits from cached parsed trees.

    Args:
        root: Root element to process (modified in place)
        context: Dict mapping placeholder names to values

    Returns:
        Number of replacements made

    Example:
        >>> context = {'FONT_NAME': 'Inter', 'FONT_SIZE': '12'}
        >>> count = replace_placeholders(root, context)
        >>> # All ${FONT_NAME} and ${FONT_SIZE} patterns replaced
    """
    count = 0

    def replace_in_string(s: str | None) -> tuple[str | None, int]:
        if s is None:
            return None, 0
        matches = _PLACEHOLDER_RE.findall(s)
        if not matches:
            return s, 0
        replaced = 0
        for name in matches:
            if name in context:
                s = s.replace(f"${{{name}}}", context[name])
                replaced += 1
        return s, replaced

    # Walk all elements
    for elem in root.iter():
        # Process element text
        new_text, n = replace_in_string(elem.text)
        if n > 0:
            elem.text = new_text
            count += n

        # Process tail text
        new_tail, n = replace_in_string(elem.tail)
        if n > 0:
            elem.tail = new_tail
            count += n

        # Process attributes
        for attr_name in list(elem.attrib):
            attr_value = elem.get(attr_name)
            new_value, n = replace_in_string(attr_value)
            if n > 0 and new_value is not None:
                elem.set(attr_name, new_value)
                count += n

    return count


# =============================================================================
# TEMPLATE ELEMENT CLONING (SSOT pattern)
# =============================================================================


def clone_template_element(
    root: etree._Element,
    template_name: str,
    *,
    remove_marker: bool = True,
    namespaces: Dict[str, str] | None = None,
) -> etree._Element:
    """Clone an element marked with data-template attribute.

    This supports the SSOT (Single Source of Truth) pattern where template
    files contain example elements with `data-template="name"` markers.
    Code clones these elements for dynamic content instead of loading
    separate fragment files.

    Args:
        root: Root element to search within
        template_name: Value of data-template attribute to find
        remove_marker: If True, removes data-template attr from the clone
        namespaces: Optional namespace map for XPath

    Returns:
        Deep copy of the template element with marker optionally removed

    Raises:
        ValueError: If no element with matching data-template is found

    Example:
        >>> # Template contains: <row data-template="row" r="${ROW_NUM}">...</row>
        >>> row_template = clone_template_element(sheet_data, 'row')
        >>> replace_placeholders(row_template, {'ROW_NUM': '1'})
        >>> sheet_data.append(row_template)
    """
    # XPath to find element with data-template attribute
    # Note: data-template is not namespaced
    xpath = f'.//*[@data-template="{template_name}"]'

    if namespaces:
        results: list[etree._Element] = root.xpath(xpath, namespaces=namespaces)  # type: ignore[assignment]
        element: etree._Element | None = results[0] if results else None
    else:
        element = root.find(xpath)

    if element is None:
        raise ValueError(
            f'No element with data-template="{template_name}" found in template'
        )

    # Deep copy so original template stays intact
    clone: etree._Element = copy.deepcopy(element)

    # Remove the marker attribute from the clone
    if remove_marker and "data-template" in clone.attrib:
        del clone.attrib["data-template"]

    return clone


def remove_template_elements(
    root: etree._Element,
    namespaces: Dict[str, str] | None = None,
) -> int:
    """Remove all elements marked with data-template attribute.

    After cloning template elements and building the dynamic content,
    call this to remove the original template examples from the final output.

    Args:
        root: Root element to process
        namespaces: Optional namespace map

    Returns:
        Number of template elements removed

    Example:
        >>> # After building all dynamic rows from template
        >>> remove_template_elements(sheet_data)
        >>> # Original <row data-template="row">...</row> is now gone
    """
    xpath = ".//*[@data-template]"

    if namespaces:
        found: list[etree._Element] = root.xpath(xpath, namespaces=namespaces)  # type: ignore[assignment]
        elements: list[etree._Element] = found
    else:
        elements = root.findall(xpath)

    count = 0
    for element in elements:
        parent = element.getparent()
        if parent is not None:
            parent.remove(element)
            count += 1

    return count


# =============================================================================
# SERIALIZATION
# =============================================================================


def serialize_tree(
    root: etree._Element,
    xml_declaration: bool = True,
    encoding: str = "UTF-8",
    strip_comments: bool = True,
) -> bytes:
    """Serialize an lxml tree to bytes.

    Args:
        root: Root element to serialize
        xml_declaration: Include XML declaration
        encoding: Output encoding
        strip_comments: Remove XML comments (default True for Office compatibility)

    Returns:
        Serialized XML bytes
    """
    if strip_comments:
        # Remove XML comments - PowerPoint doesn't handle them well
        comments: list[etree._Comment] = root.xpath("//comment()")  # type: ignore[assignment]
        for comment in comments:
            parent = comment.getparent()
            if parent is not None:
                parent.remove(comment)
    result = etree.tostring(root, xml_declaration=xml_declaration, encoding=encoding)
    assert isinstance(result, bytes)  # encoding='UTF-8' always produces bytes
    return result


def parse_xml(content: str | bytes) -> etree._Element:
    """Parse XML content into an lxml element.

    Args:
        content: XML string or bytes

    Returns:
        Parsed lxml element
    """
    if isinstance(content, str):
        content = content.encode("utf-8")
    return etree.fromstring(content)


# =============================================================================
# SCHEME NAME HELPERS
# =============================================================================
