import base64
import typing

import msgspec

import chitragupt._about as about
import chitragupt.abc.misc as misc
import chitragupt.exceptions as exceptions
import chitragupt.modules.browsers as browser


class GitIO(browser.BrowserBase):
    """
    A featureful, statically typed class for interacting with a GitHub repository.
    This implementation uses only absolute imports and is designed for an open-source,
    read-only environment (i.e. no API key is used).
    """

    def initialize(self) -> None:
        """Initialize the browser session with GitHub API-specific settings."""
        self.session.impersonate = browser.BrowserType.chrome99.value
        self.session.base_url = "https://api.github.com"
        self.session.headers = {  # type: ignore
            "Accept": "application/vnd.github.v3+json",
            "User-Agent": "ChitraGupt",
        }
        self.initialized = True

    def _check_response(self, response: typing.Any) -> None:
        """
        Check if the HTTP response status code is 200 (OK). Raise a ValueError if not.

        Parameters
        ----------
        response : Any
            The HTTP response object from self.session.get().

        Raises
        ------
        ValueError
            If the response status code is not 200.
        """
        if response.status_code != 200:
            raise ValueError(
                f"HTTP Error {response.status_code}: {response.text}"
            )

    def get_raw_file(
        self, file: str = "README.md", branch: str = "main"
    ) -> str:
        """
        Retrieve the raw (base64-decoded) contents of a file from the repository.

        Parameters
        ----------
        file : str, optional
            The path to the file in the repository (default is "README.md").
        branch : str, optional
            The branch to retrieve the file from (default is "main").

        Returns
        -------
        str
            The file content as a UTF-8 string.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser has not been initialized.
        ValueError
            If the file cannot be retrieved.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                f"{about.__repo_slug__} browser is not initialized. Please initialize the browser first."
            )

        url: str = f"repos/{about.__plugin_garden_url_slug__}/contents/{file}?ref={branch}"
        response = self.session.get(url=url)
        self._check_response(response)

        data: typing.Any = response.json()
        if not isinstance(data, dict):
            raise ValueError(
                "Unexpected response format, expected a dictionary."
            )
        if "content" not in data:
            raise ValueError(
                f"Error fetching file: {data.get('message', 'Unknown error')}"
            )

        return base64.b64decode(data["content"]).decode("utf-8")

    def get_list_of_files(self, recursive: int = 1) -> misc.GitFiles:
        """
        Retrieve a list (tree) of files from the repository.

        Parameters
        ----------
        recursive : int, optional
            The recursive depth for the file tree (default is 1).

        Returns
        -------
        GitFiles
            A GitFiles object representing the repository tree.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser has not been initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                f"{about.__repo_slug__} browser is not initialized. Please initialize the browser first."
            )

        url: str = f"repos/{about.__plugin_garden_url_slug__}/git/trees/main?recursive={recursive}"
        response = self.session.get(url=url)
        self._check_response(response)
        return msgspec.json.decode(response.content, type=misc.GitFiles)

    def get_commit_history(
        self, file: str, branch: str = "main", per_page: int = 30
    ) -> typing.Any:
        """
        Retrieve the commit history for a given file on the specified branch.

        Parameters
        ----------
        file : str
            The file path to get commit history for.
        branch : str, optional
            The branch to search commits on (default is "main").
        per_page : int, optional
            Maximum number of commits to return per page (default is 30).

        Returns
        -------
        Any
            A list or dictionary (depending on API response) of commit history.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser has not been initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                f"{about.__repo_slug__} browser is not initialized. Please initialize the browser first."
            )

        url: str = f"repos/{about.__plugin_garden_url_slug__}/commits?path={file}&sha={branch}&per_page={per_page}"
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_repository_info(self) -> typing.Any:
        """
        Retrieve basic information about the repository.

        Returns
        -------
        Any
            A dictionary of repository details.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser has not been initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                f"{about.__repo_slug__} browser is not initialized. Please initialize the browser first."
            )

        url: str = f"repos/{about.__plugin_garden_url_slug__}"
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def list_branches(self) -> typing.Any:
        """
        List all branches in the repository.

        Returns
        -------
        Any
            A list of branches (each branch as a dictionary).

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser has not been initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                f"{about.__repo_slug__} browser is not initialized. Please initialize the browser first."
            )

        url: str = f"repos/{about.__plugin_garden_url_slug__}/branches"
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def list_tags(self) -> typing.Any:
        """
        Lists all tags in the repository.

        Parameters
        ----------
        None

        Returns
        -------
        typing.Any
            A list of tags.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser has not been initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/tags".format(about.__plugin_garden_url_slug__)
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def download_archive(
        self, archive_format: str = "zipball", ref: str = "main"
    ) -> bytes:
        """
        Downloads an archive (zip or tarball) of the repository at the specified ref.

        Parameters
        ----------
        archive_format : str, optional
            The archive format; typically "zipball" or "tarball" (default is "zipball").
        ref : str, optional
            The Git ref (branch, tag, or commit SHA) to archive (default is "main").

        Returns
        -------
        bytes
            The binary content of the archive.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser has not been initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/{}/{}".format(
            about.__plugin_garden_url_slug__, archive_format, ref
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.content

    def search_code(
        self, query: str, per_page: int = 30, page: int = 1
    ) -> typing.Any:
        """
        Searches code within the repository using the GitHub search API.

        Parameters
        ----------
        query : str
            The search query string.
        per_page : int, optional
            Number of results per page (default is 30).
        page : int, optional
            The page number of results (default is 1).

        Returns
        -------
        typing.Any
            A dictionary of search results.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser has not been initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        # Build a query that restricts the search to the repository.
        search_query: str = "{}+repo:{}".format(
            query, about.__plugin_garden_url_slug__
        )
        url: str = "search/code?q={}&per_page={}&page={}".format(
            search_query, per_page, page
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_repository_topics(self) -> typing.Any:
        """
        Retrieve the list of topics for the repository.

        Returns
        -------
        typing.Any
            A dictionary of topics.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/topics".format(about.__plugin_garden_url_slug__)
        headers: dict = self.session.headers.copy()  # type: ignore
        headers["Accept"] = "application/vnd.github.mercy-preview+json"
        response = self.session.get(url=url, headers=headers)
        self._check_response(response)
        return response.json()

    def get_contributors(
        self, per_page: int = 30, page: int = 1
    ) -> typing.Any:
        """
        Retrieve the list of contributors to the repository.

        Parameters
        ----------
        per_page : int, optional
            Number of contributors per page (default is 30).
        page : int, optional
            Page number (default is 1).

        Returns
        -------
        typing.Any
            A list of contributor dictionaries.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/contributors?per_page={}&page={}".format(
            about.__plugin_garden_url_slug__, per_page, page
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_releases(self, per_page: int = 30, page: int = 1) -> typing.Any:
        """
        Retrieve the releases of the repository.

        Parameters
        ----------
        per_page : int, optional
            Number of releases per page (default is 30).
        page : int, optional
            Page number (default is 1).

        Returns
        -------
        typing.Any
            A list of release dictionaries.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/releases?per_page={}&page={}".format(
            about.__plugin_garden_url_slug__, per_page, page
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_pull_requests(
        self, state: str = "open", per_page: int = 30, page: int = 1
    ) -> typing.Any:
        """
        Retrieve pull requests for the repository.

        Parameters
        ----------
        state : str, optional
            The state of the pull requests ("open", "closed", or "all"; default is "open").
        per_page : int, optional
            Number of pull requests per page (default is 30).
        page : int, optional
            Page number (default is 1).

        Returns
        -------
        typing.Any
            A list of pull request dictionaries.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/pulls?state={}&per_page={}&page={}".format(
            about.__plugin_garden_url_slug__, state, per_page, page
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_issues(
        self, state: str = "open", per_page: int = 30, page: int = 1
    ) -> typing.Any:
        """
        Retrieve issues for the repository.

        Parameters
        ----------
        state : str, optional
            The state of the issues ("open", "closed", or "all"; default is "open").
        per_page : int, optional
            Number of issues per page (default is 30).
        page : int, optional
            Page number (default is 1).

        Returns
        -------
        typing.Any
            A list of issue dictionaries.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/issues?state={}&per_page={}&page={}".format(
            about.__plugin_garden_url_slug__, state, per_page, page
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_languages(self) -> typing.Any:
        """
        Retrieve the programming languages used in the repository.

        Returns
        -------
        typing.Any
            A dictionary where keys are language names and values are bytes of code.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/languages".format(
            about.__plugin_garden_url_slug__
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_license(self) -> typing.Any:
        """
        Retrieve the license information of the repository.

        Returns
        -------
        typing.Any
            A dictionary containing license details.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/license".format(about.__plugin_garden_url_slug__)
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_forks(self, per_page: int = 30, page: int = 1) -> typing.Any:
        """
        Retrieve the forks of the repository.

        Parameters
        ----------
        per_page : int, optional
            Number of forks per page (default is 30).
        page : int, optional
            Page number (default is 1).

        Returns
        -------
        Any
            A list of fork dictionaries.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/forks?per_page={}&page={}".format(
            about.__plugin_garden_url_slug__, per_page, page
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_stargazers(self, per_page: int = 30, page: int = 1) -> typing.Any:
        """
        Retrieve the stargazers of the repository.

        Parameters
        ----------
        per_page : int, optional
            Number of stargazers per page (default is 30).
        page : int, optional
            Page number (default is 1).

        Returns
        -------
        Any
            A list of stargazer dictionaries.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/stargazers?per_page={}&page={}".format(
            about.__plugin_garden_url_slug__, per_page, page
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_watchers(self, per_page: int = 30, page: int = 1) -> typing.Any:
        """
        Retrieve the watchers (subscribers) of the repository.

        Parameters
        ----------
        per_page : int, optional
            Number of watchers per page (default is 30).
        page : int, optional
            Page number (default is 1).

        Returns
        -------
        Any
            A list of watcher dictionaries.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/subscribers?per_page={}&page={}".format(
            about.__plugin_garden_url_slug__, per_page, page
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_readme(self, branch: str = "main") -> str:
        """
        Retrieves the repository's README file content (decoded from base64).

        Parameters
        ----------
        branch : str, optional
            The branch to retrieve the README from (default is "main").

        Returns
        -------
        str
            The README content as a UTF-8 string.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/readme?ref={}".format(
            about.__plugin_garden_url_slug__, branch
        )
        response = self.session.get(url=url)
        self._check_response(response)
        data: typing.Any = response.json()
        if "content" not in data:
            raise ValueError("No content found in the README response.")
        return base64.b64decode(data["content"]).decode("utf-8")

    def get_directory_contents(
        self, directory: str, branch: str = "main"
    ) -> typing.Any:
        """
        Retrieves the contents of a directory in the repository.

        Parameters
        ----------
        directory : str
            The path to the directory.
        branch : str, optional
            The branch to retrieve contents from (default is "main").

        Returns
        -------
        Any
            A list of file and subdirectory dictionaries.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/contents/{}?ref={}".format(
            about.__plugin_garden_url_slug__, directory, branch
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_commit(self, commit_sha: str) -> typing.Any:
        """
        Retrieves detailed information about a specific commit.

        Parameters
        ----------
        commit_sha : str
            The SHA of the commit.

        Returns
        -------
        Any
            A dictionary containing commit details.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/commits/{}".format(
            about.__plugin_garden_url_slug__, commit_sha
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def search_issues(
        self, query: str, per_page: int = 30, page: int = 1
    ) -> typing.Any:
        """
        Searches for issues within the repository using GitHub's search API.

        Parameters
        ----------
        query : str
            The search query string.
        per_page : int, optional
            Number of results per page (default is 30).
        page : int, optional
            Page number (default is 1).

        Returns
        -------
        Any
            A dictionary containing search results.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        search_query: str = "{}+repo:{}".format(
            query, about.__plugin_garden_url_slug__
        )
        url: str = "search/issues?q={}&per_page={}&page={}".format(
            search_query, per_page, page
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def compare_commits(self, base: str, head: str) -> typing.Any:
        """
        Compares two commits in the repository.

        Parameters
        ----------
        base : str
            The base commit SHA.
        head : str
            The head commit SHA.

        Returns
        -------
        Any
            A dictionary containing the comparison details.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "repos/{}/compare/{}...{}".format(
            about.__plugin_garden_url_slug__, base, head
        )
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()

    def get_rate_limit(self) -> typing.Any:
        """
        Retrieves the current rate limit status for the GitHub API.

        Returns
        -------
        Any
            A dictionary containing rate limit details.

        Raises
        ------
        ChitraGuptBrowserNotInitializedError
            If the browser is not initialized.
        """
        if not self.initialized:
            raise exceptions.ChitraGuptBrowserNotInitializedError(
                "{} browser is not initialized. Please initialize the browser first.".format(
                    about.__repo_slug__
                )
            )

        url: str = "rate_limit"
        response = self.session.get(url=url)
        self._check_response(response)
        return response.json()
