"""
Universal DSL to Qdrant Query Parser

This module provides a robust and durable process for parsing DSL notation
and converting it to appropriate Qdrant queries. It serves as the canonical
implementation for DSL parsing across the codebase.

The DSL (Domain Specific Language) uses Unicode symbols to compactly represent
component structures:
    "§[δ×3, Ƀ[ᵬ×2]]" → Section with 3 DecoratedTexts and ButtonList with 2 Buttons

This parser handles TWO types of DSL:

1. **Structure DSL** - Component hierarchy and relationships:
   - "§[δ×3, Ƀ[ᵬ×2]]" → Section with 3 DecoratedTexts and ButtonList with 2 Buttons

2. **Content DSL** - Component content with styling modifiers:
   - "δ 'Status: Online' success bold" → DecoratedText with green bold text
   - "ᵬ Click Here https://example.com" → Button with URL action

Content DSL Features:
- Symbol prefix indicates component type (δ, ᵬ, §, etc.)
- Quoted or unquoted text content
- Style modifiers (yellow, bold, success, error, italic, etc.)
- URL detection for button/link actions
- Continuation lines (indented) for multi-line content

Usage:
    from adapters.module_wrapper.dsl_parser import DSLParser, parse_dsl_to_qdrant_query

    # With ModuleWrapper
    parser = DSLParser(wrapper)
    result = parser.parse("§[δ×3, Ƀ[ᵬ×2]]")

    # Direct query generation
    queries = parser.to_qdrant_queries()

    # Standalone function
    query = parse_dsl_to_qdrant_query("§[δ×3]", symbol_mapping)

    # Content DSL parsing
    content = parser.parse_content_dsl("δ 'Hello World' success bold")
    jinja = parser.content_to_jinja(content)  # "{{ 'Hello World' | success_text | bold }}"
"""

import logging
import re
from collections import Counter
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union

from adapters.module_wrapper.types import (
    ComponentName,
    ComponentPaths,
    DSLNotation,
    HasSymbol,
    IssueList,
    Payload,
    ReverseSymbolMapping,
    Serializable,
    SuggestionList,
    Symbol,
    SymbolMapping,
)

if TYPE_CHECKING:
    from adapters.module_wrapper.core import ModuleWrapper

logger = logging.getLogger(__name__)


# =============================================================================
# DATA CLASSES
# =============================================================================


@dataclass
class DSLToken:
    """A single token from DSL tokenization.

    Token types:
    - 'symbol': Unicode symbol (e.g., §, ᵬ, ℊ)
    - 'multiplier': Repeat notation (e.g., ×3)
    - 'open': Opening bracket [
    - 'close': Closing bracket ]
    - 'component_name': Full component name (e.g., Button)
    - 'param_open': Opening brace {  (parameterized notation)
    - 'param_close': Closing brace }  (parameterized notation)
    - 'equals': Assignment operator =  (parameterized notation)
    - 'string': Quoted string literal  (parameterized notation)
    - 'number': Numeric literal         (parameterized notation)
    - 'boolean': true/false literal     (parameterized notation)
    - 'null': null/None literal         (parameterized notation)
    - 'identifier': Bare word param name (parameterized notation)
    """

    type: str
    value: str
    position: int


@dataclass
class DSLNode:
    """A node in the parsed DSL tree.

    Supports two modes:
    - **Positional**: children list (e.g., §[δ×3, Ƀ[ᵬ×2]])
    - **Parameterized**: params dict (e.g., ƒ{must=[φ{key="x", match=ʋ{value="y"}}]})

    Params values can be primitives (str, int, float, bool, None),
    other DSLNode objects, or lists thereof.
    """

    symbol: str
    component_name: str
    multiplier: int = 1
    children: List["DSLNode"] = field(default_factory=list)
    params: Dict[str, Any] = field(default_factory=dict)
    parent: Optional["DSLNode"] = None
    depth: int = 0

    @property
    def is_parameterized(self) -> bool:
        """True if this node uses parameterized notation (has params, no positional children)."""
        return bool(self.params) and not self.children

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary representation."""
        result = {
            "symbol": self.symbol,
            "name": self.component_name,
            "multiplier": self.multiplier,
            "depth": self.depth,
            "children": [child.to_dict() for child in self.children],
        }
        if self.params:
            result["params"] = _params_to_dict(self.params)
        return result

    def to_compact_dsl(self) -> str:
        """Convert back to compact DSL notation."""
        result = self.symbol
        if self.multiplier > 1:
            result = f"{self.symbol}×{self.multiplier}"
        if self.params:
            params_str = ", ".join(
                f"{k}={_param_value_to_dsl(v)}" for k, v in self.params.items()
            )
            result = f"{result}{{{params_str}}}"
        elif self.children:
            children_str = ", ".join(c.to_compact_dsl() for c in self.children)
            result = f"{result}[{children_str}]"
        return result

    def to_expanded_notation(self) -> str:
        """Convert to expanded component name notation."""
        result = self.component_name
        if self.multiplier > 1:
            result = f"{self.component_name}×{self.multiplier}"
        if self.params:
            params_str = ", ".join(
                f"{k}={_param_value_to_expanded(v)}" for k, v in self.params.items()
            )
            result = f"{result}{{{params_str}}}"
        elif self.children:
            children_str = ", ".join(c.to_expanded_notation() for c in self.children)
            result = f"{result}[{children_str}]"
        return result

    def flatten(self) -> List["DSLNode"]:
        """Flatten tree to list (pre-order traversal), including parameterized children."""
        nodes = [self]
        for child in self.children:
            nodes.extend(child.flatten())
        # Also flatten DSLNode values in params
        for value in self.params.values():
            if isinstance(value, DSLNode):
                nodes.extend(value.flatten())
            elif isinstance(value, list):
                for item in value:
                    if isinstance(item, DSLNode):
                        nodes.extend(item.flatten())
        return nodes

    def get_component_counts(self) -> Dict[str, int]:
        """Get counts of each component type in subtree."""
        counts = Counter()
        counts[self.component_name] += self.multiplier
        for child in self.children:
            child_counts = child.get_component_counts()
            for name, count in child_counts.items():
                counts[name] += count
        # Also count parameterized children
        for value in self.params.values():
            if isinstance(value, DSLNode):
                for name, count in value.get_component_counts().items():
                    counts[name] += count
            elif isinstance(value, list):
                for item in value:
                    if isinstance(item, DSLNode):
                        for name, count in item.get_component_counts().items():
                            counts[name] += count
        return dict(counts)


def _params_to_dict(params: Dict[str, Any]) -> Dict[str, Any]:
    """Convert params dict to JSON-serializable form."""
    result = {}
    for k, v in params.items():
        if isinstance(v, DSLNode):
            result[k] = v.to_dict()
        elif isinstance(v, list):
            result[k] = [
                item.to_dict() if isinstance(item, DSLNode) else item for item in v
            ]
        else:
            result[k] = v
    return result


def _param_value_to_dsl(value: Any) -> str:
    """Convert a param value to DSL string representation."""
    if isinstance(value, DSLNode):
        return value.to_compact_dsl()
    elif isinstance(value, list):
        items = ", ".join(_param_value_to_dsl(v) for v in value)
        return f"[{items}]"
    elif isinstance(value, str):
        return f'"{value}"'
    elif isinstance(value, bool):
        return "true" if value else "false"
    elif value is None:
        return "null"
    else:
        return str(value)


def _param_value_to_expanded(value: Any) -> str:
    """Convert a param value to expanded notation string."""
    if isinstance(value, DSLNode):
        return value.to_expanded_notation()
    elif isinstance(value, list):
        items = ", ".join(_param_value_to_expanded(v) for v in value)
        return f"[{items}]"
    elif isinstance(value, str):
        return f'"{value}"'
    elif isinstance(value, bool):
        return "true" if value else "false"
    elif value is None:
        return "null"
    else:
        return str(value)


@dataclass
class DSLParseResult:
    """Result of parsing a DSL string."""

    dsl: str
    is_valid: bool
    root_nodes: List[DSLNode] = field(default_factory=list)
    component_paths: List[str] = field(default_factory=list)
    component_counts: Dict[str, int] = field(default_factory=dict)
    issues: List[str] = field(default_factory=list)
    suggestions: List[str] = field(default_factory=list)
    resolved_symbols: Dict[str, str] = field(default_factory=dict)  # symbol → name

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "dsl": self.dsl,
            "is_valid": self.is_valid,
            "root": self.root_nodes[0].to_dict() if self.root_nodes else None,
            "components": [n.to_dict() for n in self.root_nodes],
            "component_paths": self.component_paths,
            "component_counts": self.component_counts,
            "issues": self.issues,
            "suggestions": self.suggestions,
            "resolved_symbols": self.resolved_symbols,
        }


@dataclass
class QdrantQuery:
    """A Qdrant query specification."""

    collection: str
    vector_name: str  # 'components', 'inputs', 'relationships'
    query_text: str
    filters: Dict[str, Any] = field(default_factory=dict)
    limit: int = 10

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for execution."""
        return {
            "collection": self.collection,
            "vector_name": self.vector_name,
            "query_text": self.query_text,
            "filters": self.filters,
            "limit": self.limit,
        }


# =============================================================================
# CONTENT DSL DATA CLASSES
# =============================================================================


@dataclass
class StyleModifier:
    """A style modifier parsed from Content DSL."""

    name: str  # Original modifier name (e.g., "yellow", "bold", "success")
    jinja_filter: str  # Jinja filter name (e.g., "color", "bold", "success_text")
    jinja_args: Optional[str] = (
        None  # Arguments for filter (e.g., "'yellow'" for color)
    )

    def to_jinja(self) -> str:
        """Convert to Jinja filter syntax."""
        if self.jinja_args:
            return f"{self.jinja_filter}({self.jinja_args})"
        return self.jinja_filter


@dataclass
class ContentLine:
    """A single line of Content DSL."""

    symbol: str  # Component symbol (δ, ᵬ, etc.)
    component_name: str  # Resolved component name
    content: str  # The text content
    modifiers: List[StyleModifier] = field(default_factory=list)
    url: Optional[str] = None  # Detected URL for buttons/links
    line_number: int = 0
    is_continuation: bool = False  # True if this line continues previous

    def to_jinja(self) -> str:
        """Convert content to Jinja expression."""
        # Escape single quotes in content
        safe_content = self.content.replace("'", "\\'")
        expr = f"'{safe_content}'"

        # Apply modifiers as Jinja filters
        for mod in self.modifiers:
            expr = f"{expr} | {mod.to_jinja()}"

        return "{{ " + expr + " }}"

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "symbol": self.symbol,
            "component": self.component_name,
            "content": self.content,
            "modifiers": [m.name for m in self.modifiers],
            "url": self.url,
            "jinja": self.to_jinja(),
        }


@dataclass
class ContentBlock:
    """A block of content lines for a single component."""

    primary: ContentLine
    continuations: List[ContentLine] = field(default_factory=list)

    @property
    def all_lines(self) -> List[ContentLine]:
        """Get all lines in the block."""
        return [self.primary] + self.continuations

    @property
    def full_content(self) -> str:
        """Get combined content from all lines."""
        parts = [self.primary.content]
        parts.extend(c.content for c in self.continuations)
        return "\n".join(parts)

    def to_jinja(self) -> str:
        """Convert entire block to Jinja expression."""
        if not self.continuations:
            return self.primary.to_jinja()

        # Multi-line: combine content then apply modifiers
        safe_content = self.full_content.replace("'", "\\'")
        expr = f"'{safe_content}'"

        # Apply modifiers from primary line
        for mod in self.primary.modifiers:
            expr = f"{expr} | {mod.to_jinja()}"

        return "{{ " + expr + " }}"

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "symbol": self.primary.symbol,
            "component": self.primary.component_name,
            "content": self.full_content,
            "modifiers": [m.name for m in self.primary.modifiers],
            "url": self.primary.url,
            "jinja": self.to_jinja(),
            "line_count": len(self.all_lines),
        }


@dataclass
class ContentDSLResult:
    """Result of parsing Content DSL."""

    raw: str
    blocks: List[ContentBlock] = field(default_factory=list)
    issues: List[str] = field(default_factory=list)

    @property
    def is_valid(self) -> bool:
        """Check if parsing was successful."""
        return len(self.blocks) > 0 and len(self.issues) == 0

    def to_jinja_list(self) -> List[str]:
        """Convert all blocks to Jinja expressions."""
        return [block.to_jinja() for block in self.blocks]

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "raw": self.raw,
            "is_valid": self.is_valid,
            "blocks": [b.to_dict() for b in self.blocks],
            "issues": self.issues,
            "jinja_expressions": self.to_jinja_list(),
        }


# Style modifier registry - maps modifier names to Jinja filters
STYLE_MODIFIERS: Dict[str, Tuple[str, Optional[str]]] = {
    # Colors (filter_name, arg or None)
    "red": ("color", "'red'"),
    "green": ("color", "'green'"),
    "blue": ("color", "'blue'"),
    "yellow": ("color", "'yellow'"),
    "orange": ("color", "'orange'"),
    "purple": ("color", "'purple'"),
    "gray": ("color", "'gray'"),
    "grey": ("color", "'gray'"),
    "white": ("color", "'white'"),
    "black": ("color", "'black'"),
    # Semantic colors
    "success": ("success_text", None),
    "error": ("error_text", None),
    "warning": ("warning_text", None),
    "info": ("info_text", None),
    "danger": ("error_text", None),
    "ok": ("success_text", None),
    # Text formatting
    "bold": ("bold", None),
    "italic": ("italic", None),
    "strike": ("strikethrough", None),
    "strikethrough": ("strikethrough", None),
    "underline": ("underline", None),
    "code": ("monospace", None),
    "mono": ("monospace", None),
    "monospace": ("monospace", None),
    # Size modifiers
    "small": ("small", None),
    "large": ("large", None),
    # Special formatting
    "price": ("price", "'USD'"),
    "price_usd": ("price", "'USD'"),
    "price_eur": ("price", "'EUR'"),
    "price_gbp": ("price", "'GBP'"),
    "percent": ("percent", None),
    "date": ("date", None),
    "time": ("time", None),
    "datetime": ("datetime", None),
}


# =============================================================================
# DSL PARSER
# =============================================================================


class DSLParser:
    """
    Universal DSL Parser for component structure notation.

    This parser handles the complete lifecycle of DSL processing:
    1. Tokenization: String → Token list
    2. Parsing: Tokens → Tree structure
    3. Validation: Check against component hierarchy
    4. Query generation: Create Qdrant query specifications

    Thread-safe and stateless after initialization.
    """

    def __init__(
        self,
        symbol_mapping: Optional[SymbolMapping] = None,
        reverse_mapping: Optional[ReverseSymbolMapping] = None,
        relationships: Optional[Dict[str, List[str]]] = None,
        wrapper: Optional["ModuleWrapper"] = None,
    ):
        """
        Initialize the DSL parser.

        Args:
            symbol_mapping: Component name → symbol mapping (e.g., {"Button": "ᵬ"})
            reverse_mapping: Symbol → component name mapping (e.g., {"ᵬ": "Button"})
            relationships: Parent → [children] mapping for validation
            wrapper: Optional ModuleWrapper to get mappings from
        """
        self._wrapper = wrapper

        if wrapper:
            self._symbol_mapping = wrapper.symbol_mapping
            self._reverse_mapping = wrapper.reverse_symbol_mapping
            self._relationships = wrapper.relationships
        else:
            self._symbol_mapping = symbol_mapping or {}
            self._reverse_mapping = reverse_mapping or {}
            self._relationships = relationships or {}

        # Build symbol set for fast lookup
        self._all_symbols: Set[str] = set(self._reverse_mapping.keys())

        # Build ASCII confusable map for visually similar symbols (e.g. 'g' → 'ℊ')
        self._ascii_confusables: Dict[str, str] = self._build_confusables()

        # Cache for repeated parses
        self._parse_cache: Dict[str, DSLParseResult] = {}

    @property
    def symbols(self) -> SymbolMapping:
        """Get component → symbol mapping."""
        return self._symbol_mapping

    @property
    def reverse_symbols(self) -> ReverseSymbolMapping:
        """Get symbol → component mapping."""
        return self._reverse_mapping

    @property
    def relationships(self) -> Dict[str, List[str]]:
        """Get parent → children relationships."""
        return self._relationships

    def refresh_from_wrapper(self):
        """Refresh mappings from wrapper (if available)."""
        if self._wrapper:
            self._symbol_mapping = self._wrapper.symbol_mapping
            self._reverse_mapping = self._wrapper.reverse_symbol_mapping
            self._relationships = self._wrapper.relationships
            self._all_symbols = set(self._reverse_mapping.keys())
            self._ascii_confusables = self._build_confusables()
            self._parse_cache.clear()

    def _build_confusables(self) -> Dict[str, str]:
        """
        Build map of ASCII chars visually confusable with Unicode DSL symbols.

        Example: 'g' (ASCII) → 'ℊ' (U+210A SCRIPT SMALL G) for Grid.
        Only maps ASCII chars that aren't already real symbols.
        """
        import unicodedata

        confusables: Dict[str, str] = {}
        for symbol in self._all_symbols:
            if len(symbol) != 1 or ord(symbol) < 128:
                continue
            uname = unicodedata.name(symbol, "")
            parts = uname.split()
            if len(parts) >= 2 and len(parts[-1]) == 1 and parts[-1].isalpha():
                ascii_char = parts[-1].lower()
                if ascii_char not in self._all_symbols:
                    confusables[ascii_char] = symbol
        return confusables

    # =========================================================================
    # TOKENIZATION
    # =========================================================================

    def tokenize(self, dsl_string: DSLNotation) -> List[DSLToken]:
        """
        Tokenize a DSL string into tokens.

        Supports both positional notation (§[δ×3]) and parameterized notation
        (ƒ{must=[φ{key="x"}]}). Both can coexist in the same DSL string.

        Args:
            dsl_string: DSL notation string

        Returns:
            List of DSLToken objects
        """
        tokens = []
        i = 0
        s = dsl_string.strip()
        brace_depth = 0  # Track {…} nesting for parameterized context

        while i < len(s):
            char = s[i]

            # Skip whitespace and commas
            if char in " ,\n\t":
                i += 1
                continue

            # Positional brackets
            if char == "[":
                tokens.append(DSLToken(type="open", value="[", position=i))
                i += 1
                continue

            if char == "]":
                tokens.append(DSLToken(type="close", value="]", position=i))
                i += 1
                continue

            # Parameterized braces
            if char == "{":
                brace_depth += 1
                tokens.append(DSLToken(type="param_open", value="{", position=i))
                i += 1
                continue

            if char == "}":
                brace_depth = max(0, brace_depth - 1)
                tokens.append(DSLToken(type="param_close", value="}", position=i))
                i += 1
                continue

            # Equals sign (for parameterized notation)
            if char == "=":
                tokens.append(DSLToken(type="equals", value="=", position=i))
                i += 1
                continue

            # Multiplier (×N, *N, or xN)
            if char in "×*x" and i + 1 < len(s) and s[i + 1].isdigit():
                j = i + 1
                while j < len(s) and s[j].isdigit():
                    j += 1
                multiplier_value = s[i + 1 : j]
                tokens.append(
                    DSLToken(
                        type="multiplier", value=f"×{multiplier_value}", position=i
                    )
                )
                i = j
                continue

            # String literals (for parameterized notation)
            if char in ('"', "'"):
                quote = char
                j = i + 1
                while j < len(s) and s[j] != quote:
                    if s[j] == "\\" and j + 1 < len(s):
                        j += 2  # skip escaped char
                    else:
                        j += 1
                if j < len(s):
                    j += 1  # consume closing quote
                string_val = s[i + 1 : j - 1]
                tokens.append(DSLToken(type="string", value=string_val, position=i))
                i = j
                continue

            # Numeric literals (including negative and float)
            if char.isdigit() or (
                char == "-" and i + 1 < len(s) and s[i + 1].isdigit()
            ):
                j = i + 1 if char == "-" else i
                while j < len(s) and s[j].isdigit():
                    j += 1
                if j < len(s) and s[j] == ".":
                    j += 1
                    while j < len(s) and s[j].isdigit():
                        j += 1
                tokens.append(DSLToken(type="number", value=s[i:j], position=i))
                i = j
                continue

            # Multi-char ASCII word check — MUST come before symbol/confusable
            # checks to prevent consuming the first char of identifiers like
            # "RecommendQuery", "value", "match", etc. as an ASCII-confusable
            # symbol (e.g., 'v' → 'ỽ' for VectorDataConfig, 'R' → Range sym).
            # Works at ALL brace depths (root level and inside {…}).
            if char.isalpha() or char == "_":
                j = i
                while j < len(s) and (s[j].isalnum() or s[j] == "_"):
                    j += 1
                word = s[i:j]

                # Multi-char words are always identifiers/keywords, not symbols
                if len(word) > 1:
                    if word in ("true", "True"):
                        tokens.append(
                            DSLToken(type="boolean", value="true", position=i)
                        )
                    elif word in ("false", "False"):
                        tokens.append(
                            DSLToken(type="boolean", value="false", position=i)
                        )
                    elif word in ("null", "None", "none"):
                        tokens.append(DSLToken(type="null", value="null", position=i))
                    elif word in self._symbol_mapping:
                        sym = self._symbol_mapping[word]
                        tokens.append(DSLToken(type="symbol", value=sym, position=i))
                    else:
                        peek = j
                        while peek < len(s) and s[peek] in " \t":
                            peek += 1
                        if peek < len(s) and s[peek] == "=":
                            tokens.append(
                                DSLToken(type="identifier", value=word, position=i)
                            )
                        else:
                            tokens.append(
                                DSLToken(type="component_name", value=word, position=i)
                            )
                    i = j
                    continue
                # Single char — fall through to symbol/confusable checks below

            # Known Unicode symbol (single char)
            if char in self._all_symbols:
                tokens.append(DSLToken(type="symbol", value=char, position=i))
                i += 1
                continue

            # ASCII confusable → Unicode symbol (e.g. 'g' → 'ℊ')
            if char in self._ascii_confusables and char not in self._all_symbols:
                resolved = self._ascii_confusables[char]
                tokens.append(DSLToken(type="symbol", value=resolved, position=i))
                i += 1
                continue

            # Unknown character - skip with warning
            logger.debug(f"Skipping unknown character '{char}' at position {i}")
            i += 1

        return tokens

    # =========================================================================
    # PARSING
    # =========================================================================

    def parse(self, dsl_string: str, use_cache: bool = True) -> DSLParseResult:
        """
        Parse a DSL string into a structured result.

        This is the main entry point for DSL parsing.

        Args:
            dsl_string: DSL notation (e.g., "§[δ×3, Ƀ[ᵬ×2]]")
            use_cache: Whether to use cached results for repeated parses

        Returns:
            DSLParseResult with parsed structure and validation info
        """
        dsl_string = dsl_string.strip()

        # Check cache
        if use_cache and dsl_string in self._parse_cache:
            return self._parse_cache[dsl_string]

        # Tokenize
        try:
            tokens = self.tokenize(dsl_string)
        except Exception as e:
            return DSLParseResult(
                dsl=dsl_string,
                is_valid=False,
                issues=[f"Tokenization error: {e}"],
            )

        if not tokens:
            return DSLParseResult(
                dsl=dsl_string,
                is_valid=False,
                issues=["Empty or invalid DSL string"],
            )

        # Parse tokens to tree
        try:
            root_nodes, _ = self._parse_tokens(tokens, 0, 0)
        except Exception as e:
            return DSLParseResult(
                dsl=dsl_string,
                is_valid=False,
                issues=[f"Parse error: {e}"],
            )

        # Build result
        result = DSLParseResult(
            dsl=dsl_string,
            is_valid=True,
            root_nodes=root_nodes,
        )

        # Collect component paths and counts
        for node in root_nodes:
            self._collect_paths(node, result.component_paths)
            counts = node.get_component_counts()
            for name, count in counts.items():
                result.component_counts[name] = (
                    result.component_counts.get(name, 0) + count
                )

        # Collect resolved symbols
        for node in root_nodes:
            for flat_node in node.flatten():
                result.resolved_symbols[flat_node.symbol] = flat_node.component_name

        # Validate structure
        self._validate_result(result)

        # Cache result
        if use_cache:
            self._parse_cache[dsl_string] = result

        return result

    def _parse_tokens(
        self,
        tokens: List[DSLToken],
        pos: int,
        depth: int,
    ) -> Tuple[List[DSLNode], int]:
        """Recursively parse tokens into DSLNode tree.

        Handles both positional notation (symbol + [children]) and
        parameterized notation (symbol + {key=value, ...}).
        """
        nodes = []

        while pos < len(tokens):
            token = tokens[pos]

            # End of positional children
            if token.type == "close":
                return nodes, pos + 1

            # End of parameterized block
            if token.type == "param_close":
                return nodes, pos + 1

            # Start of positional children (should attach to previous node)
            if token.type == "open":
                if nodes:
                    children, pos = self._parse_tokens(tokens, pos + 1, depth + 1)
                    nodes[-1].children = children
                    for child in children:
                        child.parent = nodes[-1]
                else:
                    # Orphan bracket - skip
                    pos += 1
                continue

            # Start of parameterized block (should attach to previous node)
            if token.type == "param_open":
                if nodes:
                    params, pos = self._parse_params(tokens, pos + 1, depth + 1)
                    nodes[-1].params = params
                else:
                    # Orphan brace - skip
                    pos += 1
                continue

            # Multiplier (attach to previous node)
            if token.type == "multiplier":
                if nodes:
                    nodes[-1].multiplier = int(token.value[1:])  # Skip the ×
                pos += 1
                continue

            # Symbol or component name
            if token.type in ("symbol", "component_name"):
                if token.type == "symbol":
                    symbol = token.value
                    component_name = self._reverse_mapping.get(symbol, symbol)
                else:
                    component_name = token.value
                    symbol = self._symbol_mapping.get(
                        component_name, component_name[0] if component_name else "?"
                    )

                node = DSLNode(
                    symbol=symbol,
                    component_name=component_name,
                    depth=depth,
                )
                nodes.append(node)
                pos += 1
                continue

            # Unknown token - skip
            pos += 1

        return nodes, pos

    def _parse_params(
        self,
        tokens: List[DSLToken],
        pos: int,
        depth: int,
    ) -> Tuple[Dict[str, Any], int]:
        """Parse parameterized block: key=value pairs inside {}.

        Returns:
            Tuple of (params dict, new position)
        """
        params: Dict[str, Any] = {}

        while pos < len(tokens):
            token = tokens[pos]

            # End of params block
            if token.type == "param_close":
                return params, pos + 1

            # Expect identifier = value
            if token.type == "identifier":
                key = token.value
                pos += 1

                # Expect equals
                if pos < len(tokens) and tokens[pos].type == "equals":
                    pos += 1
                else:
                    # Missing equals — treat as flag param (key=true)
                    params[key] = True
                    continue

                # Parse value
                value, pos = self._parse_param_value(tokens, pos, depth)
                params[key] = value
                continue

            # Skip commas and whitespace tokens
            pos += 1

        return params, pos

    def _parse_param_value(
        self,
        tokens: List[DSLToken],
        pos: int,
        depth: int,
    ) -> Tuple[Any, int]:
        """Parse a single parameter value.

        Values can be:
        - Primitives: string, number, boolean, null
        - Component nodes: symbol{...} (recursive)
        - Arrays: [value, value, ...]
        """
        if pos >= len(tokens):
            return None, pos

        token = tokens[pos]

        # String literal
        if token.type == "string":
            return token.value, pos + 1

        # Number literal
        if token.type == "number":
            val = token.value
            if "." in val:
                return float(val), pos + 1
            return int(val), pos + 1

        # Boolean literal
        if token.type == "boolean":
            return token.value == "true", pos + 1

        # Null literal
        if token.type == "null":
            return None, pos + 1

        # Array value [...]
        if token.type == "open":
            items, pos = self._parse_param_array(tokens, pos + 1, depth)
            return items, pos

        # Component node (symbol + { or symbol + [)
        if token.type in ("symbol", "component_name"):
            if token.type == "symbol":
                symbol = token.value
                component_name = self._reverse_mapping.get(symbol, symbol)
            else:
                component_name = token.value
                symbol = self._symbol_mapping.get(
                    component_name, component_name[0] if component_name else "?"
                )

            node = DSLNode(
                symbol=symbol,
                component_name=component_name,
                depth=depth,
            )
            pos += 1

            # Check for parameterized block or positional children
            if pos < len(tokens):
                if tokens[pos].type == "param_open":
                    node.params, pos = self._parse_params(tokens, pos + 1, depth + 1)
                elif tokens[pos].type == "open":
                    children, pos = self._parse_tokens(tokens, pos + 1, depth + 1)
                    node.children = children
                    for child in children:
                        child.parent = node

            return node, pos

        # Identifier used as value (bare word — treat as string)
        if token.type == "identifier":
            return token.value, pos + 1

        # Unknown — skip
        return None, pos + 1

    def _parse_param_array(
        self,
        tokens: List[DSLToken],
        pos: int,
        depth: int,
    ) -> Tuple[List[Any], int]:
        """Parse an array of parameter values inside [].

        Returns:
            Tuple of (items list, new position)
        """
        items: List[Any] = []

        while pos < len(tokens):
            token = tokens[pos]

            # End of array
            if token.type == "close":
                return items, pos + 1

            # Skip commas
            if (
                token.type in ("identifier",)
                and pos + 1 < len(tokens)
                and tokens[pos + 1].type == "equals"
            ):
                # This is a key=value inside an array — shouldn't happen, skip
                pos += 1
                continue

            # Parse value
            value, pos = self._parse_param_value(tokens, pos, depth)
            if value is not None:
                items.append(value)

        return items, pos

    def _collect_paths(self, node: DSLNode, paths: List[str]):
        """Collect component paths from node tree."""
        for _ in range(node.multiplier):
            paths.append(node.component_name)
        for child in node.children:
            self._collect_paths(child, paths)

    # =========================================================================
    # VALIDATION
    # =========================================================================

    def _validate_result(self, result: DSLParseResult):
        """Validate parsed result against component hierarchy."""
        for node in result.root_nodes:
            self._validate_node(node, None, result)

    def _validate_node(
        self,
        node: DSLNode,
        parent: Optional[DSLNode],
        result: DSLParseResult,
    ):
        """Validate a single node and its children."""
        # Check if symbol is known
        if (
            node.symbol not in self._all_symbols
            and node.component_name not in self._symbol_mapping
        ):
            result.issues.append(f"Unknown symbol/component: {node.symbol}")
            # Don't mark invalid - might still work

        # Check parent-child relationship
        if parent and self._relationships:
            parent_name = parent.component_name
            valid_children = self._relationships.get(parent_name, [])

            if node.component_name not in valid_children and valid_children:
                # Check for wrapper requirements
                wrapper_needed = self._get_required_wrapper(node.component_name)
                if wrapper_needed:
                    result.issues.append(
                        f"{node.component_name} should be wrapped in {wrapper_needed}"
                    )
                    wrapper_sym = self._symbol_mapping.get(
                        wrapper_needed, wrapper_needed
                    )
                    result.suggestions.append(
                        f"Use {wrapper_sym}[{node.symbol}] instead of {node.symbol}"
                    )
                    result.is_valid = False
                else:
                    result.issues.append(
                        f"{node.component_name} cannot be direct child of {parent_name}"
                    )
                    result.is_valid = False

        # Validate children recursively
        for child in node.children:
            self._validate_node(child, node, result)

    def _get_required_wrapper(self, component_name: str) -> Optional[str]:
        """Get the wrapper component needed for a component, if any.

        Queries the wrapper's registered metadata (populated by domain-specific
        wrappers via register_component_metadata_batch). Falls back to empty
        if no wrapper is configured.
        """
        if self._wrapper:
            return self._wrapper.get_all_wrapper_requirements().get(component_name)
        return None

    # =========================================================================
    # QDRANT QUERY GENERATION
    # =========================================================================

    def to_qdrant_queries(
        self,
        result: DSLParseResult,
        collection_name: str = "mcp_gchat_cards",
        search_strategy: str = "auto",
    ) -> List[QdrantQuery]:
        """
        Generate Qdrant queries from a parsed DSL result.

        Generates queries for different vectors based on strategy:
        - 'components': Search by component identity (class name, structure)
        - 'relationships': Search by DSL notation in relationship text
        - 'inputs': Search by parameter values
        - 'auto': Generates queries for all relevant vectors

        Args:
            result: Parsed DSL result
            collection_name: Qdrant collection name
            search_strategy: 'components', 'relationships', 'inputs', or 'auto'

        Returns:
            List of QdrantQuery objects for execution
        """
        queries = []

        if not result.is_valid or not result.root_nodes:
            return queries

        if search_strategy in ("auto", "relationships"):
            # Query 1: Search by DSL notation in relationships vector
            # This finds instance_patterns with matching structure
            queries.append(
                QdrantQuery(
                    collection=collection_name,
                    vector_name="relationships",
                    query_text=result.dsl,  # Use original DSL notation
                    filters={"type": "instance_pattern"},
                    limit=10,
                )
            )

        if search_strategy in ("auto", "components"):
            # Query 2: Search by component identity
            # Find components by name/type
            root_name = result.root_nodes[0].component_name
            component_names = list(result.component_counts.keys())

            queries.append(
                QdrantQuery(
                    collection=collection_name,
                    vector_name="components",
                    query_text=f"{root_name} containing {' '.join(component_names[:5])}",
                    filters={"type": "class"},
                    limit=10,
                )
            )

        if search_strategy in ("auto", "inputs"):
            # Query 3: Build an expanded text query for inputs vector
            # This matches patterns by their content structure
            expanded = (
                result.root_nodes[0].to_expanded_notation() if result.root_nodes else ""
            )
            queries.append(
                QdrantQuery(
                    collection=collection_name,
                    vector_name="inputs",
                    query_text=expanded,
                    limit=10,
                )
            )

        return queries

    def build_qdrant_filter(
        self,
        result: DSLParseResult,
        additional_filters: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """
        Build a Qdrant filter from parsed DSL result.

        Creates filters that can be used with Qdrant search to narrow results
        to components matching the DSL structure.

        Args:
            result: Parsed DSL result
            additional_filters: Extra filters to merge

        Returns:
            Qdrant filter dict for use with search/scroll
        """
        must_conditions = []

        # Filter by component names present in the DSL
        if result.component_counts:
            # Match any of the component names
            component_names = list(result.component_counts.keys())
            if component_names:
                must_conditions.append(
                    {"key": "name", "match": {"any": component_names}}
                )

        # Filter by symbol if root is known
        if result.root_nodes:
            root_symbol = result.root_nodes[0].symbol
            must_conditions.append({"key": "symbol", "match": {"value": root_symbol}})

        # Merge additional filters
        if additional_filters:
            for key, value in additional_filters.items():
                must_conditions.append({"key": key, "match": {"value": value}})

        return {"must": must_conditions} if must_conditions else {}

    # =========================================================================
    # UTILITY METHODS
    # =========================================================================

    def extract_dsl_from_text(self, text: str) -> Optional[str]:
        """
        Extract DSL notation from a text string.

        Looks for patterns like:
        - "§[δ×3, Ƀ[ᵬ×2]] Server Status" → returns "§[δ×3, Ƀ[ᵬ×2]]"
        - "Build a Ƀ[ᵬ×4] Quick Actions card" → returns "Ƀ[ᵬ×4]"

        Args:
            text: Text that may contain DSL notation

        Returns:
            DSL string if found, None otherwise
        """
        if not text:
            return None

        text = text.strip()

        # Check if text starts with a known symbol
        first_char = text[0] if text else ""

        if first_char not in self._all_symbols:
            # Scan for first symbol in text
            for i, char in enumerate(text):
                if char in self._all_symbols:
                    text = text[i:]
                    first_char = char
                    break
            else:
                return None

        # Find the end of DSL structure (after matching brackets)
        bracket_count = 0
        dsl_end = 0
        in_dsl = False

        for i, char in enumerate(text):
            if char == "[":
                bracket_count += 1
                in_dsl = True
            elif char == "]":
                bracket_count -= 1
                if bracket_count == 0 and in_dsl:
                    dsl_end = i + 1
                    break
            elif not in_dsl and char in " \t\n":
                # End of simple symbol (no brackets)
                dsl_end = i
                break

        if dsl_end > 0:
            return text[:dsl_end]
        elif not in_dsl:
            # Single symbol without brackets
            return first_char

        return None

    def normalize_dsl(self, dsl_string: str) -> str:
        """
        Normalize a DSL string to canonical form.

        - Removes extra whitespace
        - Ensures consistent comma placement
        - Sorts children alphabetically (optional)

        Args:
            dsl_string: Input DSL string

        Returns:
            Normalized DSL string
        """
        result = self.parse(dsl_string)
        if not result.is_valid or not result.root_nodes:
            return dsl_string

        # Reconstruct from parsed structure
        return ", ".join(node.to_compact_dsl() for node in result.root_nodes)

    def expand_dsl(self, dsl_string: str) -> str:
        """
        Expand DSL to full component name notation.

        Args:
            dsl_string: Compact DSL (e.g., "§[δ, ᵬ×2]")

        Returns:
            Expanded notation (e.g., "Section[DecoratedText, Button×2]")
        """
        result = self.parse(dsl_string)
        if not result.is_valid or not result.root_nodes:
            return dsl_string

        return ", ".join(node.to_expanded_notation() for node in result.root_nodes)

    def compact_to_dsl(self, component_notation: str) -> str:
        """
        Convert component name notation to compact DSL.

        Args:
            component_notation: Full names (e.g., "Section[DecoratedText, Button×2]")

        Returns:
            Compact DSL (e.g., "§[δ, ᵬ×2]")
        """
        result = self.parse(component_notation)
        if not result.is_valid or not result.root_nodes:
            return component_notation

        return ", ".join(node.to_compact_dsl() for node in result.root_nodes)

    # =========================================================================
    # CONTENT DSL PARSING
    # =========================================================================

    def parse_content_dsl(self, text: str) -> ContentDSLResult:
        """
        Parse Content DSL text into structured content blocks.

        Content DSL format:
            δ 'Status: Online' success bold
            ᵬ Click Here https://example.com
              Continue on next line
            § Header Text

        Args:
            text: Multi-line Content DSL text

        Returns:
            ContentDSLResult with parsed blocks
        """
        result = ContentDSLResult(raw=text)
        lines = text.strip().split("\n")

        current_block: Optional[ContentBlock] = None

        for line_num, line in enumerate(lines, 1):
            # Check for continuation (indented line)
            is_continuation = (
                line.startswith(("  ", "\t")) and current_block is not None
            )

            if is_continuation:
                # Parse continuation line (content only, no symbol)
                content = line.strip()
                cont_line = ContentLine(
                    symbol=current_block.primary.symbol,
                    component_name=current_block.primary.component_name,
                    content=content,
                    line_number=line_num,
                    is_continuation=True,
                )
                current_block.continuations.append(cont_line)
            else:
                # Save previous block if exists
                if current_block:
                    result.blocks.append(current_block)

                # Parse new primary line
                parsed = self._parse_content_line(line, line_num)
                if parsed:
                    current_block = ContentBlock(primary=parsed)
                elif line.strip():  # Non-empty but unparseable
                    result.issues.append(
                        f"Line {line_num}: Could not parse '{line.strip()}'"
                    )
                    current_block = None
                else:
                    current_block = None

        # Don't forget the last block
        if current_block:
            result.blocks.append(current_block)

        return result

    def _parse_content_line(
        self, line: str, line_num: int = 0
    ) -> Optional[ContentLine]:
        """
        Parse a single Content DSL line.

        Format: SYMBOL [CONTENT] [MODIFIERS...] [URL]

        Content can be:
        - Quoted: δ 'Hello World'
        - Unquoted: δ Hello World (runs until modifiers/URL)

        Args:
            line: Single line of Content DSL
            line_num: Line number for error reporting

        Returns:
            ContentLine or None if unparseable
        """
        line = line.strip()
        if not line:
            return None

        # First character should be a symbol
        first_char = line[0]
        if first_char not in self._all_symbols:
            return None

        symbol = first_char
        component_name = self._reverse_mapping.get(symbol, symbol)
        rest = line[1:].strip()

        # Parse content (quoted or unquoted)
        content, remaining = self._extract_content(rest)

        # Parse remaining for modifiers and URL
        modifiers, url = self._parse_modifiers_and_url(remaining)

        return ContentLine(
            symbol=symbol,
            component_name=component_name,
            content=content,
            modifiers=modifiers,
            url=url,
            line_number=line_num,
        )

    def _extract_content(self, text: str) -> Tuple[str, str]:
        """
        Extract content from text, handling quoted strings.

        Args:
            text: Text after the symbol

        Returns:
            Tuple of (content, remaining_text)
        """
        text = text.strip()
        if not text:
            return "", ""

        # Quoted content
        if text[0] in ('"', "'"):
            quote_char = text[0]
            end_pos = text.find(quote_char, 1)
            if end_pos > 0:
                content = text[1:end_pos]
                remaining = text[end_pos + 1 :].strip()
                return content, remaining
            else:
                # Unclosed quote - take everything
                return text[1:], ""

        # Unquoted content - parse until we hit a known modifier or URL
        words = text.split()
        content_words = []
        remaining_words = []
        found_modifier = False

        for word in words:
            if found_modifier:
                remaining_words.append(word)
            elif self._is_modifier_or_url(word):
                found_modifier = True
                remaining_words.append(word)
            else:
                content_words.append(word)

        content = " ".join(content_words)
        remaining = " ".join(remaining_words)
        return content, remaining

    def _is_modifier_or_url(self, word: str) -> bool:
        """Check if a word is a style modifier or URL."""
        word_lower = word.lower()

        # Check against known modifiers
        if word_lower in STYLE_MODIFIERS:
            return True

        # Check for URL patterns
        if word.startswith(("http://", "https://", "www.")):
            return True

        return False

    def _parse_modifiers_and_url(
        self, text: str
    ) -> Tuple[List[StyleModifier], Optional[str]]:
        """
        Parse style modifiers and URL from remaining text.

        Args:
            text: Text after content extraction

        Returns:
            Tuple of (modifiers list, url or None)
        """
        modifiers = []
        url = None

        words = text.strip().split()

        for word in words:
            # Check for URL
            if word.startswith(("http://", "https://", "www.")):
                url = word if word.startswith("http") else f"https://{word}"
                continue

            # Check for modifier
            word_lower = word.lower()
            if word_lower in STYLE_MODIFIERS:
                filter_name, filter_args = STYLE_MODIFIERS[word_lower]
                modifiers.append(
                    StyleModifier(
                        name=word_lower,
                        jinja_filter=filter_name,
                        jinja_args=filter_args,
                    )
                )

        return modifiers, url

    def content_to_jinja(self, content_result: ContentDSLResult) -> List[str]:
        """
        Convert Content DSL result to list of Jinja expressions.

        Args:
            content_result: Parsed Content DSL result

        Returns:
            List of Jinja template expressions
        """
        return content_result.to_jinja_list()

    def content_to_component_params(
        self,
        content_result: ContentDSLResult,
    ) -> List[Dict[str, Any]]:
        """
        Convert Content DSL to component parameter dictionaries.

        Useful for directly building card components.

        Args:
            content_result: Parsed Content DSL result

        Returns:
            List of dicts with component params (text, url, modifiers)
        """
        params = []

        for block in content_result.blocks:
            param = {
                "component": block.primary.component_name,
                "text": block.full_content,
                "jinja_text": block.to_jinja(),
            }

            if block.primary.url:
                param["url"] = block.primary.url

            if block.primary.modifiers:
                param["styles"] = [m.name for m in block.primary.modifiers]

            params.append(param)

        return params


# =============================================================================
# STANDALONE FUNCTIONS
# =============================================================================


def parse_dsl_to_qdrant_query(
    dsl_string: str,
    symbol_mapping: Dict[str, str],
    collection_name: str = "mcp_gchat_cards",
) -> List[Dict[str, Any]]:
    """
    Standalone function to parse DSL and generate Qdrant queries.

    Convenience function that creates a parser and generates queries.

    Args:
        dsl_string: DSL notation string
        symbol_mapping: Component name → symbol mapping
        collection_name: Qdrant collection name

    Returns:
        List of query dicts ready for execution
    """
    reverse_mapping = {v: k for k, v in symbol_mapping.items()}

    parser = DSLParser(
        symbol_mapping=symbol_mapping,
        reverse_mapping=reverse_mapping,
    )

    result = parser.parse(dsl_string)
    queries = parser.to_qdrant_queries(result, collection_name)

    return [q.to_dict() for q in queries]


def extract_dsl_from_description(
    description: str,
    symbol_mapping: Dict[str, str],
) -> Tuple[Optional[str], str]:
    """
    Extract DSL notation from a description string.

    Returns both the DSL and the remaining content.

    Args:
        description: Text that may contain DSL notation
        symbol_mapping: Component name → symbol mapping

    Returns:
        Tuple of (dsl_string or None, remaining_content)
    """
    reverse_mapping = {v: k for k, v in symbol_mapping.items()}

    parser = DSLParser(
        symbol_mapping=symbol_mapping,
        reverse_mapping=reverse_mapping,
    )

    dsl = parser.extract_dsl_from_text(description)

    if dsl:
        # Get remaining content after DSL
        remaining = description[len(dsl) :].strip()
        return dsl, remaining

    return None, description


def validate_dsl_structure(
    dsl_string: str,
    symbol_mapping: Dict[str, str],
    relationships: Dict[str, List[str]],
) -> Tuple[bool, List[str]]:
    """
    Validate a DSL structure against component hierarchy.

    Args:
        dsl_string: DSL notation string
        symbol_mapping: Component name → symbol mapping
        relationships: Parent → [children] relationship mapping

    Returns:
        Tuple of (is_valid, list of issues)
    """
    reverse_mapping = {v: k for k, v in symbol_mapping.items()}

    parser = DSLParser(
        symbol_mapping=symbol_mapping,
        reverse_mapping=reverse_mapping,
        relationships=relationships,
    )

    result = parser.parse(dsl_string)
    return result.is_valid, result.issues


# =============================================================================
# CONTENT DSL STANDALONE FUNCTIONS
# =============================================================================


def parse_content_dsl(
    text: str,
    symbol_mapping: Dict[str, str],
) -> ContentDSLResult:
    """
    Parse Content DSL text using provided symbol mapping.

    Content DSL format:
        δ 'Status: Online' success bold
        ᵬ Click Here https://example.com

    Args:
        text: Multi-line Content DSL text
        symbol_mapping: Component name → symbol mapping

    Returns:
        ContentDSLResult with parsed blocks
    """
    reverse_mapping = {v: k for k, v in symbol_mapping.items()}

    parser = DSLParser(
        symbol_mapping=symbol_mapping,
        reverse_mapping=reverse_mapping,
    )

    return parser.parse_content_dsl(text)


def content_dsl_to_jinja(
    text: str,
    symbol_mapping: Dict[str, str],
) -> List[str]:
    """
    Convert Content DSL text directly to Jinja expressions.

    Args:
        text: Content DSL text
        symbol_mapping: Component name → symbol mapping

    Returns:
        List of Jinja template expressions
    """
    result = parse_content_dsl(text, symbol_mapping)
    return result.to_jinja_list()


def get_style_modifiers() -> Dict[str, Tuple[str, Optional[str]]]:
    """
    Get the available style modifiers.

    Returns:
        Dict mapping modifier names to (jinja_filter, args) tuples
    """
    return STYLE_MODIFIERS.copy()


def add_style_modifier(
    name: str,
    jinja_filter: str,
    jinja_args: Optional[str] = None,
):
    """
    Add a custom style modifier.

    Args:
        name: Modifier name (e.g., "brand_color")
        jinja_filter: Jinja filter name (e.g., "color")
        jinja_args: Optional filter arguments (e.g., "'#FF5733'")
    """
    STYLE_MODIFIERS[name.lower()] = (jinja_filter, jinja_args)


# =============================================================================
# INTEGRATION WITH MODULE WRAPPER
# =============================================================================


def create_parser_from_wrapper(wrapper: "ModuleWrapper") -> DSLParser:
    """
    Create a DSL parser from a ModuleWrapper instance.

    Args:
        wrapper: Initialized ModuleWrapper

    Returns:
        DSLParser configured with wrapper's mappings
    """
    return DSLParser(wrapper=wrapper)
