import sys
from typing import Any, Optional

from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.edge.options import Options as EdgeOptions
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.microsoft import EdgeChromiumDriverManager

from random_user_agent.user_agent import UserAgent
from random_user_agent.params import SoftwareName, OperatingSystem

import undetected_chromedriver as uc

import time


class Browser:
    """
    Wrapper around a Selenium WebDriver instance.

    Instantiation performs all driver setup. The created driver is available as
    the `driver` attribute. Call `close()` to properly shut down the browser.
    """

    def __init__(self, headless: bool = False) -> None:
        """
        Initialize the browser and create an underlying WebDriver.

        :param headless: Whether to run the browser in headless mode.
        """
        self.driver: Optional[webdriver.Chrome] = self._build_driver(headless=headless)

    def _build_driver(self, headless: bool) -> webdriver.Chrome:
        """
        Create a WebDriver instance with a random user agent and performance
        logging, trying Chrome first and falling back to Edge.
        """
        # Random realistic user-agent
        software_names = [SoftwareName.CHROME.value]
        operating_systems = [
            OperatingSystem.WINDOWS.value,
            OperatingSystem.MACOS.value,
            OperatingSystem.LINUX.value,
        ]
        rotator = UserAgent(
            software_names=software_names,
            operating_systems=operating_systems,
            limit=100,
        )
        user_agent = rotator.get_random_user_agent()

        # Choose Chrome (preferred) or Edge
        try:
            service = Service(ChromeDriverManager().install())
            driver_kind = "Chrome"
            options: Any = ChromeOptions()
        except Exception:
            service = Service(EdgeChromiumDriverManager().install())
            driver_kind = "Edge"
            options = EdgeOptions()

        # Common options
        if headless:
            options.add_argument("--headless=new")
            options.add_argument("--disable-gpu")
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        options.add_argument(f"user-agent={user_agent}")

        # Enable performance logging to inspect network requests
        perf_prefs = {"enableNetwork": True}
        options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
        options.set_capability("goog:chromeOptions", {"perfLoggingPrefs": perf_prefs})

        try:
            if driver_kind == "Chrome":
                driver = uc.Chrome(service=service, options=options)
            else:
                driver = webdriver.Edge(service=service, options=options)
        except WebDriverException as exc:
            print("Failed to start WebDriver (Chrome/Edge).", file=sys.stderr)
            raise exc

        return driver

    def close(self) -> None:
        """Close the underlying WebDriver if it is still running."""
        if self.driver is not None:
            try:
                self.driver.quit()
            except Exception:
                # We deliberately swallow any exception here, as we're shutting down.
                pass
            finally:
                self.driver = None

    def get_xpath(self, xpath, source=None, all=False, parent=None):
        if source is None:
            source = self.driver
        try:
            if parent is not None:
                source = parent
            element_present = EC.presence_of_element_located((By.XPATH, xpath))
            WebDriverWait(source, 10).until(element_present)
            if all:
                return source.find_elements(by=By.XPATH, value=xpath)
            else:
                return source.find_element(by=By.XPATH, value=xpath)
        except Exception as e:
            print("Timed out waiting for page to load", e)
        return None

    def get_id(self, id, source=None, all=False, parent=None):
        if source is None:
            source = self.driver
        try:
            if parent is not None:
                source = parent
            element_present = EC.presence_of_element_located((By.ID, id))
            WebDriverWait(source, 10).until(element_present)
            if all:
                return source.find_elements(by=By.ID, value=id)
            else:
                return source.find_element(by=By.ID, value=id)
        except Exception as e:
            print("Timed out waiting for page to load", e)
        return None

    def get_class(self, class_name, source=None, all=False, parent=None):
        if source is None:
            source = self.driver
        try:
            if parent is not None:
                source = parent
            element_present = EC.presence_of_element_located(
                (By.CLASS_NAME, class_name)
            )
            WebDriverWait(source, 10).until(element_present)
            if all:
                return source.find_elements(by=By.CLASS_NAME, value=class_name)
            else:
                return source.find_element(by=By.CLASS_NAME, value=class_name)
        except Exception as e:
            print("Timed out waiting for page to load", e)
        return None

    def open(self, url: str) -> None:
        self.driver.get(url)

    def wait_for_navigation(self, target_url: str = None) -> None:
        initial_url = self.driver.current_url
        print(target_url, initial_url)
        if target_url is not None:
            while (
                self.driver.current_url != target_url
                and target_url not in self.driver.current_url
            ):
                time.sleep(0.1)
        else:
            while initial_url == self.driver.current_url:
                time.sleep(0.1)

    def dropdown_select(self, element, text):
        try:
            element.click()
            element.find_element(
                by=By.XPATH, value=f".//option[text()='{text}']"
            ).click()
        except Exception as e:
            print("Failed to select dropdown", e)
