import random
import time
import typing
import urllib.parse

import bs4
import curl_cffi.requests.impersonate as impersonate

import chitragupt.abc.browser as browser_abc
import chitragupt.exceptions as exceptions
import chitragupt.modules.browsers as browsers


class Google(browsers.BrowserBase):
    """
    A class to interact with the Google search engine.

    This class is used to search for web pages, images, and videos on Google.

    Attributes
    ----------
    session : curl_cffi.requests.Session
        The session object used to make requests to the Google API.
    initialized : bool
        Whether the browser is initialized or not.

    Methods
    -------
    search
        Searches for a query on Google and returns a list of results.
    image
        Searches for images on Google.
    video
        Searches for videos on Google.
    """

    @staticmethod
    def get_useragent() -> str:
        """
        Generates a random user agent string.

        Returns
        -------
        str
            A randomly generated user agent string.
        """
        return (
            f"Lynx/{random.randint(2, 3)}.{random.randint(8, 9)}.{random.randint(0, 2)} "
            f"libwww-FM/{random.randint(2, 3)}.{random.randint(13, 15)} "
            f"SSL-MM/{random.randint(1, 2)}.{random.randint(3, 5)} "
            f"OpenSSL/{random.randint(1, 3)}.{random.randint(0, 4)}.{random.randint(0, 9)}"
        )

    def initialize(
        self,
        verify: bool = True,
        headers: typing.Optional[typing.Dict[str, str]] = None,
        proxy: typing.Optional[str] = None,
        proxies: typing.Optional[
            typing.Union[typing.Dict[str, str], str]
        ] = None,
        timeout: float = 10.0,
    ) -> None:
        """
        Initializes the browser with the given parameters.

        Parameters
        ----------
        verify : bool, optional
            Whether to verify TLS certificates (default is True).
        headers : Optional[Dict[str, str]], optional
            Custom headers to use (default is None).
        proxy : Optional[str], optional
            A single proxy URL to use (default is None).
        proxies : Optional[Union[Dict[str, str], str]], optional
            A dictionary of proxies or a single proxy URL (default is None).
        timeout : float, optional
            Request timeout in seconds (default is 10.0).
        """
        self.session.impersonate = impersonate.BrowserType.chrome120.value
        self.session.verify = verify
        self.session.headers = headers or {}  # type: ignore
        self.session.proxies = proxies or proxy  # type: ignore
        self.session.timeout = timeout
        self.initialized = True

    def _get_results(
        self,
        term: str,
        results: int = 10,
        lang: str = "en",
        start: int = 0,
        safe: bool = False,
        region: typing.Optional[str] = None,
    ) -> typing.Any:
        """
        Makes a GET request to Google's search endpoint.

        Parameters
        ----------
        term : str
            The search term.
        results : int, optional
            Number of results to retrieve (default is 10).
        lang : str, optional
            The language code (default is "en").
        start : int, optional
            The starting index for results (default is 0).
        safe : bool, optional
            Whether to enable safe search (default is False).
        region : Optional[str], optional
            Region code to use for the search (default is None).

        Returns
        -------
        Any
            The response text from the search endpoint.

        Raises
        ------
        Exception
            If the request fails.
        """
        assert self.initialized, (
            "Browser must be initialized before making a request."
        )
        response = self.session.get(
            url="https://www.google.com/search",
            headers={
                "User-Agent": self.get_useragent(),
                "Accept": "*/*",
            },
            params={
                "q": term,
                "num": results + 2,
                "hl": lang,
                "start": start,
                "safe": "active" if safe else "off",
                "gl": region,
            },
            cookies={
                "CONSENT": "PENDING+987",
                "SOCS": "CAESHAgBEhIaAB",
            },
        )
        response.raise_for_status()
        return response.text

    def search(
        self,
        term: str,
        num_results: int = 10,
        lang: str = "en",
        sleep_interval: float = 0.0,
        safe: bool = False,
        region: typing.Optional[str] = None,
        start_num: int = 0,
        unique: bool = False,
    ) -> typing.List[browser_abc.GoogleSearchResult]:
        """
        Searches Google for the given term and returns a list of search results.

        Parameters
        ----------
        term : str
            The search term.
        num_results : int, optional
            The number of results to return (default is 10).
        lang : str, optional
            The language code (default is "en").
        sleep_interval : float, optional
            The sleep interval between requests (default is 0.0).
        safe : bool, optional
            Whether to enable safe search (default is False).
        region : Optional[str], optional
            Region code for the search (default is None).
        start_num : int, optional
            The starting index for results (default is 0).
        unique : bool, optional
            If True, only unique links will be returned (default is False).

        Returns
        -------
        List[GoogleSearchResult]
            A list of GoogleSearchResult objects containing search results.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                f"Browser is not initialized. Call {self.__class__.__name__}.initialize() first."
            )
        fetched_links: typing.Set[str] = set()
        start = start_num
        fetched_results = 0
        results: typing.List[browser_abc.GoogleSearchResult] = []

        while fetched_results < num_results:
            html_content = self._get_results(
                term,
                num_results - fetched_results,
                lang,
                start,
                safe,
                region,
            )
            soup = bs4.BeautifulSoup(html_content, "html.parser")
            result_blocks = soup.find_all("div", class_="ezO2md")

            new_results = 0
            for result in result_blocks:
                link_tag = result.find("a", href=True)  # type: ignore
                title_tag = (
                    link_tag.find("span", class_="CVA68e")
                    if link_tag
                    else None
                )  # type: ignore
                description_tag = result.find("span", class_="FrIlee")  # type: ignore

                if link_tag and title_tag and description_tag:
                    link = urllib.parse.unquote(
                        link_tag["href"].split("&")[0].replace("/url?q=", "")  # type: ignore
                    )
                    if unique and link in fetched_links:
                        continue

                    fetched_links.add(link)
                    title = title_tag.text.strip()
                    description = description_tag.text.strip()

                    fetched_results += 1
                    new_results += 1

                    results.append(
                        browser_abc.GoogleSearchResult(
                            link,
                            title,
                            description,
                        )
                    )

                    if fetched_results >= num_results:
                        return results

            if new_results == 0:
                break

            start += 10
            time.sleep(sleep_interval)

        return results
