"""
Physics — Timing & Wait Utilities.

UI readiness polling, post alignment, and adaptive snap recovery.
These functions wait for the Android UI to reach a known state before
the bot proceeds with interactions.

Extracted from bot_flow.py to enable isolated testing.
"""

import logging
import re
import time
from time import sleep

from GramAddict.core.diagnostic_dump import dump_ui_state
from GramAddict.core.perception.feed_analysis import FEED_MARKERS

logger = logging.getLogger(__name__)


def wait_for_post_loaded(device, timeout=5, nav_graph=None):
    """
    Polls the UI hierarchy until feed markers appear, confirming a post is on screen.

    If timeout is reached, attempts Adaptive Snap recovery:
    1. Detects trap states (Story/Reel viewer, Profile)
    2. Presses BACK to escape
    3. Micro-wobbles to force render
    """

    start = time.time()
    xml = ""
    while time.time() - start < timeout:
        try:
            xml = device.dump_hierarchy()
            if any(marker in xml for marker in FEED_MARKERS):
                logger.debug("📱 Post loaded successfully.")
                return True

            # Handle high-latency loads
            if "android.widget.ProgressBar" in xml or "loading_spinner" in xml.lower():
                # Extend timeout by 5 seconds if we're about to time out and still loading
                if time.time() - start > timeout - 1.0:
                    timeout += 5.0
                    logger.debug("⏳ Detected high-latency load (spinner active), extending timeout.")
        except Exception:
            pass
        sleep(0.5)

    logger.warning("⚠️ Post did not load within timeout. Attempting Adaptive Snap.")
    dump_ui_state(device, "post_load_timeout", {"timeout_sec": timeout})

    try:
        xml_lower = xml.lower()
        # 1. Trapped in a Story or Reel viewer? Press back.
        if "reel_viewer_root" in xml_lower or "clips_viewer" in xml_lower:
            logger.warning("🧗 [Adaptive Snap] Trapped in Story/Reel viewer. Pressing BACK.")
            device.press("back")
            sleep(1.5)
            # Give it one more chance to load the feed
            xml = device.dump_hierarchy()
            if any(marker in xml for marker in FEED_MARKERS):
                logger.info("✅ Recovered to Feed.")
                return True

        # 2. Trapped in Profile?
        if "profile_header" in xml_lower and "row_feed_photo_profile_name" not in xml_lower:
            logger.warning("🧗 [Adaptive Snap] Trapped in Profile. Pressing BACK.")
            device.press("back")
            sleep(1.5)

        # 3. Stuck on Grid? The tap didn't register. Do not wobble.
        grid_markers = ["explore_tab", "explore_grid", "grid_card_layout_container", "profile_tabs_container"]
        if any(m in xml_lower for m in grid_markers):
            logger.warning("🧗 [Adaptive Snap] Detected bot is STILL on the Grid. Tap likely missed. Aborting snap.")
            return False

        # 4. Stuck between posts (Feed markers not fully visible)? Micro-wobble.
        info = device.get_info()
        w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
        logger.warning("🧗 [Adaptive Snap] Wobbling to force render.")
        device.swipe(int(w / 2), int(h / 2), int(w / 2), int(h / 2) - 100, 0.1)
        sleep(0.5)
        device.swipe(int(w / 2), int(h / 2) - 100, int(w / 2), int(h / 2), 0.1)
    except Exception as e:
        logger.error(f"❌ [Adaptive Snap] Failed: {e}")

    return False


def wait_for_story_loaded(device, timeout=5):
    """Polls the UI hierarchy until story markers appear, confirming a story is on screen."""
    start = time.time()
    while time.time() - start < timeout:
        try:
            xml_lower = device.dump_hierarchy().lower()
            if "reel_viewer_root" in xml_lower or "story_viewer" in xml_lower:
                logger.debug("📱 Story loaded successfully.")
                return True
        except Exception:
            pass
        sleep(0.5)

    logger.warning("⚠️ Story did not load within timeout.")
    return False


def wait_for_profile_loaded(device, timeout=5):
    """Polls the UI hierarchy until profile markers appear."""
    import time

    start = time.time()
    PROFILE_MARKERS = ["profile_header", "action_bar_title", "profile_tabs_container"]

    while time.time() - start < timeout:
        try:
            xml_lower = device.dump_hierarchy().lower()
            if any(marker in xml_lower for marker in PROFILE_MARKERS):
                logger.debug("📱 Profile loaded successfully.")
                return True
        except Exception:
            pass
        sleep(0.5)

    logger.warning("⚠️ Profile did not load within timeout.")
    return False


def align_active_post(device):
    """
    Programmatic snapping correction. Finds the nearest post header and perfectly
    snaps it to the top margin. Fixes inverted scroll mapping that pushed content away.
    Loops to ensure absolute alignment if stuck deeply between posts.
    """
    aligned = False
    attempts = 0
    max_attempts = 5  # Increased for structural retry loop
    failed_bounds = set()

    # Intents for structural discovery
    intents = [
        "post author username text (exclude follow buttons)",
        "post author header profile",
        "row_feed_photo_profile_name",  # ID fallback
        "clips_viewer_author_container",  # Reels fallback
        "feed post content",  # Final desperation
    ]

    while not aligned and attempts < max_attempts:
        attempts += 1
        try:
            xml = device.dump_hierarchy()
            if "clips_video_container" in xml or "clips_viewer_container" in xml:
                logger.info("🎯 [Alignment] Reels view detected. Auto-snapping is native.")
                return True

            from GramAddict.core.telepathic_engine import TelepathicEngine

            telepath = TelepathicEngine.get_instance()

            target_node = None
            for intent in intents:
                target_node = telepath.find_best_node(
                    xml, intent, min_confidence=0.35, device=device, track=False, exclude_bounds=list(failed_bounds)
                )
                if target_node:
                    break

            if target_node:
                original_attribs = target_node.get("original_attribs", {})
                bounds = original_attribs.get("bounds")

                bounds_str = ""
                # If bounds is a tuple from SpatialNode.to_dict()
                if isinstance(bounds, (tuple, list)) and len(bounds) == 4:
                    left, t, r, b = bounds
                    bounds_str = f"[{left},{t}][{r},{b}]"
                else:
                    # Fallback to string parsing
                    if not bounds:
                        bounds = target_node.get("bounds", "")
                    m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", str(bounds))
                    if m:
                        left, t, r, b = map(int, m.groups())
                        bounds_str = f"[{left},{t}][{r},{b}]"
                    else:
                        logger.warning(f"📐 [Alignment] Could not parse bounds: {bounds}")
                        continue

                # Check if this is a false positive (e.g. bottom bar item misclassified)
                # Post headers should be in the top half usually, or at least not at the very bottom
                info = device.get_info()
                h = info.get("displayHeight", 2400)
                if t > h * 0.85:
                    logger.debug(f"📐 [Alignment] Rejecting node at y={t} (too low, likely bottom bar)")
                    failed_bounds.add(bounds_str)
                    continue

                header_y = (t + b) // 2
                target_y = 250  # Top margin for headers
                diff = header_y - target_y

                # If target is off-center (> 50px for higher precision), execute precise correction swipe
                if abs(diff) > 50:
                    info = device.get_info()
                    w = info.get("displayWidth", 1080)
                    cx = w // 2

                    max_safe_swipe = int(h * 0.4)

                    # Calculate movement
                    dist = min(abs(diff), max_safe_swipe)
                    if diff > 0:
                        # Content is too LOW. Move it UP (Swipe UP).
                        start_y = int(h * 0.7)
                        end_y = start_y - dist
                    else:
                        # Content is too HIGH. Move it DOWN (Swipe DOWN).
                        start_y = int(h * 0.3)
                        end_y = start_y + dist

                    logger.debug(f"📐 [Alignment] Attempt {attempts}: Snapping {diff}px (Swipe {start_y} -> {end_y})")
                    # Duration 1.5s = ultra-precise mechanical drag with ZERO momentum
                    device.swipe(cx, start_y, cx, end_y, duration=1.5)
                    sleep(1.0)

                    # Refresh XML for next iteration check
                    continue
                else:
                    logger.info(f"🎯 [Alignment] Perfect snap achieved after {attempts} attempts.")
                    aligned = True
            else:
                logger.debug(f"📐 [Alignment] No structural markers found on attempt {attempts}.")
                # If we can't find any markers, maybe we are stuck in a transition.
                # Micro-wobble to force a layout update.
                if attempts < 3:
                    info = device.get_info()
                    w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
                    device.swipe(w // 2, h // 2, w // 2, h // 2 - 20, duration=0.2)
                    sleep(0.5)
                    device.swipe(w // 2, h // 2 - 20, w // 2, h // 2, duration=0.2)
                    sleep(1.0)
                else:
                    break
        except Exception as e:
            logger.debug(f"📐 [Alignment] Snapping correction failed: {e}")
            break

    return aligned
