import hashlib
import logging
import re
import xml.etree.ElementTree as ET
from enum import Enum
from typing import Any, Dict

logger = logging.getLogger(__name__)


class ScreenType(Enum):
    HOME_FEED = "home_feed"
    EXPLORE_GRID = "explore_grid"
    REELS_FEED = "reels_feed"
    AUDIO_PAGE = "audio_page"
    OWN_PROFILE = "own_profile"
    OTHER_PROFILE = "other_profile"
    POST_DETAIL = "post_detail"
    STORY_VIEW = "story_view"
    DM_INBOX = "dm_inbox"
    DM_THREAD = "dm_thread"
    SEARCH_RESULTS = "search_results"
    FOLLOW_LIST = "follow_list"
    COMMENTS = "comments"
    MODAL = "modal"
    FOREIGN_APP = "foreign_app"
    NOTIFICATIONS = "notifications"
    UNKNOWN = "unknown"


class ScreenIdentity:
    """
    Understands what screen the bot is on by analyzing the XML dump.
    NO hardcoded states — purely structural analysis.

    This is the bot's EYES. It answers: "What do I see right now?"
    """

    def __init__(self, bot_username: str):
        self.bot_username = bot_username.lower()
        try:
            from GramAddict.core.qdrant_memory import ScreenMemoryDB

            self.screen_memory = ScreenMemoryDB()
            if self.screen_memory:
                self.screen_memory.purge_stale_screens()
        except ImportError:
            self.screen_memory = None

    def identify(self, xml_dump: str, screenshot_b64: str = None) -> Dict[str, Any]:
        """
        Analyzes an XML dump and returns a complete screen description.

        Returns:
            {
                'screen_type': ScreenType,
                'available_actions': ['tap like button', 'tap explore tab', ...],
                'selected_tab': 'feed_tab' | 'search_tab' | ...,
                'context': {'username': '...', 'post_count': '...', ...}
            }
        """
        if not xml_dump or not isinstance(xml_dump, str):
            return self._empty_screen()

        try:
            clean = re.sub(r"<\?xml.*?\?>", "", xml_dump).strip()
            root = ET.fromstring(clean)
        except Exception:
            return self._empty_screen()

        # Extract structural signals
        packages = set()
        resource_ids = set()
        content_descs = []
        texts = []
        selected_tab = None
        clickable_elements = []

        app_id = "com.instagram.android"

        for elem in root.iter("node"):
            pkg = elem.get("package", "")
            if pkg:
                packages.add(pkg)

            rid = elem.get("resource-id", "").strip()
            text = elem.get("text", "").strip()
            desc = elem.get("content-desc", "").strip()
            clickable = elem.get("clickable", "false") == "true"
            selected = elem.get("selected", "false") == "true"
            long_clickable = elem.get("long-clickable", "false") == "true"
            class_name = elem.get("class", "").strip()
            bounds = elem.get("bounds", "")

            if rid:
                # Normalize: "com.instagram.android:id/feed_tab" → "feed_tab"
                short_id = rid.split("/")[-1] if "/" in rid else rid
                resource_ids.add(short_id)

                # Track which tab is selected
                if selected and short_id in (
                    "feed_tab",
                    "search_tab",
                    "clips_tab",
                    "profile_tab",
                    "direct_tab",
                    "news_tab",
                ):
                    selected_tab = short_id

            if text:
                texts.append(text)
            if desc:
                content_descs.append(desc)

            if clickable and bounds:
                match = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds)
                if match:
                    left, t, r, b = map(int, match.groups())
                    cx, cy = (left + r) // 2, (t + b) // 2
                    clickable_elements.append(
                        {
                            "text": text,
                            "desc": desc,
                            "id": rid.split("/")[-1] if "/" in rid else rid,
                            "class": class_name,
                            "long_clickable": long_clickable,
                            "x": cx,
                            "y": cy,
                            "bounds": bounds,
                            "bottom": b,
                        }
                    )

        from GramAddict.core.situational_awareness import SituationalAwarenessEngine

        sae = SituationalAwarenessEngine.get_instance()
        signature = sae._compress_xml(xml_dump) if sae else self._compute_signature(resource_ids, content_descs, texts)

        # ── Foreign app check ──
        if app_id not in packages:
            return {
                "screen_type": ScreenType.FOREIGN_APP,
                "available_actions": ["press back", "force start instagram"],
                "selected_tab": None,
                "context": {"packages": list(packages)},
                "signature": signature,
            }

        desc_lower = " ".join(content_descs).lower()
        text_lower = " ".join(texts).lower()
        ids_str = " ".join(resource_ids).lower()

        # ── Identify screen type from structural signals ──
        screen_type = self._classify_screen(
            resource_ids, content_descs, texts, selected_tab, desc_lower, text_lower, ids_str, signature, screenshot_b64
        )

        # ── Extract available actions from clickable elements ──
        available_actions = self._extract_available_actions(
            clickable_elements, resource_ids, content_descs, texts, screen_type
        )

        # ── Extract context ──
        context = self._extract_context(content_descs, texts, resource_ids, screen_type)

        return {
            "screen_type": screen_type,
            "available_actions": available_actions,
            "selected_tab": selected_tab,
            "context": context,
            "signature": signature,
            "resource_ids": resource_ids,
        }

    def _classify_screen(
        self, ids, descs, texts, selected_tab, desc_lower, text_lower, ids_str, signature=None, screenshot_b64=None
    ):
        """
        Classify screen type using Semantic Memory with LLM fallback — NO hardcoded states."""

        # Priority 0: Fetch Qdrant Semantic Cache
        # We fetch this early to see if there is a 'NORMAL' override for the MODAL check.
        # We DO NOT let this override deterministic structural heuristics! Fuzzy vector matching
        # can easily confuse HOME_FEED and OWN_PROFILE if the bottom navigation bar is identical.
        cached_type_str = None
        if signature and self.screen_memory and self.screen_memory.is_connected:
            cached_type_str = self.screen_memory.get_screen_type(signature, similarity_threshold=0.98)

        is_normal_override = cached_type_str == "NORMAL"

        # Priority 1: High-Confidence Structural Fast-Paths (resource-id invariants)
        # Resource IDs are architectural constants of the APK, not localized text.
        # This complies with the Zero Maintenance rule while preventing VLM hallucinations.

        # Story View
        if "reel_viewer_root" in ids_str or "story_viewer_root" in ids_str or "reel_viewer_media_container" in ids_str:
            return ScreenType.STORY_VIEW

        # Reels Feed
        if selected_tab == "clips_tab" or "clips_video_container" in ids_str or "clips_slider" in ids_str:
            return ScreenType.REELS_FEED

        # Direct Messages
        if "direct_inbox_action_bar" in ids_str or "inbox_refreshable_thread_list_recyclerview" in ids_str:
            return ScreenType.DM_INBOX
        if (
            "direct_text_message_text_view" in ids_str
            or "message_content" in ids_str
            or "thread_title" in ids_str
            or "message_list" in ids_str
        ):
            return ScreenType.DM_THREAD

        # Notifications
        if selected_tab == "news_tab" or "notifications_list" in ids_str or "activity_feed_root" in ids_str:
            return ScreenType.NOTIFICATIONS

        # ── PROFILE DETECTION (must happen BEFORE tab-based HOME/EXPLORE) ──
        # WHY: OWN_PROFILE and OTHER_PROFILE have profile_tab visible but NOT selected.
        # If we check selected_tab == "feed_tab" first, profiles without a selected tab
        # fall through to the Qdrant cache, which can be poisoned.
        #
        # Structural Anchor: profile_tab selected=true → OWN_PROFILE (absolute invariant)
        # When viewing someone else's profile, profile_tab is NEVER selected.
        if selected_tab == "profile_tab":
            return ScreenType.OWN_PROFILE

        # Profile screens with explicit header markers
        if "profile_header" in ids_str or "profile_tab_layout" in ids_str:
            # OWN_PROFILE has "Edit Profile" or the tab switcher (Posts/Reels/Tagged)
            if "profile_header_edit_profile_button" in ids_str or "layout_button_group_view_switcher" in ids_str:
                return ScreenType.OWN_PROFILE
            # OTHER_PROFILE has "Message" or "Follow" buttons
            if (
                "profile_header_message_button" in ids_str
                or "profile_header_follow_button" in ids_str
                or "button_message" in ids_str
            ):
                return ScreenType.OTHER_PROFILE
            # Fallback: If profile_header exists but neither edit-profile nor follow-button,
            # it's likely OWN_PROFILE in a non-standard state (e.g. professional dashboard).
            # Better to guess OWN_PROFILE than to let Qdrant poison us with OTHER_PROFILE.
            logger.debug(
                "📐 [ScreenIdentity] Profile header detected but no edit/follow button. Defaulting to OWN_PROFILE."
            )
            return ScreenType.OWN_PROFILE

        # Explore Grid
        if selected_tab == "search_tab":
            return ScreenType.EXPLORE_GRID

        # Home Feed
        if selected_tab == "feed_tab":
            return ScreenType.HOME_FEED

        # Post Detail: Typically has comment box or media note view but NO bottom tab layout
        if "media_note_view" in ids_str or "comment" in ids_str or "row_feed_button_comment" in ids_str:
            if "feed_tab" not in ids_str and "profile_tab" not in ids_str:
                return ScreenType.POST_DETAIL

        # Comments
        if "layout_comment_thread" in ids_str or "comment_thread_recyclerview" in ids_str:
            return ScreenType.COMMENTS

        # Follow List
        if (
            "follow_list_container" in ids_str
            or "layout_user_list" in ids_str
            or "layout_user_row" in ids_str
            or "follow_list_username" in ids_str
        ):
            return ScreenType.FOLLOW_LIST

        # Priority 2: Modal / Overlay Overrides
        if not is_normal_override:
            if "bottom_sheet_container" in ids_str or "action_sheet" in ids_str:
                return ScreenType.MODAL

        # Priority 3: Cached Semantic Type (If deterministic heuristics failed)
        # GUARD: Profile types MUST be resolved by structural fast-paths above.
        # If they weren't, the cache is poisoned. Reject profile cache hits.
        _STRUCTURALLY_GATED_TYPES = (
            ScreenType.STORY_VIEW,
            ScreenType.REELS_FEED,
            ScreenType.OWN_PROFILE,
            ScreenType.OTHER_PROFILE,
        )
        if cached_type_str and cached_type_str != "NORMAL":
            try:
                cached_type = ScreenType[cached_type_str]
                if cached_type in _STRUCTURALLY_GATED_TYPES:
                    logger.warning(
                        f"⚠️ [ScreenIdentity] Rejecting cached {cached_type.name} — "
                        f"this type MUST be resolved structurally. Cache is unreliable."
                    )
                else:
                    return cached_type
            except KeyError:
                pass

        # Priority 4: Semantic VLM Classification Fallback
        if not screenshot_b64 and getattr(self, "device", None) is not None:
            screenshot_b64 = self.device.get_screenshot_b64()

        from GramAddict.core.config import Config
        from GramAddict.core.llm_provider import query_telepathic_llm

        cfg = Config()
        url = (
            getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate")
            if hasattr(cfg, "args")
            else "http://localhost:11434/api/generate"
        )
        model = getattr(cfg.args, "ai_telepathic_model", "llava:latest") if hasattr(cfg, "args") else "llava:latest"

        layout_context = (
            f"Selected Tab: {selected_tab}\nResource IDs: {list(ids)}\nVisible Texts context: {texts[:10]}\n"
        )
        prompt = (
            f"Identify the Instagram screen layout type based on the provided screenshot and structural signals.\n"
            f"Valid types: {[t.name for t in ScreenType]}\n"
            f"Context:\n{layout_context}\n"
            f"Reply ONLY with the exact matching enum Type Name string, or 'UNKNOWN' if no type matches."
        )

        try:
            response = query_telepathic_llm(
                model=model,
                url=url,
                system_prompt=prompt,
                user_prompt="Classify this screen layout.",
                images_b64=[screenshot_b64] if screenshot_b64 else None,
                temperature=0.0,
                use_local_edge=True,
            )

            result = response.strip().upper() if response else "UNKNOWN"

            for t in ScreenType:
                if t.name in result:
                    if is_normal_override and t == ScreenType.MODAL:
                        # Prevent the LLM from hallucinating an obstacle if explicitly verified as NORMAL
                        return ScreenType.UNKNOWN

                    # Enforce absolute structural parity: These types MUST be resolved
                    # by resource-id fast-paths. VLM guessing them poisons the Qdrant cache.
                    if t in _STRUCTURALLY_GATED_TYPES:
                        logger.warning(
                            f"⚠️ [ScreenIdentity] Rejecting VLM hallucinated {t.name} — "
                            f"this type MUST be resolved structurally to prevent cache poisoning."
                        )
                        return ScreenType.UNKNOWN

                    if signature and self.screen_memory:
                        self.screen_memory.store_screen(signature, t.name)
                    return t
        except Exception as e:
            import logging

            logging.getLogger(__name__).debug(f"LLM Classification failed: {e}")

        return ScreenType.UNKNOWN

    def _extract_available_actions(self, clickable_elements, resource_ids, content_descs, texts, screen_type):
        """Discover what actions are possible on this screen using structural fast-paths.

        PRIMARY: resource-id matching (architectural constants of the Instagram APK).
        Resource IDs like feed_tab, profile_tab, clips_tab are compile-time Android
        identifiers that NEVER change across locales. This is NOT hardcoded text matching.
        """
        actions = []

        # --- Structural Tab Detection (resource-id based) ---
        # These are architectural constants of the APK, not localized strings.
        tab_map = {
            "feed_tab": "tap home tab",
            "search_tab": "tap explore tab",
            "clips_tab": "tap reels tab",
            "profile_tab": "tap profile tab",
            "direct_tab": "tap messages tab",
            "news_tab": "tap activity heart icon notifications",
        }
        for tab_id, action in tab_map.items():
            if tab_id in resource_ids:
                actions.append(action)

        ids_str = " ".join(resource_ids).lower()
        if screen_type == ScreenType.OWN_PROFILE or screen_type == ScreenType.OTHER_PROFILE:
            if "button_message" in ids_str or "profile_header_message_button" in ids_str:
                actions.append("tap message button")
            if "profile_header_following" in ids_str or "profile_header_follow_button" in ids_str:
                actions.append("tap following list")

        # Grid items
        if screen_type == ScreenType.EXPLORE_GRID:
            actions.append("tap first post")

        # Scroll
        actions.append("scroll down")
        actions.append("scroll up")
        actions.append("press back")

        return list(set(actions))  # Deduplicate

    def _extract_context(self, content_descs, texts, resource_ids, screen_type):
        """Extract meaningful context from the screen using structural heuristics."""
        context = {}
        # Zero Maintenance: Do not rely on localized regex strings like "followers" or "liked"
        return context

    def _compute_signature(self, resource_ids, content_descs, texts):
        """Compute a stable hash for this screen state (for Qdrant lookup)."""
        # Use sorted IDs + key content for stability
        sig_parts = sorted(resource_ids)[:20]
        sig_parts.extend(sorted(set(d.lower()[:30] for d in content_descs if len(d) > 2))[:10])
        sig = "|".join(sig_parts)
        return hashlib.sha256(sig.encode()).hexdigest()[:24]

    def _empty_screen(self):
        return {
            "screen_type": ScreenType.FOREIGN_APP,
            "available_actions": ["press back", "force start instagram"],
            "selected_tab": None,
            "context": {},
            "signature": "empty",
        }
