from abc import ABC
from typing import Optional, List, Union, Any, Dict
import typing
import asyncio
from playwright.async_api import Page, Locator, expect, ElementHandle, TimeoutError
import logging
import time
from urllib.parse import urljoin

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class BasePage(ABC):
    """
    Base Page Object class providing common functionality for all page objects.

    This class serves as the foundation for all page objects and includes:
    - Common navigation methods
    - Element interaction utilities
    - Wait strategies and timeout handling
    - Screenshot and debugging capabilities
    - Error handling and logging
    """

    def __init__(self, page: Page, base_url: str = ""):
        """
        Initialize the base page object.

        Args:
            page: Playwright async Page object
            base_url: Base URL for the application (optional)
        """
        self.page = page
        self.base_url = base_url
        self.default_timeout = 30000  # 30 seconds
        self.short_timeout = 5000     # 5 seconds
        self.long_timeout = 60000     # 60 seconds

    # ==================== NAVIGATION METHODS ====================

    async def navigate_to(self, url: str, wait_until: typing.Optional[
        typing.Literal["commit", "domcontentloaded", "load", "networkidle"]
    ] = None) -> 'BasePage':
        """
        Navigate to a specific URL.

        Args:
            url: URL to navigate to (can be relative or absolute)
            wait_until: When to consider navigation complete

        Returns:
            Self for method chaining
        """
        try:
            full_url = urljoin(self.base_url, url) if self.base_url and not url.startswith(
                'http') else url
            logger.info(f"Navigating to: {full_url}")
            await self.page.goto(full_url, wait_until=wait_until,
                                timeout=self.default_timeout)
            return self
        except Exception as e:
            logger.error(f"Failed to navigate to {url}: {str(e)}")
            raise

    async def refresh_page(self, wait_until: typing.Optional[
        typing.Literal["commit", "domcontentloaded", "load", "networkidle"]
    ] = None) -> 'BasePage':
        """Refresh the current page."""
        try:
            logger.info("Refreshing page")
            await self.page.reload(wait_until=wait_until,
                                  timeout=self.default_timeout)
            return self
        except Exception as e:
            logger.error(f"Failed to refresh page: {str(e)}")
            raise

    async def go_back(self) -> 'BasePage':
        """Navigate back to the previous page."""
        try:
            logger.info("Navigating back")
            await self.page.go_back(wait_until="domcontentloaded",
                                   timeout=self.default_timeout)
            return self
        except Exception as e:
            logger.error(f"Failed to go back: {str(e)}")
            raise

    # ==================== PAGE INFORMATION METHODS ====================

    async def get_title(self) -> str:
        """Get the page title."""
        return await self.page.title()

    def get_url(self) -> str:
        """Get the current page URL."""
        return self.page.url

    async def get_page_source(self) -> str:
        """Get the page source HTML."""
        return await self.page.content()

    # ==================== WAIT METHODS ====================

    async def wait_for_page_load(self, state: typing.Optional[
        typing.Literal['domcontentloaded', 'load', 'networkidle']
    ] = None, timeout: Optional[int] = None) -> 'BasePage':
        """
        Wait for the page to fully load.

        Args:
            state: Load state to wait for ('load', 'domcontentloaded', 'networkidle')
            timeout: Timeout in milliseconds

        Returns:
            Self for method chaining
        """
        wait_timeout = timeout or self.default_timeout
        try:
            logger.info(f"Waiting for page load state: {state}")
            await self.page.wait_for_load_state(state, timeout=wait_timeout)
            return self
        except TimeoutError:
            logger.warning(f"Timeout waiting for page load state: {state}")
            raise

    async def wait_for_element_visible(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> 'BasePage':
        """
        Wait for element to be visible.

        Args:
            locator: Element locator (string or Locator object)
            timeout: Timeout in milliseconds

        Returns:
            Self for method chaining
        """
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        try:
            await expect(element).to_be_visible(timeout=wait_timeout)
            return self
        except Exception as e:
            logger.error(f"Element not visible: {locator} - {str(e)}")
            raise

    async def wait_for_element_hidden(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> 'BasePage':
        """
        Wait for element to be hidden.

        Args:
            locator: Element locator (string or Locator object)
            timeout: Timeout in milliseconds

        Returns:
            Self for method chaining
        """
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        try:
            await expect(element).to_be_hidden(timeout=wait_timeout)
            return self
        except Exception as e:
            logger.error(f"Element still visible: {locator} - {str(e)}")
            raise

    async def wait_for_element_enabled(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> 'BasePage':
        """Wait for element to be enabled."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        await expect(element).to_be_enabled(timeout=wait_timeout)
        return self

    async def wait_for_text(self, locator: Union[str, Locator], text: str, timeout: Optional[int] = None) -> 'BasePage':
        """Wait for element to contain specific text."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        await expect(element).to_contain_text(text, timeout=wait_timeout)
        return self

    # ==================== ELEMENT INTERACTION METHODS ====================

    async def click(self, locator: Union[str, Locator], timeout: Optional[int] = None, force: bool = False) -> 'BasePage':
        """
        Click on an element.

        Args:
            locator: Element locator
            timeout: Timeout in milliseconds
            force: Whether to force the click

        Returns:
            Self for method chaining
        """
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        try:
            logger.info(f"Clicking element: {locator}")
            await element.click(timeout=wait_timeout, force=force)
            return self
        except Exception as e:
            logger.error(f"Failed to click element: {locator} - {str(e)}")
            raise

    async def double_click(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> 'BasePage':
        """Double click on an element."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        try:
            logger.info(f"Double clicking element: {locator}")
            await element.dblclick(timeout=wait_timeout)
            return self
        except Exception as e:
            logger.error(
                f"Failed to double click element: {locator} - {str(e)}")
            raise

    async def right_click(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> 'BasePage':
        """Right click on an element."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        try:
            logger.info(f"Right clicking element: {locator}")
            await element.click(button="right", timeout=wait_timeout)
            return self
        except Exception as e:
            logger.error(
                f"Failed to right click element: {locator} - {str(e)}")
            raise

    async def type_text(self, locator: Union[str, Locator], text: str, clear: bool = True, timeout: Optional[int] = None) -> 'BasePage':
        """
        Type text into an input field.

        Args:
            locator: Element locator
            text: Text to type
            clear: Whether to clear existing text first
            timeout: Timeout in milliseconds

        Returns:
            Self for method chaining
        """
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        try:
            logger.info(f"Typing text into element: {locator}")
            if clear:
                await element.clear(timeout=wait_timeout)
            await element.type(text, timeout=wait_timeout)
            return self
        except Exception as e:
            logger.error(
                f"Failed to type text into element: {locator} - {str(e)}")
            raise

    async def fill_text(self, locator: Union[str, Locator], text: str, timeout: Optional[int] = None) -> 'BasePage':
        """Fill text into an input field (faster than type)."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        try:
            logger.info(f"Filling text into element: {locator}")
            await element.fill(text, timeout=wait_timeout)
            return self
        except Exception as e:
            logger.error(
                f"Failed to fill text into element: {locator} - {str(e)}")
            raise

    async def clear_text(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> 'BasePage':
        """Clear text from an input field."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        await element.clear(timeout=wait_timeout)
        return self

    async def select_option(self, locator: Union[str, Locator], value: Union[str, List[str]], timeout: Optional[int] = None) -> 'BasePage':
        """Select option(s) from a dropdown."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        try:
            logger.info(f"Selecting option(s) {value} from element: {locator}")
            await element.select_option(value, timeout=wait_timeout)
            return self
        except Exception as e:
            logger.error(
                f"Failed to select option from element: {locator} - {str(e)}")
            raise

    async def check_checkbox(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> 'BasePage':
        """Check a checkbox."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        await element.check(timeout=wait_timeout)
        return self

    async def uncheck_checkbox(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> 'BasePage':
        """Uncheck a checkbox."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        await element.uncheck(timeout=wait_timeout)
        return self

    async def hover(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> 'BasePage':
        """Hover over an element."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        await element.hover(timeout=wait_timeout)
        return self

    # ==================== ELEMENT QUERY METHODS ====================

    async def get_text(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> str:
        """Get text content of an element."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        text_content = await element.text_content(timeout=wait_timeout)
        return text_content or ""

    async def get_inner_text(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> str:
        """Get inner text of an element."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        return await element.inner_text(timeout=wait_timeout)

    async def get_attribute(self, locator: Union[str, Locator], attribute: str, timeout: Optional[int] = None) -> Optional[str]:
        """Get attribute value of an element."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        return await element.get_attribute(attribute, timeout=wait_timeout)

    async def get_value(self, locator: Union[str, Locator], timeout: Optional[int] = None) -> str:
        """Get value of an input element."""
        wait_timeout = timeout or self.default_timeout
        element = self._get_locator(locator)
        return await element.input_value(timeout=wait_timeout)

    async def is_visible(self, locator: Union[str, Locator]) -> bool:
        """Check if element is visible."""
        element = self._get_locator(locator)
        return await element.is_visible()

    async def is_enabled(self, locator: Union[str, Locator]) -> bool:
        """Check if element is enabled."""
        element = self._get_locator(locator)
        return await element.is_enabled()

    async def is_checked(self, locator: Union[str, Locator]) -> bool:
        """Check if checkbox/radio is checked."""
        element = self._get_locator(locator)
        return await element.is_checked()

    async def element_count(self, locator: Union[str, Locator]) -> int:
        """Get count of elements matching the locator."""
        element = self._get_locator(locator)
        return await element.count()

    # ==================== SCROLL METHODS ====================

    async def scroll_to_element(self, locator: Union[str, Locator]) -> 'BasePage':
        """Scroll to a specific element."""
        element = self._get_locator(locator)
        await element.scroll_into_view_if_needed()
        return self

    async def scroll_to_top(self) -> 'BasePage':
        """Scroll to the top of the page."""
        await self.page.evaluate("window.scrollTo(0, 0)")
        return self

    async def scroll_to_bottom(self) -> 'BasePage':
        """Scroll to the bottom of the page."""
        await self.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        return self

    async def scroll_by_pixels(self, x: int, y: int) -> 'BasePage':
        """Scroll by specific pixel amounts."""
        await self.page.evaluate(f"window.scrollBy({x}, {y})")
        return self

    # ==================== SCREENSHOT AND DEBUGGING ====================

    async def take_screenshot(self, name: str = "screenshot", full_page: bool = True) -> bytes:
        """Take a screenshot of the current page."""
        logger.info(f"Taking screenshot: {name}")
        return await self.page.screenshot(path=f"{name}.png", full_page=full_page)

    async def take_element_screenshot(self, locator: Union[str, Locator], name: str = "element_screenshot") -> bytes:
        """Take a screenshot of a specific element."""
        element = self._get_locator(locator)
        logger.info(f"Taking element screenshot: {name}")
        return await element.screenshot(path=f"{name}.png")

    async def highlight_element(self, locator: Union[str, Locator]) -> 'BasePage':
        """Highlight an element for debugging purposes."""
        element = self._get_locator(locator)
        element_handle = await element.element_handle()
        await self.page.evaluate("""
            element => {
                element.style.border = '3px solid red';
                element.style.backgroundColor = 'yellow';
            }
        """, element_handle)
        return self

    # ==================== KEYBOARD AND MOUSE ACTIONS ====================

    async def press_key(self, key: str) -> 'BasePage':
        """Press a keyboard key."""
        await self.page.keyboard.press(key)
        return self

    async def press_key_combination(self, *keys: str) -> 'BasePage':
        """Press a combination of keys."""
        combination = "+".join(keys)
        await self.page.keyboard.press(combination)
        return self

    # ==================== WINDOW AND FRAME METHODS ====================

    def switch_to_frame(self, frame_locator: str) -> 'BasePage':
        """Switch to a frame (note: frames are handled differently in Playwright)."""
        # In Playwright, you work with frames directly using frame_locator
        logger.info(f"Working with frame: {frame_locator}")
        return self

    def get_frame_locator(self, frame_selector: str):
        """Get a frame locator for working with frames."""
        return self.page.frame_locator(frame_selector)

    # ==================== UTILITY METHODS ====================

    async def sleep(self, seconds: float) -> 'BasePage':
        """Sleep for specified seconds (use sparingly)."""
        logger.warning(
            f"Using sleep for {seconds} seconds - consider using explicit waits instead")
        await asyncio.sleep(seconds)
        return self

    async def execute_javascript(self, script: str, *args) -> Any:
        """Execute JavaScript code."""
        return await self.page.evaluate(script, *args)

    async def add_style_tag(self, content: str) -> 'BasePage':
        """Add a style tag to the page."""
        await self.page.add_style_tag(content=content)
        return self

    async def add_script_tag(self, content: str) -> 'BasePage':
        """Add a script tag to the page."""
        await self.page.add_script_tag(content=content)
        return self

    # ==================== PRIVATE HELPER METHODS ====================

    def _get_locator(self, locator: Union[str, Locator]) -> Locator:
        """Convert string locator to Locator object if needed."""
        if isinstance(locator, str):
            return self.page.locator(locator)
        return locator

    def _log_action(self, action: str, locator: typing.Optional[Union[str, Locator]]) -> None:
        """Log user actions for debugging."""
        if locator:
            logger.info(f"Action: {action} on element: {locator}")
        else:
            logger.info(f"Action: {action}")