"""IDA Pro backend for MBA expressions and rule pattern matching.

This module provides:

1. IDAPatternAdapter - Wraps a VerifiableRule for use with IDA pattern matching
2. IDANodeVisitor - Converts SymbolicExpression → AstNode for IDA

All IDA-dependent code for rule execution should be in this module, keeping
the rule definitions in d810.mba.rules pure and backend-agnostic.
"""

from __future__ import annotations

import itertools
from d810.core.typing import TYPE_CHECKING, Any, Dict, List, Optional

import ida_hexrays

from d810.core import getLogger
from d810.errors import AstEvaluationException
from d810.expr.ast import (
    AstConstant,
    AstConstantProtocol,
    AstLeaf,
    AstLeafProtocol,
    AstNode,
    minsn_to_ast,
)
from d810.hexrays.mop_snapshot import MopSnapshot
from d810.mba.dsl import SymbolicExpression, SymbolicExpressionProtocol
from d810.mba.constraints import (
    ComparisonConstraintProtocol,
    EqualityConstraintProtocol,
    is_constraint_expr,
)

# Import egglog pattern generation (optional - graceful fallback if not available)
try:
    from d810.mba.backends.egglog_backend import (
        EGGLOG_AVAILABLE,
        PatternExpr,
        verify_pattern_equivalence,
    )
except ImportError:
    EGGLOG_AVAILABLE = False
    PatternExpr = None
    verify_pattern_equivalence = None

logger = getLogger(__name__)

# Type hints only
if TYPE_CHECKING:
    from d810.mba.rules import VerifiableRule


# =============================================================================
# Egglog-based Pattern Generation Helpers
# =============================================================================

# Map DSL operation names to commutative status
# Note: DSL uses "and", "or" (not "and_", "or_")
_COMMUTATIVE_OPS = {"add", "mul", "and", "or", "xor"}


def _symbolic_to_pattern_expr(expr: SymbolicExpression) -> "PatternExpr":
    """Convert a SymbolicExpression to a PatternExpr for egglog verification.

    Args:
        expr: The SymbolicExpression from the DSL.

    Returns:
        PatternExpr suitable for egglog equivalence checking.
    """
    if not EGGLOG_AVAILABLE:
        raise ImportError("egglog not available")

    # Note: is_variable() and is_constant() are methods, not properties
    if expr.is_variable():
        return PatternExpr.var(expr.name)

    if expr.is_constant():
        # Constants are treated as variables for pattern matching
        return PatternExpr.var(f"const_{expr.value}")

    # Binary operations
    op = expr.operation
    left = _symbolic_to_pattern_expr(expr.left)
    right = _symbolic_to_pattern_expr(expr.right) if expr.right else None

    if op == "add":
        return left + right
    elif op == "sub":
        return left - right
    elif op == "mul":
        return left * right
    elif op == "and":
        return left & right
    elif op == "or":
        return left | right
    elif op == "xor":
        return left ^ right
    elif op == "neg":
        return -left
    elif op == "bnot":
        return ~left
    else:
        # Unsupported operations (shl, shr, sar, lnot, etc.) raise ValueError
        # This is handled gracefully by _filter_equivalent_patterns
        raise ValueError(f"Unsupported operation for egglog pattern gen: {op}")


def _generate_commutative_permutations(expr: SymbolicExpression) -> List[SymbolicExpression]:
    """Generate all commutative permutations of a SymbolicExpression.

    This recursively generates all permutations where commutative operators
    have their operands swapped.

    Args:
        expr: The base expression.

    Returns:
        List of all commutative permutations (including the original).
    """
    # Note: is_variable() and is_constant() are methods, not properties
    if expr.is_variable() or expr.is_constant():
        return [expr]

    # Unary operations
    if expr.right is None:
        left_perms = _generate_commutative_permutations(expr.left)
        return [
            SymbolicExpression(expr.operation, left=lp, right=None)
            for lp in left_perms
        ]

    # Binary operations
    left_perms = _generate_commutative_permutations(expr.left)
    right_perms = _generate_commutative_permutations(expr.right)

    results = []
    for lp, rp in itertools.product(left_perms, right_perms):
        # Original order
        results.append(SymbolicExpression(expr.operation, left=lp, right=rp))

        # Swapped order (if commutative)
        if expr.operation in _COMMUTATIVE_OPS:
            results.append(SymbolicExpression(expr.operation, left=rp, right=lp))

    return results


def _filter_equivalent_patterns(
    base_expr: SymbolicExpression,
    candidates: List[SymbolicExpression],
) -> List[SymbolicExpression]:
    """Filter patterns that are equivalent to the base.

    NOTE: Egglog verification is DISABLED for performance. The commutative
    permutations generated by _generate_commutative_permutations() are
    mathematically correct based on the operation's commutativity properties.
    The egglog verification was taking 30+ seconds at startup with 170+ rules.

    Args:
        base_expr: The base expression.
        candidates: Candidate permutations to check.

    Returns:
        All candidate permutations (egglog verification skipped).
    """
    # Skip egglog verification entirely - commutative permutations are correct
    # by construction. This saves 30+ seconds at startup.
    return candidates


class _LeafWrapper:
    """Lightweight wrapper to give AstLeaf nodes a leafs_by_name interface.

    Used by IDAPatternAdapter.get_replacement() to handle AstLeaf candidates
    that don't natively have leafs_by_name attribute.
    """
    __slots__ = ('leafs_by_name', 'ea', 'dst_mop')

    def __init__(self, leaf: AstLeaf):
        self.leafs_by_name = {leaf.name: leaf} if leaf.name else {}
        self.ea = leaf.ea
        # Handle both MopSnapshot (from Phase 2) and raw mop_t
        # Phase 2 changed AstLeaf.mop to store MopSnapshot for safety
        from d810.hexrays.mop_snapshot import MopSnapshot
        if isinstance(leaf.mop, MopSnapshot):
            self.dst_mop = leaf.mop.to_mop()  # Reconstruct owned mop_t
        else:
            self.dst_mop = leaf.mop  # Already a mop_t (legacy path)


class IDANodeVisitor:
    """Converts SymbolicExpression trees to IDA AstNode trees.

    This visitor handles the conversion from the pure DSL representation
    to IDA's internal AST format for pattern matching.

    Example:
        >>> from d810.mba.dsl import Var
        >>> x, y = Var("x"), Var("y")
        >>> pattern = (x | y) - (x & y)
        >>> visitor = IDANodeVisitor()
        >>> ast_node = visitor.visit(pattern)
    """

    # Map DSL operation strings to ida_hexrays attribute names (e.g., "m_add", "m_xor")
    _OPERATION_TO_IDA_ATTR = {
        "add": "m_add",
        "sub": "m_sub",
        "mul": "m_mul",
        "and": "m_and",
        "or": "m_or",
        "xor": "m_xor",
        "shl": "m_shl",
        "shr": "m_shr",
        "sar": "m_sar",
        "bnot": "m_bnot",
        "neg": "m_neg",
        "lnot": "m_lnot",
        "zext": "m_xdu",
        # Comparison operations
        "setnz": "m_setnz",
        "setz": "m_setz",
        "setae": "m_setae",
        "setb": "m_setb",
        "seta": "m_seta",
        "setbe": "m_setbe",
        "setg": "m_setg",
        "setge": "m_setge",
        "setl": "m_setl",
        "setle": "m_setle",
    }

    def visit(self, expr: SymbolicExpression) -> AstNode:
        """Convert a SymbolicExpression to an AstNode.

        Args:
            expr: The SymbolicExpression to convert. Can also be int for constants.

        Returns:
            An AstNode representing the same expression.
        """
        if expr is None:
            return None

        # Handle raw integers (e.g., from constant expressions)
        if isinstance(expr, int):
            return AstConstant(str(expr), expr)

        # Use Protocol for structural typing - survives hot reloads
        if not isinstance(expr, SymbolicExpressionProtocol):
            raise ValueError(f"Expected SymbolicExpression, got {type(expr).__name__}")

        if expr.is_leaf():
            return self._visit_leaf(expr)

        # Handle bool_to_int specially - converts ConstraintExpr to comparison opcode
        if expr.operation == "bool_to_int":
            return self._visit_bool_to_int(expr)

        # Convert children recursively
        left = self.visit(expr.left) if expr.left else None
        right = self.visit(expr.right) if expr.right else None

        # Map operation string to IDA opcode
        opcode = self._get_ida_opcode(expr.operation)

        return AstNode(opcode, left, right)

    def _visit_leaf(self, expr: SymbolicExpression) -> AstLeaf:
        """Convert a leaf SymbolicExpression to an AstLeaf.

        Args:
            expr: The leaf SymbolicExpression to convert.

        Returns:
            An AstLeaf or AstConstant representing the leaf.
        """
        if expr.is_constant():
            # Concrete constant (has a value)
            return AstConstant(expr.name, expr.value)
        elif getattr(expr, 'is_pattern_constant', False):
            # Pattern-matching constant (value computed from constraints)
            # Use AstConstant so it can receive computed value and create mop_n
            return AstConstant(expr.name, None)
        else:
            # Variable or pattern-matching placeholder
            return AstLeaf(expr.name)

    def _visit_bool_to_int(self, expr: SymbolicExpression) -> AstNode:
        """Convert a bool_to_int operation to an AstNode.

        This handles the bridge between boolean constraints (formulas) and
        arithmetic expressions (terms). Converts to IDA's SET* comparison opcodes.

        Args:
            expr: SymbolicExpression with operation="bool_to_int" and constraint set.

        Returns:
            AstNode representing the comparison operation (SETNZ, SETZ, etc.)
        """
        constraint = expr.constraint
        if constraint is None:
            raise ValueError("bool_to_int operation requires a constraint")

        if isinstance(constraint, ComparisonConstraintProtocol):
            left_node = self._visit_constraint_operand(constraint.left)
            right_node = self._visit_constraint_operand(constraint.right)

            # Map comparison operators to IDA SET opcodes
            op_map = {
                "ne": ida_hexrays.m_setnz,  # x != y → SETNZ(x - y)
                "eq": ida_hexrays.m_setz,   # x == y → SETZ(x - y)
                "lt": ida_hexrays.m_setb,   # x < y → SETB(x, y)
                "ge": ida_hexrays.m_setae,  # x >= y → SETAE(x, y)
            }

            ida_opcode = op_map.get(constraint.op_name)
            if ida_opcode is None:
                raise ValueError(f"Unsupported comparison for IDA pattern: {constraint.op_name}")

            # For != and ==, IDA uses SETNZ/SETZ with a single operand (difference)
            if constraint.op_name in ["ne", "eq"]:
                # Check if comparing to zero
                if (isinstance(constraint.right, SymbolicExpressionProtocol)
                    and constraint.right.is_constant()
                    and constraint.right.value == 0):
                    return AstNode(ida_opcode, left_node, None)
                # Check if right is raw int zero
                if constraint.right == 0:
                    return AstNode(ida_opcode, left_node, None)
                # Otherwise, create subtraction first
                diff_node = AstNode(ida_hexrays.m_sub, left_node, right_node)
                return AstNode(ida_opcode, diff_node, None)
            else:
                return AstNode(ida_opcode, left_node, right_node)

        if isinstance(constraint, EqualityConstraintProtocol):
            # x == y → SETZ(x - y)
            left_node = self._visit_constraint_operand(constraint.left)
            right_node = self._visit_constraint_operand(constraint.right)

            # Check if comparing to zero
            if (isinstance(constraint.right, SymbolicExpressionProtocol)
                and constraint.right.is_constant()
                and constraint.right.value == 0):
                return AstNode(ida_hexrays.m_setz, left_node, None)
            # Check if right is raw int zero
            if constraint.right == 0:
                return AstNode(ida_hexrays.m_setz, left_node, None)

            diff_node = AstNode(ida_hexrays.m_sub, left_node, right_node)
            return AstNode(ida_hexrays.m_setz, diff_node, None)

        raise ValueError(f"Unsupported constraint type for IDA pattern: {type(constraint)}")

    def _visit_constraint_operand(self, operand) -> Optional[AstNode]:
        """Visit a constraint operand, handling both SymbolicExpression and raw values.

        Args:
            operand: Either a SymbolicExpression or a raw value (int, etc.)

        Returns:
            AstNode for SymbolicExpression, AstConstant for raw values, None for None.
        """
        if operand is None:
            return None
        if isinstance(operand, SymbolicExpressionProtocol):
            return self.visit(operand)
        if isinstance(operand, int):
            return AstConstant(str(operand), operand)
        # Fallback: try to visit it
        return self.visit(operand)

    def _get_ida_opcode(self, operation: str) -> int:
        """Map a DSL operation string to an IDA opcode.

        Args:
            operation: The operation string (e.g., "add", "sub", "xor").

        Returns:
            The IDA opcode constant (e.g., ida_hexrays.m_add).

        Raises:
            ValueError: If the operation is not supported.
        """
        ida_attr = self._OPERATION_TO_IDA_ATTR.get(operation)
        if ida_attr is None:
            raise ValueError(f"Unknown operation: {operation}")

        # Get the IDA opcode directly from ida_hexrays
        opcode = getattr(ida_hexrays, ida_attr, None)
        if opcode is None:
            raise ValueError(f"Opcode {ida_attr} not found in ida_hexrays")

        return opcode


class IDAPatternAdapter:
    """Adapts a VerifiableRule for use with IDA pattern matching.

    This adapter wraps a pure VerifiableRule and provides the IDA-specific
    interface required by PatternOptimizer:
    - pattern_candidates property
    - get_valid_candidates() method
    - get_replacement() method
    - check_and_replace() method

    The rule itself remains pure (no IDA dependencies). All IDA-specific
    logic is encapsulated in this adapter.

    Example:
        >>> from d810.mba.rules import VerifiableRule
        >>> from d810.mba.backends.ida import IDAPatternAdapter
        >>>
        >>> # Create adapter from rule
        >>> adapter = IDAPatternAdapter(my_rule)
        >>>
        >>> # Use in IDA context
        >>> new_ins = adapter.check_and_replace(blk, instruction)
    """

    def __init__(self, rule: VerifiableRule):
        """Initialize the adapter with a rule.

        Args:
            rule: The VerifiableRule to adapt for IDA.
        """
        self.rule = rule
        self._pattern_candidates_cache: Optional[List[AstNode]] = None
        self._replacement_pattern_cache: Optional[AstNode] = None
        self._visitor = IDANodeVisitor()

    # ==========================================================================
    # Properties delegated to the underlying rule
    # ==========================================================================

    @property
    def name(self) -> str:
        """Return the rule name."""
        return self.rule.name

    @property
    def description(self) -> str:
        """Return the rule description."""
        return self.rule.description

    @property
    def maturities(self) -> list:
        """Return the maturities this rule applies to."""
        return getattr(self.rule, 'maturities', [])

    @maturities.setter
    def maturities(self, value: list) -> None:
        """Set the maturities this rule applies to."""
        self.rule.maturities = value

    @property
    def config(self) -> dict:
        """Return the rule configuration."""
        return getattr(self.rule, 'config', {})

    # ==========================================================================
    # IDA Pattern Matching Interface
    # ==========================================================================

    @property
    def pattern_candidates(self) -> List[AstNode]:
        """Get pattern candidates as AstNodes (lazy conversion from DSL).

        This property lazily converts the DSL SymbolicExpression to AstNode
        only when accessed in IDA context.

        Pattern Generation Strategy:
        1. Generate all commutative permutations of the base pattern
        2. Use egglog (if available) to verify which permutations are equivalent
        3. Convert all verified equivalent patterns to AstNode format
        4. Return them all as candidates for pattern matching

        This eliminates the need for manually defined *_Commuted rule variants.
        The egglog verification runs ONCE at startup (lazy on first access),
        not during decompilation, so performance overhead is minimal.
        """
        if self._pattern_candidates_cache is None:
            pattern = self.rule.pattern
            if pattern is None:
                self._pattern_candidates_cache = []
                return self._pattern_candidates_cache

            # Generate all commutative permutations
            permutations = _generate_commutative_permutations(pattern)
            logger.debug(
                "Rule %s: Generated %d commutative permutations",
                self.name, len(permutations)
            )

            # Use egglog to filter only equivalent patterns
            # (Falls back to returning all permutations if egglog unavailable)
            equivalent_patterns = _filter_equivalent_patterns(pattern, permutations)
            logger.debug(
                "Rule %s: %d patterns verified equivalent by egglog",
                self.name, len(equivalent_patterns)
            )

            # Convert to AstNode format
            candidates = []
            for expr in equivalent_patterns:
                try:
                    ast_node = self._visitor.visit(expr)
                    if ast_node is not None:
                        candidates.append(ast_node)
                except Exception as e:
                    logger.debug("Failed to convert pattern to AstNode: %s", e)

            self._pattern_candidates_cache = candidates
        return self._pattern_candidates_cache

    @property
    def PATTERN(self) -> Optional[AstNode]:
        """Get the pattern as an AstNode (PatternMatchingRule interface)."""
        pattern = self.rule.pattern
        if pattern is not None:
            return self._visitor.visit(pattern)
        return None

    @property
    def REPLACEMENT_PATTERN(self) -> Optional[AstNode]:
        """Get the replacement as an AstNode (PatternMatchingRule interface).

        Cached for performance - conversion only happens once.
        """
        if self._replacement_pattern_cache is None:
            replacement = self.rule.replacement
            if replacement is not None:
                self._replacement_pattern_cache = self._visitor.visit(replacement)
        return self._replacement_pattern_cache

    def get_valid_candidates(self, instruction, stop_early: bool = True) -> list:
        """Match the instruction against this rule's patterns.

        Args:
            instruction: The IDA minsn_t instruction to match.
            stop_early: If True, return after first match.

        Returns:
            List of matched candidate AstNodes.
        """
        valid_candidates = []
        tmp = minsn_to_ast(instruction)
        if tmp is None:
            return []

        for candidate_pattern in self.pattern_candidates:
            if not candidate_pattern:
                continue
            # Use a read-only check first for structural matching (no mops copied)
            if not candidate_pattern.check_pattern_and_copy_mops(tmp, read_only=True):
                continue
            # Create a mutable copy and populate mops
            mutable_candidate = candidate_pattern.clone()
            if not mutable_candidate.check_pattern_and_copy_mops(tmp):
                continue
            # Check constraints AFTER mops are populated
            # This ensures constraint checks have access to the matched mops
            if not self._check_candidate(mutable_candidate):
                continue
            valid_candidates.append(mutable_candidate)
            if stop_early:
                return valid_candidates
        return valid_candidates

    def get_replacement(self, candidate) -> Optional[Any]:
        """Create a replacement instruction from a matched candidate.

        Args:
            candidate: The matched AstNode or AstLeaf candidate.

        Returns:
            A new minsn_t instruction, or None if replacement failed.
        """
        repl_pat = self.REPLACEMENT_PATTERN
        if not repl_pat:
            logger.debug(f"No replacement pattern for rule {self.name}")
            return None

        # Handle AstLeaf candidates specially - they don't have leafs_by_name
        if isinstance(candidate, AstLeafProtocol):
            candidate_for_update = _LeafWrapper(candidate)
            is_ok = repl_pat.update_leafs_mop(candidate_for_update)
        else:
            is_ok = repl_pat.update_leafs_mop(candidate)

        if not is_ok:
            logger.debug(f"Failed to update leaf mops for rule {self.name}")
            return None

        if not candidate.ea:
            logger.debug(f"No EA for candidate in rule {self.name}")
            return None

        if not self._materialize_replacement_constants(repl_pat, candidate):
            logger.debug(
                "Failed to materialize replacement constants for rule %s",
                self.name,
            )
            return None

        try:
            new_ins = repl_pat.create_minsn(candidate.ea, candidate.dst_mop)
        except AstEvaluationException as exc:
            logger.debug(
                "Replacement creation failed for rule %s at 0x%x: %s",
                self.name,
                candidate.ea,
                exc,
            )
            return None
        return new_ins

    def _materialize_replacement_constants(self, repl_pat, candidate) -> bool:
        """Ensure computed AstConstant leaves have concrete mops before emission."""
        try:
            leafs = repl_pat.get_leaf_list()
        except Exception:
            return True

        candidate_leafs = getattr(candidate, "leafs_by_name", {}) or {}
        dst_mop = getattr(candidate, "dst_mop", None)

        for leaf in leafs:
            if not isinstance(leaf, AstConstantProtocol):
                continue
            if getattr(leaf, "mop", None) is not None:
                continue

            value = getattr(leaf, "value", None)
            size = getattr(leaf, "expected_size", None)

            source_leaf = candidate_leafs.get(getattr(leaf, "name", None))
            if source_leaf is not None:
                source_mop = getattr(source_leaf, "mop", None)
                if source_mop is not None and source_mop.t == ida_hexrays.mop_n:
                    leaf.mop = MopSnapshot.from_mop(source_mop)
                    if hasattr(leaf, "expected_value"):
                        leaf.expected_value = source_mop.nnn.value
                    if hasattr(leaf, "expected_size"):
                        leaf.expected_size = source_mop.size
                    continue
                if value is None:
                    value = getattr(source_leaf, "value", None)
                if value is None:
                    value = getattr(source_leaf, "expected_value", None)
                if size is None:
                    size = getattr(source_leaf, "expected_size", None)

            if value is None:
                return False

            if size is None:
                size = getattr(leaf, "dest_size", None)
            if size is None and dst_mop is not None:
                size = getattr(dst_mop, "size", None)
            if size is None or int(size) <= 0:
                size = 1

            cst_mop = ida_hexrays.mop_t()
            cst_mop.make_number(int(value), int(size))
            leaf.mop = cst_mop
            if hasattr(leaf, "expected_value"):
                leaf.expected_value = int(value)
            if hasattr(leaf, "expected_size"):
                leaf.expected_size = int(size)

        return True

    def check_and_replace(self, blk, instruction) -> Optional[Any]:
        """Check if this rule matches and return a replacement instruction.

        This is the main entry point called by the optimizer system.

        Args:
            blk: The microcode block (mblock_t).
            instruction: The instruction to check (minsn_t).

        Returns:
            A new minsn_t if the rule matched, None otherwise.
        """
        valid_candidates = self.get_valid_candidates(instruction, stop_early=True)
        if len(valid_candidates) == 0:
            return None
        new_instruction = self.get_replacement(valid_candidates[0])
        return new_instruction

    def check_pattern_and_replace(self, candidate_pattern, test_ast) -> Optional[Any]:
        """Check if this rule matches a pattern and return a replacement.

        This method is used by PatternOptimizer's pattern storage lookup system.

        Args:
            candidate_pattern: A candidate AstNode pattern from the pattern storage.
            test_ast: The AstNode converted from the microcode instruction.

        Returns:
            A new minsn_t if the rule matched, None otherwise.
        """
        # First, check if the pattern matches the test AST
        if not candidate_pattern.check_pattern_and_copy_mops(test_ast):
            return None

        # Then check candidate-level constraints
        if not self._check_candidate(candidate_pattern):
            return None

        # Finally, create the replacement instruction
        new_instruction = self.get_replacement(candidate_pattern)
        return new_instruction

    def check_candidate(self, candidate) -> bool:
        """Public interface for nomut matching path constraint checking.

        This method is called by the nomut hot path in handler.py when
        D810_NOMUT_MATCHING=1. It delegates to _check_candidate().

        Args:
            candidate: An AstNode or BindingsProxy that matched the pattern

        Returns:
            True if all constraints are satisfied, False otherwise
        """
        return self._check_candidate(candidate)

    # ==========================================================================
    # Internal constraint checking (delegates to rule)
    # ==========================================================================

    def _check_candidate(self, candidate) -> bool:
        """Check if a candidate AstNode matches this rule's constraints.

        Delegates to the underlying rule's check_candidate method if available,
        otherwise performs basic constraint checking.

        Args:
            candidate: An AstNode that structurally matches PATTERN

        Returns:
            True if all constraints are satisfied, False otherwise
        """
        # If rule has check_candidate, use it
        if hasattr(self.rule, 'check_candidate'):
            return self.rule.check_candidate(candidate)

        # Fallback: check runtime constraints directly
        return self._check_runtime_constraints(candidate)

    def _check_runtime_constraints(self, candidate) -> bool:
        """Check runtime constraints against a matched candidate.

        Args:
            candidate: The matched AstNode with variable bindings.

        Returns:
            True if all constraints pass, False otherwise.
        """
        constraints = getattr(self.rule, 'CONSTRAINTS', [])
        if not constraints:
            return True

        # Build match context from candidate's matched variables
        # AstNode stores matched leaves in leafs_by_name after pattern matching
        match_context = {}
        if hasattr(candidate, 'leafs_by_name') and candidate.leafs_by_name:
            match_context = candidate.leafs_by_name
        elif hasattr(candidate, 'mop_dict'):
            match_context = candidate.mop_dict
        elif hasattr(candidate, 'get_z3_vars'):
            match_context = candidate.get_z3_vars({})

        # Add candidate itself to context
        match_context["_candidate"] = candidate

        # Check each constraint
        for constraint in constraints:
            try:
                # Check if this is a ConstraintExpr
                if is_constraint_expr(constraint):
                    if not constraint.check(match_context):
                        return False
                elif callable(constraint):
                    if not constraint(match_context):
                        return False
            except Exception as e:
                logger.debug(f"Constraint check failed for {self.name}: {e}")
                return False

        return True

    # ==========================================================================
    # Configuration interface (required by d810 optimizer system)
    # ==========================================================================

    def configure(self, kwargs: Dict[str, Any]) -> None:
        """Configure this rule with options from a JSON config.

        Args:
            kwargs: Configuration dictionary from the JSON project file.
        """
        if hasattr(self.rule, 'configure'):
            self.rule.configure(kwargs)

    def set_log_dir(self, log_dir: str) -> None:
        """Set the log directory for this rule.

        Args:
            log_dir: Path to the log directory.
        """
        if hasattr(self.rule, 'set_log_dir'):
            self.rule.set_log_dir(log_dir)


def adapt_rules(rules: List[VerifiableRule]) -> List[IDAPatternAdapter]:
    """Wrap a list of rules with IDAPatternAdapter for IDA integration.

    Args:
        rules: List of VerifiableRule instances.

    Returns:
        List of IDAPatternAdapter instances wrapping the rules.

    Example:
        >>> from d810.mba.rules import VerifiableRule
        >>> from d810.mba.backends.ida import adapt_rules
        >>>
        >>> rule_instances = VerifiableRule.instantiate_all()
        >>> ida_rules = adapt_rules(rule_instances)
    """
    return [IDAPatternAdapter(rule) for rule in rules]


__all__ = [
    'IDANodeVisitor',
    'IDAPatternAdapter',
    'adapt_rules',
]
