import asyncio
from playwright.async_api import async_playwright, Page
import os
import json
import re
from typing import List, Dict, Any, Optional, Callable
import openai
from datetime import datetime
from backend.core.logging_config import get_logger, log_info, log_error, log_warning, log_debug
from services.account_service import AccountService
from backend.utils.frames_handler import collect_all_frames, compute_frame_path, find_element_in_frames
from backend.utils.element_extractor import get_actionable_elements as extract_actionable_elements, check_element_visibility
from backend.utils.reggex import (
    normalize_step, parse_direct_url,is_remove_button,parse_type_allowed,is_open_family, 
disambiguate_add_website, parse_toggle,parse_verify_button, parse_verify_text,is_back,)

from backend.utils.page_objects import (
     ElementFinder, ClickableElementPage, FormPage, DialogPage,
    NavigationPage, TogglePage, ScrollPage, LinkPage, BasePage
)
from backend.utils.click_type_actions import (
    execute_click_type_action, prepare_element_for_action, finalize_action_result
)
from backend.utils.action_builders import (
    KeyboardActionBuilder, WaitActionBuilder, NavigationActionBuilder,EnsureToggleActionBuilder, ClickActionBuilder, TypeActionBuilder,
    HoverActionBuilder, ScrollActionBuilder, ActionBuilderFactory)

from backend.config.xpath import (
    XPATH_MS_CHILD, XPATH_SIGNIN_BUTTON, XPATH_SIGNIN_ANOTHER_WAY,
    XPATH_USE_PASSWORD, XPATH_NO_BUTTON, ID_USERNAME_ENTRY, ID_PASSWORD_ENTRY,
    XPATH_APPS_NAV, XPATH_REMOVE_BUTTON_DYNAMIC

)
from backend.config.aria_labels import (
    ARIA_CHAT_BUTTON, ARIA_SEND_BUTTON, ARIA_ADD_BUTTON, ARIA_CLOSE_BUTTON,
    ARIA_ADD_BLOCKED_WEBSITE, ARIA_ADD_ALLOWED_WEBSITE
)
from backend.config.regex import (
    extract_wait_time, extract_card_name, extract_app_name, is_first_app_click,
    extract_keyboard_press, extract_account_name, extract_family_member,
    extract_double_click_target, extract_quoted_text, has_search_results_context,
    PATTERN_CLICK_APP, PATTERN_QUOTED_TEXT
)
from backend.utils.natural_step import NaturalStepMixin

class AIAdhocWebAgent(NaturalStepMixin):
    def __init__(self, email: str, password: str, openai_client=None):
        self.email = email
        self.password = password
        self.logged_in = False
        self.logger = get_logger(__name__)
        self.client = openai.AzureOpenAI(
            api_key=os.getenv("AZURE_OPENAI_API_KEY"),
            azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
            api_version=os.getenv("AZURE_OPENAI_API_VERSION")
        )
        self.action_history = []
        self.current_context = {}
        self.account_manager = AccountService(self.logger)

    # REUSABLE HELPER METHODS - Consolidated utilities for robust operations

    async def _safe_execute(self, coro, error_msg: str = "Operation failed", log_level: str = "debug") -> tuple[bool, Any]:
        """
        Safely execute async operation with unified error handling.
        
        Args:
            coro: Coroutine to execute
            error_msg: Message to log on failure
            log_level: Logging level ('debug', 'warning', 'error')
            
        Returns:
            Tuple of (success: bool, result: Any)
        """
        try:
            result = await coro
            return True, result
        except Exception as e:
            error_details = f"{error_msg}: {str(e)}"
            getattr(self.logger, log_level)(error_details) if hasattr(self.logger, log_level) else log_debug(error_details)
            return False, None

    async def _retry_with_backoff(self, coro_func: Callable, max_attempts: int = 3, 
                                   initial_delay: float = 0.5, backoff_factor: float = 1.5,
                                   error_msg: str = "Retryable operation failed") -> tuple[bool, Any]:
        """
        Retry operation with exponential backoff.
        
        Args:
            coro_func: Async function that returns awaitable
            max_attempts: Maximum retry attempts
            initial_delay: Initial delay in seconds
            backoff_factor: Multiplier for delay on each retry
            error_msg: Error message for logging
            
        Returns:
            Tuple of (success: bool, result: Any)
        """
        delay = initial_delay
        last_error = None
        
        for attempt in range(max_attempts):
            success, result = await self._safe_execute(coro_func(), error_msg=error_msg)
            if success:
                return True, result
            last_error = result
            
            if attempt < max_attempts - 1:
                await asyncio.sleep(delay)
                delay *= backoff_factor
        
        log_warning(f"{error_msg} after {max_attempts} attempts")
        return False, last_error

    async def _try_multiple_strategies(self, strategies: List[Callable], 
                                      strategy_names: List[str] = None) -> tuple[bool, Any]:
        """
        Try multiple strategies sequentially until one succeeds.
        
        Args:
            strategies: List of async callables to try
            strategy_names: Optional names for logging (defaults to strategy_{i})
            
        Returns:
            Tuple of (success: bool, result: Any)
        """
        if not strategy_names:
            strategy_names = [f"strategy_{i}" for i in range(len(strategies))]
        
        for name, strategy in zip(strategy_names, strategies):
            success, result = await self._safe_execute(strategy(), error_msg=f"{name} failed")
            if success:
                log_debug(f"Strategy '{name}' succeeded")
                return True, result
        
        return False, None

    async def _verify_element_clickable(self, element, timeout_ms: int = 3000) -> bool:
        """
        Verify element is clickable with comprehensive checks.
        
        Args:
            element: Playwright element handle
            timeout_ms: Timeout in milliseconds
            
        Returns:
            True if clickable, False otherwise
        """
        try:
            await element.wait_for_element_state("stable", timeout=timeout_ms)
            
            # Check visibility and dimensions
            bbox = await element.bounding_box()
            if not bbox or bbox["width"] <= 0 or bbox["height"] <= 0:
                return False
            
            # Check disabled/hidden attributes
            for attr in ["disabled", "hidden", "aria-disabled"]:
                value = await element.get_attribute(attr)
                if value is not None and value != "false":
                    return False
            
            return True
        except Exception as e:
            log_debug(f"Element clickability check failed: {e}")
            return False

    async def _wait_for_condition(self, condition_func: Callable, timeout_seconds: float = 5.0,
                                 check_interval: float = 0.2) -> bool:
        """
        Wait for async condition to be true.
        
        Args:
            condition_func: Async function that returns boolean
            timeout_seconds: Total timeout
            check_interval: Time between checks
            
        Returns:
            True if condition met, False if timeout
        """
        deadline = asyncio.get_event_loop().time() + timeout_seconds
        
        while asyncio.get_event_loop().time() < deadline:
            try:
                if await condition_func():
                    return True
            except Exception as e:
                log_debug(f"Condition check failed: {e}")
            
            await asyncio.sleep(check_interval)
        
        return False

    async def _check_toggle_state(self, element) -> Optional[bool]:
        """
        Determine if a toggle/checkbox element is ON (True), OFF (False), or unknown (None).
        Handles multiple toggle implementation styles.
        
        Args:
            element: Playwright element handle
            
        Returns:
            True (ON), False (OFF), or None (cannot determine)
        """
        strategies = [
            # Strategy 1: aria-checked (standard ARIA)
            lambda: self._check_aria_checked(element),
            # Strategy 2: aria-pressed (button toggle)
            lambda: self._check_aria_pressed(element),
            # Strategy 3: data-state (custom attribute)
            lambda: self._check_data_state(element),
            # Strategy 4: HTML checkbox
            lambda: self._check_checkbox_state(element),
        ]
        
        for strategy in strategies:
            try:
                result = await strategy()
                if result is not None:
                    return result
            except Exception:
                continue
        
        return None

    async def _check_aria_checked(self, element) -> Optional[bool]:
        """Check aria-checked attribute."""
        try:
            value = await element.get_attribute("aria-checked")
            if value and value.lower() in ("true", "false"):
                return value.lower() == "true"
        except Exception:
            pass
        return None

    async def _check_aria_pressed(self, element) -> Optional[bool]:
        """Check aria-pressed attribute (for button toggles)."""
        try:
            value = await element.get_attribute("aria-pressed")
            if value and value.lower() in ("true", "false"):
                return value.lower() == "true"
        except Exception:
            pass
        return None

    async def _check_data_state(self, element) -> Optional[bool]:
        """Check data-state attribute (custom framework attribute)."""
        try:
            value = await element.get_attribute("data-state")
            if value and value.lower() in ("on", "off"):
                return value.lower() == "on"
        except Exception:
            pass
        return None

    async def _check_checkbox_state(self, element) -> Optional[bool]:
        """Check HTML checkbox input state."""
        try:
            tag = await element.evaluate("el => el.tagName.toLowerCase()")
            if tag == "input":
                input_type = await element.get_attribute("type")
                if input_type and input_type.lower() == "checkbox":
                    return await element.evaluate("el => el.checked")
        except Exception:
            pass
        return None

    async def _ensure_visible_and_focused(self, element, frame=None, scope=None) -> bool:
        """
        Ensure element is visible, in viewport, and focused.
        
        Args:
            element: Playwright element handle
            frame: Optional frame context
            scope: Optional scope/modal context
            
        Returns:
            True if successful, False otherwise
        """
        try:
            # Verify frame is still attached if provided
            if frame is not None:
                try:
                    await frame.evaluate("() => document.body")
                except Exception:
                    return False
            
            # Scroll into view if in modal
            if scope is not None:
                try:
                    await scope.evaluate("el => el.scrollIntoView()", element)
                except Exception:
                    await element.scroll_into_view_if_needed()
            else:
                await element.scroll_into_view_if_needed()
            
            # Ensure element is stable
            await element.wait_for_element_state("stable", timeout=3000)
            return True
        except Exception as e:
            log_debug(f"Failed to ensure element visible/focused: {e}")
            return False


    async def _type_in_teams_textbox(self, element, input_text: str, frame, scope=None) -> bool:
        """
        Type text in Idc Web Agent textboxes with multiple fallback strategies.
        
        Args:
            element: Playwright element handle
            input_text: Text to type
            frame: Frame context
            scope: Optional scope/modal context
            
        Returns:
            True if text was typed successfully, False otherwise
        """
        if not element or not isinstance(input_text, str):
            log_warning("Invalid element or input_text for typing")
            return False
        
        try:
            await element.focus()
            await asyncio.sleep(0.5)
            
            # Multiple typing strategies with fallbacks
            strategies = [
                (lambda: element.fill(input_text), "fill"),
                (lambda: element.type(input_text), "type"),
                (lambda: element.evaluate(
                    f"(el) => {{ el.textContent = '{input_text}'; el.dispatchEvent(new Event('input', {{ bubbles: true }})); }}"
                ), "evaluate_textContent"),
                (lambda: element.evaluate(
                    f"(el) => {{ el.innerHTML = '{input_text}'; el.dispatchEvent(new Event('input', {{ bubbles: true }})); }}"
                ), "evaluate_innerHTML"),
            ]
            
            success, _ = await self._try_multiple_strategies(
                [s[0] for s in strategies],
                [s[1] for s in strategies]
            )
            
            if success:
                log_debug(f"Textbox typing succeeded")
                return True
            
            return False
            
        except Exception as e:
            log_debug(f"Textbox typing failed: {e}")
            return False

    async def _wait_for_element_interaction_ready(self, element, frame, scope=None, timeout_seconds: int = 10) -> bool:
        """
        Wait for element to be ready for user interaction.
        
        Args:
            element: Playwright element handle
            frame: Frame context
            scope: Optional scope/modal context
            timeout_seconds: Maximum wait time
            
        Returns:
            True if ready, False otherwise
        """
        if not element or not frame:
            return False
        
        try:
            # Wait for element to be stable
            if not await self._wait_for_element_stable(element, timeout_seconds):
                return False
            
            # Verify frame is still attached
            success, _ = await self._safe_execute(
                frame.evaluate("() => document.body.offsetHeight"),
                error_msg="Frame validation failed"
            )
            if not success:
                return False
            
            # Verify element is clickable
            return await self._verify_element_clickable(element)
            
        except Exception as e:
            log_debug(f"Element interaction readiness check failed: {e}")
            return False

    async def _wait_for_page_ready(self, page: Page, timeout_seconds: int = 10) -> bool:
        """
        Wait for page to be in stable, interactive state.
        
        Args:
            page: Playwright page object
            timeout_seconds: Maximum wait time
            
        Returns:
            True if page ready, False otherwise
        """
        if not page or timeout_seconds <= 0:
            return False
        
        timeout_ms = int(timeout_seconds * 1000)
        
        # Strategy 1: Use Playwright's built-in wait
        success, _ = await self._safe_execute(
            page.wait_for_load_state("networkidle", timeout=timeout_ms),
            error_msg="Page networkidle wait failed"
        )
        
        if success:
            # Strategy 2: Additional DOM stability check
            success, _ = await self._safe_execute(
                page.wait_for_function(
                    "() => document.readyState === 'complete' && "
                    "!document.querySelector('[aria-busy=\"true\"]')",
                    timeout=2000
                ),
                error_msg="DOM stability check failed"
            )
            if success:
                return True
        
        # Fallback: Just wait a bit
        try:
            await page.wait_for_timeout(2000)
            return True
        except Exception:
            return False

    async def _wait_for_element_stable(self, element, timeout_seconds: int = 5) -> bool:
        """
        Wait for element to be in stable state (no animations/transitions).
        
        Args:
            element: Playwright element handle
            timeout_seconds: Maximum wait time
            
        Returns:
            True if stable, False otherwise
        """
        if not element or timeout_seconds <= 0:
            return False
        
        timeout_ms = int(timeout_seconds * 1000)
        
        # Primary strategy: Use Playwright's built-in stable check
        success, _ = await self._safe_execute(
            element.wait_for_element_state("stable", timeout=timeout_ms),
            error_msg="Element stable wait failed"
        )
        
        if success:
            return True
        
        # Fallback: Wait for visibility and enabled
        strategies = [
            (lambda: element.wait_for_element_state("visible", timeout=2000), "visible"),
            (lambda: element.wait_for_element_state("enabled", timeout=2000), "enabled"),
        ]
        
        for strategy, name in strategies:
            success, _ = await self._safe_execute(strategy(), error_msg=f"Element {name} wait failed")
            if not success:
                return False
        
        return True
    async def _is_element_clickable(self, element) -> bool:
        """Check if an element is clickable by verifying its state and properties."""
        try:
            # Check if element is visible and has dimensions
            bbox = await element.bounding_box()
            if not bbox or bbox["width"] <= 0 or bbox["height"] <= 0:
                return False
            
            # Check if element is not disabled
            disabled = await element.get_attribute("disabled")
            if disabled is not None:
                return False
            
            # Check if element is not hidden
            hidden = await element.get_attribute("hidden")
            if hidden is not None:
                return False
            
            # Check computed styles for visibility
            try:
                is_visible = await element.evaluate("""
                    (el) => {
                        const style = window.getComputedStyle(el);
                        return style.display !== 'none' && 
                               style.visibility !== 'hidden' && 
                               style.opacity !== '0' &&
                               el.offsetWidth > 0 && 
                               el.offsetHeight > 0;
                    }
                """)
                return is_visible
            except Exception:
                # If we can't check computed styles, assume it's clickable
                return True
                
        except Exception:
            return False

    async def login(self, page: Page):
        # Attempt login with retries; only succeed when 6+ element becomes visible
        verification_xpath = XPATH_MS_CHILD
        max_attempts = 3
        for attempt in range(1, max_attempts + 1):
            try:
                await page.goto("https://account.microsoft.com/family/windows/coldstart")
                await asyncio.sleep(5)
                await page.click(XPATH_SIGNIN_BUTTON)
                await asyncio.sleep(5)                
                await page.fill(f"#{ID_USERNAME_ENTRY}", self.email)
                await page.keyboard.press('Enter')
                # Wait until "Sign in another way" appears, then click it
                try:
                    await page.wait_for_selector(
                        XPATH_SIGNIN_ANOTHER_WAY,
                        state="visible",
                        
                        timeout=600000 # max 5 5min  wait
                    )
                    await page.click(XPATH_SIGNIN_ANOTHER_WAY)
                except Exception:
                    # If it never appears, continue silently
                    pass
                        
                try:
                    await page.click(XPATH_USE_PASSWORD)
                    
                except Exception:
                    pass
                await asyncio.sleep(2)
                await page.fill(f"#{ID_PASSWORD_ENTRY}", self.password)
                await page.keyboard.press('Enter')
                await asyncio.sleep(5)
                
                try:
                    await page.click(XPATH_NO_BUTTON)
                    
                except Exception:
                    pass
            except Exception as e:
                log_warning(f"Login attempt {attempt} navigation/input failed: {e}")

            # Give the page a moment to fully settle before visibility check
            try:
                await page.wait_for_load_state("load", timeout=5000)
            except Exception:
                pass
            await asyncio.sleep(5)

           # Wait until MS Child element becomes visible across any frame before marking login complete
            if await self._wait_for_xpath_visible_across_frames(page, verification_xpath, timeout_seconds=80):
                self.logged_in = True
                log_info("[OK] Logged into Family Safety Web.")
                return

            else:
                self.logged_in = False
                log_warning(f"Login attempt {attempt} did not reach main UI: MS Child element not visible. Retrying...")
                await asyncio.sleep(min(5 * attempt, 10))

        log_error("[✖] All login attempts exhausted. MS Child element not visible.")

    async def _wait_for_aria_label_visible_across_frames(self, page: Page, aria_label: str, timeout_seconds: int = 60) -> bool:
        """Wait until an element with the given aria-label is visible in any frame."""
        poll_interval = 0.5
        attempts = int(timeout_seconds / poll_interval)
        for _ in range(attempts):
            frames = collect_all_frames(page)
            for frame in frames:
                try:
                    handle = await frame.query_selector(f"[aria-label='{aria_label}']")
                except Exception:
                    handle = None
                if handle:
                    try:
                        bbox = await handle.bounding_box()
                        if bbox and bbox.get("width", 0) > 0 and bbox.get("height", 0) > 0:
                            return True
                    except Exception:
                        # If visibility check fails, continue polling
                        pass
            await asyncio.sleep(poll_interval)
        return False

    async def _wait_for_xpath_visible_across_frames(self, page: Page, xpath: str, timeout_seconds: int = 60) -> bool:
        """Wait until an element matching the given xpath is visible in any frame."""
        poll_interval = 0.5
        attempts = int(timeout_seconds / poll_interval)
        for _ in range(attempts):
            frames = collect_all_frames(page)
            for frame in frames:
                try:
                    handle = await frame.query_selector(f"xpath={xpath}")
                except Exception:
                    handle = None
                if handle:
                    try:
                        bbox = await handle.bounding_box()
                        if bbox and bbox.get("width", 0) > 0 and bbox.get("height", 0) > 0:
                            return True
                    except Exception:
                        # If visibility check fails, continue polling
                        pass
            await asyncio.sleep(poll_interval)
        return False

    async def detect_modal_presence(self, page: Page, timeout_ms: int = 2000) -> bool:
        """Detect if a modal dialog is currently present in the DOM."""
        try:
            frames = collect_all_frames(page)
            for frame in frames:
                try:
                    # Check for common modal/dialog selectors including the specific ModalFocusTrapZone pattern
                    dialogs = await frame.query_selector_all(
                        "[role='dialog'], [aria-modal='true'], .ms-Dialog-main, .fui-DialogBody, "
                        "[data-tid='app-details-northstar-dialog'], #APP_DETAILS_DIALOG_ROOT_ID, "
                        "[id^='ModalFocusTrapZone'], .ms-Modal-scrollableContent, "
                        ".ms-Dialog-main.main-471, div[id*='ModalFocusTrapZone']"
                    )
                    for dlg in dialogs:
                        try:
                            # Check if dialog is visible using utility function
                            if await check_element_visibility(dlg):
                                log_debug(f"[DEBUG] Modal detected: {await dlg.get_attribute('id') or await dlg.get_attribute('class')}")
                                return True
                        except Exception:
                            continue
                except Exception:
                    continue
            return False
        except Exception as e:
            log_debug(f"Modal detection failed: {e}")
            return False
    async def wait_for_modal_appearance(self, page: Page, timeout_seconds: float = 3.0) -> bool:
        """Wait for a modal to appear after an action, with timeout."""
        deadline = asyncio.get_event_loop().time() + timeout_seconds
        while asyncio.get_event_loop().time() < deadline:
            if await self.detect_modal_presence(page):
                return True
            await asyncio.sleep(0.2)
        return False

    async def get_page_context(self, page: Page) -> Dict[str, Any]:
        """Get comprehensive page context for LLM decision making."""
        try:
            url = page.url
            title = await page.title()
            
            # Get current page state
            elements = await extract_actionable_elements(page)
            
            # Filter visible and actionable elements
            visible_elements = [
                el for el in elements 
                if el.get("visible", True) and 
                el.get("display") != "none" and 
                el.get("visibility") != "hidden" and
                el.get("opacity", "1") != "0"
            ]
            
            # Get current focus and active elements
            try:
                active_element = await page.evaluate("""
                    () => {
                        const active = document.activeElement;
                        if (active) {
                            return {
                                tagName: active.tagName,
                                id: active.id,
                                className: active.className,
                                ariaLabel: active.getAttribute('aria-label'),
                                textContent: active.textContent?.slice(0, 100)
                            };
                        }
                        return null;
                    }
                """)
            except Exception:
                active_element = None
            
            # Get recent action history for context
            recent_actions = self.action_history[-5:] if self.action_history else []
              # Check if modal is present
            modal_present = await self.detect_modal_presence(page)
            
            context = {
                "timestamp": datetime.now().isoformat(),
                "url": url,
                "title": title,
                "total_elements": len(elements),
                "visible_elements": len(visible_elements),
                "active_element": active_element,
                "recent_actions": recent_actions,
                "current_context": self.current_context,
                "modal_present": modal_present,
                "elements_summary": self._summarize_elements(visible_elements[:50])  # Limit for LLM context
            }
            
            return context
            
        except Exception as e:
            log_error(f"Failed to get page context: {e}")
            return {"error": str(e)}

    def _summarize_elements(self, elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """Create a concise summary of elements for LLM consumption."""
        summarized = []
        for el in elements:
            summary = {
                "type": el.get("type", "unknown"),
                "tag": el.get("tag", ""),
                "text": el.get("text", "")[:50],  # Limit text length
                "aria-label": el.get("aria-label", ""),
                "role": el.get("role", ""),
                "position": f"({int(float(el.get('x', 0) or 0))}, {int(float(el.get('y', 0) or 0))})",
                "size": f"{int(float(el.get('width', 0) or 0))}x{int(float(el.get('height', 0) or 0))}",
                "frame": el.get("frame_path", "main")
            }
            summarized.append(summary)
        return summarized

    async def map_natural_step_to_action(self, step: str, page_context: Dict[str, Any]) -> Dict[str, Any]:
        """
        Map natural language test step to actionable element using LLM intelligence.
        
        Args:
            step: Natural language step description
            page_context: Current page context from get_page_context()
            
        Returns:
            Action dictionary with action type, locator, value, and confidence
            
        Raises:
            Returns fallback action if inputs are invalid
        """
        # Input validation
        if not step or not isinstance(step, str):
            log_warning("Invalid step input for action mapping")
            return self._create_fallback_action("Invalid step input")
        
        if not page_context or not isinstance(page_context, dict):
            log_warning("Invalid page_context for action mapping")
            return self._create_fallback_action("Invalid context")
        
        # QUICK CHECK: if the step explicitly asks to wait, return a wait action immediately
        try:
            step_lower = step.strip().lower()
            # Patterns: "wait for 5 seconds", "wait 5s", "wait 2.5 seconds"
            secs = extract_wait_time(step_lower)
            if secs:
                return {
                    "action": "wait",
                    "locator": "seconds",
                    "value": secs,
                    "description": f"Wait for {secs} seconds",
                    "confidence": 1.0,
                    "reasoning": "Explicit wait step detected"
                }
        except Exception as e:
            log_debug(f"Wait pattern detection failed: {e}")
            pass

        # Build comprehensive prompt for LLM
        system_prompt = """You are an intelligent web automation AI that converts natural language test steps into precise element actions.

            Your task is to analyze a natural language step and find the most appropriate element on the page to interact with.

            CRITICAL RULES:
            1. ALWAYS prefer accessibility-first selectors (aria-label, role, aria-labelledby)
            2. Use semantic selectors over technical ones (prefer "Chat" over "button[3]")
            3. Consider the user's intent and context from recent actions
            4. Return ONLY valid JSON with the exact format specified
            5. If multiple elements match, choose the most semantically appropriate one
            6. For navigation elements, prefer exact matches over similar ones
            7. Use XPath only when other locators fail or for specific complex selectors
            8. If we write test case's steps twice or thrice in ADO then it should be performed twice or thrice respectively. Don't skip test steps.
            9. If test steps contains "wait for 5 or 10 seconds" then it should be implemented as wait action.
            10. Use XPath only when other locators fail or for specific complex selectors

            Available element types: button, input, link, tab, menu_item, checkbox, radio, textbox, combobox, listbox, grid, row, cell, focusable, clickable

            Return format:
            {
            "action": "click" | "type" | "hover" | "scroll" | "press" | "wait",
            "locator": "aria-label" | "role" | "text" | "placeholder" | "id" | "data-testid" | "xpath" | "keyboard",
            "value": "exact locator value (or key for press, e.g. 'Enter' or 'Control+Enter')",
            "count": number (optional; only for press; default 1),
            "input_text": "text to type (only for type action)",
            "description": "human-readable description",
            "confidence": 0.0-1.0,
            "reasoning": "why this element was chosen"
            }

            For keyboard presses, set locator to "keyboard" and put the key or key chord in "value" (e.g., "Enter", "Control+Enter"). If multiple presses are needed, set "count" accordingly (e.g., 3)."""

        user_prompt = f"""Test Step: "{step}"

            Current Page Context:
            - URL: {page_context.get('url', 'Unknown')}
            - Title: {page_context.get('title', 'Unknown')}
            - Active Element: {json.dumps(page_context.get('active_element', {}), indent=2)}
            - Recent Actions: {json.dumps(page_context.get('recent_actions', []), indent=2)}

            Available Elements (showing first 50):
            {json.dumps(page_context.get('elements_summary', []), indent=2)}

            Based on the test step and available elements, determine the best action to take.
            Focus on semantic meaning and user intent rather than technical implementation details."""

        try:
            response = self.client.chat.completions.create(
                model=os.getenv("AZURE_OPENAI_DEPLOYMENT", "humanjudgementmodel"),
                messages=[
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": user_prompt}
                ],
                temperature=0.1,  # Low temperature for consistent results
                max_tokens=500
            )
            
            result = response.choices[0].message.content.strip()
            log_info(f"[LLM ACTION RAW] {result}")
            
            # Clean up the response - remove markdown formatting if present
            if result.startswith("```json"):
                result = result.replace("```json", "").replace("```", "").strip()
            
            # Parse JSON response
            action_data = json.loads(result)
            
            # Validate required fields
            required_fields = ["action", "locator", "value", "description"]
            for field in required_fields:
                if field not in action_data:
                    raise ValueError(f"Missing required field: {field}")
            
            # Add metadata
            action_data["timestamp"] = datetime.now().isoformat()
            action_data["step"] = step
            
            # Heuristics for common dialog actions in idc Web Agent App dialogs
            step_lower = step.lower()
            # Special-cases: Windows Family Safety / Edge content filter widgets
            try:
                # 1a) If the step explicitly asks to click the "Add a website you want to block" button
                if (
                    "click" in step_lower and "button" in step_lower and "add a website you want to block" in step_lower
                ):
                    action_data["action"] = "click"
                    action_data["locator"] = "aria-label"
                    action_data["value"] = ARIA_ADD_BLOCKED_WEBSITE
                    action_data["description"] = "Click the 'Add a website you want to block' submit button by aria-label"
                    action_data["reasoning"] = "Override: Use aria-label for reliable button identification"
                    action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.98)
                # 1b) If the step explicitly asks to click the "Add a website you want to allow" button
                elif (
                    "click" in step_lower and "button" in step_lower and ("add a website you want to allow" in step_lower or "add allowed sites" in step_lower or "add allowed" in step_lower)
                ):
                    action_data["action"] = "click"
                    action_data["locator"] = "aria-label"
                    action_data["value"] = ARIA_ADD_ALLOWED_WEBSITE
                    action_data["description"] = "Click the 'Add a website you want to allow' submit button by aria-label"
                    action_data["reasoning"] = "Override: Use aria-label for reliable button identification"
                    action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.98)
                # 2) If the step mentions typing or text field/textbox, target the input id
                elif (
                    ("type" in step_lower or "text field" in step_lower or "textbox" in step_lower)
                    and ("block website" in step_lower or "blocked site" in step_lower or "never allow" in step_lower or "add a website" in step_lower)
                ):
                    action_data["locator"] = "id"
                    action_data["value"] = "web-search-never-allowed-input"
                    action_data["action"] = "type"
                    action_data["description"] = "Type into the 'Never allow websites' input by id"
                    action_data["reasoning"] = "Override: Use stable input id for typing into block-website textbox"
                    action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.97)
                # 2b) If the step mentions typing or text field/textbox, target the "allow" input id
                elif (
                    ("type" in step_lower or "text field" in step_lower or "textbox" in step_lower)
                    and ("allow" in step_lower and "site" in step_lower or "allowed site" in step_lower)
                ):
                    action_data["locator"] = "id"
                    action_data["value"] = "web-search-always-allowed-input"
                    action_data["action"] = "type"
                    action_data["description"] = "Type into the 'Always allow websites' input by id"
                    action_data["reasoning"] = "Override: Use stable input id for typing into allow-website textbox"
                    action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.97)
                # 3) Windows apps (Notepad, Calculator, Clock, etc.)
                elif "click" in step_lower and "windows" in step_lower and any(app in step_lower for app in ["notepad", "calculator", "clock", "media player", "security"]):
                    m = re.search(r"windows\s+(\w+(?:\s+\w+)*)", step_lower)
                    if m:
                        app_name = " ".join(word.capitalize() for word in m.group(1).split())
                        action_data["action"] = "click"
                        action_data["locator"] = "xpath"
                        action_data["value"] = f"//span[contains(text(), '{app_name}')]/ancestor::*"
                        action_data["description"] = f"Click {app_name} app by span text"
                        action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.95)
                # 4) Close button - use aria-label instead of text
                elif ("close" in step_lower and "button" in step_lower) or ("verify" in step_lower and "close" in step_lower):
                    action_data["action"] = "click"
                    action_data["locator"] = "aria-label"
                    action_data["value"] = ARIA_CLOSE_BUTTON
                    action_data["description"] = "Click Close button by aria-label"
                    action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.96)
            except Exception:
                pass
            # Explicit quoted app name anywhere with mention of search results → XPath on span text
            try:
                has_results_context = re.search(r"\b(search\s*results|results)\b", step_lower)
                quoted = re.search(r"['\"]([^'\"]+)['\"]", step)
                if has_results_context and quoted:
                    raw_name = quoted.group(1).strip()
                    app_name_for_xpath = raw_name
                    action_data["action"] = "click"
                    action_data["locator"] = "xpath"
                    # Build clean XPath without stray spaces/periods
                    action_data["value"] = f"//span[contains(text(), '{app_name_for_xpath}')]"
                    action_data["description"] = f"Click {app_name_for_xpath} app item by span text"
                    action_data["reasoning"] = "Override: Use XPath on span text when quoted app name is provided with search results context"
                    action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.96)
            except Exception:
                pass
            # Detect pattern variants: Click (in|on|the) "<user>" card → click span containing <user>
            try:
                user_text = extract_card_name(step)
                if user_text:

                    # Build safe XPath literal for user_text
                    def _xpath_literal(s: str) -> str:
                        if "'" not in s:
                            return f"'{s}'"
                        if '"' not in s:
                            return f'"{s}"'
                        # Contains both quotes → use concat with split on single quotes
                        parts = s.split("'")
                        concat_parts = []
                        for i, part in enumerate(parts):
                            if part:
                                concat_parts.append(f"'{part}'")
                            if i != len(parts) - 1:
                                concat_parts.append('"' + "'" + '"')
                        return "concat(" + ", ".join(concat_parts) + ")"

                    xp_value = f"//span[contains(text(), {_xpath_literal(user_text)})]"
                    action_data["action"] = "click"
                    action_data["locator"] = "xpath"
                    action_data["value"] = xp_value
                    action_data["description"] = f"Click span containing '{user_text}' in card"
                    action_data["reasoning"] = "Explicit step pattern: Click \"<user>\" card → use span text XPath"
                    action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.97)
            except Exception:
                pass
            # Detect pattern: Click <App> app from the (search results) list
            try:
                app_name = extract_app_name(step_lower)
                if app_name:
                    # Title-case the app name as it typically appears
                    normalized_name = app_name.title()
                    action_data["action"] = "click"
                    action_data["locator"] = "app_store_item"
                    action_data["value"] = normalized_name
                    action_data["description"] = f"Click {normalized_name} app card from results list"
                    action_data["reasoning"] = "Override: Target idc Web Agent store item cards by data-testid instead of generic text"
                    action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.92)
            except Exception:
                pass

            # Detect pattern: Click the first app from the list / first result
            try:
                if is_first_app_click(step_lower):
                    action_data["action"] = "click"
                    action_data["locator"] = "app_store_item_index"
                    action_data["value"] = 0  # zero-based index for the first item
                    action_data["description"] = "Click the first app card from results list"
                    action_data["reasoning"] = "Override: Select first visible Idc Web Agent store item card by index"
                    action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.9)
            except Exception:
                pass
            # Prefer visible button text for dialog CTA buttons
            if ("click" in step_lower and " open" in step_lower) or step_lower.strip() == "open" or "'open'" in step_lower:
                action_data["action"] = "click"
                action_data["locator"] = "data-testid"
                action_data["value"] = "open-app"
                action_data["description"] = action_data.get("description", "Click Open button by data-testid")
                action_data["reasoning"] = "Override: Use data-testid 'open-app' for Idc Web Agent app launch CTA"
                action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.95)
            elif "try it out" in step_lower or ("try it" in step_lower and "button" in step_lower):
                action_data["action"] = "click"
                action_data["locator"] = "data-testid"
                action_data["value"] = "try-it-button"
                action_data["description"] = action_data.get("description", "Click Try it out button by data-testid")
                action_data["reasoning"] = "Override: Use data-testid 'try-it-button' for Idc Web Agent app trial CTA"
                action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.95)
            elif ("click" in step_lower and " add" in step_lower) or step_lower.strip() == "add" or "'add'" in step_lower:
                action_data["action"] = "click"
                # Prefer aria-label Add, many dialogs use aria-label 'Add' or 'Add <App>'
                action_data["locator"] = "aria-label"
                action_data["value"] = ARIA_ADD_BUTTON
                action_data["description"] = action_data.get("description", "Click Add button by aria-label")
                action_data["reasoning"] = "Override: Use aria-label 'Add' for install CTA"
                action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.85)
            elif ("click" in step_lower and " save" in step_lower) or step_lower.strip() == "save" or "'save'" in step_lower:
                action_data["action"] = "click"
                # Save buttons typically have visible text content
                action_data["locator"] = "text"
                action_data["value"] = "Save"
                action_data["description"] = action_data.get("description", "Click Save button by text content")
                action_data["reasoning"] = "Override: Use text 'Save' for save button"
                action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.9)
            elif ("click" in step_lower and " preview" in step_lower) or step_lower.strip() == "preview" or "'preview'" in step_lower:
                action_data["action"] = "click"
                action_data["locator"] = "text"
                action_data["value"] = action_data.get("value", "Preview") or "Preview"
                action_data["description"] = action_data.get("description", "Click Preview button by text content")
                action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.85)
            elif ("click" in step_lower and " send" in step_lower) or step_lower.strip() == "send" or "'send'" in step_lower:
                action_data["action"] = "click"
                # Prefer title first (common in Idc Web Agent send buttons), fallback to text if not present
                if action_data.get("locator") not in ["title", "text"]:
                    action_data["locator"] = "title"
                    action_data["value"] = action_data.get("value", "Send") or "Send"
                action_data["description"] = action_data.get("description", "Click Send button by title")
                action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.85)
            # --- New heuristics to respect aria-label over text for quoted CTA names ---
            # If the step explicitly mentions "Create a new event" prefer aria-label exact (with trailing period variant)
            if "create a new event" in step_lower:
                action_data["action"] = "click"
                action_data["locator"] = "aria-label"
                # Try the exact accessible name used by Idc Web Agent first (includes trailing period)
                action_data["value"] = "Create a new event"
                action_data["description"] = action_data.get("description", "Click 'Create a new event' by aria-label")
                action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.9)
            else:
                # Generic rule: if LLM chose text for a quoted button name, upgrade to aria-label for better stability
                if action_data.get("action") == "click" and action_data.get("locator") == "text":
                    # Detect a quoted label in the step and use that as aria-label
                    quoted = extract_quoted_text(step)
                    if quoted and len(quoted) <= 50:
                        action_data["locator"] = "aria-label"
                        # Preserve exact quoted value (page fast-path also tries period/no-period variants)
                        action_data["value"] = quoted
                        action_data["description"] = action_data.get("description", f"Click '{quoted}' by aria-label")
                        action_data["confidence"] = max(float(action_data.get("confidence", 0)), 0.85)

            return action_data
            
        except Exception as e:
            log_error(f"LLM action mapping failed: {e}")
            # Fallback to basic text matching
            return self._fallback_action_mapping(step, page_context)

    def _create_fallback_action(self, reason: str = "No action could be determined", confidence: float = 0.3) -> Dict[str, Any]:
        """
        Create a safe fallback action when no better option is available.
        
        Args:
            reason: Reason for fallback
            confidence: Confidence score for the action
            
        Returns:
            Fallback action dictionary
        """
        return {
            "action": "click",
            "locator": "text",
            "value": "Chat",
            "description": "Generic fallback action",
            "confidence": confidence,
            "reasoning": reason
        }

    def _normalize_keyboard_key(self, raw_key: str) -> str:
        """
        Normalize keyboard key name to standard format.
        
        Args:
            raw_key: Raw key name (e.g., 'ctrl', 'CTRL', 'ctrl+enter')
            
        Returns:
            Normalized key name (e.g., 'Control+Enter')
        """
        if not raw_key:
            return ""
        
        key_map = {
            "ctrl": "Control",
            "control": "Control",
            "shift": "Shift",
            "alt": "Alt",
            "enter": "Enter",
            "tab": "Tab",
            "esc": "Escape",
            "escape": "Escape",
            "space": "Space",
            "backspace": "Backspace",
            "delete": "Delete",
            "home": "Home",
            "end": "End",
            "pageup": "PageUp",
            "pagedown": "PageDown",
            "arrowup": "ArrowUp",
            "arrowdown": "ArrowDown",
            "arrowleft": "ArrowLeft",
            "arrowright": "ArrowRight",
        }
        
        parts = [p.strip() for p in raw_key.split("+") if p.strip()]
        normalized_parts = [key_map.get(p.lower(), p.capitalize()) for p in parts]
        return "+".join(normalized_parts)

    def _fallback_action_mapping(self, step: str, page_context: Dict[str, Any]) -> Dict[str, Any]:
        """
        Fallback action mapping when LLM fails or is unavailable.
        Uses pattern matching for common automation scenarios.
        
        Args:
            step: Natural language step
            page_context: Current page context (not used in fallback, but maintained for compatibility)
            
        Returns:
            Fallback action dictionary
        """
        if not step or not isinstance(step, str):
            return self._create_fallback_action("Invalid step input")
        
        step_lower = step.strip().lower()
        # Respect explicit wait instructions in fallback mapping too
        secs = extract_wait_time(step_lower)
        if secs:
            return {
                "action": "wait",
                "locator": "seconds",
                "value": secs,
                "description": f"Wait for {secs} seconds",
                "confidence": 0.9,
                "reasoning": "Fallback: explicit wait instruction"
            }
        
        # Keyboard press heuristics: parse patterns like "press Ctrl+S", "press Tab 3 times"
        press_result = extract_keyboard_press(step_lower)
        if press_result:
            raw_key, count = press_result
            normalized_key = self._normalize_keyboard_key(raw_key)
            return {
                "action": "press",
                "locator": "keyboard",
                "value": normalized_key,
                    "count": count,
                    "description": f"Press {normalized_key}{' x'+str(count) if count>1 else ''}",
                    "confidence": 0.85,
                    "reasoning": "Fallback: generic keyboard press request"
                }

        # Basic keyword matching - PRIORITY: Apps navigation must run first
        if "apps" in step_lower and ("navigation" in step_lower or "first" in step_lower) and "calendar" not in step_lower and "chat" not in step_lower:
            return {
                "action": "click",
                "locator": "xpath",
                "value": XPATH_APPS_NAV,
                "description": "Click Apps navigation button in left sidebar (FIRST STEP PRIORITY)",
                "confidence": 0.95,
                "reasoning": "Fallback: Apps navigation detected with FIRST STEP priority, using XPath contains() to avoid Calendar/Chat"
            }
        elif "chat" in step_lower:
            return {
                "action": "click",
                "locator": "aria-label",
                "value": ARIA_CHAT_BUTTON,
                "description": "Click Chat button in sidebar",
                "confidence": 0.7,
                "reasoning": "Fallback: Chat keyword detected"
            }
        elif "type" in step_lower and "message" in step_lower:
            return {
                "action": "type",
                "locator": "placeholder",
                "value": "Type a message",
                "input_text": "Hello From Agentic AI Agent",
                "description": "Type message in chat input",
                "confidence": 0.6,
                "reasoning": "Fallback: Message typing detected"
            }
        elif "send" in step_lower:
            return {
                "action": "click",
                "locator": "title",
                "value": ARIA_SEND_BUTTON,
                "description": "Click send button by title",
                "confidence": 0.8,
                "reasoning": "Fallback: Send action detected - using specific title attribute"
            }
        elif "save" in step_lower:
            return {
                "action": "click",
                "locator": "text",
                "value": "Save",
                "description": "Click save button by text content",
                "confidence": 0.8,
                "reasoning": "Fallback: Save action detected - using text content"
            }
        elif "ms child" in step_lower or ("child" in step_lower and "card" in step_lower):
            return {
                "action": "click",
                "locator": "xpath",
                "value": XPATH_MS_CHILD,
                "description": "Click on MS Child card",
                "confidence": 0.9,
                "reasoning": "Fallback: MS Child card detected - using XPath contains()"
            }
        else:
            return self._create_fallback_action("No specific pattern matched in fallback mapping")

    async def execute_action(self, page: Page, action: Dict[str, Any]) -> Dict[str, Any]:
        """
        Execute a mapped action on the page using Page Object Model pattern.
        
        This refactored version delegates to specialized page objects and builders
        for different action types, improving maintainability and testability.
        
        Args:
            page: Playwright page object
            action: Action dictionary with action, locator, value, etc.
        
        Returns:
            Result dictionary with success, action, timestamp, error, details
        """
        result = {
            "success": False,
            "action": action,
            "timestamp": datetime.now().isoformat(),
            "error": None,
            "details": {}
        }
        
        try:
            locator = action["locator"]
            value = action["value"]
            action_type = action["action"]
            
            # ============================================================================
            # SECTION 1: SIMPLE ACTIONS (No element finding)
            # ============================================================================
            
            # Keyboard press action
            if action_type == "press" and locator == "keyboard":
                builder = KeyboardActionBuilder(page)
                return await builder.execute(action)
            
            # Wait/delay action
            if action_type == "wait":
                builder = WaitActionBuilder(page)
                return await builder.execute(action)
            
            # Navigation actions
            if action_type in ("navigate", "goto") and locator == "url":
                builder = NavigationActionBuilder(page)
                return await builder.execute_navigate(action)
            
            # Back navigation
            if action_type in ("back", "go_back", "navigate_back") and locator in ("history", "keyboard", "url", None, ""):
                builder = NavigationActionBuilder(page)
                return await builder.execute_back(action)
            
            # Ensure toggle action
            if action_type == "ensure_toggle":
                # Extract parameters
                desired_on = bool(action.get("desired_on", True))  # Target state: True=ON, False=OFF
                label_text = str(value)  # Label/name of the toggle

                # ========================================================================
                # HELPER FUNCTION 1: Determine if toggle is currently ON or OFF
                # ========================================================================
                async def _is_on(el) -> Optional[bool]:
                    """
                    Check if a toggle element is in the ON state by examining various
                    attributes that different toggle implementations use.
                    
                    Returns:
                        True if ON, False if OFF, None if state cannot be determined
                    """
                    # Strategy 1: Check aria-checked attribute (ARIA switch pattern)
                    try:
                        aria_checked = await el.get_attribute("aria-checked")
                        if isinstance(aria_checked, str) and aria_checked.lower() in ("true", "false"):
                            return aria_checked.lower() == "true"
                    except Exception:
                        pass
                    
                    # Strategy 2: Check for switch role with aria-pressed or data-state
                    try:
                        role_attr = await el.get_attribute("role")
                        if role_attr == "switch":
                            # Try aria-pressed (button-like toggle)
                            aria_pressed = await el.get_attribute("aria-pressed")
                            if isinstance(aria_pressed, str) and aria_pressed.lower() in ("true", "false"):
                                return aria_pressed.lower() == "true"
                            # Try data-state (custom attribute used by some frameworks)
                            data_state = await el.get_attribute("data-state")
                            if isinstance(data_state, str) and data_state.lower() in ("on", "off"):
                                return data_state.lower() == "on"
                    except Exception:
                        pass
                    
                    # Strategy 3: Check if it's an HTML checkbox input
                    try:
                        tag = await el.evaluate("el => el.tagName.toLowerCase()")
                        input_type = await el.get_attribute("type")
                        if tag == "input" and (input_type or "").lower() == "checkbox":
                            # For checkboxes, check the 'checked' property
                            return await el.evaluate("el => !!el.checked")
                    except Exception:
                        pass
                    
                    # Could not determine state
                    return None

                # ========================================================================
                # HELPER FUNCTION 2: Find toggle element by its label/name
                # ========================================================================
                async def _find_toggle_by_label_text() -> Optional[Any]:
                    """
                    Search across all frames to find a toggle element matching the label.
                    Uses multiple search strategies to handle different toggle implementations.
                    
                    Returns:
                        Element handle if found, None otherwise
                    """
                    frames = collect_all_frames(page)
                    label = label_text.replace("'", "\\'")  # Escape quotes for CSS selectors
                    
                    for f in frames:
                        # Strategy 1: Find role='switch' with aria-label or text matching
                        try:
                            el = await f.query_selector(f"[role='switch'][aria-label*='{label}']")
                            if el:
                                return el
                            el = await f.query_selector(f"[role='switch']:has-text('{label}')")
                            if el:
                                return el
                        except Exception:
                            pass
                        
                        # Strategy 2: Find checkbox input near the label text
                        try:
                            el = await f.query_selector(f":has-text('{label}') input[type='checkbox']")
                            if el:
                                return el
                        except Exception:
                            pass
                        
                        # Strategy 3: Find toggle/checkbox in container with label
                        try:
                            el = await f.query_selector(f":has-text('{label}') [role='switch'], :has-text('{label}') input[type='checkbox']")
                            if el:
                                return el
                        except Exception:
                            pass
                        
                        # Strategy 4: XPath approach - find ancestor container by text, then toggle inside
                        try:
                            xpath = (
                                f"//*[contains(normalize-space(.), '{label}')]/ancestor::*[self::div or self::li][1]"
                                f"//*[(@role='switch') or (self::input and @type='checkbox')][1]"
                            )
                            el = await f.query_selector(f"xpath={xpath}")
                            if el:
                                return el
                        except Exception:
                            pass
                    
                    return None

                # ========================================================================
                # HELPER FUNCTION 3: Wait for toggle to become enabled/interactive
                # ========================================================================
                async def _wait_enabled(el, timeout_ms: int = 3000) -> bool:
                    """
                    Poll an element until it's enabled and interactive.
                    Checks for 'disabled' attribute and 'aria-disabled' attribute.
                    
                    Args:
                        el: Element to check
                        timeout_ms: Maximum wait time in milliseconds
                    
                    Returns:
                        True if element became enabled, False if timeout
                    """
                    start = asyncio.get_event_loop().time()
                    while (asyncio.get_event_loop().time() - start) * 1000 < timeout_ms:
                        try:
                            # Check if element is disabled
                            disabled = await el.get_attribute("disabled")
                            aria_disabled = await el.get_attribute("aria-disabled")
                            
                            # Element is enabled if no disabled attribute and aria-disabled is false/absent
                            if not disabled and (aria_disabled is None or aria_disabled.lower() == "false"):
                                return True
                        except Exception:
                            pass
                        
                        # Brief pause before checking again
                        await asyncio.sleep(0.1)
                    
                    return False

                # ========================================================================
                # HELPER FUNCTION 4: Click toggle with robust fallback strategies
                # ========================================================================
                async def _robust_click_toggle() -> bool:
                    """
                    Attempt to click the toggle element using multiple fallback strategies.
                    Handles cases where element is disabled, detached, or partially obscured.
                    
                    Returns:
                        True if click succeeded, False otherwise
                    """
                    nonlocal toggle_el  # Can modify the outer scope's toggle_el variable
                    
                    # Try up to 5 times with different strategies
                    for attempt in range(5):
                        try:
                            # Ensure element is scrolled into view
                            try:
                                await toggle_el.scroll_into_view_if_needed()
                            except Exception:
                                pass
                            
                            # Wait for element to be enabled (not disabled)
                            enabled = await _wait_enabled(toggle_el, 2500)
                            if not enabled:
                                # Element is disabled, try to re-find it
                                refreshed = await _find_toggle_by_label_text()
                                if refreshed:
                                    toggle_el = refreshed
                            
                            # Attempt 1: Standard click
                            try:
                                await toggle_el.click()
                                return True
                            except Exception:
                                # Click failed, try re-finding element (in case it was detached)
                                refreshed = await _find_toggle_by_label_text()
                                if refreshed:
                                    toggle_el = refreshed
                                
                                # Attempt 2: Dispatch click event manually
                                try:
                                    await toggle_el.dispatch_event("click")
                                    return True
                                except Exception:
                                    # Attempt 3: Click via JavaScript
                                    try:
                                        await toggle_el.evaluate("el => el.click()")
                                        return True
                                    except Exception:
                                        pass
                        except Exception:
                            pass
                        
                        # Brief pause before retrying
                        await asyncio.sleep(0.2)
                    
                    return False

                # ========================================================================
                # HELPER FUNCTION 5: Verify toggle state changed to desired state
                # ========================================================================
                async def _verify_state_with_retry(expect_on: Optional[bool], timeout_s: float = 3.0, interval_s: float = 0.2) -> Optional[bool]:
                    """
                    Re-find the toggle element and poll its state until it matches expected
                    state or timeout is reached.
                    
                    This handles cases where the toggle state takes time to update in the DOM.
                    
                    Args:
                        expect_on: Expected state (True=ON, False=OFF, None=any state)
                        timeout_s: Maximum time to wait in seconds
                        interval_s: Time between state checks in seconds
                    
                    Returns:
                        Final state if it matched or timeout expired, None if no state found
                    """
                    deadline = asyncio.get_event_loop().time() + timeout_s
                    last_state: Optional[bool] = None
                    
                    while asyncio.get_event_loop().time() < deadline:
                        try:
                            # Re-find toggle in case it was detached/updated
                            refreshed = await _find_toggle_by_label_text()
                            if refreshed:
                                toggle_el_local = refreshed
                            else:
                                toggle_el_local = toggle_el
                            
                            # Check current state
                            last_state = await _is_on(toggle_el_local)
                            
                            if expect_on is None:
                                # No specific expectation; return first known state
                                if last_state is not None:
                                    return last_state
                            else:
                                # Check if state matches expectation
                                if last_state == expect_on:
                                    return last_state
                        except Exception:
                            pass
                        
                        # Wait before retrying
                        await asyncio.sleep(interval_s)
                    
                    return last_state

                try:
                    toggle_el = await _find_toggle_by_label_text()
                    if not toggle_el:
                        result["error"] = f"Toggle not found for label '{label_text}'"
                        return result
                    current_state = await _is_on(toggle_el)
                    # If unknown, try one click to probe state and then adjust if needed
                    if current_state is None:
                        try:
                            await toggle_el.click()
                            await asyncio.sleep(0.2)
                            current_state = await _is_on(toggle_el)
                            # If still unknown, assume the click toggled; if desired_on True leave as is, else click again
                            if current_state is None:
                                if not desired_on:
                                    await toggle_el.click()
                                result["success"] = True
                                result["details"]["action_performed"] = f"ensure_toggle:{'on' if desired_on else 'off'} (unknown-state heuristic)"
                                return result
                        except Exception:
                            pass
                    if current_state is not None:
                        if current_state == desired_on:
                            # Already in desired state. If ensuring ON, bounce OFF then ON per requirement.
                            if desired_on:
                                try:
                                    ok = await _robust_click_toggle()  # turn OFF
                                    if not ok:
                                        raise Exception("click off failed")
                                    await asyncio.sleep(0.3)
                                    ok = await _robust_click_toggle()  # turn back ON
                                    if not ok:
                                        raise Exception("click on failed")
                                    await asyncio.sleep(0.3)
                                    verify_state = await _verify_state_with_retry(True)
                                    if verify_state is None or verify_state is True:
                                        result["success"] = True
                                        result["details"]["action_performed"] = "ensure_toggle:bounce off->on"
                                        return result
                                    else:
                                        # Attempt final corrective click to ON and verify again
                                        ok2 = await _robust_click_toggle()
                                        await asyncio.sleep(0.3)
                                        verify_state2 = await _verify_state_with_retry(True)
                                        if verify_state2 is None or verify_state2 is True:
                                            result["success"] = True
                                            result["details"]["action_performed"] = "ensure_toggle:bounce off->on corrected"
                                            return result
                                        result["error"] = "Bounce failed to restore ON state"
                                        return result
                                except Exception as e:
                                    result["error"] = f"Bounce off->on failed: {e}"
                                    return result
                            else:
                                # Ensuring OFF and already OFF; bounce ON then OFF per requirement.
                                try:
                                    ok = await _robust_click_toggle()  # turn ON
                                    if not ok:
                                        raise Exception("click on failed")
                                    await asyncio.sleep(0.3)
                                    ok = await _robust_click_toggle()  # turn back OFF
                                    if not ok:
                                        raise Exception("click off failed")
                                    await asyncio.sleep(0.3)
                                    verify_state = await _verify_state_with_retry(False)
                                    if verify_state is None or verify_state is False:
                                        result["success"] = True
                                        result["details"]["action_performed"] = "ensure_toggle:bounce on->off"
                                        return result
                                    else:
                                        # Attempt final corrective click to OFF and verify again
                                        ok2 = await _robust_click_toggle()
                                        await asyncio.sleep(0.3)
                                        verify_state2 = await _verify_state_with_retry(False)
                                        if verify_state2 is None or verify_state2 is False:
                                            result["success"] = True
                                            result["details"]["action_performed"] = "ensure_toggle:bounce on->off corrected"
                                            return result
                                        result["error"] = "Bounce failed to restore OFF state"
                                        return result
                                except Exception as e:
                                    result["error"] = f"Bounce on->off failed: {e}"
                                    return result
                        else:
                            ok = await _robust_click_toggle()
                            if not ok:
                                result["error"] = "Toggle click failed"
                                return result
                            await asyncio.sleep(0.3)
                            verify_state = await _verify_state_with_retry(desired_on)
                            if verify_state is None or verify_state == desired_on:
                                result["success"] = True
                                result["details"]["action_performed"] = f"ensure_toggle:clicked to {'on' if desired_on else 'off'}"
                                return result
                            else:
                                result["error"] = "Toggle click did not change to desired state"
                                return result
                except Exception as e:
                    result["error"] = f"Ensure toggle failed: {e}"
                    return result

            if action_type == "ensure_toggle":
                builder = EnsureToggleActionBuilder(page)
                return await builder.execute(action)
            
            # ============================================================================
            # SECTION 2: ELEMENT-BASED ACTIONS (Click, Type, Hover, Scroll)
            # ============================================================================

            if action_type in ("click", "dblclick", "type"):
                result = await execute_click_type_action(page, action, result)
            elif action_type in ("hover", "scroll"):
                finder, element, frame, scope = await prepare_element_for_action(page, locator, value)
                if not element:
                    result["error"] = f"Element not found with {locator}='{value}'"
                elif action_type == "hover":
                    try:
                        await element.hover()
                        result["success"] = True
                        result["details"]["action_performed"] = "hover"
                    except Exception as e:
                        result["error"] = f"Hover failed: {e}"
                    result = await finalize_action_result(result, finder, element, frame)
                elif action_type == "scroll":
                    scroller = ScrollPage(page, frame, scope)
                    if await scroller.scroll_to_element(element):
                        result["success"] = True
                        result["details"]["action_performed"] = "scroll"
                    else:
                        result["error"] = "Scroll failed"
                    result = await finalize_action_result(result, finder, element, frame)
        
        except Exception as e:
            result["error"] = str(e)
            result["details"]["exception"] = str(e)
        
        # Record action in history
        self.action_history.append({
            "step": action.get("step", ""),
            "action": action,
            "result": result,
            "timestamp": datetime.now().isoformat()
        })
        
        return result



    async def perform_action(self, page: Page, action: dict) -> bool:
        """Legacy method for backward compatibility."""
        result = await self.execute_action(page, action)
        return result["success"]

    async def map_step_to_action(self, step: str, page_summary: str) -> dict:
        """Legacy method for backward compatibility."""
        # Create a mock page context for compatibility
        mock_context = {
            "url": "unknown",
            "title": "unknown",
            "elements_summary": []
        }
        action = await self.map_natural_step_to_action(step, mock_context)
        return {
            "action": action["action"],
            "locator": action["locator"],
            "value": action["value"],
            "input_text": action.get("input_text", ""),
            "description": action["description"]
        }

    async def summarize_page(self, page: Page, max_elements=50, max_html_chars=1000) -> str:
        """Legacy method for backward compatibility."""
        context = await self.get_page_context(page)
        return json.dumps(context, indent=2) 