"""Macro parser for template content.

Parses @macro(query) syntax in template content and provides compilation
to transform macros into valid Jinja2 syntax.

Supported macros:
- @fragment(query) -> {% include 'fragment_name' %}
- @text(query) -> {{ text('uuid_or_external_id') }}
- @json(query) -> {{ json('uuid_or_external_id') }}
- @schema(query) -> {{ schema('name_or_uuid') }}
"""

import re
from dataclasses import dataclass

# Regex to match @type(query) macros
# Group 1: macro type (e.g., "fragment", "text", "json", "schema")
# Group 2: query string (e.g., "header", "article summary")
MACRO_PATTERN = re.compile(r"@(\w+)\((.*?)\)")

# Regex patterns for Jinja2 syntax detection in rendered output
# These match Jinja2 constructs that survived rendering (e.g., from {% raw %} blocks)
JINJA_EXPRESSION_PATTERN = re.compile(r"\{\{.*?\}\}", re.DOTALL)  # {{ ... }}
JINJA_STATEMENT_PATTERN = re.compile(r"\{%.*?%\}", re.DOTALL)  # {% ... %}
JINJA_COMMENT_PATTERN = re.compile(r"\{#.*?#\}", re.DOTALL)  # {# ... #}

# Valid macro types that we support
VALID_MACRO_TYPES = frozenset({"fragment", "text", "json", "schema"})


@dataclass
class MacroMatch:
    """Represents a parsed macro from template content."""

    macro_type: str
    query: str
    original_text: str
    start: int
    end: int

    def __repr__(self) -> str:
        return f"MacroMatch({self.macro_type}, {self.query!r})"


def parse_macros(content: str) -> list[MacroMatch]:
    """Parse all valid macros from template content.

    Args:
        content: Template content potentially containing macros

    Returns:
        List of MacroMatch objects for valid macros, in order of appearance
    """
    matches: list[MacroMatch] = []
    for match in MACRO_PATTERN.finditer(content):
        macro_type = match.group(1)
        query = match.group(2).strip()

        # Only include valid macro types
        if macro_type in VALID_MACRO_TYPES:
            matches.append(
                MacroMatch(
                    macro_type=macro_type,
                    query=query,
                    original_text=match.group(0),
                    start=match.start(),
                    end=match.end(),
                )
            )

    return matches


def deduplicate_macros(macros: list[MacroMatch]) -> list[MacroMatch]:
    """Deduplicate macros by (macro_type, query) keeping first occurrence.

    When the same macro (same type and query) appears multiple times in content,
    this returns only the first occurrence. Useful for resolution flows where
    the user should only need to resolve each unique macro once.

    Args:
        macros: List of MacroMatch objects (from parse_macros)

    Returns:
        List of unique MacroMatch objects, preserving order of first appearance
    """
    seen: set[tuple[str, str]] = set()
    unique: list[MacroMatch] = []
    for macro in macros:
        key = (macro.macro_type, macro.query)
        if key not in seen:
            seen.add(key)
            unique.append(macro)
    return unique


def compile_content(content: str, replacements: dict[str, str]) -> str:
    """Apply macro replacements to template content.

    Args:
        content: Original template content with macros
        replacements: Mapping of original macro text to replacement Jinja syntax

    Returns:
        Compiled content with macros replaced
    """
    result = content
    for original, replacement in replacements.items():
        result = result.replace(original, replacement)
    return result


def build_jinja_replacement(macro_type: str, resolved_value: str) -> str:
    """Build the Jinja2 replacement syntax for a resolved macro.

    Args:
        macro_type: Type of macro (fragment, text, json, schema)
        resolved_value: The resolved value (fragment name, content part UUID/external_id, or schema name/UUID)

    Returns:
        Valid Jinja2 syntax string

    Note:
        - fragment uses the template NAME (not UUID) because the renderer resolves by name
        - text/json use UUIDs or external_ids to reference content parts
        - schema uses the schema NAME (or UUID) because the renderer supports both
    """
    if macro_type == "fragment":
        # Fragment uses name, not UUID
        return f"{{% include '{resolved_value}' %}}"
    elif macro_type == "text":
        return f"{{{{ text('{resolved_value}') }}}}"
    elif macro_type == "json":
        return f"{{{{ json('{resolved_value}') }}}}"
    elif macro_type == "schema":
        return f"{{{{ schema('{resolved_value}') }}}}"
    else:
        raise ValueError(f"Unknown macro type: {macro_type}")


@dataclass
class JinjaSyntaxMatch:
    """Represents a Jinja2 syntax match found in rendered content."""

    syntax_type: str  # "expression", "statement", or "comment"
    content: str  # The full matched text including delimiters
    start: int
    end: int

    def __repr__(self) -> str:
        return f"JinjaSyntaxMatch({self.syntax_type}, {self.content!r})"


def extract_jinja_syntax(content: str) -> list[JinjaSyntaxMatch]:
    """Extract all Jinja2 syntax from rendered content.

    Finds Jinja2 expressions ({{ }}), statements ({% %}), and comments ({# #})
    that remain in the rendered output. These typically come from {% raw %} blocks
    or escaped content that should be preserved for later use.

    Args:
        content: Rendered template content

    Returns:
        List of JinjaSyntaxMatch objects in order of appearance, deduplicated
    """
    matches: list[JinjaSyntaxMatch] = []
    seen_content: set[str] = set()

    # Find expressions {{ ... }}
    for match in JINJA_EXPRESSION_PATTERN.finditer(content):
        text = match.group(0)
        if text not in seen_content:
            seen_content.add(text)
            matches.append(
                JinjaSyntaxMatch(
                    syntax_type="expression",
                    content=text,
                    start=match.start(),
                    end=match.end(),
                )
            )

    # Find statements {% ... %}
    for match in JINJA_STATEMENT_PATTERN.finditer(content):
        text = match.group(0)
        if text not in seen_content:
            seen_content.add(text)
            matches.append(
                JinjaSyntaxMatch(
                    syntax_type="statement",
                    content=text,
                    start=match.start(),
                    end=match.end(),
                )
            )

    # Find comments {# ... #}
    for match in JINJA_COMMENT_PATTERN.finditer(content):
        text = match.group(0)
        if text not in seen_content:
            seen_content.add(text)
            matches.append(
                JinjaSyntaxMatch(
                    syntax_type="comment",
                    content=text,
                    start=match.start(),
                    end=match.end(),
                )
            )

    # Sort by position in content
    matches.sort(key=lambda m: m.start)
    return matches
