"""
ChitraGupt Browser Modules.

This package contains browser modules used for various web-based OSINT operations.
It includes implementations for multiple browser types and search engines
(e.g., duckduckgo, google, thirteentabs), which can be used for querying and
retrieving online data.
"""

import enum
import io
import pathlib
import typing

import curl_cffi.const
import curl_cffi.curl
import curl_cffi.requests
import curl_cffi.requests.impersonate
import curl_cffi.requests.session

import chitragupt.core.logger
import chitragupt.exceptions


class BrowserType(
    str,
    enum.Enum,
):  # todo: remove in version 1.x
    edge99 = "edge99"
    edge101 = "edge101"
    chrome99 = "chrome99"
    chrome100 = "chrome100"
    chrome101 = "chrome101"
    chrome104 = "chrome104"
    chrome107 = "chrome107"
    chrome110 = "chrome110"
    chrome116 = "chrome116"
    chrome119 = "chrome119"
    chrome120 = "chrome120"
    chrome123 = "chrome123"
    chrome124 = "chrome124"
    chrome99_android = "chrome99_android"
    safari15_3 = "safari15_3"
    safari15_5 = "safari15_5"
    safari17_0 = "safari17_0"
    safari17_2_ios = "safari17_2_ios"


class HTTPMethodParams(
    typing.TypedDict,
    total=False,
):
    params: typing.Optional[
        typing.Union[
            typing.Dict[
                str,
                str,
            ],
            typing.List[str],
            typing.Tuple[str],
        ]
    ]
    data: typing.Optional[
        typing.Union[
            typing.Dict[
                str,
                str,
            ],
            typing.List[typing.Tuple[str]],
            str,
            io.BytesIO,
            bytes,
        ]
    ]
    json: typing.Optional[
        typing.Dict[
            str,
            str,
        ]
    ]
    headers: typing.Optional[curl_cffi.requests.HeaderTypes]
    cookies: typing.Optional[curl_cffi.requests.CookieTypes]
    files: typing.Optional[
        typing.Dict[
            str,
            str,
        ]
    ]
    auth: typing.Optional[
        typing.Tuple[
            str,
            str,
        ]
    ]
    timeout: typing.Union[
        float,
        typing.Tuple[
            float,
            float,
        ],
    ]
    allow_redirects: bool
    max_redirects: int
    proxies: typing.Optional[curl_cffi.requests.ProxySpec]
    proxy: typing.Optional[str]
    proxy_auth: typing.Optional[
        typing.Tuple[
            str,
            str,
        ]
    ]
    verify: typing.Optional[bool]
    referer: typing.Optional[str]
    accept_encoding: typing.Optional[str]
    content_callback: typing.Optional[
        typing.Callable[
            ...,
            typing.Any,
        ]
    ]
    impersonate: typing.Optional[curl_cffi.requests.BrowserTypeLiteral]
    ja3: typing.Optional[str]
    akamai: typing.Optional[str]
    extra_fp: typing.Optional[
        typing.Union[
            curl_cffi.requests.ExtraFingerprints,
            curl_cffi.requests.impersonate.ExtraFpDict,
        ]
    ]
    thread: typing.Optional[curl_cffi.requests.session.ThreadType]
    default_headers: typing.Optional[bool]
    default_encoding: typing.Union[
        str,
        typing.Callable[
            [bytes],
            str,
        ],
    ]
    quote: typing.Union[
        str,
        typing.Literal[False],
    ]
    curl_options: typing.Optional[
        typing.Dict[
            str,
            str,
        ]
    ]
    http_version: typing.Optional[curl_cffi.const.CurlHttpVersion]
    debug: bool
    interface: typing.Optional[str]
    cert: typing.Optional[
        typing.Union[
            str,
            typing.Tuple[
                str,
                str,
            ],
        ]
    ]
    stream: bool
    max_recv_speed: int
    multipart: typing.Optional[curl_cffi.curl.CurlMime]


class BrowserBase:
    """
    A base browser client for sending HTTP requests using curl-cffi.

    This class provides methods to send GET, POST, OPTIONS, DELETE, HEAD, PUT, and PATCH requests,
    as well as additional utility functions for making generic requests, downloading and uploading files,
    setting default headers, resetting the session, updating the logger, retrieving session info,
    and streaming responses.
    """

    def __init__(
        self,
        logger: typing.Optional[chitragupt.core.logger.GuptLogger] = None,
        curl: typing.Optional[curl_cffi.curl.Curl] = None,
        thread: typing.Optional[
            typing.Literal[
                "eventlet",
                "gevent",
            ]
        ] = None,
        use_thread_local_curl: bool = True,
        **kwargs: typing.Unpack[curl_cffi.requests.session.BaseSessionParams],
    ) -> None:
        """
        Initialize the BrowserBase instance.

        Parameters
        ----------
        logger : Optional[GuptLogger]
            A logger instance for event logging.
        curl : Optional[Curl]
            An optional pre-created Curl object.
        thread : Optional[str]
            The threading library to use ("eventlet" or "gevent").
        use_thread_local_curl : bool
            Whether to use a thread-local Curl object.
        **kwargs : Unpack[BaseSessionParams]
            Additional parameters for the Session constructor.
        """
        self.logger = logger
        self.initialized: bool = False
        self.session = curl_cffi.requests.Session(
            curl=curl,
            thread=thread,
            use_thread_local_curl=use_thread_local_curl,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                "BrowserBase initialized.",
                chitragupt.core.logger.LogType.DEBUG,
            )

    def get(
        self,
        url: str,
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> curl_cffi.requests.Response:
        """
        Send a GET request.

        Parameters
        ----------
        url : str
            The target URL.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.

        Returns
        -------
        Response
            The response from the GET request.
        """
        if self.logger:
            self.logger.log(
                f"Sending GET request to {url} with kwargs: {kwargs}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        response = self.session.get(
            url,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                f"GET request to {url} completed with status {response.status_code}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return response

    def post(
        self,
        url: str,
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> curl_cffi.requests.Response:
        """
        Send a POST request.

        Parameters
        ----------
        url : str
            The target URL.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.

        Returns
        -------
        Response
            The response from the POST request.
        """
        if self.logger:
            self.logger.log(
                f"Sending POST request to {url} with kwargs: {kwargs}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        response = self.session.post(
            url,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                f"POST request to {url} completed with status {response.status_code}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return response

    def options(
        self,
        url: str,
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> curl_cffi.requests.Response:
        """
        Send an OPTIONS request.

        Parameters
        ----------
        url : str
            The target URL.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.

        Returns
        -------
        Response
            The response from the OPTIONS request.
        """
        if self.logger:
            self.logger.log(
                f"Sending OPTIONS request to {url} with kwargs: {kwargs}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        response = self.session.options(
            url,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                f"OPTIONS request to {url} completed with status {response.status_code}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return response

    def delete(
        self,
        url: str,
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> curl_cffi.requests.Response:
        """
        Send a DELETE request.

        Parameters
        ----------
        url : str
            The target URL.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.

        Returns
        -------
        Response
            The response from the DELETE request.
        """
        if self.logger:
            self.logger.log(
                f"Sending DELETE request to {url} with kwargs: {kwargs}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        response = self.session.delete(
            url,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                f"DELETE request to {url} completed with status {response.status_code}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return response

    def head(
        self,
        url: str,
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> curl_cffi.requests.Response:
        """
        Send a HEAD request.

        Parameters
        ----------
        url : str
            The target URL.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.

        Returns
        -------
        Response
            The response from the HEAD request.
        """
        if self.logger:
            self.logger.log(
                f"Sending HEAD request to {url} with kwargs: {kwargs}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        response = self.session.head(
            url,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                f"HEAD request to {url} completed with status {response.status_code}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return response

    def put(
        self,
        url: str,
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> curl_cffi.requests.Response:
        """
        Send a PUT request.

        Parameters
        ----------
        url : str
            The target URL.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.

        Returns
        -------
        Response
            The response from the PUT request.
        """
        if self.logger:
            self.logger.log(
                f"Sending PUT request to {url} with kwargs: {kwargs}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        response = self.session.put(
            url,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                f"PUT request to {url} completed with status {response.status_code}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return response

    def patch(
        self,
        url: str,
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> curl_cffi.requests.Response:
        """
        Send a PATCH request.

        Parameters
        ----------
        url : str
            The target URL.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.

        Returns
        -------
        Response
            The response from the PATCH request.
        """
        if self.logger:
            self.logger.log(
                f"Sending PATCH request to {url} with kwargs: {kwargs}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        response = self.session.patch(
            url,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                f"PATCH request to {url} completed with status {response.status_code}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return response

    def request(
        self,
        method: str,
        url: str,
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> curl_cffi.requests.Response:
        """
        Send an HTTP request using the specified method.

        Parameters
        ----------
        method : str
            The HTTP method to use (e.g., "GET", "POST", etc.).
        url : str
            The target URL.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.

        Returns
        -------
        Response
            The response from the HTTP request.

        Raises
        ------
        AttributeError
            If the specified HTTP method is not supported.
        """
        method_lower = method.lower()
        if not hasattr(
            self.session,
            method_lower,
        ):
            raise AttributeError(f"HTTP method '{method}' is not supported.")
        if self.logger:
            self.logger.log(
                f"Sending {method.upper()} request to {url} with kwargs: {kwargs}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        response = getattr(
            self.session,
            method_lower,
        )(
            url,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                f"{method.upper()} request to {url} completed with status {response.status_code}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return response

    def download_to_file(
        self,
        url: str,
        file_path: typing.Union[
            str,
            pathlib.Path,
        ],
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> None:
        """
        Download content from the specified URL and save it to a file.

        Parameters
        ----------
        url : str
            The URL to download content from.
        file_path : Union[str, pathlib.Path]
            The file path where the content will be saved.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.
        """
        response = self.get(
            url,
            **kwargs,
        )
        file_path = pathlib.Path(file_path)
        file_path.parent.mkdir(
            parents=True,
            exist_ok=True,
        )
        with file_path.open("wb") as f:
            f.write(response.content)
        if self.logger:
            self.logger.log(
                f"Downloaded content from {url} to {file_path}",
                chitragupt.core.logger.LogType.DEBUG,
            )

    def upload_file(
        self,
        url: str,
        file_path: typing.Union[
            str,
            pathlib.Path,
        ],
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> curl_cffi.requests.Response:
        """
        Upload a file to the specified URL.

        Parameters
        ----------
        url : str
            The target URL for the file upload.
        file_path : Union[str, pathlib.Path]
            The file to be uploaded.
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters. If 'files' is not provided, the file will be added automatically.

        Returns
        -------
        Response
            The response from the file upload request.
        """
        file_path = pathlib.Path(file_path)
        if "files" not in kwargs:
            kwargs["files"] = {"file": str(file_path)}
        if self.logger:
            self.logger.log(
                f"Uploading file {file_path} to {url} with kwargs: {kwargs}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        response = self.session.post(
            url,
            **kwargs,
        )
        if self.logger:
            self.logger.log(
                f"Upload request completed with status {response.status_code}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return response

    def set_default_headers(
        self,
        headers: typing.Dict[
            str,
            str,
        ],
    ) -> None:
        """
        Set the default headers for the session.

        Parameters
        ----------
        headers : Dict[str, str]
            A dictionary of headers to set as default.
        """
        self.session.default_headers = headers  # type: ignore
        if self.logger:
            self.logger.log(
                f"Default headers updated to: {headers}",
                chitragupt.core.logger.LogType.DEBUG,
            )

    def reset_session(
        self,
    ) -> None:
        """Reset the current session by closing it and reinitializing with the same parameters."""
        self.session.close()
        # Access the internal parameters (assumed to be stored in _params)
        self.session = curl_cffi.requests.Session(**self.session._params)  # type: ignore
        if self.logger:
            self.logger.log(
                "Session has been reset.",
                chitragupt.core.logger.LogType.DEBUG,
            )

    def update_logger(
        self,
        logger: chitragupt.core.logger.GuptLogger,
    ) -> None:
        """
        Update the logger for this BrowserBase instance.

        Parameters
        ----------
        logger : GuptLogger
            The new logger instance.
        """
        self.logger = logger
        if self.logger:
            self.logger.log(
                "Logger has been updated in BrowserBase.",
                chitragupt.core.logger.LogType.DEBUG,
            )

    def get_session_info(
        self,
    ) -> str:
        """
        Retrieve a string representation of the current session information.

        Returns
        -------
        str
            A string containing session information.
        """
        info = f"Session: {self.session}"
        if self.logger:
            self.logger.log(
                f"Session info requested: {info}",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return info

    def request_stream(
        self,
        method: str,
        url: str,
        chunk_size: int = 1024,
        **kwargs: typing.Unpack[HTTPMethodParams],
    ) -> typing.Generator[
        bytes,
        None,
        None,
    ]:
        """
        Send an HTTP request and yield response content in chunks.

        Parameters
        ----------
        method : str
            The HTTP method to use.
        url : str
            The target URL.
        chunk_size : int, optional
            The size (in bytes) of each chunk (default is 1024).
        **kwargs : Unpack[HTTPMethodParams]
            Additional request parameters.

        Yields
        ------
        bytes
            Chunks of the response content.
        """
        response = self.request(
            method,
            url,
            stream=True,
            **kwargs,  # type: ignore
        )
        yield from response.iter_content(chunk_size=chunk_size)

    def __enter__(
        self,
    ):
        """
        Enter the runtime context related to this object.

        Returns
        -------
        BrowserBase
            The BrowserBase instance.
        """
        if self.logger:
            self.logger.log(
                "Entering BrowserBase context.",
                chitragupt.core.logger.LogType.DEBUG,
            )
        return self

    def __exit__(
        self,
        exc_type,
        exc_value,
        traceback,
    ) -> None:
        """Exit the runtime context, closing the session."""
        if self.logger:
            self.logger.log(
                "Exiting BrowserBase context.",
                chitragupt.core.logger.LogType.DEBUG,
            )
        self.session.close()
        if self.logger:
            self.logger.log(
                "Session has been closed.",
                chitragupt.core.logger.LogType.DEBUG,
            )
