from time import time
from asyncio import sleep as asyncio_sleep, Lock

from camoufox import DefaultAddons
from playwright.sync_api import BrowserContext, Playwright
from playwright.async_api import (
    BrowserContext as AsyncBrowserContext,
    Playwright as AsyncPlaywright,
)
from camoufox.pkgman import installed_verstr as camoufox_version
from camoufox.utils import launch_options as generate_launch_options

from ._page import PageInfo, PagePool
from scrapling.parser import Selector
from scrapling.core._types import Any, cast, Dict, Optional, TYPE_CHECKING
from scrapling.engines.toolbelt.fingerprints import get_os_name
from ._validators import validate, PlaywrightConfig, CamoufoxConfig
from ._config_tools import _compiled_stealth_scripts, _launch_kwargs, _context_kwargs
from scrapling.engines.toolbelt.navigation import intercept_route, async_intercept_route

__ff_version_str__ = camoufox_version().split(".", 1)[0]


class SyncSession:
    def __init__(self, max_pages: int = 1):
        self.max_pages = max_pages
        self.page_pool = PagePool(max_pages)
        self._max_wait_for_page = 60
        self.playwright: Optional[Playwright] = None
        self.context: Optional[BrowserContext] = None
        self._closed = False

    def _get_page(
        self,
        timeout: int | float,
        extra_headers: Optional[Dict[str, str]],
        disable_resources: bool,
    ) -> PageInfo:  # pragma: no cover
        """Get a new page to use"""

        # No need to check if a page is available or not in sync code because the code blocked before reaching here till the page closed, ofc.
        assert self.context is not None, "Browser context not initialized"
        page = self.context.new_page()
        page.set_default_navigation_timeout(timeout)
        page.set_default_timeout(timeout)
        if extra_headers:
            page.set_extra_http_headers(extra_headers)

        if disable_resources:
            page.route("**/*", intercept_route)

        if getattr(self, "stealth", False):
            for script in _compiled_stealth_scripts():
                page.add_init_script(script=script)

        return self.page_pool.add_page(page)

    def get_pool_stats(self) -> Dict[str, int]:
        """Get statistics about the current page pool"""
        return {
            "total_pages": self.page_pool.pages_count,
            "busy_pages": self.page_pool.busy_count,
            "max_pages": self.max_pages,
        }


class AsyncSession:
    def __init__(self, max_pages: int = 1):
        self.max_pages = max_pages
        self.page_pool = PagePool(max_pages)
        self._max_wait_for_page = 60
        self.playwright: Optional[AsyncPlaywright] = None
        self.context: Optional[AsyncBrowserContext] = None
        self._closed = False
        self._lock = Lock()

    async def _get_page(
        self,
        timeout: int | float,
        extra_headers: Optional[Dict[str, str]],
        disable_resources: bool,
    ) -> PageInfo:  # pragma: no cover
        """Get a new page to use"""
        if TYPE_CHECKING:
            assert self.context is not None, "Browser context not initialized"

        async with self._lock:
            # If we're at max capacity after cleanup, wait for busy pages to finish
            if self.page_pool.pages_count >= self.max_pages:
                start_time = time()
                while time() - start_time < self._max_wait_for_page:
                    await asyncio_sleep(0.05)
                    if self.page_pool.pages_count < self.max_pages:
                        break
                else:
                    raise TimeoutError(
                        f"No pages finished to clear place in the pool within the {self._max_wait_for_page}s timeout period"
                    )

            assert self.context is not None, "Browser context not initialized"
            page = await self.context.new_page()
            page.set_default_navigation_timeout(timeout)
            page.set_default_timeout(timeout)
            if extra_headers:
                await page.set_extra_http_headers(extra_headers)

            if disable_resources:
                await page.route("**/*", async_intercept_route)

            if getattr(self, "stealth", False):
                for script in _compiled_stealth_scripts():
                    await page.add_init_script(script=script)

            return self.page_pool.add_page(page)

    def get_pool_stats(self) -> Dict[str, int]:
        """Get statistics about the current page pool"""
        return {
            "total_pages": self.page_pool.pages_count,
            "busy_pages": self.page_pool.busy_count,
            "max_pages": self.max_pages,
        }


class DynamicSessionMixin:
    def __validate__(self, **params):
        config = validate(params, model=PlaywrightConfig)

        self.max_pages = config.max_pages
        self.headless = config.headless
        self.hide_canvas = config.hide_canvas
        self.disable_webgl = config.disable_webgl
        self.real_chrome = config.real_chrome
        self.stealth = config.stealth
        self.google_search = config.google_search
        self.wait = config.wait
        self.proxy = config.proxy
        self.locale = config.locale
        self.extra_headers = config.extra_headers
        self.useragent = config.useragent
        self.timeout = config.timeout
        self.cookies = config.cookies
        self.disable_resources = config.disable_resources
        self.cdp_url = config.cdp_url
        self.network_idle = config.network_idle
        self.load_dom = config.load_dom
        self.wait_selector = config.wait_selector
        self.init_script = config.init_script
        self.wait_selector_state = config.wait_selector_state
        self.selector_config = config.selector_config
        self.additional_args = config.additional_args
        self.page_action = config.page_action
        self.user_data_dir = config.user_data_dir
        self._headers_keys = {header.lower() for header in self.extra_headers.keys()} if self.extra_headers else set()
        self.__initiate_browser_options__()

    def __initiate_browser_options__(self):
        if TYPE_CHECKING:
            assert isinstance(self.proxy, tuple)

        if not self.cdp_url:
            # `launch_options` is used with persistent context
            self.launch_options = dict(
                _launch_kwargs(
                    self.headless,
                    self.proxy,
                    self.locale,
                    tuple(self.extra_headers.items()) if self.extra_headers else tuple(),
                    self.useragent,
                    self.real_chrome,
                    self.stealth,
                    self.hide_canvas,
                    self.disable_webgl,
                )
            )
            self.launch_options["extra_http_headers"] = dict(self.launch_options["extra_http_headers"])
            self.launch_options["proxy"] = dict(self.launch_options["proxy"]) or None
            self.launch_options["user_data_dir"] = self.user_data_dir
            self.launch_options.update(cast(Dict, self.additional_args))
            self.context_options = dict()
        else:
            # while `context_options` is left to be used when cdp mode is enabled
            self.launch_options = dict()
            self.context_options = dict(
                _context_kwargs(
                    self.proxy,
                    self.locale,
                    tuple(self.extra_headers.items()) if self.extra_headers else tuple(),
                    self.useragent,
                    self.stealth,
                )
            )
            self.context_options["extra_http_headers"] = dict(self.context_options["extra_http_headers"])
            self.context_options["proxy"] = dict(self.context_options["proxy"]) or None
            self.context_options.update(cast(Dict, self.additional_args))


class StealthySessionMixin:
    def __validate__(self, **params):
        config: CamoufoxConfig = validate(params, model=CamoufoxConfig)

        self.max_pages = config.max_pages
        self.headless = config.headless
        self.block_images = config.block_images
        self.disable_resources = config.disable_resources
        self.block_webrtc = config.block_webrtc
        self.allow_webgl = config.allow_webgl
        self.network_idle = config.network_idle
        self.load_dom = config.load_dom
        self.humanize = config.humanize
        self.solve_cloudflare = config.solve_cloudflare
        self.wait = config.wait
        self.timeout = config.timeout
        self.page_action = config.page_action
        self.wait_selector = config.wait_selector
        self.init_script = config.init_script
        self.addons = config.addons
        self.wait_selector_state = config.wait_selector_state
        self.cookies = config.cookies
        self.google_search = config.google_search
        self.extra_headers = config.extra_headers
        self.proxy = config.proxy
        self.os_randomize = config.os_randomize
        self.disable_ads = config.disable_ads
        self.geoip = config.geoip
        self.selector_config = config.selector_config
        self.additional_args = config.additional_args
        self.page_action = config.page_action
        self.user_data_dir = config.user_data_dir
        self._headers_keys = {header.lower() for header in self.extra_headers.keys()} if self.extra_headers else set()
        self.__initiate_browser_options__()

    def __initiate_browser_options__(self):
        """Initiate browser options."""
        self.launch_options: Dict[str, Any] = generate_launch_options(
            **{
                "geoip": self.geoip,
                "proxy": dict(self.proxy) if self.proxy and isinstance(self.proxy, tuple) else self.proxy,
                "addons": self.addons,
                "exclude_addons": [] if self.disable_ads else [DefaultAddons.UBO],
                "headless": self.headless,
                "humanize": True if self.solve_cloudflare else self.humanize,
                "i_know_what_im_doing": True,  # To turn warnings off with the user configurations
                "allow_webgl": self.allow_webgl,
                "block_webrtc": self.block_webrtc,
                "block_images": self.block_images,  # Careful! it makes some websites don't finish loading at all like stackoverflow even in headful mode.
                "os": None if self.os_randomize else get_os_name(),
                "user_data_dir": self.user_data_dir,
                "ff_version": __ff_version_str__,
                "firefox_user_prefs": {
                    # This is what enabling `enable_cache` does internally, so we do it from here instead
                    "browser.sessionhistory.max_entries": 10,
                    "browser.sessionhistory.max_total_viewers": -1,
                    "browser.cache.memory.enable": True,
                    "browser.cache.disk_cache_ssl": True,
                    "browser.cache.disk.smart_size.enabled": True,
                },
                **cast(Dict, self.additional_args),
            }
        )

    @staticmethod
    def _detect_cloudflare(page_content: str) -> str | None:
        """
        Detect the type of Cloudflare challenge present in the provided page content.

        This function analyzes the given page content to identify whether a specific
        type of Cloudflare challenge is present. It checks for three predefined
        challenge types: non-interactive, managed, and interactive. If a challenge
        type is detected, it returns the corresponding type as a string. If no
        challenge type is detected, it returns None.

        Args:
            page_content (str): The content of the page to analyze for Cloudflare
                challenge types.

        Returns:
            str: A string representing the detected Cloudflare challenge type, if
                found. Returns None if no challenge matches.
        """
        challenge_types = (
            "non-interactive",
            "managed",
            "interactive",
        )
        for ctype in challenge_types:
            if f"cType: '{ctype}'" in page_content:
                return ctype

        # Check if turnstile captcha is embedded inside the page (Usually inside a closed Shadow iframe)
        selector = Selector(content=page_content)
        if selector.css('script[src*="challenges.cloudflare.com/turnstile/v"]'):
            return "embedded"

        return None
