#!/usr/bin/env python3
"""
UI Builder Agent for constructing mobile app page transition graphs.
This agent traverses mobile app UIs using a robust traversal algorithm to build a graph
of page transitions based on UI elements and user interactions.
"""

import json
import hashlib
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass, asdict
import time
import os
from datetime import datetime
from pathlib import Path
from collections import deque


@dataclass
class UINode:
    """Represents a UI page node in the transition graph."""
    node_id: str
    elements: List[Dict[str, Any]]
    screenshot_path: Optional[str] = None
    paths: List[List[Tuple[str, Dict[str, Any]]]] = None  # List of paths from root to this node
    
    def __post_init__(self):
        if self.paths is None:
            self.paths = []


@dataclass
class UIEdge:
    """Represents a transition edge between UI nodes."""
    source_id: str
    target_id: str
    action: Dict[str, Any]  # Details of the action that caused the transition
    transition_type: str = "tap"  # Type of transition: "tap" or "back" or "swipe"


@dataclass
class UITransitionGraph:
    """Represents the complete UI transition graph."""
    nodes: Dict[str, UINode]
    edges: List[UIEdge]

    def to_dict(self) -> Dict[str, Any]:
        """Convert the graph to a dictionary for serialization."""
        return {
            "nodes": {node_id: asdict(node) for node_id, node in self.nodes.items()},
            "edges": [asdict(edge) for edge in self.edges]
        }

    def save_to_file(self, file_path: str):
        """Save the graph to a JSON file."""
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)


class UIBuilderRobustAgent:
    """
    Agent that builds a UI transition graph by traversing mobile app pages
    using a robust traversal algorithm with swipe support.
    """

    def __init__(self, llm, max_similarity_threshold: float = 0.9, log_dir: str = None, verbose: bool = True):
        """
        Initialize the UI Builder Agent.
        
        Args:
            llm: Language model for processing
            max_similarity_threshold: Threshold for detecting similar pages (0.9 = 90%)
            log_dir: Directory to save log files
            verbose: Whether to print detailed logs
        """
        self.llm = llm
        self.max_similarity_threshold = max_similarity_threshold
        self.visited_pages = {}  # node_id -> UINode
        self.processed_transitions = set()  # Set of (source_id, target_id) tuples
        self.session = None
        self.output_file = None
        self.visualizer_callback = None
        self.log_dir = log_dir
        self.log_file = None
        self.log_file_handle = None
        self.blacklist_texts = set()  # Blacklist for filtering clickable elements
        self.current_traversing_node = None  # Track the currently traversing node
        self.verbose = verbose  # Control verbosity of logs
        self.error_state = False  # Track if agent is in error state
        self.error_message = None  # Store error message for visualization
        
        # Path caching for efficient navigation
        self._node_paths = {}  # node_id -> {source_id -> path}
        self._root_paths = {}  # node_id -> path from root
        self._root_node_id = None  # Root node ID
        
        # Swipe action tracking
        self.swipe_actions = []  # List of swipe actions performed
        
        # Global set of elements to be clicked to avoid duplicate processing across pages
        self.tobe_clicked_elements = set()

        if log_dir:
            self._setup_logging()

    def set_blacklist_texts(self, blacklist_texts: set):
        """
        Set the blacklist texts for filtering clickable elements.
        
        Args:
            blacklist_texts: Set of texts to filter out from clickable elements
        """
        self.blacklist_texts = blacklist_texts

    def _notify_visualizer_traversing_node(self, graph: UITransitionGraph, current_node_id: str):
        """
        Notify the visualizer of the current traversing node.
        
        Args:
            graph: The current graph
            current_node_id: The ID of the current traversing node
        """
        if self.visualizer_callback:
            try:
                # Create a copy of the graph and add the current traversing node info
                graph_with_current = UITransitionGraph(nodes=graph.nodes.copy(), edges=graph.edges.copy())
                # Add a special attribute to indicate the current traversing node
                graph_dict = graph_with_current.to_dict()
                graph_dict['current_traversing_node'] = current_node_id
                # Add error state if present
                if self.error_state:
                    graph_dict['error_state'] = True
                    if self.error_message:
                        graph_dict['error_message'] = self.error_message
                # Create a temporary object with the to_dict method for the callback
                class TempGraph:
                    def __init__(self, graph_dict, nodes, edges):
                        self.graph_dict = graph_dict
                        self.nodes = nodes
                        self.edges = edges
                        
                    def to_dict(self):
                        return self.graph_dict
                        
                # Pass the nodes and edges from the original graph
                temp_graph = TempGraph(graph_dict, graph.nodes, graph.edges)
                self.visualizer_callback(temp_graph)
            except Exception as e:
                self._log(f"Warning: Failed to notify visualizer: {e}", node_id=current_node_id, 
                         namespace="VISUALIZER_NOTIFY", level="ERROR")

    def _set_error_state(self, message: str):
        """
        Set the agent in error state with a message.
        
        Args:
            message: Error message to store
        """
        self.error_state = True
        self.error_message = message
        self._log(f"Error state set: {message}", level="ERROR")

    def _notify_visualizer_error(self, graph: UITransitionGraph):
        """
        Notify the visualizer of an error state.
        
        Args:
            graph: Current graph state
        """
        if self.visualizer_callback:
            try:
                # Create a copy of the graph and add error information
                graph_with_error = UITransitionGraph(nodes=graph.nodes.copy(), edges=graph.edges.copy())
                # Add a special attribute to indicate the error state
                graph_dict = graph_with_error.to_dict()
                graph_dict['error_state'] = True
                if self.error_message:
                    graph_dict['error_message'] = self.error_message
                    
                # Create a temporary object with the to_dict method for the callback
                class TempGraph:
                    def __init__(self, graph_dict, nodes, edges):
                        self.graph_dict = graph_dict
                        self.nodes = nodes
                        self.edges = edges
                        
                    def to_dict(self):
                        return self.graph_dict
                        
                # Pass the nodes and edges from the original graph
                temp_graph = TempGraph(graph_dict, graph.nodes, graph.edges)
                self.visualizer_callback(temp_graph)
            except Exception as e:
                self._log(f"Warning: Failed to notify visualizer of error: {e}", level="ERROR")

    def _convert_elements_to_serializable(self, elements: List) -> List:
        """
        Convert UI elements to serializable format.
        
        Args:
            elements: List of UI elements to convert
            
        Returns:
            List of serializable UI elements
        """
        converted_elements = []
        for element in elements:
            # Convert element to dict, handling potential serialization issues
            try:
                if hasattr(element, '__dict__'):
                    element_dict = {}
                    for key, value in element.__dict__.items():
                        try:
                            # Try to serialize the value
                            json.dumps(value)
                            element_dict[key] = value
                        except (TypeError, ValueError):
                            # If not serializable, convert to string
                            element_dict[key] = str(value)
                    converted_elements.append(element_dict)
                else:
                    converted_elements.append(element)
            except Exception:
                converted_elements.append(element)
        
        return converted_elements

    def _setup_logging(self):
        """Set up logging to file."""
        # Create log directory if it doesn't exist
        Path(self.log_dir).mkdir(parents=True, exist_ok=True)
        
        # Create log file with timestamp
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        self.log_file = os.path.join(self.log_dir, f"ui_builder_log_{timestamp}.txt")
        
        # Open log file for writing
        self.log_file_handle = open(self.log_file, 'w', encoding='utf-8')

    def _log(self, message: str, node_id: str = None, namespace: str = None, level: str = "INFO"):
        """
        Log message to both console and file with standardized format.
        
        Args:
            message: Message to log
            node_id: Node ID for context (optional)
            namespace: Function namespace (optional)
            level: Log level (INFO, ERROR, etc.)
        """
        # If not verbose, skip logging
        if not self.verbose:
            return
            
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        node_info = f"[{node_id[:8]}] " if node_id else ""
        namespace_info = f"[{namespace}] " if namespace else ""
        level_info = f"[{level}] " if level and level != "INFO" else ""
        
        formatted_message = f"[{timestamp}] {node_info}{namespace_info}{level_info}{message}"
        
        # Print to console
        print(formatted_message)
        
        # Write to log file if logging is enabled
        if self.log_file_handle:
            # Remove ANSI color codes for log file
            clean_message = formatted_message.replace('\033[31m', '').replace('\033[0m', '')
            self.log_file_handle.write(f"{clean_message}\n")
            self.log_file_handle.flush()

    def set_visualizer_callback(self, callback):
        """
        Set a callback function to notify the visualizer of updates.
        
        Args:
            callback: Function to call when graph is updated
        """
        self.visualizer_callback = callback

    def _safe_parse_element(self, element) -> Dict:
        """
        Safely parse an element, whether it's a dict, string or other type.
        
        Args:
            element: Element to parse
            
        Returns:
            Dictionary representation of the element
        """
        if isinstance(element, dict):
            return element
        elif isinstance(element, str):
            try:
                # Try to parse string as JSON
                return json.loads(element)
            except json.JSONDecodeError:
                # If parsing fails, return a dict with the string as a value
                return {"raw_string": element}
        else:
            # For other types, convert to string and wrap in dict
            return {"value": str(element)}

    def _flatten_elements_with_children(self, elements: List) -> List:
        """
        Recursively flatten elements and their children into a single list.
        
        Args:
            elements: List of elements, each potentially having children
            
        Returns:
            Flattened list of all elements including nested children
        """
        flattened = []
        
        for element in elements:
            # Parse element safely
            parsed_element = self._safe_parse_element(element)
            flattened.append(parsed_element)
            
            # Process children if they exist
            if 'children' in parsed_element and isinstance(parsed_element['children'], list):
                # Recursively flatten children and add to the list
                child_elements = self._flatten_elements_with_children(parsed_element['children'])
                flattened.extend(child_elements)
                
        return flattened

    def _calculate_page_similarity(self, elements1: List, elements2: List) -> float:
        """
        Calculate similarity between two pages based on their UI elements.
        
        Args:
            elements1: UI elements of first page
            elements2: UI elements of second page
            
        Returns:
            Similarity score between 0 and 1
        """
        if not elements1 or not elements2:
            return 0.0

        # Convert elements directly to strings for comparison
        try:
            elements1_str = json.dumps(elements1, sort_keys=True)
            elements2_str = json.dumps(elements2, sort_keys=True)
        except (TypeError, ValueError):
            # Fallback to string representation if JSON serialization fails
            elements1_str = str(elements1)
            elements2_str = str(elements2)

        # If identical, return 1.0
        if elements1_str == elements2_str:
            return 1.0

        # Calculate similarity based on string overlap
        # This is a simple approach - for more accuracy, we could use difflib or other methods
        len1 = len(elements1_str)
        len2 = len(elements2_str)
        
        if len1 == 0 and len2 == 0:
            return 1.0
        if len1 == 0 or len2 == 0:
            return 0.0
            
        # Use a simple character-level similarity measure
        # This is not perfect but gives a rough estimate
        common_chars = sum(1 for a, b in zip(elements1_str, elements2_str) if a == b)
        max_len = max(len1, len2)
        
        return common_chars / max_len if max_len > 0 else 0.0

    def _generate_page_id(self, elements: List) -> str:
        """
        Generate a unique ID for a page based on its UI elements.
        
        Args:
            elements: UI elements of the page
            
        Returns:
            Unique page ID
        """
        # Flatten elements to include children
        flat_elements = self._flatten_elements_with_children(elements)
        
        # Create a hash based on key UI element properties
        element_strings = []
        for element in flat_elements:
            text = element.get('text', '')
            bounds = element.get('bounds', '')
            element_strings.append(f"{text}|{bounds}")
        
        # Sort to ensure consistent hashing regardless of element order
        element_strings.sort()
        page_content = "|".join(element_strings)
        
        return hashlib.md5(page_content.encode('utf-8')).hexdigest()

    def _extract_clickable_elements(self, elements: List) -> List:
        """
        Extract clickable UI elements from the page, including children.
        
        Args:
            elements: All UI elements on the page
            
        Returns:
            List of clickable elements
        """
        # Flatten elements to include children
        flat_elements = self._flatten_elements_with_children(elements)
        
        clickable_elements = []
        for element in flat_elements:
            # Check if element is clickable based on type
            element_type = element.get('type', '')
            
            # Only extract elements with type 'clickable'
            if element_type == 'clickable':
                # Make sure it has bounds for tapping
                if 'bounds' in element and element['bounds']:
                    # Check if element text or any of its children's text is in the blacklist
                    element_text = element.get('text', '')
                    in_blacklist = False
                    
                    # Check element's own text
                    if element_text and any(black_text in element_text for black_text in self.blacklist_texts):
                        in_blacklist = True
                    
                    # Check children's text if not already in blacklist
                    if not in_blacklist and 'children' in element and isinstance(element['children'], list):
                        for child in element['children']:
                            if isinstance(child, dict):
                                child_text = child.get('text', '')
                                if child_text and any(black_text in child_text for black_text in self.blacklist_texts):
                                    in_blacklist = True
                                    break
                    
                    # Only add element if it's not in the blacklist
                    if not in_blacklist:
                        # Fill empty text with children's text if needed
                        if not element.get('text', ''):
                            children_texts = []
                            if 'children' in element and isinstance(element['children'], list):
                                for child in element['children']:
                                    if isinstance(child, dict) and 'text' in child and child['text']:
                                        children_texts.append(child['text'])
                            if children_texts:
                                # Create a copy of the element with filled text
                                element_copy = element.copy()
                                element_copy['text'] = ' '.join(children_texts)
                                clickable_elements.append(element_copy)
                                continue
                        
                        clickable_elements.append(element)  # Keep original element
                    
        return clickable_elements

    def _parse_bounds(self, bounds_str: str) -> Tuple[int, int, int, int]:
        """
        Parse bounds string into coordinates.
        
        Args:
            bounds_str: Bounds string in format "x1,y1,x2,y2"
            
        Returns:
            Tuple of (x1, y1, x2, y2)
        """
        coords = list(map(int, bounds_str.split(',')))
        return coords[0], coords[1], coords[2], coords[3]

    def _calculate_tap_coordinates(self, bounds_str: str) -> Tuple[int, int]:
        """
        Calculate tap coordinates from bounds (center of element).
        
        Args:
            bounds_str: Bounds string in format "x1,y1,x2,y2"
            
        Returns:
            Tuple of (x, y) coordinates for tap
        """
        x1, y1, x2, y2 = self._parse_bounds(bounds_str)
        x = (x1 + x2) // 2
        y = (y1 + y2) // 2
        return x, y

    def get_initial_ui_elements(self) -> List:
        """
        Get initial UI elements for the task.
        
        Returns:
            List of UI elements on the initial page
        """
        self._log(f"[INIT] Getting initial UI elements")
        
        # Get initial UI elements using the session directly
        result = self.session.mobile.get_all_ui_elements(timeout_ms=5000)
        
        if result.success:
            # Convert elements to serializable format
            elements = self._convert_elements_to_serializable(result.elements)
            
            if not elements:
                raise Exception("No UI elements found on initial page")
                
            return elements
        else:
            raise Exception(f"Failed to get initial UI elements: {result.error_message}")

    def build_ui_map(self, max_depth: int, output_file: str) -> UITransitionGraph:
        """
        Build a UI transition map using robust traversal with swipe support.
        
        Args:
            max_depth: Maximum traversal depth
            output_file: File path to save the resulting graph
            
        Returns:
            UITransitionGraph representing the app's UI structure
        """
        self._log(f"Starting UI map building with max depth: {max_depth}", namespace="BUILD_UI_MAP")
        self._log(f"Output file: {output_file}", namespace="BUILD_UI_MAP")
        
        # Reset error state
        self.error_state = False
        self.error_message = None
        
        # Store output file for saving during traversal
        self.output_file = output_file
        
        # Get initial UI elements
        try:
            initial_elements = self.get_initial_ui_elements()
        except Exception as e:
            self._set_error_state(f"Failed to get initial UI elements: {str(e)}")
            # Create empty graph to return
            graph = UITransitionGraph(nodes={}, edges=[])
            self._notify_visualizer_error(graph)
            return graph
            
        self._log(f"Got initial UI elements: {len(initial_elements)} elements", namespace="BUILD_UI_MAP")
        
        # Create initial node
        initial_page_id = self._generate_page_id(initial_elements)
        initial_node = UINode(
            node_id=initial_page_id,
            elements=initial_elements,
            paths=[[(initial_page_id, None)]]  # Path to itself
        )
        
        # Initialize graph
        graph = UITransitionGraph(nodes={initial_page_id: initial_node}, edges=[])
        self.visited_pages[initial_page_id] = initial_node
        self._root_node_id = initial_page_id  # Set root node ID
        self._root_paths[initial_page_id] = []  # Root node has empty path to itself
        
        self._log(f"Created initial node: {initial_page_id[:8]}", namespace="BUILD_UI_MAP")
        
        # Start robust traversal
        traversal_success = self._robust_traverse(
            graph=graph,
            current_node_id=initial_page_id,
            current_elements=initial_elements,
            current_depth=0,
            max_depth=max_depth
        )
        
        if not traversal_success:
            self._log("UI map building failed during traversal", namespace="BUILD_UI_MAP", level="ERROR")
            self._set_error_state("UI map building failed during traversal")
            self._notify_visualizer_error(graph)
        
        # Save final graph
        self._log(f"Saving final UI transition graph to {output_file}", namespace="BUILD_UI_MAP")
        self._save_graph_to_file(graph, output_file, context="final graph")
        
        # Print statistics
        self._log(f"UI Map Building Complete:", namespace="BUILD_UI_MAP")
        self._log(f"  Nodes (unique pages): {len(graph.nodes)}", namespace="BUILD_UI_MAP")
        self._log(f"  Edges (transitions): {len(graph.edges)}", namespace="BUILD_UI_MAP")
        self._log(f"  Max traversal depth: {max_depth}", namespace="BUILD_UI_MAP")
        
        # Close log file if it was opened
        if self.log_file_handle:
            self.log_file_handle.close()
        
        return graph

    def _navigate_to_node(self, target_node_id: str) -> bool:
        """
        Navigate to a specific node, using back navigation or root navigation as needed.
        
        Args:
            target_node_id: Target node ID to navigate to
            
        Returns:
            True if navigation was successful, False otherwise
        """
        self._log(f"Attempting to navigate to node {target_node_id[:8]}", namespace="NAVIGATE_TO_NODE")
        
        # If we're already at the target node, nothing to do
        # This would need to be checked by comparing current UI with target node UI
        
        # Try to find path from current location to target
        # For now, we'll use the existing path finding logic
        if target_node_id in self._root_paths:
            path = self._root_paths[target_node_id]
            if path and len(path) > 1:  # Only navigate if there's a path with steps
                self._log(f"Found path with {len(path)-1} steps to target node", namespace="NAVIGATE_TO_NODE")
                # Skip the first element since it's the current node
                self._replay_path(path[1:])
                return True
        
        # If no path found, try going home and replaying path from root
        self._log("No direct path found, navigating from root", namespace="NAVIGATE_TO_NODE")
        self._navigate_from_root(target_node_id)
        return True

    def _robust_traverse(self, graph: UITransitionGraph, current_node_id: str, 
                         current_elements: List, current_depth: int, max_depth: int) -> bool:
        """
        Robustly traverse a page and its sub-pages (with swipe navigation).
        
        Args:
            graph: The graph being built
            current_node_id: ID of the current node
            current_elements: UI elements of the current page
            current_depth: Current depth in the traversal
            max_depth: Maximum allowed depth
            
        Returns:
            True if traversal was successful, False otherwise
        """
        # Set the current traversing node
        self.current_traversing_node = current_node_id
        
        # Notify visualizer of the current traversing node
        self._notify_visualizer_traversing_node(graph, current_node_id)
        
        self._log(f"Starting robust traversal of page {current_node_id[:8]} at depth {current_depth}", 
                  node_id=current_node_id, namespace="ROBUST_TRAVERSE")
        
        # Check depth limit
        if current_depth >= max_depth:
            self._log(f"Reached maximum depth {max_depth}, returning to previous level", 
                      node_id=current_node_id, namespace="ROBUST_TRAVERSE")
            return True
            
        # If this is not the first visit to this node, try to navigate back to the correct state
        if current_depth > 0:
            if not self._ensure_correct_state(current_node_id, current_elements):
                self._log(f"Failed to ensure correct state for node {current_node_id[:8]}", 
                          node_id=current_node_id, namespace="ROBUST_TRAVERSE", level="ERROR")
                self._set_error_state(f"Failed to ensure correct state for node {current_node_id[:8]}")
                self._notify_visualizer_error(graph)
                return False
        
        # Initialize clicked elements tracking list for this node traversal
        clicked_elements = set()
        
        # Traverse swipe sub-pages
        sub_page_index = 0
        sub_page_node_id = current_node_id
        sub_page_node_elements = current_elements
        while True:
            # If not the first sub-page, try to navigate to it
            if sub_page_index > 0:
                pre_page_node_id = sub_page_node_id
                pre_page_node_elements = sub_page_node_elements
                (sub_page_node_id, sub_page_node_elements) = self._navigate_to_subpage(sub_page_index, pre_page_node_id, pre_page_node_elements)
                if (sub_page_node_id is None) or (sub_page_node_elements is None):
                    break
            
                # Add the new sub-page node to the graph and create relationship with previous page
                # Generate action description for the swipe navigation
                swipe_action_desc = {
                    "swipe_direction": "up",
                    "start_x": 540,
                    "start_y": 1600,
                    "end_x": 540,
                    "end_y": 400,
                    "duration_ms": 500
                }
                
                # Add swipe subpage to graph using the dedicated function
                self._add_swipe_subpage_to_graph(
                    graph=graph,
                    pre_page_node_id=pre_page_node_id,
                    sub_page_node_id=sub_page_node_id,
                    sub_page_node_elements=sub_page_node_elements,
                    swipe_action_desc=swipe_action_desc
                )

            # Process clickable elements on this sub-page
            # Pass the clicked_elements set to avoid re-clicking elements already clicked in previous sub-pages
            self._process_clickable_elements(
                graph=graph,
                current_node_id=sub_page_node_id,
                current_elements=sub_page_node_elements,
                current_depth=current_depth,
                max_depth=max_depth,
                clicked_elements=clicked_elements  # Pass the clicked elements tracking set
            )
            
            # Move to next sub-page
            sub_page_index += 1
            
            # Safety check to prevent infinite loops
            if sub_page_index > 20:  # Arbitrary limit
                self._log("Reached swipe limit, stopping swipe traversal", 
                          node_id=current_node_id, namespace="ROBUST_TRAVERSE")
                break

        # Clear the current traversing node when finished processing
        self.current_traversing_node = None
        
        # Notify visualizer that we're no longer traversing this node
        if self.visualizer_callback:
            try:
                self.visualizer_callback(graph)
            except Exception as e:
                self._log(f"Warning: Failed to notify visualizer: {e}", node_id=current_node_id, 
                          namespace="ROBUST_TRAVERSE", level="ERROR")
        
        return True

    def _check_if_at_expected_page(self, expected_elements: List, node_id: str = None) -> Tuple[bool, float, Any]:
        """
        Check if the current page matches the expected page.
        
        Args:
            expected_elements: Expected UI elements for the target page
            node_id: Node ID for logging context (optional)
            
        Returns:
            Tuple of (is_match, similarity_score, current_ui_result)
        """
        # Get current UI
        current_ui = self.session.mobile.get_all_ui_elements(timeout_ms=5000)
        if not current_ui.success:
            self._log(f"Failed to get current UI: {current_ui.error_message}", 
                      node_id=node_id, namespace="CHECK_PAGE", level="ERROR")
            return False, 0.0, current_ui
            
        # Calculate similarity
        similarity = self._calculate_page_similarity(current_ui.elements, expected_elements)
        
        # Check if we're at the correct page
        is_match = similarity >= 0.95
        if is_match:
            self._log(f"Already at correct page (similarity: {similarity:.2f})", 
                      node_id=node_id, namespace="CHECK_PAGE")
        else:
            self._log(f"Current page does not match expected (similarity: {similarity:.2f})", 
                      node_id=node_id, namespace="CHECK_PAGE")
            
        return is_match, similarity, current_ui

    def _save_graph_to_file(self, graph: UITransitionGraph, file_path: str, node_id: str = None, context: str = None):
        """
        Save the graph to a file with proper error handling and logging.
        
        Args:
            graph: The graph to save
            file_path: The file path to save to
            node_id: Node ID for logging context (optional)
            context: Context information for logging (optional)
        """
        if not file_path:
            return
            
        try:
            graph.save_to_file(file_path)
            context_msg = f" ({context})" if context else ""
            self._log(f"Saved graph to {file_path}{context_msg}", 
                      node_id=node_id, namespace="SAVE_GRAPH")
        except Exception as e:
            context_msg = f" ({context})" if context else ""
            self._log(f"Failed to save graph{context_msg}: {e}", 
                      node_id=node_id, namespace="SAVE_GRAPH", level="ERROR")

    def _ensure_correct_state(self, node_id: str, expected_elements: List) -> bool:
        """
        Ensure we're at the correct page by navigating back or using root navigation if needed.
        
        Args:
            node_id: Expected node ID
            expected_elements: Expected UI elements for this node
            
        Returns:
            True if we're at the correct state, False otherwise
        """
        self._log(f"Ensuring correct state for node {node_id[:8]}", 
                  node_id=node_id, namespace="ENSURE_STATE")
        
        # Check current UI
        is_match, similarity, current_ui = self._check_if_at_expected_page(expected_elements, node_id)
        if not current_ui.success:
            return False
            
        if is_match:
            return True
            
        # Try navigating back until we find the correct page or a known page
        self._log("Attempting to navigate back to correct page", 
                  node_id=node_id, namespace="ENSURE_STATE")
                  
        max_back_attempts = 10
        
        for i in range(max_back_attempts):
            self._navigate_back(current_node_id=node_id)
            
            # Check current UI after back navigation
            is_match, similarity, current_ui = self._check_if_at_expected_page(expected_elements, node_id)
            if not current_ui.success:
                break
                
            if is_match:
                return True
            
            # Generate ID for current page
            current_page_id = self._generate_page_id(current_ui.elements)
                
            # Check if we've seen this page before (never-seen page C case)
            if current_page_id not in self.visited_pages:
                # This is a never-seen page C - stop mapping
                self._log(f"Encountered never-seen page {current_page_id[:8]}, stopping mapping", 
                          node_id=node_id, namespace="ENSURE_STATE", level="WARNING")
                # Record unexpected page elements in the graph for debugging purposes
                self._record_unexpected_page(current_page_id, current_ui.elements, node_id)
                return False
                
            # Check if current_page_id is in any path that leads to our target node_id
            if node_id in self.visited_pages:
                # Use the new path finding method
                path_to_target = self._find_path_using_node_paths(current_page_id, node_id)
                if path_to_target:
                    self._log(f"Found path from current page {current_page_id[:8]} to target {node_id[:8]}, replaying path", 
                              node_id=node_id, namespace="ENSURE_STATE")
                    self._replay_path(path_to_target)
                    
                    # Verify we're at the correct page now
                    is_match, similarity, current_ui = self._check_if_at_expected_page(expected_elements, node_id)
                    if current_ui.success and is_match:
                        self._log(f"Successfully replayed path to correct page (similarity: {similarity:.2f})", 
                                  node_id=node_id, namespace="ENSURE_STATE")
                        return True
        
        # If all attempts failed, we cannot calibrate the sandbox state
        self._log("Failed to calibrate sandbox state to expected page", 
                  node_id=node_id, namespace="ENSURE_STATE", level="ERROR")
        
        # Record the last encountered page for debugging if we have valid UI data
        if current_ui.success:
            current_page_id = self._generate_page_id(current_ui.elements)
            self._record_unexpected_page(current_page_id, current_ui.elements, node_id)
            
        return False

    def _record_unexpected_page(self, page_id: str, elements: List, expected_node_id: str):
        """
        Record an unexpected page in the graph for debugging purposes.
        
        Args:
            page_id: ID of the unexpected page
            elements: UI elements of the unexpected page
            expected_node_id: ID of the node we were expecting to find
        """
        # Add a special prefix to identify unexpected pages in the graph
        unexpected_page_id = f"unexpected_{page_id}"
        
        # Check if we've already recorded this unexpected page
        if unexpected_page_id in self.visited_pages:
            return
        
        # Create a note about why this page was recorded
        annotated_elements = elements.copy()
        annotated_elements.append({
            "debug_info": {
                "type": "unexpected_page",
                "reason": f"Encountered during navigation to expected page {expected_node_id[:8]}",
                "expected_page": expected_node_id,
                "timestamp": datetime.now().isoformat()
            }
        })
        
        # Create a new node for the unexpected page
        unexpected_node = UINode(
            node_id=unexpected_page_id,
            elements=annotated_elements,
            paths=[]  # Empty paths since this is not part of normal navigation
        )
        
        # Add to visited pages
        self.visited_pages[unexpected_page_id] = unexpected_node
        
        self._log(f"Recorded unexpected page {page_id[:8]} for debugging", 
                  node_id=expected_node_id, namespace="RECORD_UNEXPECTED", level="DEBUG")

    def _find_path_using_node_paths(self, source_node_id: str, target_node_id: str) -> List[Tuple[str, Dict[str, Any]]]:
        """
        Find path between two nodes using the paths stored in the target node.
        
        Args:
            source_node_id: Source node ID
            target_node_id: Target node ID
            
        Returns:
            List of (node_id, action) tuples representing path from source to target
        """
        # Check if target node exists
        if target_node_id not in self.visited_pages:
            return []
            
        target_node = self.visited_pages[target_node_id]
        
        # Look for a path that contains the source node and leads to the target node
        for path in target_node.paths:
            # Check if source_node_id is in this path
            for i, (path_node_id, action) in enumerate(path):
                if path_node_id == source_node_id:
                    # Found a path that contains source_node_id and leads to our target
                    # Return the remaining path from source position to target
                    return path[i+1:]
        
        # If we didn't find a path in the target node's paths, try the reverse
        # Check if source node exists
        if source_node_id not in self.visited_pages:
            return []
            
        source_node = self.visited_pages[source_node_id]
        
        # Look for a path that contains the target node in source node's paths
        for path in source_node.paths:
            # Check if target_node_id is in this path
            for i, (path_node_id, action) in enumerate(path):
                if path_node_id == target_node_id:
                    # Found a path from source to target
                    return path[1:i+1]  # Return path from source to target (excluding source)
        
        # No path found
        return []

    def _navigate_to_subpage(self, subpage_index: int, pre_node_id, pre_node_elements) -> tuple:
        """
        Navigate to a specific swipe sub-page.
        
        Args:
            subpage_index: Index of the sub-page to navigate to
            pre_node_id: ID of the previous node
            pre_node_elements: Elements of the previous node
            
        Returns:
            Tuple of (new_page_id, new_elements) if navigation was successful, (None, None) otherwise
        """
        self._log(f"Navigating to swipe sub-page {subpage_index}", namespace="NAVIGATE_SUBPAGE")
        
        # Navigate back to base page first
        if not self._ensure_correct_state(pre_node_id, pre_node_elements):
            self._log(f"Failed to ensure correct state for node {pre_node_id[:8]}", 
                      namespace="NAVIGATE_SUBPAGE", level="ERROR")
            return (None, None)

        self._log(f"Performing swipe {subpage_index} to reach target sub-page", namespace="NAVIGATE_SUBPAGE")
        # Swipe up to go to next page
        # Assuming standard screen dimensions, adjust as needed
        swipe_result = self.session.mobile.swipe(540, 1600, 540, 400, duration_ms=500)
        if not swipe_result.success:
            self._log(f"Swipe {subpage_index} failed: {swipe_result.error_message}",
                        namespace="NAVIGATE_SUBPAGE", level="ERROR")
            return (None, None)
        time.sleep(2)
        
        # Check if this is a new page or already visited page
        current_ui = self.session.mobile.get_all_ui_elements(timeout_ms=5000)
        if not current_ui.success:
            self._log(f"Failed to get UI after swipe: {current_ui.error_message}",
                      namespace="NAVIGATE_SUBPAGE", level="ERROR")
            return (None, None)
            
        # Convert elements to serializable format
        new_elements = self._convert_elements_to_serializable(current_ui.elements)
        
        # Generate ID for the new page
        new_page_id = self._generate_page_id(new_elements)
        
        # Check if this is a similar/identical page we've seen before
        is_similar = False
        for visited_id, visited_node in self.visited_pages.items():
            similarity = self._calculate_page_similarity(new_elements, visited_node.elements)
            if similarity >= self.max_similarity_threshold:
                is_similar = True
                break
        
        # If it's a similar page, we've likely reached the end of unique pages
        # Return None to indicate we should stop swiping
        if is_similar:
            self._log(f"Encountered already visited page {new_page_id[:8]} (similarity >= {self.max_similarity_threshold}), stopping swipe navigation",
                      namespace="NAVIGATE_SUBPAGE")
            # Navigate back to the base page
            if not self._ensure_correct_state(pre_node_id, pre_node_elements):
                self._log(f"Failed to ensure correct state for node {pre_node_id[:8]}", 
                          namespace="NAVIGATE_SUBPAGE", level="ERROR")
            return (None, None)
            
        # Otherwise, this is a new page
        self._log(f"Successfully navigated to new sub-page {subpage_index} with ID {new_page_id[:8]}",
                  namespace="NAVIGATE_SUBPAGE")
        return (new_page_id, new_elements)

    def _swipe_to_next_page(self) -> bool:
        """
        Swipe to the next page if possible.
        
        Returns:
            True if swipe was successful and there's a new page, False otherwise
        """
        self._log("Attempting to swipe to next page", namespace="SWIPE_NEXT")
        
        # Get current UI before swipe
        before_swipe = self.session.mobile.get_all_ui_elements(timeout_ms=5000)
        if not before_swipe.success:
            self._log(f"Failed to get UI before swipe: {before_swipe.error_message}", 
                      namespace="SWIPE_NEXT", level="ERROR")
            return False
            
        # Get current page ID before swipe
        current_page_id = self._generate_page_id(before_swipe.elements)
            
        # Perform swipe (swipe up to scroll down)
        # Assuming standard screen dimensions, adjust as needed
        swipe_result = self.session.mobile.swipe(540, 1600, 540, 400, duration_ms=500)
        if not swipe_result.success:
            self._log(f"Swipe failed: {swipe_result.error_message}", 
                      namespace="SWIPE_NEXT", level="ERROR")
            return False
            
        time.sleep(2)
        
        # Get current UI after swipe
        after_swipe = self.session.mobile.get_all_ui_elements(timeout_ms=5000)
        if not after_swipe.success:
            self._log(f"Failed to get UI after swipe: {after_swipe.error_message}", 
                      namespace="SWIPE_NEXT", level="ERROR")
            return False
            
        # Check if page actually changed
        similarity = self._calculate_page_similarity(before_swipe.elements, after_swipe.elements)
        if similarity >= 0.95:
            self._log(f"Page did not change significantly after swipe (similarity: {similarity:.2f})", 
                      namespace="SWIPE_NEXT")
            return False
            
        # Generate ID for the new page after swipe
        new_page_id = self._generate_page_id(after_swipe.elements)
            
        # Create swipe action description
        swipe_action_desc = {
            "swipe_direction": "up",
            "start_x": 540,
            "start_y": 1600,
            "end_x": 540,
            "end_y": 400,
            "duration_ms": 500
        }
            
        # Update path caches for navigation
        if new_page_id not in self._node_paths:
            self._node_paths[new_page_id] = {}
        self._node_paths[new_page_id][current_page_id] = [(new_page_id, swipe_action_desc)]
        
        if current_page_id in self._root_paths:
            self._root_paths[new_page_id] = self._root_paths[current_page_id] + [(new_page_id, swipe_action_desc)]
        else:
            self._root_paths[new_page_id] = [(new_page_id, swipe_action_desc)]
            
        self._log("Successfully swiped to new page", namespace="SWIPE_NEXT")
        return True

    def _process_clickable_elements(self, graph: UITransitionGraph, current_node_id: str, 
                                    current_elements: List, current_depth: int, max_depth: int,
                                    clicked_elements: set = None) -> bool:
        """
        Process clickable elements on the current page.
        
        Args:
            graph: The graph being built
            current_node_id: ID of the current node
            current_elements: UI elements of the current page
            current_depth: Current depth in the traversal
            max_depth: Maximum allowed depth
            clicked_elements: Set of elements already clicked in previous sub-pages
            
        Returns:
            True if processing was successful, False otherwise
        """
        self._log(f"Processing clickable elements on page {current_node_id[:8]}", 
                  node_id=current_node_id, namespace="PROCESS_ELEMENTS")
        
        # Initialize clicked elements set if not provided
        if clicked_elements is None:
            clicked_elements = set()
        
        # Extract clickable elements
        clickable_elements = self._extract_clickable_elements(current_elements)
        self._log(f"Found {len(clickable_elements)} clickable elements", 
                  node_id=current_node_id, namespace="PROCESS_ELEMENTS")
        
        now_tobe_clicked_elements = set()
        for element_identifier in self.tobe_clicked_elements:
            now_tobe_clicked_elements.add(element_identifier)
        
        for i, element in enumerate(clickable_elements):
            element_identifier = (
                element.get('text', ''),
                element.get('resourceId', ''),
                element.get('className', '')
            )
            self.tobe_clicked_elements.add(element_identifier)

        # Process each clickable element
        for i, element in enumerate(clickable_elements):
            # Create a unique identifier for the element based on its properties
            element_identifier = (
                element.get('text', ''),
                element.get('resourceId', ''),
                element.get('className', '')
            )
            
            # Check if this element has already been clicked in a previous sub-page
            if element_identifier in clicked_elements:
                self._log(f"Skipping element {i+1}/{len(clickable_elements)} as it was already clicked in a previous sub-page", 
                          node_id=current_node_id, namespace="PROCESS_ELEMENTS")
                continue
            
            # Check if this element is already scheduled to be clicked in another page traversal
            # This prevents duplicate processing of fixed UI elements that appear on multiple pages
            if element_identifier in now_tobe_clicked_elements:
                self._log(f"Skipping element {i+1}/{len(clickable_elements)} as it is already scheduled to be processed in another page traversal", 
                          node_id=current_node_id, namespace="PROCESS_ELEMENTS")
                continue
            
            # Process the element using the new function
            result = self._build_single_element(element, i, len(clickable_elements),
                                          current_node_id, current_elements,
                                          graph, current_depth, max_depth, clicked_elements)
            
            # Add the element to the clicked set after processing
            clicked_elements.add(element_identifier)
            
            if not result:
                self._set_error_state(f"Failed to process element {i+1}/{len(clickable_elements)} on node {current_node_id[:8]}")
                self._notify_visualizer_error(graph)
                return False  # Stop processing if the function returns False
        
        return True

    def _build_single_element(self, element, element_index: int, total_elements: int,
                           current_node_id: str, current_elements: List, graph: UITransitionGraph,
                           current_depth: int, max_depth: int, clicked_elements: set = None) -> bool:
        """
        Tap on a single UI element and handle the result.
        
        Args:
            element: The UI element to tap on
            element_index: Index of the current element (0-based)
            total_elements: Total number of elements to process
            current_node_id: Current node ID
            current_elements: Current UI elements
            graph: The graph being built
            current_depth: Current depth in the traversal
            max_depth: Maximum allowed depth
            clicked_elements: Set of elements already clicked in previous sub-pages (optional)
            
        Returns:
            True if processing should continue, False if it should stop
        """
        # Process element and get new state
        success, new_ui_result, new_elements, action_desc = self._process_element_and_get_new_state(
            element, element_index, total_elements, current_node_id, current_elements)
        
        if not success:
            # If we have a UI result but not successful, it means we got a UI but it's not valid
            # If we don't have a UI result, it means we failed to get UI
            return True  # Continue processing other elements
            
        # Generate ID for the new page
        new_page_id = self._generate_page_id(new_elements)
        
        # Check if this is a similar page
        is_similar = False
        similar_page_id = None
        for visited_id, visited_node in self.visited_pages.items():
            similarity = self._calculate_page_similarity(new_elements, visited_node.elements)
            if similarity >= self.max_similarity_threshold:
                is_similar = True
                similar_page_id = visited_id
                break
        
        # Create edge for transition
        if is_similar:
            target_id = similar_page_id
            # 调用处理相似页面建图逻辑
            if not self._handle_similar_page_graph_building(graph, current_node_id,
                                                          current_elements, target_id, action_desc):
                return False
        else:
            target_id = new_page_id
            # 调用处理新页面建图逻辑
            if not self._handle_new_page_graph_building(graph, current_node_id, current_elements,
                                                      new_page_id, new_elements, action_desc,
                                                      current_depth, max_depth):
                return False
                
        # Add the transition to processed transitions after successful handling
        transition_key = (current_node_id, target_id)
        self.processed_transitions.add(transition_key)
        
        return True

    def _handle_similar_page_graph_building(self, graph: UITransitionGraph, current_node_id: str, 
                                          current_elements: List, target_id: str, action_desc: dict) -> bool:
        """
        Handle graph building logic for similar (already visited) pages.
        
        Args:
            graph: The graph being built
            current_node_id: Current node ID
            current_elements: Current UI elements
            target_id: Target node ID (similar page)
            action_desc: Action description
            
        Returns:
            True if processing should continue, False otherwise
        """
        self._log(f"Transition to already visited page {target_id[:8]}, not adding to graph", 
                  node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="TRANSITION")
        if not self._ensure_correct_state(current_node_id, current_elements):
            self._log(f"Failed to ensure correct state for node {current_node_id[:8]}", 
                      node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="ERROR")
            self._set_error_state(f"Failed to ensure correct state for node {current_node_id[:8]}")
            self._notify_visualizer_error(graph)
            return False
        return True

    def _handle_new_page_graph_building(self, graph: UITransitionGraph, current_node_id: str,
                                      current_elements: List, new_page_id: str, new_elements: List,
                                      action_desc: dict, current_depth: int, max_depth: int) -> bool:
        """
        Handle graph building logic for new pages.
        
        Args:
            graph: The graph being built
            current_node_id: Current node ID
            current_elements: Current UI elements
            new_page_id: New page ID
            new_elements: New UI elements
            action_desc: Action description
            current_depth: Current depth in the traversal
            max_depth: Maximum allowed depth
            
        Returns:
            True if processing was successful, False otherwise
        """
        # Create edge for tap transition
        tap_edge = UIEdge(
            source_id=current_node_id,
            target_id=new_page_id,
            action=action_desc,
            transition_type="tap"
        )
        
        # Add edge to graph
        graph.edges.append(tap_edge)
        
        # Create new path by extending the current node's path
        new_path = []
        if current_node_id in self.visited_pages and self.visited_pages[current_node_id].paths:
            # Extend each existing path of the current node
            for path in self.visited_pages[current_node_id].paths:
                new_path.append(path + [(new_page_id, action_desc)])
        else:
            # This is a direct child of root, create new path
            if self._root_node_id:
                new_path = [[(self._root_node_id, None)] + [(new_page_id, action_desc)]]
            else:
                new_path = [[(new_page_id, action_desc)]]
        
        new_node = UINode(
            node_id=new_page_id,
            elements=new_elements,
            paths=new_path
        )
        graph.nodes[new_page_id] = new_node
        self.visited_pages[new_page_id] = new_node
        
        # Add to root paths
        if current_node_id in self._root_paths:
            self._root_paths[new_page_id] = self._root_paths[current_node_id] + [(new_page_id, action_desc)]
        else:
            self._root_paths[new_page_id] = [(new_page_id, action_desc)]
        
        # Notify visualizer of the new node
        if self.visualizer_callback:
            try:
                self.visualizer_callback(graph)
            except Exception as e:
                self._log(f"Warning: Failed to notify visualizer: {e}", 
                          node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="ERROR")
        
        # Save current state of the graph
        if self.output_file:
            self._save_graph_to_file(
                graph,
                self.output_file,
                node_id=current_node_id,
                context="new node added"
            )
        
        # Recursively traverse the new page if we haven't reached max depth
        if current_depth + 1 < max_depth:
            if not self._robust_traverse(
                graph=graph,
                current_node_id=new_page_id,
                current_elements=new_elements,
                current_depth=current_depth + 1,
                max_depth=max_depth
            ):
                self._log(f"Failed to traverse new page {new_page_id[:8]}", 
                          node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="ERROR")
                self._ensure_correct_state(current_node_id, current_elements)
                return False  # Stop processing
        
        # Navigate back to current page after exploration
        if not self._ensure_correct_state(current_node_id, current_elements):
            self._log(f"Failed to ensure correct state for node {current_node_id[:8]}", 
                      node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="ERROR")
            self._set_error_state(f"Failed to ensure correct state for node {current_node_id[:8]}")
            self._notify_visualizer_error(graph)
            return False
        return True

    def _process_element_and_get_new_state(self, element, element_index: int, total_elements: int,
                                         current_node_id: str, current_elements: List) -> tuple:
        """
        Process a single element: parse it, tap on it, and get the new UI state.
        
        Args:
            element: The UI element to process
            element_index: Index of the current element (0-based)
            total_elements: Total number of elements to process
            current_node_id: Current node ID
            current_elements: Current UI elements
            
        Returns:
            Tuple of (success: bool, new_ui_result, new_elements, action_desc)
        """
        # Parse element safely
        parsed_element = self._safe_parse_element(element)
        
        # Calculate tap coordinates
        bounds = parsed_element.get('bounds')
        if not bounds:
            self._log(f"Skipping element {element_index+1}/{total_elements} - no bounds", 
                      node_id=current_node_id, namespace="PROCESS_ELEMENTS")
            return (False, None, None, None)
            
        x, y = self._calculate_tap_coordinates(bounds)
        
        # Get element text
        element_text = parsed_element.get('text', '')
        
        # Create action description
        action_desc = {
            "element_text": element_text,
            "element_class": parsed_element.get('className', ''),
            "tap_x": x,
            "tap_y": y
        }
        
        self._log(f"Tapping on element {element_index+1}/{total_elements}: {action_desc}", 
                  node_id=current_node_id, namespace="PROCESS_ELEMENTS")
        
        # Tap on the element
        tap_result = self._tap_element(x=x, y=y, current_node_id=current_node_id, element_desc=action_desc)
        
        if not tap_result.success:
            self._log(f"Tap failed: {tap_result.error_message}", 
                      node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="ERROR")
            if not self._ensure_correct_state(current_node_id, current_elements):
                self._log(f"Failed to ensure correct state for node {current_node_id[:8]}", 
                          node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="ERROR")
                self._set_error_state(f"Failed to ensure correct state for node {current_node_id[:8]} after tap failure")
            return (False, None, None, action_desc)
            
        # Wait for UI to stabilize
        time.sleep(2)

        # Get new UI state
        new_ui_result = self.session.mobile.get_all_ui_elements(timeout_ms=5000)
        if not new_ui_result.success:
            self._log(f"Failed to get new UI state: {new_ui_result.error_message}", 
                      node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="ERROR")
            if not self._ensure_correct_state(current_node_id, current_elements):
                self._log(f"Failed to ensure correct state for node {current_node_id[:8]}", 
                          node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="ERROR")
                self._set_error_state(f"Failed to ensure correct state for node {current_node_id[:8]} after UI state failure")
            return (False, new_ui_result, None, action_desc)
        
        # Convert elements to serializable format
        new_elements = self._convert_elements_to_serializable(new_ui_result.elements)
        
        if not new_elements:
            self._log(f"No UI elements found after tap", 
                      node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="WARNING")
            if not self._ensure_correct_state(current_node_id, current_elements):
                self._log(f"Failed to ensure correct state for node {current_node_id[:8]}", 
                          node_id=current_node_id, namespace="PROCESS_ELEMENTS", level="ERROR")
                self._set_error_state(f"Failed to ensure correct state for node {current_node_id[:8]} after no elements found")
            return (False, new_ui_result, new_elements, action_desc)
        
        return (True, new_ui_result, new_elements, action_desc)

    def _navigate_back(self, current_node_id: str = None):
        """
        Navigate back to the previous page and wait for UI to stabilize.
        
        Args:
            current_node_id: ID of the current page node (optional)
        """
        self._log("Navigating back to previous page", 
                  node_id=current_node_id, namespace="NAVIGATE_BACK")
        back_result = self.session.mobile.send_key(4)  # 4 is BACK key
        if not back_result.success:
            self._log(f"Failed to navigate back: {back_result.error_message}", 
                      node_id=current_node_id, namespace="NAVIGATE_BACK", level="ERROR")
        else:
            self._log("Successfully navigated back", 
                      node_id=current_node_id, namespace="NAVIGATE_BACK", level="INFO")
            
        # Wait for UI to stabilize after back navigation
        time.sleep(2)

    def _go_to_home_screen(self, current_node_id: str = None) -> Any:
        """
        Navigate to the home screen and log the action.
        
        Args:
            current_node_id: ID of the current page node (optional)
            
        Returns:
            Result of the home screen navigation operation
        """
        self._log("Navigating to home screen", 
                  node_id=current_node_id, namespace="HOME_SCREEN")
        
        # Perform the home screen navigation action
        home_result = self.session.mobile.send_key(3)  # 3 is HOME key
        
        # Log the result
        if home_result.success:
            self._log("Successfully navigated to home screen", 
                      node_id=current_node_id, namespace="HOME_SCREEN", level="INFO")
        else:
            self._log(f"Failed to navigate to home screen: {home_result.error_message}", 
                      node_id=current_node_id, namespace="HOME_SCREEN", level="ERROR")
            
        # Wait for UI to stabilize
        time.sleep(2)
            
        return home_result

    def _tap_element(self, x: int, y: int, current_node_id: str, element_desc: Dict[str, Any] = None) -> Any:
        """
        Tap on an element at specified coordinates and log the action.
        
        Args:
            x: X coordinate to tap
            y: Y coordinate to tap
            current_node_id: ID of the current page node
            element_desc: Description of the element being tapped
            
        Returns:
            Result of the tap operation
        """
        # Log the tap action with timestamp and current node ID
        element_info = f" ({element_desc})" if element_desc else ""
        self._log(f"Tapping at ({x}, {y}){element_info}", 
                  node_id=current_node_id, namespace="TAP_ELEMENT")
        
        # Perform the tap action
        tap_result = self.session.mobile.tap(x=x, y=y)
        
        # Wait for UI to stabilize
        time.sleep(2)
        
        # Log the result
        if tap_result.success:
            self._log(f"Tap successful at ({x}, {y})", 
                      node_id=current_node_id, namespace="TAP_ELEMENT", level="INFO")
        else:
            self._log(f"Tap failed at ({x}, {y}): {tap_result.error_message}", 
                      node_id=current_node_id, namespace="TAP_ELEMENT", level="ERROR")
            
        return tap_result

    def _find_path_from_root(self, target_node_id: str) -> List[Tuple[str, Dict[str, Any]]]:
        """
        Find path from root to target node.
        
        Args:
            target_node_id: Target node ID
            
        Returns:
            List of (node_id, action) tuples representing path from root to target
        """
        # First try to use cached path
        if target_node_id in self._root_paths:
            return self._root_paths[target_node_id]
        
        # Build reverse mapping from edges
        reverse_edges = {}  # node_id -> list of (source_node_id, action)
        for edge in self.processed_transitions:
            if isinstance(edge, UIEdge):
                if edge.target_id not in reverse_edges:
                    reverse_edges[edge.target_id] = []
                reverse_edges[edge.target_id].append((edge.source_id, edge.action))
            elif isinstance(edge, tuple) and len(edge) >= 2:
                # Handle tuple format (source_id, target_id, ...)
                target_id = edge[1]
                if target_id not in reverse_edges:
                    reverse_edges[target_id] = []
                # We don't have the action in this case, so use a placeholder
                reverse_edges[target_id].append((edge[0], {}))
        
        # Reconstruct path from target to root using BFS
        path = []
        current_id = target_node_id
        
        # Limit iterations to prevent infinite loops
        max_iterations = 50
        iterations = 0
        
        visited = set()
        
        while current_id != self._root_node_id and current_id in reverse_edges and iterations < max_iterations:
            if current_id in visited:
                # Detected cycle, break
                break
            visited.add(current_id)
            
            # For now, take the first parent (in the future, we might want to be more sophisticated)
            if reverse_edges[current_id]:
                source_id, action = reverse_edges[current_id][0]
                path.append((current_id, action))
                current_id = source_id
            else:
                break
            iterations += 1
        
        # If we reached the root, reverse the path and return it
        if current_id == self._root_node_id:
            path.reverse()
            # Cache this path for future use
            self._root_paths[target_node_id] = path
            return path
        else:
            # No path found from root
            return []

    def _navigate_from_root(self, target_node_id: str):
        """
        Navigate from root to target node by replaying the path.
        
        Args:
            target_node_id: Target node ID to navigate to
        """
        self._log(f"Navigating from root to node {target_node_id[:8]}", 
                  node_id=target_node_id, namespace="NAVIGATE_FROM_ROOT")
        
        # First, go back to home screen
        home_result = self._go_to_home_screen(current_node_id=target_node_id)
        if not home_result.success:
            self._log(f"Failed to go home: {home_result.error_message}", 
                      node_id=target_node_id, namespace="NAVIGATE_FROM_ROOT", level="ERROR")
        
        # Find path from root to target
        path = self._find_path_from_root(target_node_id)
        self._log(f"Found path with {len(path)} steps to target node", 
                  node_id=target_node_id, namespace="NAVIGATE_FROM_ROOT")
        
        # Replay path
        self._replay_path(path)

    def _replay_path(self, path: List[Tuple[str, Dict[str, Any]]]):
        """
        Replay a sequence of actions to navigate along a path.
        
        Args:
            path: List of (node_id, action) tuples representing path to replay
        """
        self._log(f"Starting to replay path with {len(path)} steps", namespace="REPLAY_PATH")
        
        # Replay path
        success_count = 0
        for i, (node_id, action) in enumerate(path):
            self._log(f"Step {i+1}/{len(path)}: Navigating to node {node_id[:8]}", 
                      node_id=node_id, namespace="REPLAY_PATH")
            x = action.get('tap_x')
            y = action.get('tap_y')
            
            if x is not None and y is not None:
                tap_result = self._tap_element(x=x, y=y, current_node_id=node_id, element_desc=action)
                if not tap_result.success:
                    self._log(f"Tap failed during navigation: {tap_result.error_message}", 
                              node_id=node_id, namespace="REPLAY_PATH", level="ERROR")
                else:
                    success_count += 1
                    self._log(f"Step {i+1}/{len(path)}: Successfully tapped at ({x}, {y})", 
                              node_id=node_id, namespace="REPLAY_PATH", level="INFO")
            else:
                self._log(f"Invalid tap coordinates in action: {action}", 
                          node_id=node_id, namespace="REPLAY_PATH", level="ERROR")
        
        self._log(f"Finished replaying path. Successfully executed {success_count}/{len(path)} steps", 
                  namespace="REPLAY_PATH", level="INFO")

    def _add_swipe_subpage_to_graph(self, graph: UITransitionGraph, pre_page_node_id: str, 
                                   sub_page_node_id: str, sub_page_node_elements: List, 
                                   swipe_action_desc: Dict[str, Any]) -> bool:
        """
        Add a swipe sub-page to the graph and create relationship with previous page.
        
        Args:
            graph: The graph being built
            pre_page_node_id: ID of the previous page node
            sub_page_node_id: ID of the new sub-page node
            sub_page_node_elements: UI elements of the new sub-page
            swipe_action_desc: Description of the swipe action
            
        Returns:
            True if successfully added to graph, False otherwise
        """
        # Create edge for swipe transition
        swipe_edge = UIEdge(
            source_id=pre_page_node_id,
            target_id=sub_page_node_id,
            action=swipe_action_desc,
            transition_type="swipe"
        )
        
        # Check if we've already processed this swipe transition
        transition_key = (pre_page_node_id, sub_page_node_id, "swipe")
        if transition_key in self.processed_transitions:
            # Already processed, skip
            return False
            
        # Add edge to graph and mark as processed
        graph.edges.append(swipe_edge)
        self.processed_transitions.add(transition_key)
        
        # Update path caches
        if sub_page_node_id not in self._node_paths:
            self._node_paths[sub_page_node_id] = {}
        self._node_paths[sub_page_node_id][pre_page_node_id] = [(sub_page_node_id, swipe_action_desc)]
        
        if pre_page_node_id in self._root_paths:
            self._root_paths[sub_page_node_id] = self._root_paths[pre_page_node_id] + [(sub_page_node_id, swipe_action_desc)]
        else:
            self._root_paths[sub_page_node_id] = [(sub_page_node_id, swipe_action_desc)]
        
        # Create new path by extending the current node's path
        new_path = []
        if pre_page_node_id in self.visited_pages and self.visited_pages[pre_page_node_id].paths:
            # Extend each existing path of the current node
            for path in self.visited_pages[pre_page_node_id].paths:
                new_path.append(path + [(sub_page_node_id, swipe_action_desc)])
        else:
            # This is a direct child of root, create new path
            if self._root_node_id:
                new_path = [[(self._root_node_id, None)] + [(sub_page_node_id, swipe_action_desc)]]
            else:
                new_path = [[(sub_page_node_id, swipe_action_desc)]]
        
        # Create new node for the sub-page
        new_node = UINode(
            node_id=sub_page_node_id,
            elements=sub_page_node_elements,
            paths=new_path
        )
        
        # Add node to graph and visited pages
        graph.nodes[sub_page_node_id] = new_node
        self.visited_pages[sub_page_node_id] = new_node
        
        # Notify visualizer of the new node
        if self.visualizer_callback:
            try:
                self.visualizer_callback(graph)
            except Exception as e:
                self._log(f"Warning: Failed to notify visualizer: {e}", 
                          node_id=sub_page_node_id, namespace="ADD_SWIPE_PAGE", level="ERROR")
        
        # Save current state of the graph
        if self.output_file:
            self._save_graph_to_file(
                graph,
                self.output_file,
                node_id=sub_page_node_id,
                context="new swipe node added"
            )
            
        return True