import logging
from typing import Optional

from GramAddict.core.perception.action_memory import ActionMemory
from GramAddict.core.perception.intent_resolver import IntentResolver
from GramAddict.core.perception.semantic_evaluator import SemanticEvaluator
from GramAddict.core.perception.spatial_parser import SpatialNode, SpatialParser

logger = logging.getLogger(__name__)


class TelepathicEngine:
    """
    The Spatial Perception Engine (Facade)

    Replaces the legacy 2100+ LoC Regex/XML engine with a structural Topo-Graph approach.
    Delegates all responsibilities to SOLID, testable sub-modules.
    """

    _instance = None

    # Delegate Modules
    _parser: SpatialParser
    _resolver: IntentResolver
    _memory: ActionMemory
    _evaluator: SemanticEvaluator

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    @classmethod
    def reset(cls):
        cls._instance = None

    def __init__(self):
        self._parser = SpatialParser()
        self._resolver = IntentResolver()
        self._memory = ActionMemory()
        self._evaluator = SemanticEvaluator()

    def wipe(self):
        """Wipe all persistent state for Blank Start."""
        logger.warning("🔥 [TelepathicEngine] Wiping state (delegating to ActionMemory layer).")
        try:
            self._memory.ui_memory.wipe_collection()
        except Exception as e:
            logger.warning(f"Could not wipe Qdrant collection: {e}")

    # ──────────────────────────────────────────────
    # Core Resolution Engine
    # ──────────────────────────────────────────────

    def find_best_node(
        self, xml_string: str, intent_description: str, device=None, track: bool = True, **kwargs
    ) -> Optional[dict]:
        print("FIND_BEST_NODE CALLED")

        """
        Public facade for resolving a node.
        Translates Android UI bounds into standard GramAddict node dicts.
        """
        logger.debug(f"🧠 [SpatialEngine] Resolving intent: '{intent_description}'")

        # 0. DM Thread Guard: Block profile intents inside DM threads
        is_dm_thread = "direct_thread_header" in xml_string or "row_thread_composer_edittext" in xml_string
        if is_dm_thread:
            profile_keywords = ["profile", "follow", "first image", "grid", "avatar", "story ring", "feed"]
            if any(k in intent_description.lower() for k in profile_keywords):
                logger.warning(f"🛡️ [DM Guard] Blocked profile/feed intent '{intent_description}' inside DM thread.")
                return {"blocked_by_dm_thread": True}

        # 0.5 Comments Disabled Guard
        if "comment" in intent_description.lower():
            if "comments are turned off" in xml_string.lower():
                logger.warning("🛡️ [Comment Guard] Comments are disabled on this post.")
                return {"skip": True, "semantic": "comments disabled"}

        # 1.25 Structural Fast-Paths (Deterministically bypass VLM for fixed UI elements)
        nodes_dicts = self._extract_semantic_nodes(xml_string)
        fast_node = self._structural_fast_path(intent_description, nodes_dicts, kwargs.get("skip_positions"), xml_string)
        if fast_node:
            return fast_node

        # 1. Parse into Spatial Topology
        root = self._parser.parse(xml_string)
        if not root:
            logger.warning("Failed to parse Spatial Topology.")
            return None

        # 2. Extract interactable candidates
        candidates = self._parser.get_clickable_nodes(root)

        # 3. Resolve intent against candidates
        best_node = self._resolver.resolve(intent_description, candidates)

        if not best_node:
            logger.warning(f"No viable nodes found for intent: '{intent_description}'")
            return None

        # 3.5 Following Button Guard
        if "follow" in intent_description.lower() and "unfollow" not in intent_description.lower():
            semantic = (
                (best_node.text or "") + " " + (best_node.content_desc or "") + " " + (best_node.resource_id or "")
            )
            semantic = semantic.lower()
            if "following" in semantic or "gefolgt" in semantic or "requested" in semantic or "angefragt" in semantic:
                return {"skip": True, "semantic": "already_followed"}

        # 4. Track action
        if track:
            self._memory.track_click(intent_description, best_node, xml_string)

        # Translate to old GramAddict dict format for backward compatibility
        return self._translate_node(best_node)

    def _translate_node(self, node: SpatialNode) -> dict:
        """Adapts the new SpatialNode to the legacy GramAddict dictionary format."""
        return {
            "x": node.center_x,
            "y": node.center_y,
            "bounds": f"[{node.x1},{node.y1}][{node.x2},{node.y2}]",
            "text": node.text,
            "description": node.content_desc,
            "id": node.resource_id,
            "class": node.class_name,
            "original_attribs": node.to_dict(),
        }

    def _extract_semantic_nodes(
        self, xml_string: str, semantic_query: str = None, threshold: float = 0.7, **kwargs
    ) -> list:
        """
        Legacy proxy for extracting semantic nodes.
        If a query is provided, it tries to find the best node matching it.
        If no query is provided, it returns all clickable nodes to support zero-latency feed loops.
        """
        if semantic_query:
            best_node = self.find_best_node(xml_string, semantic_query, **kwargs)
            return [best_node] if best_node else []

        # If no query, return all interactable nodes as dicts (used by zero latency loops)
        root = self._parser.parse(xml_string)
        if not root:
            return []
        nodes = self._parser.get_clickable_nodes(root)
        return [self._translate_node(n) for n in nodes]

    def _structural_fast_path(self, intent_description: str, nodes: list, skip_positions: set = None, xml_string: str = "") -> Optional[dict]:
        if skip_positions is None:
            skip_positions = set()

        intent_lower = intent_description.lower()
        if "first image in explore grid" in intent_lower:
            grid_items = [
                n
                for n in nodes
                if n.get("y", 9999) < 2000
                and (
                    "grid card layout container" in (n.get("semantic_string", "") or "").lower()
                    or "image button" in (n.get("semantic_string", "") or "").lower()
                )
                and (n.get("x", -1), n.get("y", -1)) not in skip_positions
            ]
            if grid_items:
                # Sort by y (row) then by x (col)
                grid_items.sort(key=lambda n: (n.get("y", 9999), n.get("x", 9999)))
                return grid_items[0]

        # --- DM Engine Structural Fast Paths ---
        if "find the message input text field" in intent_lower:
            for n in nodes:
                if "row_thread_composer_edittext" in n.get("id", "") or "row_thread_composer_edittext" in n.get("resource_id", ""):
                    return n

        if "find the send message button" in intent_lower:
            for n in nodes:
                if "row_thread_composer_button_send" in n.get("id", "") or "row_thread_composer_button_send" in n.get("resource_id", ""):
                    return n

        if "find unread message threads" in intent_lower:
            # We must be extremely strict here: It's only unread if it has the "unread" text or indicator dot
            unread_candidates = []
            
            # 1. Find all explicit unread dots in the UI
            dot_nodes = [
                d for d in nodes 
                if "thread_indicator_status_dot" in (d.get("id", "") or d.get("resource_id", ""))
            ]
            
            import re
            
            for n in nodes:
                is_unread = False
                res_id = n.get("id", "") or n.get("resource_id", "")
                
                if "row_inbox_container" in res_id and (n.get("x", -1), n.get("y", -1)) not in skip_positions:
                    content_desc = (n.get("description", "") or "").lower()
                    semantic = (n.get("semantic_string", "") or "").lower()
                    
                    # 1. Check for explicit 'unread' in description
                    if "unread" in content_desc or "unread" in semantic:
                        is_unread = True
                    
                    # 2. Check if an unread dot falls inside this container's bounds
                    if not is_unread and dot_nodes:
                        bounds_str = n.get("bounds", "")
                        m = re.match(r"\[\d+,(\d+)\]\[\d+,(\d+)\]", bounds_str)
                        if m:
                            y1, y2 = int(m.group(1)), int(m.group(2))
                            for dot in dot_nodes:
                                dot_y = dot.get("y", -1)
                                if y1 <= dot_y <= y2:
                                    is_unread = True
                                    break
                        
                    if is_unread and n.get("y", 0) > 200:
                        unread_candidates.append(n)
            
            if unread_candidates:
                unread_candidates.sort(key=lambda n: n.get("y", 9999))
                return unread_candidates[0]

        if "find the last received message text" in intent_lower:
            msg_candidates = []
            for n in nodes:
                res_id = n.get("id", "") or n.get("resource_id", "")
                # The actual message text bubble
                if "direct_text_message_text_view" in res_id or "message_content" in res_id:
                    msg_candidates.append(n)
            
            if msg_candidates:
                # Sort by y descending (bottom-most message is the last one)
                msg_candidates.sort(key=lambda n: n.get("y", 0), reverse=True)
                return msg_candidates[0]

        return None

    # ──────────────────────────────────────────────
    # Action Memory Delegation
    # ──────────────────────────────────────────────

    def confirm_click(self, intent: str = None):
        self._memory.confirm_click(intent)

    def reject_click(self, intent: str = None):
        self._memory.reject_click(intent)

    def decay_click(self, intent: str = None):
        self._memory.reject_click(intent)  # Alias to reject

    def verify_success(self, intent: str, post_click_xml: str, device=None, confidence: float = 0.0) -> bool:
        pre_click_xml = ""
        if self._memory._last_click_context:
            pre_click_xml = self._memory._last_click_context.get("xml_context", "")
        return self._memory.verify_success(intent, pre_click_xml, post_click_xml, device=device, confidence=confidence)

    # ──────────────────────────────────────────────
    # Semantic Evaluator Delegation
    # ──────────────────────────────────────────────

    def evaluate_grid_visuals(self, device, persona_interests: list[str]) -> Optional[dict]:
        root = self._parser.parse(device.dump_hierarchy())
        if not root:
            return None

        # Find grid-like visual nodes
        all_nodes = self._parser.get_all_nodes(root)
        grid_candidates = [
            n
            for n in all_nodes
            if not n.text and ("photo" in n.content_desc.lower() or "video" in n.content_desc.lower() or n.area > 50000)
        ]

        best = self._evaluator.evaluate_grid_visuals(device, persona_interests, grid_candidates)
        if best:
            return self._translate_node(best)
        return None

    def evaluate_post_vibe(self, device, persona_interests: list[str]) -> Optional[dict]:
        return self._evaluator.evaluate_post_vibe(device, persona_interests)

    def evaluate_profile_vibe(self, device, persona_interests: list[str]) -> Optional[dict]:
        return self._evaluator.evaluate_profile_vibe(device, persona_interests)

    def classify_screen_content(self, xml_hierarchy: str, target_class: str) -> Optional[str]:
        return self._evaluator.classify_screen_content(xml_hierarchy, target_class)

    def _is_forbidden_action(self, node: dict) -> bool:
        """
        Layer 5: Forbids clicks on specific dangerous elements like the quick_capture root container.
        """
        res_id = node.get("resource_id", "").lower()
        if "quick_capture" in res_id:
            return True
        return False

    def _get_current_username(self) -> str:
        return ""

    def _is_instagram_context(self, xml_string: str) -> bool:
        return "com.instagram.android" in xml_string

    def _structural_sanity_check(self, node: dict, intent_description: str, screen_height: int = 2400) -> bool:
        """
        Structural guard to ensure nodes are in valid locations for their intents.
        """
        intent = intent_description.lower()
        y = node.get("y", 0)
        semantic = (node.get("semantic_string", "") or "").lower()

        # 1. Navigation Tab Guard (Must be at the bottom)
        nav_intents = [
            "tap direct message icon inbox",
            "tap inbox",
            "tap heart icon notifications",
            "tap home tab",
            "tap explore tab",
            "tap reels tab",
            "tap profile tab",
            "tap messages tab",
        ]
        is_nav_intent = any(n in intent for n in nav_intents)
        if is_nav_intent:
            if y < screen_height * 0.85:
                return False
            return True

        # 2. Block non-nav intents from clicking in the nav zone
        if y >= screen_height * 0.85:
            # Not a nav intent, but trying to click the nav bar
            return False

        # 3. Post Username Guard
        if "post username" in intent:
            if "story" in semantic and y < screen_height * 0.2:
                # E.g. "Your Story" circle at the top
                return False
            return True

        # 4. Profile Picture/Story Ring Guard
        if "story ring" in intent or "avatar" in intent:
            current_user = self._get_current_username()
            if current_user and current_user in semantic:
                # Don't click our own story
                return False

        # 5. Grid Item Guard
        if "grid item" in intent:
            if y > screen_height * 0.8:  # Too low
                return False
            if "tab" in semantic and "grid" not in semantic:
                # Not a grid item
                return False

        # 6. Block massive layout containers UNLESS specifically looking for feed/post
        MAX_CONTAINER_AREA = 500000
        area = node.get("area", 0)

        is_feed_or_post = "feed" in intent or "post" in intent
        is_grid_item = "grid" in intent or "list" in intent

        if area > MAX_CONTAINER_AREA:
            # If specifically looking for a grid item, REJECT massive containers (e.g., RecyclerViews)
            if is_grid_item:
                return False
            # Otherwise, only allow if looking for feed/post
            if not is_feed_or_post:
                return False

        return True
