"""
Situational Awareness Engine (SAE)

Replaces ALL hardcoded obstacle detection with a single AI-driven
perception-action-learning loop. The bot perceives the screen,
recalls past solutions from Qdrant, plans escapes for new situations,
and learns from every episode — positive AND negative.

After initial learning, 95%+ of situations are handled from memory
alone with ZERO LLM calls. This is "Tesla fleet learning" for bots.
"""

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

from GramAddict.core.utils import random_sleep

logger = logging.getLogger(__name__)


class SituationType(Enum):
    NORMAL = "normal"
    OBSTACLE_LOCKED_SCREEN = "obstacle_locked_screen"
    OBSTACLE_MODAL = "obstacle_modal"
    OBSTACLE_FOREIGN_APP = "obstacle_foreign_app"
    OBSTACLE_SYSTEM = "obstacle_system"
    DANGER_ACTION_BLOCKED = "danger_action_blocked"


class EscapeAction:
    """Represents a planned escape action."""

    def __init__(self, action_type: str, x: int = 0, y: int = 0, reason: str = "", resource_id: str = ""):
        self.action_type = action_type  # 'click', 'back', 'app_start', 'home_then_app'
        self.x = x
        self.y = y
        self.reason = reason
        self.resource_id = resource_id

    def to_dict(self) -> dict:
        return {
            "action_type": self.action_type,
            "x": self.x,
            "y": self.y,
            "reason": self.reason,
            "resource_id": self.resource_id,
        }

    @classmethod
    def from_dict(cls, d: dict) -> "EscapeAction":
        return cls(
            d.get("action_type", "back"), d.get("x", 0), d.get("y", 0), d.get("reason", ""), d.get("resource_id", "")
        )


class SituationEpisodeDB:
    """
    Qdrant-backed episodic memory for the SAE.
    Stores situation → action → outcome triples.
    Enables instant recall for known situations (0 LLM calls).
    Stores BOTH positive and negative episodes for full learning.
    """

    def __init__(self):
        from GramAddict.core.qdrant_memory import QdrantBase

        self._db = QdrantBase("sae_episodes_v1", vector_size=768)

    def recall(self, situation_signature: str) -> Optional[Dict]:
        """
        Looks up a past episode matching this situation.
        Returns the best SUCCESSFUL action, or None.
        Skips actions that previously FAILED for this exact situation.
        """
        if not self._db.is_connected:
            return None

        vec = self._db._get_embedding(situation_signature[:2000])
        if not vec:
            return None

        try:
            results = self._db.client.query_points(
                collection_name=self._db.collection_name,
                query=vec,
                limit=5,
                score_threshold=0.88,
            ).points

            if not results:
                return None

            # Sort by confidence desc, prefer successful episodes
            best_positive = None
            failed_actions = set()

            for r in results:
                p = r.payload
                if not p.get("success", False):
                    # Track failed actions so we don't repeat them
                    failed_actions.add(p.get("action", {}).get("action_type", ""))
                    continue
                conf = p.get("confidence", 0.0)
                if conf >= 0.3 and (best_positive is None or conf > best_positive.payload.get("confidence", 0)):
                    best_positive = r

            if best_positive:
                action_data = best_positive.payload.get("action", {})
                # Don't return an action type that has also failed before for this situation
                if action_data.get("action_type") not in failed_actions:
                    logger.info(
                        f"🧠 [SAE Recall] Instant memory hit! Situation matched with confidence "
                        f"{best_positive.payload.get('confidence', 0):.2f}. Action: {action_data.get('reason', 'unknown')}"
                    )
                    return action_data

            return None
        except Exception as e:
            logger.debug(f"SAE recall error: {e}")
            return None

    def learn(self, situation_signature: str, action: EscapeAction, success: bool):
        """
        Stores an episode in Qdrant. Both successes AND failures.
        Successful episodes get high confidence; failures get stored
        as negative examples so the bot never repeats them.
        """
        if not self._db.is_connected:
            return

        vec = self._db._get_embedding(situation_signature[:2000])
        if not vec:
            return

        # Unique key: situation + action type (ignoring success flag for the seed so we update the same entry)
        seed = f"{situation_signature}|{action.action_type}|{action.x},{action.y}"
        point_id = self._db.generate_uuid(seed)

        current_conf = 0.0
        has_existing = False
        try:
            points = self._db.client.retrieve(
                collection_name=self._db.collection_name, ids=[point_id], with_payload=True, with_vectors=False
            )
            if points:
                has_existing = True
                current_conf = points[0].payload.get("confidence", 0.0)
        except Exception:
            pass

        if success:
            confidence = min(1.0, current_conf + 0.5) if has_existing else 0.8
        else:
            confidence = current_conf - 0.5 if has_existing else -0.5

        if confidence < 0.1 and not success:
            self._db.client.delete(collection_name=self._db.collection_name, points_selector=[point_id])
            logger.info("🗑️ [SAE Learn] Action decayed below threshold. Deleted from memory.")
            return

        payload = {
            "situation": situation_signature[:500],
            "action": action.to_dict(),
            "success": success,
            "confidence": confidence,
            "timestamp": time.time(),
            "recall_count": 0,
        }

        outcome = "✅ SUCCESS" if success else "❌ FAILURE"
        self._db.upsert_point(
            seed,
            payload,
            vector=vec,
            log_success=f"🧠 [SAE Learn] {outcome}: '{action.reason}' → Stored for future recall",
        )

    def boost(self, situation_signature: str, action: EscapeAction):
        """Boost confidence of a successful episode on re-use."""
        # Simple: just re-store with higher confidence
        # The upsert with same seed will overwrite
        pass  # Future: increment recall_count and confidence


class SituationalAwarenessEngine:
    """
    The autonomous brain. Perceives the screen, recalls past solutions,
    plans escapes, executes, verifies, and learns. ZERO hardcoded rules.
    """

    _instance = None

    @classmethod
    def get_instance(cls, device=None):
        if cls._instance is None:
            cls._instance = cls(device)
        elif device is not None:
            cls._instance.device = device
        return cls._instance

    @classmethod
    def reset(cls):
        """Reset the singleton instance."""
        cls._instance = None

    def __init__(self, device):
        self.device = device
        self.episodes = SituationEpisodeDB()
        self._consecutive_failures = 0

    # ──────────────────────────────────────────────
    # 1. PERCEIVE: Compress XML → Classify situation
    # ──────────────────────────────────────────────

    def _compress_xml(self, xml_dump: str) -> str:
        """
        Compresses a full XML hierarchy into a compact situation signature.
        Keeps only: package names, resource-ids, content-descs, text fields.
        Strips all bounds/styling/index noise. ~300-500 tokens output.
        """
        if not xml_dump or not isinstance(xml_dump, str):
            return "EMPTY_SCREEN"

        try:
            # Remove XML declaration
            clean = re.sub(r"<\?xml.*?\?>", "", xml_dump).strip()
            root = ET.fromstring(clean)
        except Exception:
            # If XML is broken, extract what we can with regex
            packages = set(re.findall(r'package="([^"]+)"', xml_dump))
            texts = re.findall(r'text="([^"]{1,80})"', xml_dump)
            descs = re.findall(r'content-desc="([^"]{1,80})"', xml_dump)
            return f"PACKAGES: {', '.join(packages)}\nTEXTS: {'; '.join(texts[:15])}\nDESCS: {'; '.join(descs[:15])}"

        packages = set()
        elements = []

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

            rid = a.get("resource-id", "").strip()
            text = a.get("text", "").strip()
            desc = a.get("content-desc", "").strip()
            bounds = a.get("bounds", "")
            clickable = a.get("clickable", "false")

            # Only keep nodes with meaningful content
            if not rid and not text and not desc:
                continue

            parts = []
            if rid:
                parts.append(f"id={rid.split('/')[-1]}")
            if text:
                if len(text) > 20:
                    text = text[:10] + "..." + text[-10:]
                parts.append(f"text='{text}'")
            if desc:
                if len(desc) > 20:
                    desc = desc[:10] + "..." + desc[-10:]
                parts.append(f"desc='{desc}'")
            if clickable == "true":
                parts.append("CLICKABLE")
            if bounds:
                nums = [int(n) for n in re.findall(r"\d+", bounds)]
                if len(nums) == 4:
                    cx = (nums[0] + nums[2]) // 2
                    cy = (nums[1] + nums[3]) // 2
                    parts.append(f"bounds={bounds} center=({cx},{cy})")
                else:
                    parts.append(f"bounds={bounds}")

            elements.append(" | ".join(parts))

        sig = f"PACKAGES: {', '.join(sorted(packages))}\n"
        sig += "\n".join(elements[-50:])  # Keep the last 50 elements (highest Z-index/foreground)
        return sig[:3000]

    def _compute_situation_hash(self, compressed: str) -> str:
        """Deterministic hash for situation dedup."""
        # Remove volatile parts (timestamps, counters) but keep structural identity
        stable = re.sub(r"\d{2}:\d{2}", "HH:MM", compressed)
        stable = re.sub(r"Battery \d+ per cent", "Battery NN per cent", stable)
        return hashlib.sha256(stable.encode()).hexdigest()[:32]

    def _get_model_config(self):
        """
        Get model/URL config from Config() — the SINGLE source of truth.
        Crashes loudly if config is unavailable (Fail Fast, no silent defaults).
        """
        from GramAddict.core.config import Config

        cfg = Config()
        args = cfg.args
        return {
            "model": getattr(args, "ai_model", None),
            "url": getattr(args, "ai_model_url", None),
            "telepathic_model": getattr(args, "ai_telepathic_model", None),
            "telepathic_url": getattr(args, "ai_telepathic_url", None),
        }

    def perceive(self, xml_dump: str) -> SituationType:
        """
        Autonomous situation classification — ZERO hardcoded UI element identifiers.

        Flow:
        1. Empty/invalid XML → FOREIGN_APP
        2. Hardware check (screen off) → LOCKED_SCREEN
        3. Package-based detection (app-agnostic, zero maintenance):
           - Permission controller packages → OBSTACLE_SYSTEM
           - App package missing → OBSTACLE_FOREIGN_APP
        4. Qdrant semantic cache → instant recall of learned screen types
        5. ScreenIdentity structural delegation → if MODAL, return OBSTACLE_MODAL
        6. LLM classification fallback → autonomous discovery of new obstacle types

        NO hardcoded resource-ids, NO hardcoded button texts, NO localized strings.
        The bot discovers and learns ALL obstacles autonomously via the LLM + Qdrant loop.
        """
        if not xml_dump or not isinstance(xml_dump, str):
            return SituationType.OBSTACLE_FOREIGN_APP

        # ── Hardware Guard: Screen Off / Locked ──
        if not getattr(self.device.deviceV2, "info", {}).get("screenOn", True):
            logger.info("📱 [SAE Perceive] Screen is physically OFF.")
            return SituationType.OBSTACLE_LOCKED_SCREEN

        # ── System Dialog / Permission Detect (package-based, app-agnostic) ──
        packages = set(re.findall(r'package=["\'](.[^"\']+)["\']+', xml_dump))
        app_id = getattr(self.device, "app_id", "com.instagram.android")

        # Permission controller packages are Android system-level — not app-specific.
        # These will NEVER change with an Instagram update. Completely safe to check.
        system_dialog_pkgs = {
            "com.google.android.permissioncontroller",
            "com.android.permissioncontroller",
            "com.samsung.android.permissioncontroller",
        }
        if any(pkg in system_dialog_pkgs for pkg in packages):
            logger.info("📱 [SAE Perceive] System permission dialog explicitly detected.")
            return SituationType.OBSTACLE_SYSTEM

        # ── Foreign Environment Detection (package-based, zero maintenance) ──
        # If our app package is completely absent from the screen → foreign app.
        # Package names are Android-level identifiers, NOT Instagram UI elements.
        is_foreign = bool(packages) and app_id not in packages

        if is_foreign:
            # Any package that isn't our app AND isn't just systemui = foreign
            dominant_pkgs = packages - {"com.android.systemui"}
            if dominant_pkgs:
                logger.info(f"🚨 [SAE Perceive] Foreign package detected: {dominant_pkgs} → OBSTACLE_FOREIGN_APP")
                return SituationType.OBSTACLE_FOREIGN_APP

            # SystemUI-only edge case: could be lock screen, notification shade, etc.
            # Use LLM for disambiguation.
            try:
                from GramAddict.core.llm_provider import query_telepathic_llm

                cfg = self._get_model_config()
                screen_off = not getattr(self.device.deviceV2, "info", {}).get("screenOn", True)

                prompt = (
                    "You are a Situation Classifier for a mobile automation agent.\n"
                    "Analyze the given Android UI XML dump. Is this a physical DEVICE_LOCK_SCREEN, "
                    "a system PERMISSION_DIALOG, or a NOTIFICATION_SHADE / FOREIGN_APP?\n"
                    f"Hardware Screen Status: {'OFF (Locked)' if screen_off else 'ON'}.\n"
                    "Respond ONLY with a valid JSON object strictly matching this schema: "
                    "{\"situation\": \"OBSTACLE_LOCKED_SCREEN\" | \"OBSTACLE_SYSTEM\" | \"OBSTACLE_FOREIGN_APP\"}\n\n"
                    f"XML:\n{self._compress_xml(xml_dump)[:2500]}"
                )

                res = query_telepathic_llm(
                    model=cfg["model"],
                    url=cfg["url"],
                    system_prompt="Strict JSON classifier.",
                    user_prompt=prompt,
                    use_local_edge=True,
                )
                import json

                data = json.loads(res)
                situ_str = data.get("situation", "")

                if situ_str == "OBSTACLE_LOCKED_SCREEN":
                    logger.info("🧠 [Smart Perceive] SystemUI classified as: LOCKED_SCREEN.")
                    return SituationType.OBSTACLE_LOCKED_SCREEN
                elif situ_str == "OBSTACLE_SYSTEM":
                    logger.info("🧠 [Smart Perceive] SystemUI classified as: SYSTEM_DIALOG.")
                    return SituationType.OBSTACLE_SYSTEM
                else:
                    logger.info("🧠 [Smart Perceive] SystemUI classified as: FOREIGN_APP.")
                    return SituationType.OBSTACLE_FOREIGN_APP
            except Exception as e:
                logger.warning(f"⚠️ [Smart Perceive] LLM Classification failed ({e}). Defaulting to FOREIGN_APP.")
                return SituationType.OBSTACLE_FOREIGN_APP

        # ── In-App Obstacle Detection (100% autonomous, ZERO hardcoded UI identifiers) ──
        # The bot learns ALL obstacle types via the LLM + Qdrant feedback loop.
        # First encounter: LLM classifies → result is cached in Qdrant.
        # All subsequent encounters: instant O(1) recall from cache.
        from GramAddict.core.qdrant_memory import ScreenMemoryDB

        screen_memory = ScreenMemoryDB()
        compressed = self._compress_xml(xml_dump)

        # ── Priority 1: Qdrant Semantic Cache (O(1), zero LLM calls) ──
        cached_type = screen_memory.get_screen_type(compressed)
        if cached_type:
            if cached_type == "OBSTACLE_MODAL":
                return SituationType.OBSTACLE_MODAL
            elif cached_type == "DANGER_ACTION_BLOCKED":
                return SituationType.DANGER_ACTION_BLOCKED
            elif cached_type == "NORMAL":
                return SituationType.NORMAL

        # ── Priority 2: ScreenIdentity structural delegation ──
        # ScreenIdentity classifies the screen using its own structural logic.
        # If it says MODAL → trust it (it uses the same zero-trust approach).
        from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType

        screen_id = ScreenIdentity(getattr(self.device, "bot_username", ""))
        screen_result = screen_id.identify(xml_dump)
        screen_type = screen_result.get("screen_type", ScreenType.UNKNOWN)

        if screen_type == ScreenType.MODAL:
            logger.info("🧠 [SAE Perceive] ScreenIdentity classified as MODAL → OBSTACLE_MODAL")
            screen_memory.store_screen(compressed, "OBSTACLE_MODAL")
            return SituationType.OBSTACLE_MODAL

        if screen_type == ScreenType.FOREIGN_APP:
            logger.info("🧠 [SAE Perceive] ScreenIdentity classified as FOREIGN_APP → OBSTACLE_FOREIGN_APP")
            return SituationType.OBSTACLE_FOREIGN_APP

        if screen_type == ScreenType.DANGER_ACTION_BLOCKED:
            logger.info("🧠 [SAE Perceive] ScreenIdentity classified as DANGER_ACTION_BLOCKED")
            screen_memory.store_screen(compressed, "DANGER_ACTION_BLOCKED")
            return SituationType.DANGER_ACTION_BLOCKED

        # If ScreenIdentity positively identified a known screen type (not UNKNOWN),
        # we trust it as NORMAL — no LLM needed.
        if screen_type != ScreenType.UNKNOWN:
            screen_memory.store_screen(compressed, "NORMAL")
            return SituationType.NORMAL

        # ── Priority 3: LLM autonomous classification (first-encounter learning) ──
        # This is the ONLY path that reaches the LLM. After classification,
        # the result is cached in Qdrant — so this screen type is learned forever.
        try:
            from GramAddict.core.llm_provider import query_telepathic_llm

            cfg = self._get_model_config()

            prompt = (
                "You are a Situation Classifier for a mobile automation agent.\n"
                "Analyze the given Android UI XML dump AND screenshot. Classify the screen into one of:\n"
                "- OBSTACLE_MODAL: Any blocking overlay, dialog, popup, survey, rating prompt, "
                "browser window, camera/creation flow, or any UI that blocks normal feed browsing.\n"
                "- DANGER_ACTION_BLOCKED: Instagram's rate-limit or action-block warning "
                "(e.g. 'Try Again Later', 'Action Blocked', or any restriction/verification screen).\n"
                "- NORMAL: A standard usable screen (feed, explore, profile, DM, etc.)\n\n"
                "Respond ONLY with a valid JSON object: "
                '{"situation": "OBSTACLE_MODAL" | "DANGER_ACTION_BLOCKED" | "NORMAL"}\n\n'
                f"XML:\n{compressed[:2500]}"
            )

            screenshot_b64 = getattr(self.device, "get_screenshot_b64", lambda: None)()
            res = query_telepathic_llm(
                model=cfg["telepathic_model"],
                url=cfg["telepathic_url"],
                system_prompt="Strict JSON classifier.",
                user_prompt=prompt,
                images_b64=[screenshot_b64] if screenshot_b64 else None,
                use_local_edge=True,
            )
            import json

            data = json.loads(res)
            situ_str = data.get("situation", "NORMAL")

            if situ_str == "OBSTACLE_MODAL":
                logger.info("🧠 [Smart Perceive] Screen classified as: OBSTACLE_MODAL.")
                screen_memory.store_screen(compressed, "OBSTACLE_MODAL")
                return SituationType.OBSTACLE_MODAL
            elif situ_str == "DANGER_ACTION_BLOCKED":
                logger.info("🧠 [Smart Perceive] Screen classified as: DANGER_ACTION_BLOCKED.")
                screen_memory.store_screen(compressed, "DANGER_ACTION_BLOCKED")
                return SituationType.DANGER_ACTION_BLOCKED
            else:
                logger.info("🧠 [Smart Perceive] Screen classified as: NORMAL.")
                screen_memory.store_screen(compressed, "NORMAL")
                return SituationType.NORMAL
        except Exception as e:
            logger.warning(f"⚠️ [Smart Perceive] Modal classification failed ({e}). Defaulting to NORMAL.")

        return SituationType.NORMAL

    def unlearn_current_state(self, xml_dump: str):
        """Purges the current screen's signature from Qdrant to self-heal from hallucinations."""
        compressed = self._compress_xml(xml_dump)
        from GramAddict.core.qdrant_memory import ScreenMemoryDB

        screen_memory = ScreenMemoryDB()
        screen_memory.purge_screen(compressed)
        logger.info("🗑️ [Smart Perceive] Purged cached screen signature to force autonomous re-evaluation.")

    # ──────────────────────────────────────────────
    # 2. PLAN: AI-driven escape strategy
    # ──────────────────────────────────────────────

    def _plan_escape_via_llm(
        self, xml_dump: str, compressed: str, situation_type: SituationType, failed_actions: set = None
    ) -> Optional[EscapeAction]:
        """
        LLM-powered escape planning for situations where structural scan fails.
        Called ONLY when recall AND structural planning both miss.
        """
        from GramAddict.core.llm_provider import query_telepathic_llm

        cfg = self._get_model_config()
        model = cfg["telepathic_model"]
        url = cfg["telepathic_url"]

        system_prompt = (
            "You are an Android UI navigation agent. Your job is to escape obstacles "
            "(dialogs, modals, foreign apps, system popups) and return to Instagram. "
            "Analyze the screen content (Screenshot AND XML) and return a JSON escape action.\n\n"
            "Rules:\n"
            "- If you see a dismiss/close/cancel/skip/not now button, click it\n"
            "- If the Situation type is obstacle_locked_screen, action must be 'unlock'\n"
            "- If the Situation type is obstacle_foreign_app, action must be 'kill_foreign_apps'\n"
            "- If the Situation type is obstacle_system, you MUST look for 'Deny', 'Don't allow', or 'Cancel' and click it. \n"
            "  NEVER click 'Allow', 'OK', or 'Confirm' on system permissions.\n"
            "  If no negative action button exists, action must be 'back'\n"
            "- If there is NO obstacle and the screen is a normal Instagram view (false positive), action must be 'false_positive'\n"
            "- If nothing else works, suggest 'app_start' to force-reopen Instagram\n"
            "- NEVER click 'OK'/'Confirm'/'Accept' on surveys or prompts\n"
            "- When you choose to click, you MUST use the EXACT coordinates provided in `center=(x,y)` for that element in the XML\n"
            '- Return ONLY valid JSON: {"action": "click"|"back"|"app_start"|"unlock"|"kill_foreign_apps"|"false_positive", "x": N, "y": N, "reason": "..."}'
        )

        user_prompt = f"Situation type: {situation_type.value}\n\n" f"Screen content:\n{compressed}\n\n"
        if failed_actions:
            user_prompt += f"Failed actions this session (DO NOT REPEAT): {list(failed_actions)}\n\n"

        user_prompt += "What action should I take to clear this obstacle and return to Instagram? Return JSON only."

        try:
            screenshot_b64 = getattr(self.device, "get_screenshot_b64", lambda: None)()

            resp = query_telepathic_llm(
                url=url,
                model=model,
                user_prompt=user_prompt,
                system_prompt=system_prompt,
                images_b64=[screenshot_b64] if screenshot_b64 else None,
                temperature=0.0,
            )
            if resp:
                import json

                try:
                    data = json.loads(resp)
                except json.JSONDecodeError:
                    # Try extracting JSON via regex if LLM was chatty
                    import re

                    match = re.search(r"\{.*\}", resp, re.DOTALL)
                    if match:
                        data = json.loads(match.group(0))
                    else:
                        raise ValueError(f"Could not parse JSON from: {resp}")

                return EscapeAction(
                    action_type=data.get("action", "back"),
                    x=int(data.get("x", 0)),
                    y=int(data.get("y", 0)),
                    reason=data.get("reason", "LLM-planned escape"),
                )
        except Exception as e:
            logger.warning(f"🧠 [SAE] LLM escape planning failed: {e}")

        return EscapeAction("back", reason="LLM planning failed, defaulting to BACK")

    # ──────────────────────────────────────────────
    # 3. EXECUTE & VERIFY
    # ──────────────────────────────────────────────

    def _execute_escape(self, action: EscapeAction):
        """Execute a single escape action."""
        if action.action_type == "click":
            logger.info(f"👆 [SAE Act] Clicking ({action.x}, {action.y}): {action.reason}")
            self.device.click(action.x, action.y)  # Native click for safety (no human_click edge risks)
            random_sleep(0.8, 1.5)

        elif action.action_type == "back":
            logger.info(f"⬅️ [SAE Act] Pressing BACK: {action.reason}")
            self.device.press("back")
            random_sleep(0.8, 1.5)

        elif action.action_type == "unlock":
            logger.info(f"🔓 [SAE Act] Unlocking device: {action.reason}")
            self.device.unlock()
            random_sleep(1.0, 2.0)
            app_id = getattr(self.device, "app_id", "com.instagram.android")
            self.device.app_start(app_id, use_monkey=True)
            random_sleep(1.5, 2.5)

        elif action.action_type == "app_start":
            logger.info(f"🚀 [SAE Act] Force-starting app: {action.reason}")
            app_id = getattr(self.device, "app_id", "com.instagram.android")
            self.device.app_start(app_id, use_monkey=True)
            random_sleep(2.0, 3.5)

        elif action.action_type == "kill_foreign_apps":
            logger.info(f"🔪 [SAE Act] Killing foreign apps: {action.reason}")
            # The reason string will contain the package name or 'all'
            app_id = getattr(self.device, "app_id", "com.instagram.android")
            try:
                # We can dump current package again, or just get it from device
                current_pkg = self.device.deviceV2.app_current().get("package")
                if current_pkg and current_pkg != app_id and current_pkg not in ("com.android.systemui", "android"):
                    logger.info(f"🔪 Stopping {current_pkg}")
                    self.device.app_stop(current_pkg)
                    random_sleep(1.0, 2.0)
                # Ensure we are back
                self.device.app_start(app_id, use_monkey=True)
                random_sleep(1.5, 2.5)
            except Exception as e:
                logger.debug(f"Failed to kill foreign app: {e}")

        elif action.action_type == "home_then_app":
            logger.info(f"🏠 [SAE Act] HOME → App Start: {action.reason}")
            self.device.press("home")
            random_sleep(0.5, 1.0)
            app_id = getattr(self.device, "app_id", "com.instagram.android")
            self.device.app_start(app_id, use_monkey=True)
            random_sleep(2.0, 3.5)

    # ──────────────────────────────────────────────
    # 4. MAIN ENTRY POINT: ensure_clear_screen()
    # ──────────────────────────────────────────────

    def ensure_clear_screen(self, max_attempts: int = 7, initial_xml: str = None) -> bool:
        """
        The main autonomous loop. Called before every navigation action.
        Perceives → Recalls → Plans → Acts → Verifies → Learns.
        Returns True if an obstacle was successfully cleared, False if already clear or failed.
        """
        from GramAddict.core.exceptions import ActionBlockedError

        failed_this_session = set()

        last_situation = None
        situation_attempts = 0

        for attempt in range(max_attempts):
            # ── PERCEIVE ──
            if attempt == 0 and initial_xml:
                xml_dump = initial_xml
            else:
                xml_dump = self.device.dump_hierarchy()

            situation = self.perceive(xml_dump)

            if last_situation != situation:
                situation_attempts = 0
                last_situation = situation
                failed_this_session.clear()

            # ── CLEAR: Nothing to do ──
            if situation == SituationType.NORMAL:
                if attempt > 0:
                    logger.info(f"✅ [SAE] Screen cleared after {attempt} attempt(s)")
                self._consecutive_failures = 0
                return True

            # ── DANGER: Account safety — hard stop ──
            if situation == SituationType.DANGER_ACTION_BLOCKED:
                logger.error("🚫 [SAE CRITICAL] Instagram Action Block detected! Halting to protect account.")
                raise ActionBlockedError("Instagram action block detected by SAE.")

            logger.warning(f"🔍 [SAE] Obstacle detected: {situation.value} (attempt {attempt + 1}/{max_attempts})")

            # ── O(1) Fast-Path for Foreign Apps ──
            if situation == SituationType.OBSTACLE_FOREIGN_APP:
                logger.warning("⚡ [SAE Fast-Path] Foreign App detected. Bypassing LLM and killing immediately.")
                action = EscapeAction("kill_foreign_apps", reason="O(1) fast-path to eliminate foreign app")
                self._execute_escape(action)

                # Check if we recovered
                post_xml = self.device.dump_hierarchy()
                if self.perceive(post_xml) == SituationType.NORMAL:
                    logger.info("✅ [SAE Fast-Path] Foreign App cleared successfully!")
                    self._consecutive_failures = 0
                    return True

                # If we didn't recover, log it and let the loop continue
                logger.warning("⚠️ [SAE Fast-Path] kill_foreign_apps did not return to NORMAL. Retrying...")
                self._consecutive_failures += 1
                continue

            # ── COMPRESS for memory lookup ──
            compressed = self._compress_xml(xml_dump)

            # ── RECALL from memory ──
            recalled = self.episodes.recall(compressed)
            if recalled:
                recalled_key = f"{recalled.get('action_type', '')}:{recalled.get('x', 0)},{recalled.get('y', 0)}"
                if recalled_key not in failed_this_session:
                    action = EscapeAction.from_dict(recalled)
                    logger.info(f"🧠 [SAE] Using recalled strategy: {action.reason}")
                else:
                    logger.info("🧠 [SAE] Recalled strategy already failed this session. Using LLM planning.")
                    recalled = None

            if not recalled:
                if situation_attempts < 3:
                    logger.info("🧠 [SAE] Autonomous Blank Start: Escalating to LLM-assisted escape planning...")
                    action = self._plan_escape_via_llm(xml_dump, compressed, situation, failed_this_session)
                elif situation_attempts == 3:
                    action = EscapeAction(
                        "app_start",
                        reason=f"Escalation level 4: force app restart after {situation_attempts} failed attempts on this situation",
                    )
                else:
                    action = EscapeAction(
                        "home_then_app",
                        reason=f"Nuclear escalation: HOME + app_start after {situation_attempts} failed attempts on this situation",
                    )

            # ── EXECUTE ──
            if action.action_type == "false_positive":
                logger.warning(
                    "🧠 [SAE Unlearn] LLM identified false positive obstacle. Overwriting Qdrant memory to NORMAL."
                )
                from GramAddict.core.qdrant_memory import ScreenMemoryDB

                ScreenMemoryDB().store_screen(compressed, "NORMAL")
                self._consecutive_failures = 0
                return True
            else:
                self._execute_escape(action)

            # ── VERIFY ──
            post_xml = self.device.dump_hierarchy()
            post_situation = self.perceive(post_xml)
            reached_normal = post_situation == SituationType.NORMAL
            situation_changed = post_situation != situation

            if reached_normal:
                # ── LEARN FULL SUCCESS ──
                self.episodes.learn(compressed, action, True)
                logger.info(f"✅ [SAE] Obstacle cleared! Strategy '{action.reason}' worked. Stored for future recall.")
                self._consecutive_failures = 0
                return True
            elif situation_changed:
                # ── LEARN PARTIAL SUCCESS ──
                self.episodes.learn(compressed, action, True)
                logger.info(
                    f"🔄 [SAE] Situation changed from {situation.value} to {post_situation.value}. Continuing recovery..."
                )
                # We do not increment consecutive_failures or situation_attempts because we made progress
                # The next loop iteration will clear failed_this_session since last_situation != situation
            else:
                # ── LEARN FAILURE ──
                self.episodes.learn(compressed, action, False)
                action_key = f"{action.action_type}:{action.x},{action.y}"
                failed_this_session.add(action_key)
                logger.warning(
                    f"⚠️ [SAE] Escape attempt {attempt + 1} failed ('{action.reason}'). Trying next strategy..."
                )
                self._consecutive_failures += 1
                situation_attempts += 1

        logger.error(f"❌ [SAE] UNRECOVERABLE: Failed to clear screen after {max_attempts} attempts")
        return False

    def recover_to_app(self) -> bool:
        """
        Called when the Perimeter Guard detects we're in a foreign app.
        Uses the full SAE loop to get back.
        """
        logger.warning("🚨 [SAE] Foreign app takeover detected. Initiating autonomous recovery...")
        return self.ensure_clear_screen(max_attempts=5)

    def is_instagram_foreground(self, xml_dump: str = None) -> bool:
        """Quick check: is Instagram the primary app on screen?"""
        if xml_dump is None:
            xml_dump = self.device.dump_hierarchy()
        situation = self.perceive(xml_dump)
        return situation == SituationType.NORMAL
