import logging
import re
import xml.etree.ElementTree as ET

logger = logging.getLogger(__name__)


class HoneypotRadome:
    """
    Project Dojo: The Anti-Test Sensor.
    Filters the Android XML Hierarchy to remove "invisible traps" and honeypots
    that Instagram uses to detect deterministic bots (e.g., 1x1 pixel buttons,
    off-screen elements with clickable=True).
    """

    def __init__(self, display_width=1080, display_height=2400):
        self.display_width = display_width
        self.display_height = display_height
        self.bounds_pattern = re.compile(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]")

    def sanitize_xml(self, xml_string: str) -> str:
        """
        Parses raw UI Automator XML and strips out fake or impossible nodes.
        Returns the sanitized XML string.
        """
        try:
            # Android XML dumps often have multiple root nodes or formatting issues,
            # let's try reading it safely.
            # Handle potential encoding issues from dump_hierarchy
            clean_xml = xml_string.replace("&#10;", "").replace("&#13;", "")

            root = ET.fromstring(clean_xml)
            removed_count = self._filter_node(root)

            if removed_count > 0:
                logger.info(
                    f"🛡️ [Honeypot Radome] Stripped {removed_count} phantom nodes from view.",
                    extra={"color": "\x1b[33m"},
                )

            # Convert back to string
            return ET.tostring(root, encoding="unicode")
        except Exception as e:
            logger.warning(f"🛡️ [Honeypot Radome] XML Parse failed, returning raw. Err: {e}")
            return xml_string

    def _filter_node(self, node: ET.Element) -> int:
        removed = 0
        children_to_remove = []

        for child in node:
            if self._is_honeypot(child):
                children_to_remove.append(child)
                removed += 1
            else:
                removed += self._filter_node(child)

        for child in children_to_remove:
            node.remove(child)

        return removed

    def _is_honeypot(self, node: ET.Element) -> bool:
        """
        Detects if a node is physically impossible to be clicked by a human.
        """
        bounds = node.get("bounds")
        if not bounds:
            return False

        match = self.bounds_pattern.match(bounds)
        if not match:
            return False

        x1, y1, x2, y2 = map(int, match.groups())
        width = x2 - x1
        height = y2 - y1

        is_clickable = node.get("clickable", "false").lower() == "true"

        # Rule 1: The Zero-Point Trap (Element is exactly on 0,0 with no dimensions)
        if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0:
            return True

        # Rule 2: The Micro-Pixel Trap (Bot detectors often use 1x1 or 2x2 clickable overlay pixels)
        if is_clickable and width <= 2 and height <= 2:
            return True

        # Rule 3: The Off-Screen Trap (Buttons rendered wildly out of bounds to bait mindless loops)
        if x1 >= self.display_width or y1 >= self.display_height:
            return True

        # Rule 4: The Negative Coordinate Trap
        if x2 <= 0 or y2 <= 0:
            return True

        # Rule 5: The Transparent Interceptor (Giant invisible overlays capturing touches)
        # If a clickable element takes up >90% of screen but has no text, description, or id, it's a touch trap.
        has_text = bool(node.get("text", ""))
        has_desc = bool(node.get("content-desc", ""))
        has_id = bool(node.get("resource-id", ""))
        if is_clickable and width >= (self.display_width * 0.9) and height >= (self.display_height * 0.9):
            if not has_text and not has_desc and not has_id:
                return True

        # Rule 6: Android Accessibility Trap (A node is clickable but explicitly not visible)
        # Sometimes uiautomator injects 'visible-to-user' manually, or it has bounds but isn't enabled.
        if is_clickable and node.get("visible-to-user", "true").lower() == "false":
            return True

        return False
