"""
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 backend.core.logging_config import log_info, log_debug, log_warning
from backend.utils.frames_handler import collect_all_frames


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 bounding box
            bbox = await element.bounding_box()
            return bbox and bbox.get("width", 0) > 0 and bbox.get("height", 0) > 0
        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."""
        try:
            return {
                "tag": await element.evaluate("el => el.tagName.toLowerCase()"),
                "text": (await element.inner_text()).strip()[:100],
                "aria-label": await element.get_attribute("aria-label"),
                "id": await element.get_attribute("id"),
                "class": await element.get_attribute("class")
            }
        except Exception:
            return {}


class ElementFinder(BasePage):
    """Handles finding elements using multiple strategies."""
    
    def __init__(self, page: Page, frame: Frame = None, scope: ElementHandle = None):
        super().__init__(page, frame, scope)
        self.variant_patterns = []
    
    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
            
            # Exact match
            try:
                el = await frame.query_selector(f"[aria-label='{variant}']")
                if el:
                    candidates.append(el)
            except Exception:
                pass
            
            # Button with aria-label
            try:
                el = await frame.query_selector(f"button[aria-label='{variant}']")
                if el:
                    candidates.append(el)
            except Exception:
                pass
            
            # Role=button with aria-label
            try:
                el = await frame.query_selector(f"[role='button'][aria-label='{variant}']")
                if el:
                    candidates.append(el)
            except Exception:
                pass
            
            # Use Playwright's label locator
            try:
                loc = self.page.locator(f":scope >> text-is('{variant}')")
                handle = await loc.element_handle()
                if handle:
                    candidates.append(handle)
            except Exception:
                pass
            
            # Pick first visible candidate
            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 = []
            
            # Prefer buttons and interactive elements
            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 = 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
    
    @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 ClickableElementPage(BasePage):
    """Handles clicking on elements with multiple strategies."""
    
    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:
                # Scroll into view
                await self.scroll_into_view(element)
                await self.page.wait_for_timeout(250)
                
                # Standard click
                try:
                    if action_type == "dblclick":
                        await element.dblclick()
                    else:
                        await element.click()
                    return True
                except Exception as e1:
                    # Fallback to coordinate click
                    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
                    
                    # JavaScript click fallback
                    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
                
                # Last attempt: force click
                try:
                    await element.click(force=True)
                    return True
                except Exception:
                    log_debug(f"Click failed after {max_attempts} attempts: {e}")
                    return False
        
        return False


class FormPage(BasePage):
    """Handles form interactions (typing, filling)."""
    
    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:
                # Scroll into view
                await self.scroll_into_view(element)
                
                # Standard fill
                try:
                    await element.fill(text)
                    return True
                except Exception as e1:
                    # Fallback: Focus and keyboard type
                    try:
                        await element.focus()
                        await self.page.keyboard.type(text, delay=20)
                        return True
                    except Exception:
                        # JavaScript value setting
                        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


class TogglePage(BasePage):
    """Handles toggle switch interactions."""
    
    async def is_toggle_on(self, element: ElementHandle) -> Optional[bool]:
        """Determine if toggle is ON or OFF."""
        try:
            # aria-checked attribute
            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 attribute
            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 attribute
            data_state = await element.get_attribute("data-state")
            if data_state and data_state.lower() in ("on", "off"):
                return data_state.lower() == "on"
            
            # HTML checkbox
            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:
                # Already in desired state - bounce it
                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:
                # Not in desired state - toggle to it
                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."""
        clicker = ClickableElementPage(self.page, self.current_frame, self.scope)
        return await clicker.click(element)


class NavigationPage(BasePage):
    """Handles navigation actions."""
    
    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:
                # History empty, try keyboard shortcut
                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


class DialogPage(BasePage):
    """Handles modal dialog interactions."""
    
    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 = 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


class ScrollPage(BasePage):
    """Handles scrolling actions."""
    
    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


class LinkPage(BasePage):
    """Handles link navigation."""
    
    async def click_link(self, element: ElementHandle) -> bool:
        """Click link with popup handling."""
        try:
            # Click the link
            clicker = ClickableElementPage(self.page, self.current_frame, self.scope)
            if not await clicker.click(element):
                return False
            
            # Handle popup if it opens
            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)
            
            # Wait for popup
            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
