import logging
import os
import random
import re

try:
    import psutil
except ImportError:
    psutil = None
from datetime import datetime
from time import sleep

from colorama import Fore, Style

from GramAddict.core.account_switcher import verify_and_switch_account
from GramAddict.core.active_inference import ActiveInferenceEngine
from GramAddict.core.config import Config
from GramAddict.core.darwin_engine import DarwinEngine
from GramAddict.core.device_facade import create_device, get_device_info
from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
from GramAddict.core.dojo_engine import DojoEngine

# Cognitive Stack
from GramAddict.core.dopamine_engine import DopamineEngine
from GramAddict.core.growth_brain import GrowthBrain
from GramAddict.core.log import configure_logger
from GramAddict.core.perception.feed_analysis import (
    extract_post_content as _extract_post_content_impl,
)
from GramAddict.core.persistent_list import PersistentList
from GramAddict.core.physics.humanized_input import (
    humanized_click as _humanized_click_impl,
)
from GramAddict.core.physics.humanized_input import (
    humanized_horizontal_swipe as _humanized_horizontal_swipe_impl,
)

# ── Decomposed Modules (Phase 1 extraction) ──
from GramAddict.core.physics.humanized_input import (
    humanized_scroll as _humanized_scroll_impl,
)
from GramAddict.core.physics.timing import (
    align_active_post as _align_active_post_impl,
)
from GramAddict.core.physics.timing import (
    wait_for_post_loaded as _wait_for_post_loaded_impl,
)
from GramAddict.core.physics.timing import (
    wait_for_profile_loaded as _wait_for_profile_loaded_impl,
)
from GramAddict.core.physics.timing import (
    wait_for_story_loaded as _wait_for_story_loaded_impl,
)
from GramAddict.core.q_nav_graph import QNavGraph
from GramAddict.core.sensors.honeypot_radome import HoneypotRadome
from GramAddict.core.session_state import SessionState, SessionStateEncoder
from GramAddict.core.swarm_protocol import SwarmProtocol
from GramAddict.core.telepathic_engine import TelepathicEngine
from GramAddict.core.unfollow_engine import _run_zero_latency_unfollow_loop
from GramAddict.core.utils import (
    check_if_updated,
    close_instagram,
    get_instagram_version,
    open_instagram,
    random_sleep,
    set_time_delta,
    wait_for_next_session,
)
from GramAddict.core.zero_latency_engine import ZeroLatencyEngine


def log_metabolic_rate():
    if psutil is None:
        logging.getLogger(__name__).debug("🧬 [Metabolism] psutil not installed. Skipping memory log.")
        return
    try:
        process = psutil.Process(os.getpid())
        mem_info = process.memory_info()
        logging.getLogger(__name__).info(
            f"🧬 [Metabolism] RSS: {mem_info.rss / 1024 / 1024:.2f} MB | VMS: {mem_info.vms / 1024 / 1024:.2f} MB"
        )
    except Exception as e:
        logging.getLogger(__name__).debug(f"🧬 [Metabolism] Failed to log memory: {e}")


logger = logging.getLogger(__name__)


def check_production_integrity():
    """
    Ensures that the production environment is not poisoned by test mocks.
    Fails loudly if critical components are detected as MagicMocks.
    """
    import sys

    # We no longer skip this in tests. Production integrity must hold everywhere.
    pass

    try:
        from unittest.mock import MagicMock

        # Check QdrantClient
        try:
            from qdrant_client import QdrantClient

            if isinstance(QdrantClient, MagicMock):
                print(
                    f"{Fore.RED}💀 CRITICAL: Production environment poisoned by MagicMock (QdrantClient).{Style.RESET_ALL}"
                )
                print(
                    f"{Fore.YELLOW}Verify that tests/e2e/conftest.py is not being loaded. Check sys.modules leakage.{Style.RESET_ALL}"
                )
                sys.exit(1)
        except ImportError:
            pass

        # Check DeviceFacade (another common victim)
        from GramAddict.core.device_facade import DeviceFacade

        if isinstance(DeviceFacade, MagicMock):
            print(
                f"{Fore.RED}💀 CRITICAL: Production environment poisoned by MagicMock (DeviceFacade).{Style.RESET_ALL}"
            )
            sys.exit(1)

    except ImportError:
        # unittest.mock might not even be available in some stripped envs
        pass


def start_bot(**kwargs):
    # 0. Integrity Check
    check_production_integrity()

    configs = Config(first_run=True, **kwargs)
    configure_logger(configs.debug, configs.username)
    check_if_updated()

    from GramAddict.core.benchmark_guard import check_model_benchmarks

    check_model_benchmarks(configs)

    from GramAddict.core.llm_provider import log_openrouter_burn

    log_openrouter_burn()

    # Check for direct execution modes that bypass normal bot state
    configs.parse_args()

    try:
        from GramAddict.core.llm_provider import prewarm_ollama_models

        prewarm_ollama_models(configs)
    except Exception as e:
        logger.debug(f"Prewarm failed: {e}")

    sessions = PersistentList("sessions", SessionStateEncoder)
    device = create_device(configs.device_id, configs.app_id, configs.args)

    # ── Initialize Biomechanical Physics ──
    from GramAddict.core.physics.biomechanics import PhysicsBody

    handedness = getattr(configs.args, "handedness", "right") or "right"
    PhysicsBody.reset()  # Clean state for new session
    PhysicsBody.get_session_instance(device, handedness=handedness)
    logger.info(f"🦴 [Biomechanics] Session initialized: {handedness}-handed thumb model")

    # Initialize Cognitive Stack with proper dependencies
    username = getattr(configs.args, "username", "") or "unknown_user"

    # Parse persona interests from config (comma-separated string → list)
    persona_raw = getattr(
        configs.args,
        "ai_target_audience",
        getattr(configs.args, "persona_interests", getattr(configs.args, "target_audience", "")),
    )
    persona_interests = [p.strip() for p in persona_raw.split(",") if p.strip()] if persona_raw else []

    global_goal = getattr(configs.args, "goal", None)
    if global_goal:
        persona_interests.insert(0, global_goal)
        logger.info(
            f"🎯 [Autonomous Directive] Overriding target audience with high-level goal: {global_goal}",
            extra={"color": f"{Style.BRIGHT}{Fore.GREEN}"},
        )

    from GramAddict.core.goap import GoalExecutor
    from GramAddict.core.interaction import LLMWriter
    from GramAddict.core.qdrant_memory import DMMemoryDB, ParasocialCRMDB
    from GramAddict.core.resonance_engine import ResonanceEngine

    dopamine = DopamineEngine()
    crm_db = ParasocialCRMDB()
    dm_memory_db = DMMemoryDB()
    resonance_oracle = ResonanceEngine(username, persona_interests=persona_interests, crm=crm_db)
    writer = LLMWriter(username, persona_interests, configs)
    active_inference = ActiveInferenceEngine(username)

    # Core Autonomous Engines

    GoalExecutor.get_instance(device, username)
    zero_engine = ZeroLatencyEngine(device)
    nav_graph = QNavGraph(device)
    growth_brain = GrowthBrain(username, persona_interests=persona_interests)

    info = device.get_info()
    radome = HoneypotRadome(info.get("displayWidth", 1080), info.get("displayHeight", 2400))

    swarm = SwarmProtocol(username)
    darwin = DarwinEngine(username)

    telepathic = TelepathicEngine.get_instance()

    # ── Stage 0: Blank Start (Scorched Earth) ──
    if getattr(configs.args, "blank_start", False):
        logger.warning(f"⚠️ [Blank Start] Wiping ALL persistent AI memories for '{username}'...")
        # 1. Wipe all global AI caches (Heuristics, Screen Types, Navigation Graph, etc.)
        from GramAddict.core.qdrant_memory import wipe_all_ai_caches

        try:
            wipe_all_ai_caches()
        except (ConnectionError, OSError) as e:
            logger.warning(f"⚠️ Failed to wipe global AI caches (Qdrant unreachable): {e}")
        except Exception as e:
            logger.error(f"⚠️ Failed to wipe global AI caches: {e}")

        # 2. Wipe user-specific PathMemory
        from GramAddict.core.goap import PathMemory

        try:
            path_mem = PathMemory(username)
            path_mem.wipe()
        except (ConnectionError, OSError) as e:
            logger.warning(f"⚠️ Failed to wipe user-specific PathMemory (Qdrant unreachable): {e}")
        except Exception as e:
            logger.warning(f"⚠️ Failed to wipe user-specific PathMemory: {e}")

    cognitive_stack = {
        "active_inference": active_inference,
        "dopamine": dopamine,
        "swarm": swarm,
        "resonance": resonance_oracle,
        "growth_brain": growth_brain,
        "radome": radome,
        "nav_graph": nav_graph,
        "zero_engine": zero_engine,
        "telepathic": telepathic,
        "darwin": darwin,
        "crm": crm_db,
        "dm_memory": dm_memory_db,
        "writer": writer,
    }

    from GramAddict.core.behaviors import PluginRegistry
    from GramAddict.core.behaviors.ad_guard import AdGuardPlugin
    from GramAddict.core.behaviors.anomaly_handler import AnomalyHandlerPlugin
    from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin
    from GramAddict.core.behaviors.close_friends_guard import CloseFriendsGuardPlugin
    from GramAddict.core.behaviors.comment import CommentPlugin
    from GramAddict.core.behaviors.darwin_dwell import DarwinDwellPlugin
    from GramAddict.core.behaviors.follow import FollowPlugin
    from GramAddict.core.behaviors.grid_like import GridLikePlugin
    from GramAddict.core.behaviors.like import LikePlugin
    from GramAddict.core.behaviors.obstacle_guard import ObstacleGuardPlugin
    from GramAddict.core.behaviors.perfect_snapping import PerfectSnappingPlugin
    from GramAddict.core.behaviors.post_data_extraction import PostDataExtractionPlugin
    from GramAddict.core.behaviors.post_interaction import PostInteractionPlugin
    from GramAddict.core.behaviors.profile_guard import ProfileGuardPlugin
    from GramAddict.core.behaviors.profile_visit import ProfileVisitPlugin
    from GramAddict.core.behaviors.rabbit_hole import RabbitHolePlugin
    from GramAddict.core.behaviors.repost import RepostPlugin
    from GramAddict.core.behaviors.resonance_evaluator import ResonanceEvaluatorPlugin
    from GramAddict.core.behaviors.scrape_profile import ScrapeProfilePlugin
    from GramAddict.core.behaviors.story_view import StoryViewPlugin

    PluginRegistry.reset()
    plugin_registry = PluginRegistry.get_instance()
    plugin_registry.register(ProfileGuardPlugin())
    plugin_registry.register(StoryViewPlugin())
    plugin_registry.register(FollowPlugin())
    plugin_registry.register(GridLikePlugin())
    plugin_registry.register(CarouselBrowsingPlugin())
    plugin_registry.register(AdGuardPlugin())
    plugin_registry.register(CloseFriendsGuardPlugin())
    plugin_registry.register(AnomalyHandlerPlugin())
    plugin_registry.register(ObstacleGuardPlugin())
    plugin_registry.register(PerfectSnappingPlugin())
    plugin_registry.register(PostDataExtractionPlugin())
    plugin_registry.register(ResonanceEvaluatorPlugin())
    plugin_registry.register(RabbitHolePlugin())
    plugin_registry.register(DarwinDwellPlugin())
    plugin_registry.register(ProfileVisitPlugin())
    plugin_registry.register(LikePlugin())
    plugin_registry.register(CommentPlugin())
    plugin_registry.register(RepostPlugin())
    plugin_registry.register(PostInteractionPlugin())
    plugin_registry.register(ScrapeProfilePlugin())

    cognitive_stack["plugin_registry"] = plugin_registry

    is_first_session = True
    has_scanned_own_profile = False

    dojo = DojoEngine.get_instance(device)
    dojo.start()
    cognitive_stack["dojo"] = dojo

    try:
        bot_start_time = datetime.now()
        max_runtime = getattr(configs.args, "max_runtime_minutes", None)

        dopamine.global_start_time = bot_start_time
        dopamine.global_max_runtime_minutes = max_runtime
        GoalExecutor.global_start_time = bot_start_time
        GoalExecutor.global_max_runtime_minutes = max_runtime

        while True:
            if max_runtime:
                from datetime import timedelta

                elapsed = datetime.now() - bot_start_time
                if elapsed > timedelta(minutes=max_runtime):
                    logger.info(
                        f"🛑 [Timeout] Maximum runtime of {max_runtime} minutes reached. Stopping bot.",
                        extra={"color": f"{Fore.RED}"},
                    )
                    break

            set_time_delta(configs.args)
            inside_working_hours, time_left = SessionState.inside_working_hours(
                configs.args.working_hours, configs.args.time_delta_session
            )
            if not inside_working_hours:
                wait_for_next_session(time_left, None, sessions, device)

            get_device_info(device)
            session_state = SessionState(configs)
            session_state.set_limits_session()
            sessions.append(session_state)
            device.wake_up()

            log_metabolic_rate()
            logger.info(
                "-------- START AGENT SESSION: "
                + str(session_state.startTime.strftime("%H:%M:%S - %Y/%m/%d"))
                + " --------",
                extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"},
            )

            if open_instagram(device, force_restart=False):
                if is_first_session:
                    # Do not blindly assume we are on HomeFeed if the app was already open somewhere else.
                    # QNavGraph will try to dynamically resolve from UNKNOWN using the bottom navigation bar.
                    nav_graph.current_state = "UNKNOWN"
                    logger.info("Initializing Top-Level Graph context...")

                    if not verify_and_switch_account(device, nav_graph, username):
                        logger.error(f"Cannot verify or switch to target account '{username}'. Halting session.")
                        break

                    is_first_session = False
                try:
                    running_ig_version = get_instagram_version(device)
                    logger.debug(f"Instagram version: {running_ig_version}")
                except Exception as e:
                    logger.error(f"Error retrieving the IG version: {e}")

            # ════════════════════════════════════════════════════════════════════════════
            # 🤖 AGENT ORCHESTRATOR LOOP
            # ════════════════════════════════════════════════════════════════════════════
            dopamine.reset_session()

            # Establish Initial Strategy from Config
            growth_brain.strategy = getattr(configs.args, "agent_strategy", "aggressive_growth")

            logger.info(
                f"🧠 [Agent Orchestrator] Session started. Strategy: {growth_brain.strategy} | Persona: {getattr(configs.args, 'agent_persona', 'unknown')}"
            )
            # 1. Starten wir den GOAP Executor, um die UI-Struktur autonom zu erfassen
            goap = GoalExecutor.get_instance(device, username)

            # --- PHASE 0: Autonomous Profile Scanning ---
            if getattr(configs.args, "ai_learn_own_profile", False) and not has_scanned_own_profile:
                logger.info(
                    "🧠 [Identity Boot] Autonomous Profile Scanning Triggered: Learning own content...",
                    extra={"color": f"{Fore.MAGENTA}"},
                )
                success = goap.achieve("learn own profile")
                if success:
                    sleep(2.0)
                    try:
                        profile_xml = device.dump_hierarchy()
                        all_nodes = telepathic._extract_semantic_nodes(profile_xml)

                        raw_bio_text = []
                        for node in all_nodes:
                            text = node.get("original_attribs", {}).get("text", "")
                            desc = node.get("original_attribs", {}).get("desc", "")
                            if len(text) > 4:
                                raw_bio_text.append(text)
                            if len(desc) > 4:
                                raw_bio_text.append(desc)

                        # Ensure grid is visible by scrolling down slightly
                        _humanized_scroll(device, is_skip=True)
                        sleep(1.5)

                        # Tap first grid post to learn from actual captions
                        if nav_graph.do("tap first image post in profile grid"):
                            post_loaded = _wait_for_post_loaded(device, timeout=5)
                            if post_loaded:
                                logger.info(
                                    "📸 [Identity Boot] Reading recent posts to analyze actual content vibe...",
                                    extra={"color": f"{Fore.CYAN}"},
                                )
                                for _ in range(3):
                                    post_xml = device.dump_hierarchy()
                                    if isinstance(post_xml, str):
                                        post_data = _extract_post_content(post_xml)
                                        if post_data.get("caption"):
                                            raw_bio_text.append(post_data["caption"])
                                        elif post_data.get("description"):
                                            raw_bio_text.append(post_data["description"])

                                    _humanized_scroll(device, is_skip=False)
                                    sleep(2.0)

                                device.press("back")
                                sleep(1.5)

                        # Deduplicate while preserving order
                        unique_texts = list(dict.fromkeys(raw_bio_text))
                        condensed_profile = " | ".join(unique_texts[:30])  # Take top substantive elements

                        logger.debug(f"Captured Profile Payload: {condensed_profile[:200]}...")

                        prompt = (
                            "You are an analytical profiling engine. Read the following text ripped straight from an Instagram profile page "
                            "(which contains bio, follower counts, button labels, and recent post descriptions). "
                            "Determine the exact 'persona' (2-3 words) and 'vibe' (3-4 adjectives) that represents THIS specific user.\n\n"
                            f"PROFILE TEXT: {condensed_profile}\n\n"
                            'Respond ONLY in valid JSON format: {"persona": "<value>", "vibe": "<value>"}'
                        )

                        from GramAddict.core.llm_provider import query_llm

                        model = getattr(configs.args, "ai_condenser_model")
                        url = getattr(configs.args, "ai_condenser_url")

                        response_dict = query_llm(url=url, model=model, prompt=prompt, format_json=True, timeout=120)
                        if response_dict and isinstance(response_dict, dict) and "persona" in response_dict:
                            new_persona_raw = response_dict.get("persona", "")
                            new_vibe = response_dict.get("vibe", "")

                            if new_persona_raw and new_vibe:
                                new_persona_list = (
                                    [p.strip() for p in new_persona_raw.split(",") if p.strip()]
                                    if "," in new_persona_raw
                                    else [new_persona_raw]
                                )
                                resonance_oracle.update_identity(new_persona_list, new_vibe)
                                growth_brain.persona_interests = new_persona_list

                                # Overwrite config values in-memory
                                setattr(configs.args, "agent_persona", new_persona_raw)
                                setattr(configs.args, "ai_vibe", new_vibe)
                    except Exception as e:
                        logger.error(f"Failed to learn own profile autonomously: {e}")
                else:
                    logger.warning("🧠 [Identity Boot] Failed to navigate to own profile.")

                has_scanned_own_profile = True

            while not dopamine.is_app_session_over():
                # ── 1. Generate available tasks from mission + plugins ──
                from GramAddict.core.goal_decomposer import GoalDecomposer

                decomposer = GoalDecomposer(
                    plugins=configs.config.get("plugins", {}) if configs.config else {},
                    actions={
                        k: getattr(configs.args, k, None)
                        for k in ("feed", "explore", "reels")
                        if getattr(configs.args, k, None)
                    },
                    mission=configs.config.get("mission", {}) if configs.config else {},
                )
                available_tasks = decomposer.generate_tasks()

                if not available_tasks:
                    # No plugins enabled = nothing to do. Fall back to legacy desire system.
                    current_desire = growth_brain.get_current_desire(dopamine)
                    if current_desire == "ShiftContext":
                        logger.info("🧠 [Free Will] Boredom critical. Forcing app restart.")
                        device.app_stop(device.app_id)
                        random_sleep(2.0, 4.0)
                        device.app_start(device.app_id, use_monkey=True)
                        random_sleep(4.0, 6.0)
                        dopamine.boredom = max(0.0, dopamine.boredom * 0.2)
                        continue

                    # Legacy desire → target mapping (kept for backward compatibility)
                    target_map = {
                        "DiscoverNewContent": ["ExploreFeed", "ReelsFeed"],
                        "NurtureCommunity": ["HomeFeed", "StoriesFeed"],
                        "SocialReciprocity": ["FollowingList"],
                    }

                    dm_config = configs.get_plugin_config("dm_reply")
                    if dm_config.get("enabled", False):
                        target_map["SocialReciprocity"].append("MessageInbox")

                    import secrets

                    options = target_map.get(current_desire, ["HomeFeed"])
                    current_target = secrets.choice(options)
                else:
                    # ── 2. Select a concrete Task ──
                    selected_task = growth_brain.select_task(dopamine, available_tasks)

                    if selected_task is None:
                        # ShiftContext signal from high boredom
                        logger.info("🧠 [Free Will] Boredom critical. Forcing app restart to clear context.")
                        device.app_stop(device.app_id)
                        random_sleep(2.0, 4.0)
                        device.app_start(device.app_id, use_monkey=True)
                        random_sleep(4.0, 6.0)
                        dopamine.boredom = max(0.0, dopamine.boredom * 0.2)
                        continue

                    current_target = selected_task.target_screen
                    logger.info(
                        f"🎯 [GoalDecomposer] Task: {selected_task.intent} "
                        f"→ {current_target} (budget={selected_task.budget_posts})"
                    )

                logger.info(f"🧠 [Agent Orchestrator] Routed to {current_target}")

                logger.info(f"⚡ Navigating to {current_target}")
                success = nav_graph.navigate_to(current_target, zero_engine)

                if success:
                    if current_target == "ExploreFeed":
                        # [Phase 2] Visual selection of the first post
                        logger.info("📱 [Vision Core] Evaluating explore grid for the most resonant post...")
                        res_eval = telepathic.evaluate_grid_visuals(device, persona_interests)

                        if res_eval:
                            logger.info(f"✨ [Vision Core] Clicking visual match: {res_eval.get('semantic')}")
                            _humanized_click(device, res_eval["x"], res_eval["y"])
                        else:
                            logger.info("📱 Falling back to default: Opening first explore item from the grid...")
                            nav_graph.do("tap first image in explore grid")

                        # Wait for post to actually load (poll for feed markers)
                        post_loaded = _wait_for_post_loaded(device, nav_graph=nav_graph, timeout=5)
                        if not post_loaded:
                            logger.warning("❌ Post failed to open from grid. Retrying next loop.")
                            continue
                    elif current_target == "StoriesFeed":
                        logger.info("📱 Locating story tray on HomeFeed...")
                        if not nav_graph.do("tap story ring avatar"):
                            logger.warning("❌ Failed to tap story ring avatar. Retrying next loop.")
                            continue
                        post_loaded = _wait_for_story_loaded(device, timeout=5)
                        if not post_loaded:
                            logger.warning("❌ Stories failed to open from HomeFeed. Retrying next loop.")
                            continue

                    if current_target == "StoriesFeed":
                        result = _run_zero_latency_stories_loop(device, configs, session_state, cognitive_stack)
                    elif current_target == "FollowingList":
                        result = _run_zero_latency_unfollow_loop(
                            device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack
                        )
                    elif current_target == "MessageInbox":
                        result = _run_zero_latency_dm_loop(
                            device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack
                        )
                    elif current_target == "SearchFeed":
                        result = _run_zero_latency_search_loop(
                            device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack
                        )
                    else:
                        is_reels = current_target == "ReelsFeed"
                        result = _run_zero_latency_feed_loop(
                            device,
                            zero_engine,
                            nav_graph,
                            configs,
                            session_state,
                            current_target,
                            cognitive_stack,
                            is_reels=is_reels,
                        )

                    # Evaluate outcome from loop
                    if result in ("BOREDOM_CHANGE_FEED", "FEED_EXHAUSTED"):
                        logger.info(f"🧠 [Free Will] Sub-routine in {current_target} exhausted/bored.")
                    if result == "BOREDOM_CHANGE_FEED":
                        dopamine.reset_boredom()  # Reset boredom allowing new desire
                        continue  # Loops back to get_current_desire()

                    elif result == "CONTEXT_LOST":
                        logger.warning(
                            f"⚠️ Context was lost in {current_target}. Forcing app restart and returning to HomeFeed to escape softlock."
                        )
                        device.app_stop(device.app_id)
                        random_sleep(1.0, 2.0)
                        device.app_start(device.app_id, use_monkey=True)
                        random_sleep(3.0, 5.0)
                        nav_graph.current_state = "UNKNOWN"

                        # Force context reset to HomeFeed so we don't repeat the same error loop
                        continue
                    else:
                        logger.info(f"Session concluding due to state: {result}")
                        break  # Session over or unhandled state
                else:
                    logger.error(f"Aborting target {current_target} due to navigation failure.")
                    break

            logger.info(f"Session complete. Boredom: {dopamine.boredom:.1f}%. Sleeping before next iteration...")
            close_instagram(device)
            random_sleep(30, 60)

    except KeyboardInterrupt:
        logger.info("🛑 Caught KeyboardInterrupt! Exiting immediately.")
        raise
    finally:
        if "dojo" in locals() and dojo.is_running:
            dojo.stop()

        # ❄️ Release VRAM
        try:
            from GramAddict.core.llm_provider import unload_ollama_models

            unload_ollama_models(configs)
            # Give the thread a tiny bit of time to send the request before process exits
            sleep(0.5)
        except Exception as e:
            logger.debug(f"Failed to trigger VRAM cleanup: {e}")


# FEED_MARKERS: imported from GramAddict.core.perception.feed_analysis (see top imports)


def _wait_for_post_loaded(device, timeout=5, nav_graph=None):
    """Delegate to physics.timing. See GramAddict.core.physics.timing."""
    return _wait_for_post_loaded_impl(device, timeout=timeout, nav_graph=nav_graph)


def _wait_for_story_loaded(device, timeout=5):
    """Delegate to physics.timing. See GramAddict.core.physics.timing."""
    return _wait_for_story_loaded_impl(device, timeout=timeout)


def _wait_for_profile_loaded(device, timeout=5):
    """Delegate to physics.timing. See GramAddict.core.physics.timing."""
    return _wait_for_profile_loaded_impl(device, timeout=timeout)


def _humanized_scroll(device, is_skip=False, resonance_score=None):
    """Delegate to physics module. See GramAddict.core.physics.humanized_input."""
    _humanized_scroll_impl(device, is_skip=is_skip, resonance_score=resonance_score)


def _humanized_click(device, x, y, double=False, sleep_mod=1.0):
    """Delegate to physics module. See GramAddict.core.physics.humanized_input."""
    _humanized_click_impl(device, x, y, double=double, sleep_mod=sleep_mod)


def _humanized_horizontal_swipe(device, start_x, end_x, y, duration_ms):
    """Delegate to physics module. See GramAddict.core.physics.humanized_input."""
    _humanized_horizontal_swipe_impl(device, start_x, end_x, y, duration_ms)


# has_carousel_in_view: imported from GramAddict.core.perception.feed_analysis (see top imports)


def _interact_with_profile(device, configs, username, session_state, sleep_mod, logger, cognitive_stack=None):
    """Deep interaction on a profile: Stories, Grid Likes, Follows"""
    import random

    from colorama import Fore

    if cognitive_stack is None:
        cognitive_stack = {}
    _validate_cognitive_stack(cognitive_stack, "ProfileInteraction")

    if hasattr(session_state, "my_username") and username == session_state.my_username:
        logger.info(f"🤝 [Deep Interaction] Skipping own profile @{username} to prevent self-interactions.")
        return

    # info = device.get_info()
    # w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)

    xml_check = device.dump_hierarchy()
    if not isinstance(xml_check, str):
        return

    xml_check_lower = xml_check.lower()

    # ── 1. Profile Guards (Private / Empty) ──
    if "this account is private" in xml_check_lower or "konto ist privat" in xml_check_lower:
        logger.info(
            f"🔒 [Profile Guard] @{username} is private. Aborting deep interaction.", extra={"color": f"{Fore.YELLOW}"}
        )
        return

    if "no posts yet" in xml_check_lower or "noch keine beiträge" in xml_check_lower:
        logger.info(
            f"📭 [Profile Guard] @{username} has no posts. Aborting deep interaction.",
            extra={"color": f"{Fore.YELLOW}"},
        )
        return

    if getattr(configs.args, "ignore_close_friends", False):
        if "close friend" in xml_check_lower:
            logger.info(
                f"💚 [Profile Guard] @{username} is a Close Friend. Ignoring completely.", extra={"color": "\\033[32m"}
            )
            return

    # ── 1.5 Visual Vibe Check (AI Aesthetic Quality Guard) ──
    vibe_check_pct = float(getattr(configs.args, "visual_vibe_check_percentage", 0)) / 100.0
    if vibe_check_pct > 0 and random.random() < vibe_check_pct:
        from GramAddict.core.telepathic_engine import TelepathicEngine

        telepathic = cognitive_stack.get("telepathic") or TelepathicEngine.get_instance()
        persona_interests = cognitive_stack.get("persona_interests", []) if cognitive_stack else []
        vibe_result = telepathic.evaluate_profile_vibe(device, persona_interests)

        if vibe_result:
            score = vibe_result.get("quality_score", 5)
            matches_niche = vibe_result.get("matches_niche", True)
            if score < 5 or not matches_niche:
                logger.warning(
                    f"🚫 [Vibe Check] Profile @{username} rejected (Score: {score}, Niche: {matches_niche}). Reason: {vibe_result.get('reason')}"
                )
                return
            else:
                logger.info(
                    f"✅ [Vibe Check] Profile @{username} approved (Score: {score}). Continuing interaction.",
                    extra={"color": "\\033[36m"},
                )

    # Profile Scraping (Phase 11: Data Extraction)
    if getattr(configs.args, "scrape_profiles", False):
        try:
            logger.info(f"📊 [Scraping] Extracting metadata for @{username}...", extra={"color": f"{Fore.CYAN}"})
            from GramAddict.core.telepathic_engine import TelepathicEngine

            telepathic = TelepathicEngine.get_instance()
            crm = cognitive_stack.get("crm") if cognitive_stack else None

            # Simple heuristic for extraction (followers/following)
            f_node = telepathic.find_best_node(xml_check, "Followers count text or number", device=device)
            fg_node = telepathic.find_best_node(xml_check, "Following count text or number", device=device)
            bio_node = telepathic.find_best_node(xml_check, "User biography or description text", device=device)

            scraped_data = {
                "username": username,
                "followers": f_node.get("text") if f_node else "unknown",
                "following": fg_node.get("text") if fg_node else "unknown",
                "bio": bio_node.get("text") if bio_node else "No bio",
            }

            logger.info(
                f"✅ [Scraping] Data acquired: {scraped_data['followers']} followers, {scraped_data['following']} following."
            )
            session_state.add_interaction(source=username, succeed=False, followed=False, scraped=True)

            if crm:
                crm.log_interaction(username, "scrape", metadata=scraped_data)
        except Exception as e:
            logger.warning(f"⚠️ [Scraping] Error during profiling: {e}")

    # ── Execute Plugin Registry Behaviors ──
    from GramAddict.core.behaviors import BehaviorContext, PluginRegistry

    ctx = BehaviorContext(
        device=device,
        configs=configs,
        session_state=session_state,
        cognitive_stack=cognitive_stack,
        context_xml=xml_check,
        sleep_mod=sleep_mod,
        username=username,
    )

    registry = PluginRegistry.get_instance()
    results = registry.execute_all(ctx)

    # Check if any plugin requested skipping further profile interaction
    for result in results:
        if result.executed and result.should_skip:
            logger.debug("⏭️ Profile interaction aborted early by a plugin.")
            return
    # Let the native UI momentum scroll finish just like a human watching the feed
    sleep(random.uniform(1.2, 2.0))

    # Hesitation mistake (humans sometimes pause randomly)
    if random.randint(1, 100) <= 5:
        sleep(random.uniform(1.5, 3.5))


def _align_active_post(device):
    """Delegate to physics.timing. See GramAddict.core.physics.timing."""
    return _align_active_post_impl(device)


def _extract_post_content(context_xml: str) -> dict:
    """Delegate to perception module. See GramAddict.core.perception.feed_analysis."""
    return _extract_post_content_impl(context_xml)


def _run_zero_latency_stories_loop(device, configs, session_state, cognitive_stack):
    """
    Top-Level Stories Bingewatching Loop
    Mimics a user opening the story tray and endlessly tapping through stories.
    Relies on DopamineEngine for early exit (boredom).
    """
    import random
    from time import sleep

    from colorama import Fore

    _validate_cognitive_stack(cognitive_stack, "StoriesLoop")
    logger.info("🎬 [StoriesFeed] Starting native story binging loop...", extra={"color": f"{Fore.CYAN}"})

    dopamine = cognitive_stack.get("dopamine")
    info = device.get_info()
    w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
    sleep_mod = float(getattr(configs.args, "speed_multiplier", 1.0))

    stories_arg_str = getattr(configs.args, "stories", "999") or "999"
    try:
        min_st, max_st = map(int, stories_arg_str.split("-"))
        limit = random.randint(min_st, max_st)
    except Exception:
        try:
            limit = int(stories_arg_str)
        except ValueError:
            limit = 999

    iteration = 0

    while not dopamine.is_app_session_over() and iteration < limit:
        iteration += 1

        # Check for boredom
        if dopamine.wants_to_change_feed():
            logger.info("🧠 [Dopamine] Bored. Escaping StoriesFeed to seek new stimuli.")
            device.press("back")  # Attempt to back out to feed
            sleep(random.uniform(1.0, 2.0) * sleep_mod)
            return "BOREDOM_CHANGE_FEED"

        xml_dump = device.dump_hierarchy()
        if not xml_dump:
            logger.warning("Failed to dump UI hierarchy in StoriesFeed.")
            return "CONTEXT_LOST"

        # ── Perimeter Guard: Verify we're still inside Instagram ──
        # Production bug 2026-05-03: A story's swipe-up link opened the Play Store,
        # and the loop kept tapping blindly on com.android.vending for 5+ iterations.
        packages = set(re.findall(r'package="([^"]+)"', xml_dump))
        app_id = getattr(device, "app_id", "com.instagram.android")
        if packages and app_id not in packages:
            logger.error(
                f"🚨 [StoriesFeed] FOREIGN APP DETECTED! Packages: {packages}. "
                f"A story link likely opened an external app. Aborting loop."
            )
            device.press("back")
            sleep(1.5)
            return "CONTEXT_LOST"

        if getattr(configs.args, "ignore_close_friends", False):
            if "close friend" in xml_dump.lower():
                logger.info(
                    "💚 [Anti-Friend] Story is from a Close Friend. Swiping horizontally to skip User.",
                    extra={"color": "\\033[32m"},
                )
                _humanized_horizontal_swipe(
                    device, start_x=int(w * 0.8), end_x=int(w * 0.2), y=int(h * 0.5), duration_ms=250
                )
                sleep(random.uniform(0.5, 1.0) * sleep_mod)
                continue

        # Tap right to go next
        _humanized_click(device, int(w * 0.85), int(h * 0.5), sleep_mod=sleep_mod)
        sleep(random.uniform(2.0, 5.0) * sleep_mod)

    logger.info("🎬 [StoriesFeed] Session completed naturally.")
    device.press("back")
    return "FEED_EXHAUSTED"


def _validate_cognitive_stack(cognitive_stack, context_name):
    """
    Validates that the cognitive stack has all required engine dependencies
    injected properly. This is the ultimate zero-trust guard for plugins.
    """
    if not isinstance(cognitive_stack, dict):
        raise TypeError(f"[{context_name}] CognitiveStack must be a dict, got {type(cognitive_stack)}")

    required_engines = [
        "dopamine",
        "darwin",
        "resonance",
        "active_inference",
        "growth_brain",
        "swarm",
        "writer",
        "nav_graph",
        "zero_engine",
        "telepathic",
    ]

    missing = [eng for eng in required_engines if eng not in cognitive_stack or cognitive_stack[eng] is None]
    if missing:
        import logging

        logger = logging.getLogger(__name__)
        logger.error(f"🚨 [{context_name}] CognitiveStack missing required engines: {missing}")
        raise ValueError(f"[{context_name}] CognitiveStack missing required engines: {missing}")


def _run_zero_latency_feed_loop(
    device, zero_engine, nav_graph, configs, session_state, job_target, cognitive_stack, is_reels=False
):
    """
    The ultra-fast autonomous Free Will loop.

    ALL engines are wired in a closed feedback loop:
    - ResonanceEngine evaluates content → drives Dopamine + Darwin + Interactions
    - ActiveInference predicts UI state → modulates caution level
    - GrowthBrain applies circadian pacing → modulates ALL sleep durations
    - Darwin is the SOLE dwell controller → no duplicate sleep calls
    - SwarmProtocol emits pheromones after successful interactions
    """
    _validate_cognitive_stack(cognitive_stack, "FeedLoop")
    logger.info(f"🔄 Entering Zero-Latency Interaction Pool. Feed: {job_target}")

    dopamine = cognitive_stack.get("dopamine")
    darwin = cognitive_stack.get("darwin")
    cognitive_stack.get("resonance")
    ai = cognitive_stack.get("active_inference")
    growth = cognitive_stack.get("growth_brain")
    cognitive_stack.get("swarm")

    # Track interaction outcomes for end-of-session learning
    session_outcomes = []
    session_outcomes = []
    shared_state = {"consecutive_marker_misses": 0, "consecutive_ads": 0, "session_outcomes": session_outcomes}

    from GramAddict.core.session_state import SessionState

    iteration = 0
    while not dopamine.is_app_session_over():
        limit_tuple = session_state.check_limit(SessionState.Limit.ALL)
        if any(limit_tuple):
            logger.info("🚧 [Limits] Total interactions limit reached. Ending session.")
            break

        iteration += 1
        # ── Global Governance (GrowthBrain Strategy Oracle) ──
        governance_decision = growth.evaluate_governance(dopamine, job_target, is_reels) if growth else "STAY"

        if governance_decision == "SHIFT_CONTEXT":
            # Store session learning before leaving
            if growth:
                growth.refine_persona(session_outcomes)
            return "BOREDOM_CHANGE_FEED"

        elif governance_decision == "CHECK_CURIOSITY":
            logger.info("👀 [Curiosity] Spontaneously checking DMs / Notifications...")

            # 🛡️ Structural Guard: Curiosity targets (DMs, Notifications) are ONLY available on HomeFeed.
            # We must navigate there first, breaking current context.
            if not nav_graph.navigate_to("HomeFeed", zero_engine):
                logger.warning("❌ [Curiosity] Failed to navigate to HomeFeed. Aborting curiosity check.")
                continue
            sleep(random.uniform(1.0, 2.5))

            dm_config = configs.get_plugin_config("dm_reply")
            if dm_config.get("enabled", False):
                explore_target = random.choice(["MessageInbox", "Notifications"])
            else:
                explore_target = "Notifications"

            if explore_target == "MessageInbox":
                nav_graph.do("tap direct message icon inbox")
                sleep(random.uniform(3.0, 7.0))
            else:
                nav_graph.do("tap heart icon notifications")
                sleep(random.uniform(3.0, 7.0))
                _humanized_scroll(device, is_skip=True)
                sleep(random.uniform(2.0, 4.0))

            # Return to feed
            nav_graph.navigate_to("HomeFeed", zero_engine)
            sleep(random.uniform(1.0, 2.5))
            logger.info("🔙 [Curiosity] Done exploring. Returning to feed.")

        # ── Circadian Pacing (GrowthBrain) ──
        circadian = growth.get_circadian_pacing() if growth else 1.0
        caution_mod = ai.get_sleep_modifier() if ai else 1.0
        sleep_mod = circadian * caution_mod  # Combined sleep multiplier

        if dopamine.wants_to_doomscroll():
            logger.info("🏃 [Drive] Doomscrolling engaged. Fast-skipping feed.", extra={"color": f"{Fore.CYAN}"})
            # Reverse-flick correction logic is now handled internally by _humanized_scroll
            _humanized_scroll(device, is_skip=True)
            sleep(random.uniform(0.1, 0.4) * sleep_mod)
            continue

        context_xml = device.dump_hierarchy()
        if cognitive_stack.get("radome"):
            context_xml = cognitive_stack.get("radome").sanitize_xml(context_xml)

        # ── Perimeter Guard: Verify we're still inside Instagram ──
        # Parity with story loop guard (production bug 2026-05-03).
        if context_xml:
            feed_packages = set(re.findall(r'package="([^"]+)"', context_xml))
            feed_app_id = getattr(device, "app_id", "com.instagram.android")
            if feed_packages and feed_app_id not in feed_packages:
                logger.error(f"🚨 [FeedLoop] FOREIGN APP DETECTED! Packages: {feed_packages}. Aborting loop.")
                device.press("back")
                sleep(1.5)
                return "CONTEXT_LOST"

        # ── Execute Plugin Registry Behaviors (Feed Level) ──
        from GramAddict.core.behaviors import BehaviorContext, PluginRegistry

        ctx = BehaviorContext(
            device=device,
            configs=configs,
            session_state=session_state,
            cognitive_stack=cognitive_stack,
            context_xml=context_xml,
            sleep_mod=sleep_mod,
            post_data={},
            username="",
            shared_state=shared_state,
        )
        registry = PluginRegistry.get_instance()
        plugin_results = registry.execute_all(ctx)

        # Pull extracted data from ctx back into local variables
        # (for legacy interaction engine below until extracted)

        should_continue_loop = True
        for result in plugin_results:
            if result.metadata.get("return_code") == "CONTEXT_LOST":
                return "CONTEXT_LOST"
            if result.executed and result.should_skip:
                should_continue_loop = False
                break

        if not should_continue_loop:
            continue

        res_score = ctx.shared_state.get("res_score", 0.5)

        # ── Advance to next post ──

        # ── Advance to next post ──
        _humanized_scroll(device, resonance_score=res_score)
        sleep(random.uniform(0.5, 1.2) * sleep_mod)

    # ── End of session: Store learnings ──
    if growth:
        growth.refine_persona(session_outcomes)

    if darwin:
        now = datetime.now()
        duration_minutes = (now - session_state.startTime).total_seconds() / 60.0
        followers_gained = sum(session_state.totalFollowed.values())
        darwin.evaluate_session_end(duration_minutes, followers_gained)

    logger.info("🏁 [Drive] Feed loop terminated. Session over.")
    return "FEED_EXHAUSTED"


def _run_zero_latency_search_loop(
    device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack
):
    """
    Executes the autonomous Search & Interact logic.
    """
    _validate_cognitive_stack(cognitive_stack, "SearchLoop")
    logger.info("🧠 [Search Engine] Initiating keyword discovery...", extra={"color": f"{Style.BRIGHT}{Fore.CYAN}"})

    import random

    from GramAddict.core.bot_flow import _humanized_click, sleep
    from GramAddict.core.stealth_typing import ghost_type

    # Select search term
    search_str = getattr(configs.args, "search", "")
    interests_str = getattr(configs.args, "persona_interests", "")

    all_terms = [t.strip() for t in (search_str + "," + interests_str).split(",") if t.strip()]
    if not all_terms:
        all_terms = ["photography", "travel", "nature"]  # Fail-safe

    keyword = random.choice(all_terms)
    logger.info(f"🔎 Searching for keyword: '{keyword}'")

    # 1. Navigation to Search is handled by nav_graph.navigate_to("SearchFeed") or Explore
    # We assume we are on the Explore tab now (Global Navigation Bar)

    try:
        xml = device.dump_hierarchy()
        telepathic = cognitive_stack.get("telepathic")

        # ── Perimeter Guard: Verify we're still inside Instagram ──
        if xml:
            search_packages = set(re.findall(r'package="([^"]+)"', xml))
            search_app_id = getattr(device, "app_id", "com.instagram.android")
            if search_packages and search_app_id not in search_packages:
                logger.error(f"🚨 [SearchLoop] FOREIGN APP DETECTED! Packages: {search_packages}. Aborting loop.")
                device.press("back")
                sleep(1.5)
                return "CONTEXT_LOST"

        # Find search bar
        search_bar = telepathic.find_best_node(xml, "Search edit text box or magnifying glass input", device=device)
        if search_bar:
            _humanized_click(device, search_bar["x"], search_bar["y"])
            sleep(1.5)
            ghost_type(device, keyword, speed="fast")
            device.press("enter")
            sleep(3.0)

            # 2. Pick a result (Top, Accounts, or Tags)
            results_xml = device.dump_hierarchy()
            target_result = telepathic.find_best_node(
                results_xml, "First relevant search result (Account or Hashtag)", device=device
            )

            if target_result:
                logger.info(f"👉 Selecting search result: {target_result.get('semantic', 'Top result')}")
                _humanized_click(device, target_result["x"], target_result["y"])
                sleep(3.0)

                # 3. If we are on a hashtag feed or account profile, start a mini-feed loop
                # We reuse _run_zero_latency_feed_loop but with a special boredom multiplier
                return _run_zero_latency_feed_loop(
                    device, zero_engine, nav_graph, configs, session_state, f"Search:{keyword}", cognitive_stack
                )

        return "BOREDOM_CHANGE_FEED"
    except Exception as e:
        logger.error(f"⚠️ [Search Engine] Failed: {e}")
        return "CONTEXT_LOST"
