"""
Goal-Oriented Action Planner (GOAP)

The bot's autonomous brain. Replaces ALL hardcoded navigation with
goal-driven behavior. The bot perceives the screen, understands where
it is, plans what to do next, executes, verifies, and learns.

Like a GPS navigation system:
- You tell it WHERE you want to go (goal)
- It figures out the route (plan)
- It guides you step by step (execute)
- It reroutes if you take a wrong turn (recover)
- It remembers shortcuts (learn)
"""

import logging
import time
from typing import Any, Dict, List

from GramAddict.core.navigation.knowledge import NavigationKnowledge
from GramAddict.core.navigation.path_memory import PathMemory
from GramAddict.core.navigation.planner import GoalPlanner
from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType
from GramAddict.core.utils import random_sleep

logger = logging.getLogger(__name__)

# Re-export for backward compatibility (optional but helps minimize import breakage)
__all__ = ["GoalExecutor", "ScreenIdentity", "ScreenType", "PathMemory", "NavigationKnowledge", "GoalPlanner"]

# ══════════════════════════════════════════════════════
# GOAL EXECUTOR — The Main Brain Loop
# ══════════════════════════════════════════════════════


class GoalExecutor:
    """
    The autonomous brain. Achieves goals through perceive→plan→execute→verify→learn.

    Usage:
        goap = GoalExecutor(device, bot_username="marisaundmarc")
        goap.achieve("like a post from explore")
    """

    _instance = None
    global_start_time = None
    global_max_runtime_minutes = None

    @classmethod
    def get_instance(cls, device=None, bot_username=""):
        if cls._instance is None:
            cls._instance = cls(device, bot_username)
        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, bot_username: str = ""):
        self.device = device
        self.username = bot_username
        self.screen_id = ScreenIdentity(bot_username)
        self.screen_id.device = device
        self.planner = GoalPlanner(bot_username)
        self.path_memory = PathMemory(bot_username)
        self.max_steps = 15  # Safety: never execute more than 15 steps
        self._sae = None  # Lazy-loaded, injectable for tests
        self.action_failures = {}  # Tracking for failed actions in current goal session

    def _get_sae(self):
        """Get or create the SAE instance. Injectable for tests."""
        if self._sae is None:
            from GramAddict.core.situational_awareness import SituationalAwarenessEngine

            self._sae = SituationalAwarenessEngine.get_instance(self.device)
        return self._sae

    def perceive(self, xml_dump: str = None) -> Dict[str, Any]:
        """Perceive the current screen state."""
        if xml_dump is None:
            xml_dump = self.device.dump_hierarchy()
        return self.screen_id.identify(xml_dump)

    def achieve(self, goal: str, max_steps: int = None) -> bool:
        """
        Main entry point. Achieves a goal autonomously.

        Args:
            goal: Natural language goal like "like a post from explore"
            max_steps: Maximum steps before giving up

        Returns:
            True if goal achieved, False if failed
        """
        if max_steps is None:
            max_steps = self.max_steps

        logger.info(f"🎯 [GOAP] Pursuing goal: '{goal}'")
        self.action_failures.clear()

        # ── Try recalled path first ──
        screen = self.perceive()
        start_screen = screen["screen_type"].value
        recalled = self.path_memory.recall_path(goal, start_screen)

        if recalled:
            logger.info(f"🧠 [GOAP] Using memorized path ({len(recalled)} steps)")
            success = self._execute_recalled_path(recalled, goal)
            if success:
                return True
            logger.warning("🧠 [GOAP] Memorized path failed. Falling back to live planning...")

        # ── Live planning ──
        steps_taken = []
        last_action = None
        last_screen_type = None
        consecutive_back_presses = 0
        MAX_CONSECUTIVE_BACK = 3
        explored_nav_actions = set()
        visited_screens = set()
        for step_num in range(max_steps):
            # ── Global Hard Kill Check ──
            max_rt = GoalExecutor.global_max_runtime_minutes
            start_time = GoalExecutor.global_start_time
            if max_rt and start_time:
                from datetime import datetime, timedelta

                if datetime.now() - start_time > timedelta(minutes=max_rt):
                    logger.error(
                        f"🛑 [Timeout] Maximum runtime of {max_rt} minutes reached during GOAP execution. Hard stopping planner.",
                        extra={"color": "\\033[31m"},
                    )
                    return False

            # PERCEIVE
            screen = self.perceive()
            screen_type = screen["screen_type"]
            visited_screens.add(screen_type)

            if last_screen_type and screen_type != last_screen_type:
                logger.debug(
                    f"📍 [GOAP State] Screen transitioned from {last_screen_type.name} to {screen_type.name}. Clearing explored actions."
                )
                explored_nav_actions.clear()
                consecutive_back_presses = 0  # Progress was made

            # ── Loop Prevention: Mask Failed Actions ──
            MAX_RETRIES = 2
            original_available = screen.get("available_actions", []).copy()
            masked_available = []
            for act in original_available:
                fail_count = self.action_failures.get((screen_type, act), 0)
                if fail_count >= MAX_RETRIES:
                    logger.warning(
                        f"🚫 [GOAP] Masking action '{act}' due to {fail_count} consecutive failures to prevent loops."
                    )
                else:
                    masked_available.append(act)
            screen["available_actions"] = masked_available

            logger.debug(
                f"📍 [GOAP Step {step_num + 1}] Goal: '{goal}' | On: {screen_type.value} | "
                f"Available: {screen.get('available_actions', [])[:5]}"
            )

            # Handle obstacles
            if screen_type == ScreenType.FOREIGN_APP or screen_type == ScreenType.MODAL:
                obstacle_name = "Foreign app" if screen_type == ScreenType.FOREIGN_APP else "Modal"
                logger.warning(f"🚨 [GOAP] {obstacle_name} detected. Using SAE to clear...")

                # SAE Feedback Loop!
                # If we hit this, the LAST action caused an obstacle! Mask it!
                if last_action and last_screen_type:
                    self.action_failures[(last_screen_type, last_action)] = (
                        self.action_failures.get((last_screen_type, last_action), 0) + MAX_RETRIES
                    )  # Instantly mask it for this session
                    from GramAddict.core.screen_topology import ScreenTopology

                    if ScreenTopology.is_structural_action(last_screen_type, last_action):
                        logger.warning(
                            f"🛡️ [SAE Feedback] Structural action '{last_action}' caused an obstacle. "
                            f"Masking for this session. (Never burned permanently)"
                        )
                    else:
                        logger.warning(
                            f"🛡️ [SAE Feedback] Content action '{last_action}' caused an obstacle. "
                            f"Masking for this session to break loop, but preventing permanent Qdrant poisoning."
                        )
                        # We specifically DO NOT call self.planner.knowledge.learn_trap here anymore!
                        # Burning dynamic actions like "tap follow button" permanently destroys the bot's capabilities across sessions.

                if not self._get_sae().ensure_clear_screen():
                    if screen_type == ScreenType.FOREIGN_APP:
                        self.path_memory.learn_path(goal, start_screen, steps_taken, False)
                        return False
                continue

            # PLAN
            action = self.planner.plan_next_step(
                goal,
                screen,
                explored_nav_actions=explored_nav_actions,
                action_failures=self.action_failures,
                visited_screens=visited_screens,
            )

            if action is None:
                # Goal achieved!
                logger.info(f"✅ [GOAP] Goal '{goal}' achieved in {step_num} steps!")
                self.path_memory.learn_path(goal, start_screen, steps_taken, True)

                # Record dynamic knowledge: This goal lands us on THIS screen
                self.planner.knowledge.learn_goal_requirement(goal, screen_type)
                return True

            logger.info(f"🧭 [GOAP Step {step_num + 1}] Action: '{action}'")
            last_action = action
            last_screen_type = screen_type

            # EXECUTE
            success = self._execute_action(action, goal=goal)

            if success:
                steps_taken.append({"action": action})

                if action == "force start instagram":
                    logger.info("🔄 [GOAP State] App restarted. Purging memory/traps to attempt fresh routing.")
                    self.action_failures.clear()
                    explored_nav_actions.clear()
                    visited_screens.clear()
                    consecutive_back_presses = 0
                    continue

                # Check if it was a navigation action (vs a goal action). If we are not on the required screen,
                # any action taken is essentially a navigation attempt.
                explored_nav_actions.add(action)
                # Reset failures for this action since it eventually succeeded
                self.action_failures[(screen_type, action)] = 0

                if "scroll" in action.lower():
                    logger.debug(
                        "📍 [GOAP State] Scrolled successfully. Clearing explored actions to allow retrying off-screen elements."
                    )
                    explored_nav_actions.clear()
                    # Keep action_failures for synthetic intents, but clear them for structural actions
                    # so that the HD Map can retry route actions that might now be visible!
                    from GramAddict.core.screen_topology import ScreenTopology

                    keys_to_clear = [
                        k
                        for k in self.action_failures.keys()
                        if k[0] == screen_type and ScreenTopology.is_structural_action(screen_type, k[1])
                    ]
                    for k in keys_to_clear:
                        del self.action_failures[k]

                # ── Back-Press Circuit Breaker → Escalation ──
                if action == "press back":
                    consecutive_back_presses += 1
                    if consecutive_back_presses >= MAX_CONSECUTIVE_BACK:
                        logger.warning(
                            f"🛑 [GOAP] Back-pressed {MAX_CONSECUTIVE_BACK} times with no screen transition. "
                            f"Escalating to force restart."
                        )

                        # Unlearn the trap path
                        from GramAddict.core.qdrant_memory import NavigationMemoryDB

                        if len(steps_taken) > consecutive_back_presses:
                            last_real_action = steps_taken[-consecutive_back_presses - 1]["action"]
                            logger.debug(
                                f"[GOAP Unlearn] last_real_action={last_real_action}, " f"start_screen={start_screen}"
                            )
                            NavigationMemoryDB().unlearn_transition(start_screen, last_real_action)

                        # ── ESCALATION: Force restart instead of aborting ──
                        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)
                        steps_taken.append({"action": "force start instagram"})

                        logger.info("🔄 [GOAP Escalation] App restarted. Purging all failure state for fresh attempt.")
                        self.action_failures.clear()
                        explored_nav_actions.clear()
                        visited_screens.clear()
                        consecutive_back_presses = 0
                        continue
                else:
                    consecutive_back_presses = 0
            else:
                self.action_failures[(screen_type, action)] = self.action_failures.get((screen_type, action), 0) + 1
                # Track failed actions in explored_nav_actions so the planner
                # knows NOT to return the same synthetic intent again.
                # Without this, synthetic intents (not in available_actions)
                # bypass the masking logic and loop forever.
                explored_nav_actions.add(action)

                if self.action_failures[(screen_type, action)] >= MAX_RETRIES:
                    # ── Topology Guard: Never poison structural HD Map actions ──
                    from GramAddict.core.screen_topology import ScreenTopology

                    if ScreenTopology.is_structural_action(screen_type, action):
                        logger.warning(
                            f"🛡️ [Topology Guard] NOT burning structural action '{action}' — "
                            f"it's in the HD Map. VLM may have failed, but the route is valid."
                        )
                    else:
                        self.planner.knowledge.learn_trap(screen_type, action, "repeated_failure_or_null_action")
                        logger.error(
                            f"💀 [GOAP Execute] Action '{action}' failed {MAX_RETRIES} times. Marked as permanent trap."
                        )
                else:
                    logger.warning(f"⚠️ [GOAP Execute] Action '{action}' failed. Continuing with replanning...")

            random_sleep(0.5, 1.5)

        logger.warning(f"⚠️ [GOAP] Goal '{goal}' failed after {max_steps} steps.")
        self.path_memory.learn_path(goal, start_screen, steps_taken, False)

        # Memory Purge Logic: Wipe the path memory for this start_screen/goal combo
        # so it doesn't get stuck in a broken loop in future sessions!
        self.path_memory.forget_path(goal, start_screen)
        logger.warning(
            f"🧹 [Memory Purge] Wiped PathMemory cache for '{goal}' starting at '{start_screen}' to force re-discovery."
        )

        return False

    def _execute_action(self, action: str, goal: str = None, **kwargs) -> bool:
        """Execute a single natural-language action using the TelepathicEngine."""

        if action == "press back":
            self.device.press("back")
            random_sleep(0.8, 1.5)
            return True

        if action == "scroll down":
            # Swipe up to scroll down
            self.device.swipe(540, 1600, 540, 800, duration=0.3)
            random_sleep(1.0, 2.0)
            return True

        if action == "force start instagram":
            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)
            return True

        if action == "type and post comment":
            return self._execute_type_and_post_comment(kwargs.get("text", ""))

        # Use TelepathicEngine for any semantic click
        from GramAddict.core.telepathic_engine import TelepathicEngine

        engine = TelepathicEngine.get_instance()

        xml_dump = self.device.dump_hierarchy()
        best_node = engine.find_best_node(xml_dump, action, min_confidence=0.75, device=self.device, goal=goal)

        if not best_node or best_node.get("skip"):
            logger.warning(f"⚠️ [GOAP Execute] TelepathicEngine found nothing for '{action}'")
            return best_node.get("skip", False) if best_node else False

        if best_node.get("blocked_by_modal"):
            logger.warning(f"🛡️ [GOAP Execute] Action '{action}' is blocked by an active modal. Aborting click.")
            # Let SAE clear the screen anomaly autonomously
            self._get_sae().ensure_clear_screen(max_attempts=3)
            return False

        # ── Pre-Click Semantic Match Guard ──
        # For toggle intents (follow/like/save), verify the selected node
        # semantically matches the intent BEFORE clicking. This prevents
        # VLM hallucinations from clicking photo grid items when looking
        # for follow buttons.
        from GramAddict.core.perception.action_memory import _intent_matches_node

        node_semantic = (
            f"text: '{best_node.get('text', '')}', "
            f"desc: '{best_node.get('description', '')}', "
            f"id: '{best_node.get('id', '')}'"
        )
        if not _intent_matches_node(action, node_semantic):
            logger.warning(
                f"🛡️ [GOAP Execute] Pre-click rejection: node does not match intent '{action}'. "
                f"Node: {node_semantic}"
            )
            return False

        # Execute click
        self.device.click(obj=best_node)

        # ── Smart UI Stabilization Poll ──
        # Instead of a static sleep (which is either too long on fast devices or
        # too short on slow ones), we poll dump_hierarchy multiple times.
        # As soon as the XML changes, we know the UI has transitioned.
        MAX_POLLS = 5
        POLL_INTERVAL = 0.5  # seconds between polls
        post_xml = xml_dump  # Start with pre-click state
        for poll in range(MAX_POLLS):
            time.sleep(POLL_INTERVAL)
            post_xml = self.device.dump_hierarchy()
            if post_xml != xml_dump:
                logger.debug(f"[GOAP Poll] UI change detected on poll {poll + 1}/{MAX_POLLS}.")
                break
        else:
            logger.debug(f"[GOAP Poll] No UI change after {MAX_POLLS} polls ({MAX_POLLS * POLL_INTERVAL}s).")
        pre_action_screen = self.perceive(xml_dump)  # Screen state BEFORE the click
        post_screen = self.perceive(post_xml)
        post_screen_type = post_screen["screen_type"]
        pre_action_screen_type = pre_action_screen["screen_type"]

        # Determine if this was a navigation or an interaction
        from GramAddict.core.screen_topology import ScreenTopology

        is_navigation = any(k in action.lower() for k in ["tab", "open", "go to", "navigate", "following list"])
        if not is_navigation:
            is_navigation = ScreenTopology.is_structural_action(pre_action_screen_type, action)
        action_success = False

        # ── UI Change Detection with Noise Threshold ──
        # Raw string diffs of < 50 bytes are noise (timestamps, whitespace, counters).
        # A real navigation changes the XML by hundreds/thousands of bytes.
        MIN_UI_CHANGE_BYTES = 50
        xml_delta = abs(len(post_xml) - len(xml_dump))
        ui_changed = post_xml != xml_dump and xml_delta >= MIN_UI_CHANGE_BYTES
        logger.debug(
            f"[GOAP Verify] ui_changed={ui_changed}, "
            f"xml_len_pre={len(xml_dump)}, xml_len_post={len(post_xml)}, delta={xml_delta}b"
        )

        if is_navigation:
            if ui_changed:
                # ── Step-Aware Navigation Validation (SSOT) ──
                # Validates against the EXPECTED screen for THIS ACTION, not the final goal.
                # This enables multi-step routes: "tap profile tab" should land on
                # OWN_PROFILE (intermediate), even if the goal is "open following list".
                from GramAddict.core.screen_topology import ScreenTopology

                expected_step_screen = ScreenTopology.expected_screen_for_action(action, pre_action_screen_type)

                if expected_step_screen:
                    if post_screen_type == expected_step_screen:
                        action_success = True
                        logger.info(f"✅ [GOAP Step] '{action}' → {post_screen_type.name} (matches HD Map)")
                        self.planner.knowledge.learn_screen_mapping(action, post_screen_type)
                    else:
                        logger.warning(
                            f"❌ [GOAP Step] '{action}' expected {expected_step_screen.name}, "
                            f"got {post_screen_type.name}. Rejecting."
                        )
                        action_success = False
                        # Unlearn: purge poisoned Qdrant vectors for this transition
                        # so the memory doesn't reinforce a broken path in future sessions
                        try:
                            from GramAddict.core.qdrant_memory import NavigationMemoryDB

                            NavigationMemoryDB().unlearn_transition(pre_action_screen_type.value, action)
                            logger.info(
                                f"🧹 [Unlearn] Purged Qdrant vector: " f"{pre_action_screen_type.value} → '{action}'"
                            )
                        except Exception as e:
                            logger.debug(f"Unlearn failed (non-critical): {e}")
                else:
                    # Unknown action (not in HD Map) — accept any screen change as progress
                    action_success = True
                    logger.info(f"✅ [GOAP Step] Navigation '{action}' → {post_screen_type.name} (discovery).")
                    self.planner.knowledge.learn_screen_mapping(action, post_screen_type)
            else:
                # Check if we're already on the target screen (no-op is OK)
                goal_lower = (goal or "").lower()
                already_there = self.planner._is_goal_achieved(
                    goal_lower, post_screen_type, post_screen.get("context", {})
                )
                if already_there:
                    logger.info(
                        f"✅ [GOAP Step] No UI change — but goal '{goal}' is already achieved on {post_screen_type.name}. Not punishing."
                    )
                    action_success = True
                else:
                    logger.warning(f"❌ [GOAP Step] No UI change detected after '{action}'.")
                    action_success = False
        else:
            # For interactions (like, follow) or unknown goals, use XML delta + semantic verify
            # REGRESSION FIX 2026-05-01: Toggle actions (like/save) produce tiny XML deltas
            # (e.g. checked="false" → "true" = 1 byte). We must NOT gate interactions on
            # MIN_UI_CHANGE_BYTES. ANY change at all warrants semantic verification.
            interaction_xml_changed = post_xml != xml_dump
            if post_screen_type == ScreenType.FOREIGN_APP:
                logger.error(
                    f"❌ [GOAP Verify] Interaction '{action}' caused navigation to FOREIGN_APP (e.g. Play Store). Rejecting as catastrophic failure."
                )
                action_success = False
            elif interaction_xml_changed:
                score = best_node.get("score", 0.0) if best_node else 0.0
                verification = engine.verify_success(action, post_xml, device=self.device, confidence=score)
                if verification is True:
                    action_success = True
                    logger.info(f"✅ [GOAP Step] Interaction '{action}' successful.")
                elif verification is False:
                    logger.warning(f"❌ [GOAP Verify] Semantic verification failed for '{action}'.")
                    action_success = False
                elif verification is None:
                    logger.warning(
                        f"⚠️ [GOAP Verify] Semantic verification INCONCLUSIVE for '{action}'. Will not blacklist."
                    )
                    action_success = None
            else:
                logger.warning(f"❌ [GOAP Verify] No UI change detected after interaction '{action}'.")
                action_success = None  # Inconclusive if UI didn't change at all

        # Optional: Log if the overarching goal was met during this step
        if goal and action_success:
            from GramAddict.core.screen_topology import ScreenTopology

            goal_target = ScreenTopology.goal_to_target_screen(goal.lower() if goal else "")
            if goal_target and post_screen_type == goal_target:
                logger.info(f"🎉 [GOAP Verify] OVERARCHING Goal '{goal}' achieved during step '{action}'.")
        if action_success is True:
            engine.confirm_click(action)
            return True
        elif action_success is False:
            engine.reject_click(action)
            return False
        else:
            # action_success is None (INCONCLUSIVE)
            logger.warning(f"⚠️ [GOAP Execute] Applying AGGRESSIVE PENALTY for inconclusive action '{action}'.")
            engine.decay_click(action)
            # Double penalty to burn ambiguous paths faster (outer loop adds +1, so total +2 = instantly hits MAX_RETRIES)
            self.action_failures[(pre_action_screen_type, action)] = (
                self.action_failures.get((pre_action_screen_type, action), 0) + 1
            )
            return False

    def _execute_recalled_path(self, steps: List[Dict], goal: str) -> bool:
        """Execute a memorized path."""
        # Pre-check: Is the goal already met? Don't execute stale paths.
        screen = self.perceive()
        if self.planner.plan_next_step(goal, screen) is None:
            logger.info(f"🎯 [GOAP Recall] Goal '{goal}' already achieved. Skipping recalled path.")
            return True

        for i, step in enumerate(steps):
            action = step.get("action", "")
            logger.info(f"🧠 [GOAP Recall Step {i + 1}/{len(steps)}] '{action}'")

            success = self._execute_action(action, goal=goal)
            if not success:
                logger.warning(f"⚠️ [GOAP Recall] Step '{action}' failed. Path may be stale.")
                return False

            random_sleep(0.5, 1.0)

        # Verify goal achieved
        screen = self.perceive()
        achieved = self.planner.plan_next_step(goal, screen) is None
        return achieved

    def _execute_type_and_post_comment(self, fallback_text: str) -> bool:
        """Handles the specific typing interaction, prioritizing Meta AI chips if available."""
        from GramAddict.core.telepathic_engine import TelepathicEngine
        import xml.etree.ElementTree as ET
        
        logger.info("💬 [GOAP] Executing 'type and post comment'...")
        engine = TelepathicEngine.get_instance()
        
        # 1. Tap the comment input field to open the keyboard
        xml_dump = self.device.dump_hierarchy()
        if "com.google.android.inputmethod.latin" not in xml_dump:
            logger.info("⌨️ [GOAP] Tapping comment composer to open keyboard...")
            best_node = engine.find_best_node(xml_dump, "tap comment input field", min_confidence=0.7, device=self.device)
            if best_node:
                self.device.click(obj=best_node)
                random_sleep(1.5, 2.5)
                xml_dump = self.device.dump_hierarchy()
            else:
                logger.warning("⚠️ [GOAP] Could not find comment input field.")
                return False

        # 2. Check for Meta AI chips (Supportive, Funny, etc.)
        meta_ai_chips = []
        try:
            root = ET.fromstring(xml_dump.encode("utf-8"))
            for node in root.iter("node"):
                node_text = node.attrib.get("text", "")
                if node_text in ["Supportive", "Rewrite", "Absurd", "Casual", "Funny", "Heartfelt", "Professional"]:
                    meta_ai_chips.append(node_text)
        except Exception as e:
            logger.debug(f"XML parse error for Meta AI chips: {e}")

        if meta_ai_chips:
            logger.info(f"✨ [Meta AI] Detected Meta AI chips: {meta_ai_chips}")
            logger.info("🧠 [Meta AI] Asking VLM to select the best tone...")
            
            # Use Telepathic Engine to pick the best chip via VLM
            chip_node = engine.find_best_node(
                xml_dump, 
                "tap the best Meta AI tone chip to generate a comment (e.g. Supportive, Funny, Casual)", 
                min_confidence=0.6, 
                device=self.device
            )
            
            if chip_node:
                logger.info(f"✨ [Meta AI] VLM selected chip: '{chip_node.get('text', 'Unknown')}'")
                self.device.click(obj=chip_node)
                random_sleep(3.0, 4.5)  # Wait for Meta AI to generate the text
            else:
                logger.warning("⚠️ [Meta AI] VLM failed to pick a chip, falling back to manual typing.")
                self.device.type_text(fallback_text)
                random_sleep(1.0, 2.0)
        else:
            # 3. Fallback: Type the text provided by our own VLM writer
            logger.info(f"⌨️ [GOAP] Typing comment manually: {fallback_text}")
            self.device.type_text(fallback_text)
            random_sleep(1.0, 2.0)

        # 4. Click the Post button
        xml_dump = self.device.dump_hierarchy()
        post_btn = engine.find_best_node(xml_dump, "tap post comment button", min_confidence=0.7, device=self.device)
        if post_btn:
            self.device.click(obj=post_btn)
            random_sleep(1.5, 2.5)
            logger.info("✅ [GOAP] Comment posted successfully.")
            return True
        else:
            logger.warning("⚠️ [GOAP] Could not find post button after typing.")
            return False

    # ── Convenience methods (backward compatibility with navigate_to) ──

    def navigate_to_screen(self, target: str) -> bool:
        """Navigate to a screen by name. Wrapper for achieve(). Delegates to ScreenTopology SSOT."""
        from GramAddict.core.screen_topology import ScreenTopology

        goal = ScreenTopology.screen_name_to_goal(target)
        return self.achieve(goal)

    def get_current_screen_type(self) -> ScreenType:
        """Quick screen check."""
        screen = self.perceive()
        return screen["screen_type"]
