"""
Page Object Model for Family Safety Agent UI interactions.

This module provides a structured approach to page interactions using the Page Object Model pattern.
Each page object encapsulates selectors and interaction logic for specific UI components.
"""

import asyncio
import re
from typing import Optional, Dict, Any, List, Tuple
from playwright.async_api import Page, Frame, ElementHandle
from ..core.logging_config import log_info, log_debug, log_warning
from .frames_handler import FramesHandler
from .element_extractor import ElementExtractor


class BasePage:
    """Base page object with common utilities for all pages."""
    
    def __init__(self, page: Page, frame: Frame = None, scope: ElementHandle = None):
        self.page = page
        self.main_frame = page.main_frame
        self.current_frame = frame or page.main_frame
        self.scope = scope  # Optional dialog/modal scope
    
    async def wait_for_element_attached(self, selector: str, timeout_ms: int = 2000) -> Optional[ElementHandle]:
        """Wait for element to be attached to DOM."""
        try:
            frame = self.scope or self.current_frame
            await frame.wait_for_selector(selector, timeout=timeout_ms)
            return await frame.query_selector(selector)
        except Exception as e:
            log_debug(f"Element wait failed: {e}")
            return None
    
    async def wait_for_element_visible(self, selector: str, timeout_ms: int = 2000) -> Optional[ElementHandle]:
        """Wait for element to be visible."""
        try:
            frame = self.scope or self.current_frame
            el = await frame.query_selector(selector)
            if el:
                await el.wait_for_element_state("visible", timeout=timeout_ms)
            return el
        except Exception as e:
            log_debug(f"Element visibility wait failed: {e}")
            return None
    
    async def is_element_clickable(self, element: ElementHandle) -> bool:
        """Check if element is clickable."""
        try:
            # Check for disabled attribute
            disabled = await element.get_attribute("disabled")
            aria_disabled = await element.get_attribute("aria-disabled")
            
            if disabled or (aria_disabled and aria_disabled.lower() == "true"):
                return False
            
            # Check visibility using ElementExtractor
            return await ElementExtractor.check_element_visibility(element)
        except Exception:
            return False
    
    async def scroll_into_view(self, element: ElementHandle):
        """Scroll element into view if needed."""
        try:
            await element.scroll_into_view_if_needed()
        except Exception as e:
            log_debug(f"Scroll into view failed: {e}")
    
    async def get_element_info(self, element: ElementHandle) -> Dict[str, Any]:
        """Extract debug information from element using ElementExtractor."""
        try:
            return await ElementExtractor.extract_element_info(element, "element")
        except Exception as e:
            log_debug(f"Element info extraction failed: {e}")
            return {}
    
    @staticmethod
    def _get_value_variants(value: str) -> List[str]:
        """Generate value variants (with/without trailing period)."""
        variants = {str(value).strip()}
        
        if value.endswith("."):
            variants.add(value.rstrip("."))
        else:
            variants.add(value + ".")
        
        return list(variants)


class PageManager(BasePage):
    """
    Unified Page Manager combining all page object functionalities.
    
    This class integrates element finding, clicking, form interactions,
    toggle handling, navigation, dialog management, scrolling, and link navigation.
    """
    
    def __init__(self, page: Page, frame: Frame = None, scope: ElementHandle = None):
        super().__init__(page, frame, scope)
    
    # ==================== ELEMENT FINDER METHODS ====================
    
    async def find_by_aria_label(self, label: str) -> Optional[ElementHandle]:
        """Find element by aria-label with variants (with/without period)."""
        variants = self._get_value_variants(label)
        
        for variant in variants:
            candidates = []
            frame = self.scope or self.current_frame
            
            try:
                el = await frame.query_selector(f"[aria-label='{variant}']")
                if el:
                    candidates.append(el)
            except Exception:
                pass
            
            try:
                el = await frame.query_selector(f"button[aria-label='{variant}']")
                if el:
                    candidates.append(el)
            except Exception:
                pass
            
            try:
                el = await frame.query_selector(f"[role='button'][aria-label='{variant}']")
                if el:
                    candidates.append(el)
            except Exception:
                pass
            
            try:
                loc = self.page.locator(f":scope >> text-is('{variant}')")
                handle = await loc.element_handle()
                if handle:
                    candidates.append(handle)
            except Exception:
                pass
            
            for candidate in candidates:
                if await self.is_element_clickable(candidate):
                    return candidate
        
        return None
    
    async def find_by_text(self, text: str, selector: str = None) -> Optional[ElementHandle]:
        """Find element by text content."""
        variants = self._get_value_variants(text)
        frame = self.scope or self.current_frame
        
        for variant in variants:
            candidates = []
            
            for selector_pattern in [
                f"button:has-text('{variant}')",
                f"[role='button']:has-text('{variant}')",
                f"[data-testid*='button']:has-text('{variant}')",
                f"[onclick]:has-text('{variant}')",
                f":text-is('{variant}')"
            ]:
                try:
                    el = await frame.query_selector(selector_pattern)
                    if el:
                        candidates.append(el)
                except Exception:
                    pass
            
            for candidate in candidates:
                if await self.is_element_clickable(candidate):
                    return candidate
        
        return None
    
    async def find_by_id(self, element_id: str) -> Optional[ElementHandle]:
        """Find element by ID."""
        try:
            frame = self.scope or self.current_frame
            return await frame.query_selector(f"#{element_id}")
        except Exception:
            return None
    
    async def find_by_placeholder(self, placeholder: str) -> Optional[ElementHandle]:
        """Find element by placeholder."""
        try:
            frame = self.scope or self.current_frame
            return await frame.query_selector(f"[placeholder='{placeholder}']")
        except Exception:
            return None
    
    async def find_by_xpath(self, xpath: str) -> Optional[ElementHandle]:
        """Find element by XPath."""
        try:
            frame = self.scope or self.current_frame
            return await frame.query_selector(f"xpath={xpath}")
        except Exception:
            return None
    
    async def find_in_frames(self, locator: str, value: str) -> Tuple[Optional[ElementHandle], Optional[Frame]]:
        """Search across all frames for an element."""
        frames = FramesHandler.collect_all_frames(self.page)
        
        for frame in frames:
            el = None
            
            if locator == "aria-label":
                el = await self._find_aria_label_in_frame(frame, value)
            elif locator == "text":
                el = await self._find_text_in_frame(frame, value)
            elif locator == "id":
                el = await self._find_id_in_frame(frame, value)
            elif locator == "placeholder":
                el = await self._find_placeholder_in_frame(frame, value)
            elif locator == "xpath":
                el = await self._find_xpath_in_frame(frame, value)
            
            if el:
                return el, frame
        
        return None, None
    
    async def _find_aria_label_in_frame(self, frame: Frame, label: str) -> Optional[ElementHandle]:
        """Find aria-label in specific frame."""
        variants = self._get_value_variants(label)
        for variant in variants:
            try:
                el = await frame.query_selector(f"button[aria-label='{variant}']")
                if el and await self.is_element_clickable(el):
                    return el
            except Exception:
                pass
        return None
    
    async def _find_text_in_frame(self, frame: Frame, text: str) -> Optional[ElementHandle]:
        """Find text in specific frame."""
        variants = self._get_value_variants(text)
        for variant in variants:
            try:
                el = await frame.query_selector(f"button:has-text('{variant}')")
                if el and await self.is_element_clickable(el):
                    return el
            except Exception:
                pass
        return None
    
    async def _find_id_in_frame(self, frame: Frame, element_id: str) -> Optional[ElementHandle]:
        """Find ID in specific frame."""
        try:
            return await frame.query_selector(f"#{element_id}")
        except Exception:
            return None
    
    async def _find_placeholder_in_frame(self, frame: Frame, placeholder: str) -> Optional[ElementHandle]:
        """Find placeholder in specific frame."""
        try:
            return await frame.query_selector(f"[placeholder='{placeholder}']")
        except Exception:
            return None
    
    async def _find_xpath_in_frame(self, frame: Frame, xpath: str) -> Optional[ElementHandle]:
        """Find XPath in specific frame."""
        try:
            return await frame.query_selector(f"xpath={xpath}")
        except Exception:
            return None
    
    # ==================== CLICKABLE ELEMENT METHODS ====================
    
    async def click(self, element: ElementHandle, action_type: str = "click") -> bool:
        """Click element with fallback strategies."""
        max_attempts = 3
        
        for attempt in range(max_attempts):
            try:
                await self.scroll_into_view(element)
                await self.page.wait_for_timeout(250)
                
                try:
                    if action_type == "dblclick":
                        await element.dblclick()
                    else:
                        await element.click()
                    return True
                except Exception as e1:
                    try:
                        bbox = await element.bounding_box()
                        if bbox:
                            x = bbox["x"] + bbox["width"] / 2
                            y = bbox["y"] + bbox["height"] / 2
                            
                            if action_type == "dblclick":
                                await self.page.mouse.dblclick(x, y)
                            else:
                                await self.page.mouse.click(x, y)
                            return True
                    except Exception:
                        pass
                    
                    try:
                        if action_type == "dblclick":
                            await element.evaluate(
                                "(node) => { node.click(); node.click(); }"
                            )
                        else:
                            await element.evaluate("(node) => node.click()")
                        return True
                    except Exception:
                        raise e1
            
            except Exception as e:
                if attempt < max_attempts - 1:
                    await self.page.wait_for_timeout(1000)
                    continue
                
                try:
                    await element.click(force=True)
                    return True
                except Exception:
                    log_debug(f"Click failed after {max_attempts} attempts: {e}")
                    return False
        
        return False
    
    # ==================== FORM PAGE METHODS ====================
    
    async def type_text(self, element: ElementHandle, text: str) -> bool:
        """Type text with fallback strategies."""
        max_attempts = 3
        
        for attempt in range(max_attempts):
            try:
                await self.scroll_into_view(element)
                
                try:
                    await element.fill(text)
                    return True
                except Exception as e1:
                    try:
                        await element.focus()
                        await self.page.keyboard.type(text, delay=20)
                        return True
                    except Exception:
                        try:
                            await element.evaluate(
                                "(el, v) => {"
                                "  if ('value' in el) {"
                                "    el.value = v;"
                                "    el.dispatchEvent(new Event('input', { bubbles: true }));"
                                "    el.dispatchEvent(new Event('change', { bubbles: true }));"
                                "  }"
                                "}",
                                text
                            )
                            return True
                        except Exception:
                            raise e1
            
            except Exception as e:
                if attempt < max_attempts - 1:
                    await self.page.wait_for_timeout(1000)
                    continue
                
                log_debug(f"Type failed after {max_attempts} attempts: {e}")
                return False
        
        return False
    
    # ==================== TOGGLE PAGE METHODS ====================
    
    async def is_toggle_on(self, element: ElementHandle) -> Optional[bool]:
        """Determine if toggle is ON or OFF."""
        try:
            aria_checked = await element.get_attribute("aria-checked")
            if aria_checked and aria_checked.lower() in ("true", "false"):
                return aria_checked.lower() == "true"
            
            aria_pressed = await element.get_attribute("aria-pressed")
            if aria_pressed and aria_pressed.lower() in ("true", "false"):
                return aria_pressed.lower() == "true"
            
            data_state = await element.get_attribute("data-state")
            if data_state and data_state.lower() in ("on", "off"):
                return data_state.lower() == "on"
            
            tag = await element.evaluate("el => el.tagName.toLowerCase()")
            if tag == "input":
                input_type = await element.get_attribute("type")
                if input_type == "checkbox":
                    return await element.evaluate("el => !!el.checked")
            
            return None
        except Exception as e:
            log_debug(f"Could not determine toggle state: {e}")
            return None
    
    async def toggle_switch(self, element: ElementHandle, desired_on: bool) -> bool:
        """Toggle switch to desired state."""
        try:
            current = await self.is_toggle_on(element)
            
            if current == desired_on:
                await self._click_toggle(element)
                await self.page.wait_for_timeout(300)
                await self._click_toggle(element)
                await self.page.wait_for_timeout(300)
                return True
            else:
                return await self._click_toggle(element)
        except Exception as e:
            log_debug(f"Toggle switch failed: {e}")
            return False
    
    async def _click_toggle(self, element: ElementHandle) -> bool:
        """Click toggle element."""
        return await self.click(element)
    
    # ==================== NAVIGATION PAGE METHODS ====================
    
    async def navigate_to_url(self, url: str) -> bool:
        """Navigate to URL."""
        try:
            await self.page.goto(url, wait_until="load")
            return True
        except Exception as e:
            log_debug(f"Navigation failed: {e}")
            return False
    
    async def go_back(self) -> bool:
        """Go back in browser history."""
        try:
            resp = await self.page.go_back(wait_until="load")
            if resp is None:
                try:
                    await self.page.keyboard.press("Alt+Left")
                except Exception:
                    pass
            return True
        except Exception as e:
            log_debug(f"Go back failed: {e}")
            return False
    
    # ==================== DIALOG PAGE METHODS ====================
    
    async def detect_modal(self) -> bool:
        """Detect if modal dialog is present."""
        try:
            dialogs = await self.page.query_selector_all(
                "[role='dialog'], [aria-modal='true'], .fui-DialogBody, "
                "[data-tid='app-details-northstar-dialog'], #APP_DETAILS_DIALOG_ROOT_ID"
            )
            
            for dialog in dialogs:
                try:
                    bbox = await dialog.bounding_box()
                    if bbox and bbox.get("width", 0) > 0 and bbox.get("height", 0) > 0:
                        return True
                except Exception:
                    pass
            
            return False
        except Exception:
            return False
    
    async def get_modal_root(self) -> Optional[ElementHandle]:
        """Get the modal/dialog root element."""
        try:
            frames = FramesHandler.collect_all_frames(self.page)
            for frame in frames:
                try:
                    dialogs = await frame.query_selector_all(
                        "[role='dialog'], [aria-modal='true'], [data-tid='app-details-northstar-dialog']"
                    )
                    
                    for dialog in dialogs:
                        try:
                            bbox = await dialog.bounding_box()
                            if bbox and bbox.get("width", 0) > 0 and bbox.get("height", 0) > 0:
                                return dialog
                        except Exception:
                            pass
                except Exception:
                    pass
            
            return None
        except Exception:
            return None
    
    async def wait_for_modal(self, timeout_ms: int = 3000) -> bool:
        """Wait for modal to appear."""
        start = asyncio.get_event_loop().time()
        
        while (asyncio.get_event_loop().time() - start) * 1000 < timeout_ms:
            if await self.detect_modal():
                return True
            await self.page.wait_for_timeout(100)
        
        return False
    
    # ==================== SCROLL PAGE METHODS ====================
    
    async def scroll_to_element(self, element: ElementHandle) -> bool:
        """Scroll element into view."""
        try:
            await self.scroll_into_view(element)
            return True
        except Exception as e:
            log_debug(f"Scroll failed: {e}")
            return False
    
    async def scroll_to_bottom(self) -> bool:
        """Scroll page to bottom."""
        try:
            await self.page.evaluate("() => window.scrollTo(0, document.body.scrollHeight)")
            return True
        except Exception as e:
            log_debug(f"Scroll to bottom failed: {e}")
            return False
    
    # ==================== LINK PAGE METHODS ====================
    
    async def click_link(self, element: ElementHandle) -> bool:
        """Click link with popup handling."""
        try:
            if not await self.click(element):
                return False
            
            await self._handle_popup()
            return True
        except Exception as e:
            log_debug(f"Link click failed: {e}")
            return False
    
    async def _handle_popup(self):
        """Handle popups from link clicks."""
        try:
            pages_before = len(self.page.context.pages)
            
            try:
                popup = await self.page.wait_for_event("popup", timeout=2000)
                if popup:
                    try:
                        await popup.wait_for_load_state("load")
                        target_url = popup.url
                        if target_url:
                            await self.page.goto(target_url, wait_until="load")
                    finally:
                        try:
                            await popup.close()
                        except Exception:
                            pass
            except Exception:
                pass
        except Exception:
            pass


# ==================== BACKWARD COMPATIBILITY CLASSES ====================

class ElementFinder(PageManager):
    """Backward compatibility: Element finding functionality."""
    pass


class ClickableElementPage(PageManager):
    """Backward compatibility: Click handling functionality."""
    pass


class FormPage(PageManager):
    """Backward compatibility: Form interaction functionality."""
    pass


class TogglePage(PageManager):
    """Backward compatibility: Toggle handling functionality."""
    pass


class NavigationPage(PageManager):
    """Backward compatibility: Navigation functionality."""
    pass


class DialogPage(PageManager):
    """Backward compatibility: Dialog/Modal functionality."""
    pass


class ScrollPage(PageManager):
    """Backward compatibility: Scrolling functionality."""
    pass


class LinkPage(PageManager):
    """Backward compatibility: Link navigation functionality."""
    pass
