import base64
import json
import os
from typing import Optional

import requests
from actions_logging.app_logging import logger
from common.common import raise_with_context
from github.constants import API_REPOS_URL, DEFAULT_TOKEN_ENV_VAR, GH_BASE_URL, GITHUB_TOKEN_ENV_VAR, OWNER
from github.env import exit_on_error_and_write_summary
from validation.regex import regex_match


class GithubClient:
    _instance = None

    def __new__(cls, token: str = None, token_env_var: str = None, singleton: bool = True):
        # Return singleton instance if requested
        if singleton and cls._instance is not None:
            return cls._instance

        # Create a new instance (either for singleton or independent instance)
        instance = super(GithubClient, cls).__new__(cls)

        # Store as singleton if this is the first time and singleton is requested
        if singleton and cls._instance is None:
            cls._instance = instance

        return instance

    def __init__(self, token: str = None, token_env_var: str = None, singleton: bool = True):
        # Only initialize if not already initialized
        if not hasattr(self, "initialized") or not singleton:
            self.token = token
            self.token_env_var = token_env_var
            self.headers = None
            self.initialized = True

    def get_headers(self):
        if not self.headers:
            self.headers = create_git_headers(self.token, self.token_env_var)
        return self.headers


def get_github_client(token: str = None, token_env_var: str = None, singleton: bool = True) -> GithubClient:
    """
    Creates and returns an instance of `GithubClient`.

    Args:
        token (str, optional): A GitHub personal access token. If not provided,
            the `token_env_var` will be used to retrieve the token from the environment.
        token_env_var (str, optional): The name of the environment variable that
            contains the GitHub token. Used if `token` is not provided.
        singleton (bool, optional): If True, ensures that the same instance of
            `GithubClient` is returned for subsequent calls. Defaults to True.

    Returns:
        GithubClient: An instance of the `GithubClient` class.
    """

    return GithubClient(token, token_env_var, singleton)


def create_git_headers(token: str = None, token_env_var: str = None):
    """
    Generate HTTP headers for GitHub API requests.

    This function creates a dictionary of headers required for making requests
    to the GitHub API. It retrieves the GitHub token either from the provided
    arguments or from environment variables.

    Args:
        token (str, optional): A GitHub token provided directly as a string.
        token_env_var (str, optional): The name of the environment variable
            containing the GitHub token.

    Returns:
        dict: A dictionary containing the headers for GitHub API requests.

    Raises:
        RuntimeError: If the token environment variable is not found or is empty.
        Exception: If any other error occurs during header creation.

    Notes:
        - If `token` is not provided, the function attempts to retrieve the token
          from the environment variable specified by `token_env_var`.
        - If `token_env_var` is not provided, the function uses the environment
          variable `GITHUB_TOKEN_ENV_VAR` to determine the name of the token
          environment variable. If that is not found, it defaults to
          `GLOBAL_CICD_GIT_TOKEN`.
        - Logs warnings or info messages when falling back to default behavior.
    """
    try:

        def get_token_from_env_var(env_var_key):
            token = os.getenv(env_var_key, None)
            if token is None:
                raise RuntimeError(f"Token env var {env_var_key} not found")
            if not token:
                raise RuntimeError(f"Token env var {env_var_key} is empty")
            return token

        github_token = token
        if not github_token:
            if not token_env_var:
                token_env_var = os.getenv(GITHUB_TOKEN_ENV_VAR, None)
            if not token_env_var:
                logger.warning(f"Token environment variable name {GITHUB_TOKEN_ENV_VAR}  not found")
                token_env_var = DEFAULT_TOKEN_ENV_VAR
            logger.info(f"Token environment variable name {token_env_var} will be used to fetch the github token")
            github_token = get_token_from_env_var(token_env_var)

        headers = {
            "Accept": "application/vnd.github+json",
            "Authorization": f"Bearer {github_token}",
            "X-GitHub-Api-Version": "2022-11-28",
            "Content-Type": "application/x-www-form-urlencoded",
        }
        return headers
    except Exception as e:
        exit_on_error_and_write_summary(f"Failed creating github headers: {e}")


def paginated_get(
    url: str, page_size: int = 100, items_list_key: Optional[str] = None, client: Optional[GithubClient] = None
) -> list[dict]:
    """
    Perform a paginated GET request to the given URL.
    This function will keep fetching pages until all items are retrieved.

    :param url: The URL to fetch data from.
    :param page_size: The number of items to fetch per page.
    :param key_containing_items: Key containing items list. If unset, the entire response is considered list of items.
    :param client: Optional GithubClient instance. If not provided, a new instance will be created.
    :return: A list of all items retrieved from the paginated GET request.
    """
    try:
        headers = (client or get_github_client()).get_headers()
        page = 1
        items = []

        logger.info(f"Making a paginated get from {url}, {page_size} items per page")
        while True:
            params = {"per_page": page_size, "page": page}
            logger.debug(f"Fetching from {url} with params {params}")
            response = requests.get(url, headers=headers, params=params)
            response.raise_for_status()

            if response.status_code == 200:
                if items_list_key:
                    results = response.json().get(items_list_key, [])
                else:
                    results = response.json()
                logger.debug(f"Found {len(results)} items in page {page} of {url}")
                items.extend(results)

                # If the response contains a 'Link' header, it indicates the response is paginated
                # If so, we need to check if there are more pages to fetch
                response_header_link = response.headers.get("Link", "")
                if 'rel="next"' in response_header_link:
                    logger.debug("Response header indicates more pages available. Moving to next page.")
                    page += 1
                else:
                    logger.debug("No more pages available. Breaking out of the loop.")
                    break
            else:
                raise_with_context(
                    None,
                    RuntimeError,
                    f"Failed to make paginated get request to {url} at page {page} ({page_size} per page): "
                    + response.text,
                )

        logger.info(f"Found a total of {len(items)} results in {url}")
        return items
    except Exception as e:
        raise_with_context(e)


def create_repository_dispatch_event(
    full_repo_name: str, event_type_name: str, client_payload: str, client: Optional[GithubClient] = None
):
    """
    Triggers a repository dispatch event on GitHub.

    Parameters:
    full_repo_name (str): The full name of the repository on which to trigger the event.
                          It should be in the format 'owner/repo'.
    event_type_name (str): The type of the event to dispatch. This is a custom string that
                           can be used to differentiate between different types of dispatch events.
    client_payload (str): A JSON-formatted string that will be included in the payload of the dispatch event.
                          This can be used to send additional information about the event.
                          for example: '{"scriptsVersion": "1.2.3"}'

    Returns:
    bool: True if the dispatch event was successfully created, otherwise the function will exit the script with code 1.

    Raises:
    SystemExit: If the response status code from the GitHub API is not 204, indicating that the dispatch event
                was not successfully created, the function will log the error, write a message to the
                GitHub step summary, and exit the script with code 1.
    """
    try:
        headers = (client or get_github_client()).get_headers()
        payload = {"event_type": event_type_name, "client_payload": json.loads(client_payload)}
        response = requests.post(f"{API_REPOS_URL}/{full_repo_name}/dispatches", headers=headers, json=payload)
        response.raise_for_status()
        logger.info(
            (
                f"api request to create repository dispatch event to {full_repo_name} with "
                f"payload {payload} return {response.status_code}"
            )
        )
        if response.status_code != 204:
            logger.warning(f"{response.status_code} received. and text {response.text}. should be 204")
        return True
    # Raises an HTTPError if the status is 4xx or 5xx
    except requests.exceptions.HTTPError as http_err:
        exit_on_error_and_write_summary(f"HTTP error occurred in create_repository_dispatch_event: {http_err}")
    except Exception as err:
        exit_on_error_and_write_summary(f"Other error occurred in create_repository_dispatch_event: {err}")


def get_changed_files(full_repo_name: str, pr_number: str, client: Optional[GithubClient] = None):
    """
    Based on the PR id, get list of changed files
    Returns full file details as JSON objects, including status, additions, deletions, etc
    """
    # see https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests-files
    try:
        url: str = f"{API_REPOS_URL}/{full_repo_name}/pulls/{pr_number}/files?per_page=100"
        headers = (client or get_github_client()).get_headers()
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        if response.status_code == 200:
            logger.info(f"We managed to fetch the list of files that belongs to PR {pr_number}")
            pr_files = response.json()
            return pr_files
        else:
            logger.warning(
                f"{response.status_code} received in get_changed_files. and text {response.text}. should be 200"
            )
    except Exception as e:
        exit_on_error_and_write_summary(
            f"ERROR. Exception: {e} occurred while fetching the list of files that belongs to PR {pr_number}"
        )


def get_git_all_tags(repo, dir_name, tags=[], next_url="", client: Optional[GithubClient] = None) -> list[str]:
    try:
        if not client:
            client = get_github_client()
        headers = client.get_headers()
        git_url = f"{API_REPOS_URL}/{repo}/tags?per_page=100"
        if next_url:
            git_url = next_url
        response = requests.get(git_url, headers=headers)
        if response.status_code != 200:
            exit_on_error_and_write_summary(
                (
                    f"error in get repo tags - status code: {response.status_code} - "
                    f"response text: {response.text} - exiting.."
                )
            )
        res_tags = response.json()
        for tag in res_tags:
            tag_name = tag["name"]
            try:
                if tag_name.split("-")[1] == dir_name:
                    tags.append(tag_name)
            except Exception:
                pass
        if response.links:
            if response.links.get("next"):
                if response.links["next"].get("url"):
                    next_page = response.links["next"]["url"]
                    get_git_all_tags(repo, dir_name=dir_name, tags=tags, next_url=next_page, client=client)
        return tags
    except Exception as e:
        exit_on_error_and_write_summary(f"error in get_git_all_tags- {e}")


def get_default_branch(full_repo_name, client: Optional[GithubClient] = None):
    try:
        url: str = f"{API_REPOS_URL}/{full_repo_name}"
        headers = (client or get_github_client()).get_headers()
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        def_branch = response.json()["default_branch"]
        return def_branch
    except requests.exceptions.HTTPError as http_err:
        exit_on_error_and_write_summary(f"HTTP error occurred in get_default_branch: {http_err}")
    except Exception as err:
        exit_on_error_and_write_summary(f"Other error occurred in get_default_branch: {err}")


def get_repository_branches(
    full_repo_name: str, branch_pattern: str = None, page_size: int = 100, client: Optional[GithubClient] = None
) -> list[dict]:
    """
    Get all branches in the given GitHub repository.

    :param full_repo_name: Repository in the format "owner/repo"
    :param branch_pattern: Optional regex pattern to match against branch names.
    :param page_size: Number of items to fetch per page.
    :return: List of branch names.
    """
    try:
        all_branches = paginated_get(f"{API_REPOS_URL}/{full_repo_name}/branches", page_size=page_size, client=client)
        logger.debug(f"Found {len(all_branches)} branches in repository {full_repo_name}")
        if branch_pattern:
            logger.debug(f"Filtering branches with pattern {branch_pattern}")
            filtered_branches = [branch for branch in all_branches if regex_match(branch["name"], branch_pattern)]
            logger.info(
                (
                    f"Found {len(filtered_branches)} branches matching the pattern {branch_pattern} in "
                    f"repository {full_repo_name}"
                )
            )
            return filtered_branches

        return all_branches
    except Exception as e:
        raise_with_context(e)


def branch_exist(
    full_repo_name: str, branch_name: str, branch_pattern: str = None, client: Optional[GithubClient] = None
) -> bool:
    """
    Check if a specific given branch exists in the repository. If branch_name is not set, then it will check if any
    branch matches the regex pattern set in branch_pattern.

    :param full_repo_name: Repository in the format "owner/repo"
    :param branch_name: Name of the branch to check in the repository
    :param branch_pattern: Optional regex pattern to match against branch names. Only used if branch_name isnt provided.
    :return: True if the branch exists in the repository, otherwise False.
    """

    try:
        headers = (client or get_github_client()).get_headers()
        if branch_name:
            branches_url = f"{API_REPOS_URL}/{full_repo_name}/branches/{branch_name}"
            logger.debug(f"Checking if branch {branch_name} exists in repository {full_repo_name}")
            branches_response = requests.get(branches_url, headers=headers)
            branches_response.raise_for_status()

            if branches_response.status_code == 200:
                logger.debug(f"Branch {branch_name} exists in repository {full_repo_name}")
                return True
            else:
                logger.debug(
                    (
                        f"Branch {branch_name} does not exist in repository {full_repo_name}: "
                        f"HTTP response code {branches_response.status_code}: {branches_response.text}"
                    )
                )
                return False
        elif branch_pattern:
            logger.debug(f"Checking if any branch matches the pattern {branch_pattern} in repository {full_repo_name}")
            branches = get_repository_branches(full_repo_name, branch_pattern)
            return len(branches) > 0
        else:
            raise_with_context(
                None, ValueError, "No branch name or pattern provided to check for existence in the repository."
            )
    except Exception as e:
        raise_with_context(
            e, RuntimeError, f"Error checking if branch {branch_name} exists in repository {full_repo_name}"
        )


def get_diff_files_between_branches(full_repo_name, base_branch, head_branch, client: Optional[GithubClient] = None):
    try:
        headers = (client or get_github_client()).get_headers()
        url = f"{API_REPOS_URL}/{full_repo_name}/compare/{base_branch}...{head_branch}?per_page=100"
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        if response.status_code == 200:
            diff_data = response.json()
            files = [file["filename"] for file in diff_data["files"]]
            return files
    except requests.exceptions.HTTPError as http_err:
        exit_on_error_and_write_summary(f"HTTP error occurred in get_diff_files_between_branches: {http_err}")
    except Exception as err:
        exit_on_error_and_write_summary(f"Other error occurred in get_diff_files_between_branches: {err}")


def get_file_content(repo_name: str, file_path: str, ref: str = "", client: Optional[GithubClient] = None):
    try:
        headers = (client or get_github_client()).get_headers()
        url = f"{API_REPOS_URL}/{repo_name}/contents/{file_path}"
        if ref:
            url += f"?ref={ref}"
        logger.info(f"file contents url: {url}")
        res = requests.get(url, headers=headers)
        res.raise_for_status()
        logger.info_green(f"successfully fetched {file_path}")
        file_content = res.json().get("content")
        if not file_content:
            logger.warning(f"Empty file {file_path}")
            return file_content
        decoded_content = base64.b64decode(file_content).decode("utf-8")
        return decoded_content
    except requests.exceptions.HTTPError as http_err:
        exit_on_error_and_write_summary(f"HTTP error occurred in get_file_content: {http_err}")
    except Exception as e:
        exit_on_error_and_write_summary(f"unexpected error in get_file_content: {e}")


def get_tags(repo_name: str, next_page="", client: Optional[GithubClient] = None):
    tags = []
    try:
        headers = (client or get_github_client()).get_headers()
        url = f"{API_REPOS_URL}/{repo_name}/tags?per_page=100"
        if next_page:
            url = next_page
        res = requests.get(url, headers=headers)
        res.raise_for_status()
        for tag_data in res.json():
            tag = tag_data["name"]
            tags.append(tag)
        if res.links:
            if res.links.get("next"):
                if res.links["next"].get("url"):
                    next_page = res.links["next"]["url"]
                    tags.extend(get_tags(repo_name, next_page=next_page))
        return tags
    except requests.exceptions.HTTPError as http_err:
        exit_on_error_and_write_summary(f"HTTP error occurred in get_tags: {http_err}")
    except Exception as e:
        exit_on_error_and_write_summary(f"unexpected error in get_tags: {e}")


def get_github_path_data(repo_name: str, file_path: str, ref: str = None, client: Optional[GithubClient] = None):
    """
    this function will return the metadata of a file or folder in a github repo
    can be used to get file content or the list of files in a folder
    """
    try:
        extra_args = f"?ref={ref}" if ref else ""
        headers = (client or get_github_client()).get_headers()
        api_url = f"{API_REPOS_URL}/{repo_name}/contents/{file_path}{extra_args}"
        logger.debug(f"Fetching URL: {api_url}")
        response = requests.get(api_url, headers=headers)
        if response.status_code == 200:
            return response.json()
        else:
            exit_on_error_and_write_summary(
                f"Error fetching the folder contents: {response.status_code}, {response.text}"
            )
    except requests.exceptions.HTTPError as http_err:
        exit_on_error_and_write_summary(f"HTTP error occurred in get_file_content: {http_err}")
    except Exception as e:
        exit_on_error_and_write_summary(f"unexpected error in get_file_content: {e}")


def get_commits_after_last_tag(
    repo_name: str, tag, commits=[], next_page="", client: Optional[GithubClient] = None
) -> list:
    try:
        if not tag:
            return commits

        headers = (client or get_github_client()).get_headers()

        url = f"{API_REPOS_URL}/{repo_name}/compare/{tag}...HEAD?per_page=100"

        if next_page:
            url = next_page
        res = requests.get(url, headers=headers)
        res.raise_for_status()
        comparison = res.json()

        if "commits" not in comparison:
            return commits

        for commit in comparison["commits"]:
            message = commit["commit"]["message"].split("\n")[0]
            commits.insert(0, message)
        if res.links:
            if res.links.get("next"):
                if res.links["next"].get("url"):
                    next_page = res.links["next"]["url"]
                    return get_commits_after_last_tag(repo_name, tag=tag, commits=commits, next_page=next_page)
        return commits
    except requests.exceptions.HTTPError as http_err:
        exit_on_error_and_write_summary(f"HTTP error occurred in get_tags: {http_err}")
    except Exception as e:
        exit_on_error_and_write_summary(f"unexpected error in get_tags: {e}")


def get_repo_runtime(repo: str, client: Optional[GithubClient] = None) -> str:
    try:
        headers = (client or get_github_client()).get_headers()
        response = requests.get(f"{API_REPOS_URL}/{repo}/languages", headers=headers)
        languages = response.json()

        # Finding the language with the maximum value
        language = max(languages, key=languages.get)
        logger.info_green(f"Repository language is {language}, will use appropriate runtime")
        return language
    except requests.exceptions.HTTPError as http_err:
        exit_on_error_and_write_summary(f"HTTP error occurred in get_repo_runtime: {http_err}")
    except Exception as e:
        exit_on_error_and_write_summary(f"unexpected error in get_repo_runtime: {e}")


def create_env_in_repo(repo: str, env_name: str, client: Optional[GithubClient] = None) -> bool:
    """
    Create an environment in the given GitHub repository.

    :param repo: Repository in the format "owner/repo"
    :param env_name: Name of the environment to create
    :return: True if the environment was successfully created, otherwise the function will exit the script with code 1.
    """
    try:
        if not repo:
            raise_with_context(None, ValueError, "Repository name is required to create an environment")
        if not env_name:
            raise_with_context(None, ValueError, "Environment name is required to create an environment")

        url = f"{API_REPOS_URL}/{repo}/environments/{env_name}"
        headers = (client or get_github_client()).get_headers()

        logger.debug(f"Creating environment {env_name} in repo {repo} using  URL: {url}")
        response = requests.put(url, headers=headers)
        response.raise_for_status()

        if response.status_code == 200:
            logger.info(f"Environment {env_name} created successfully in repo {repo}")
            return True
        else:
            raise_with_context(
                None,
                RuntimeError,
                (
                    f"Failed to create environment {env_name} in repo {repo}. HTTP status code {response.status_code}. "
                    f"Response: {response.json()}"
                ),
            )
    except Exception as e:
        raise_with_context(e, RuntimeError, f"Error creating environment {env_name} in repo {repo}")


def get_pull_requests(
    repo: str, state: str = "open", page_size: int = 100, client: Optional[GithubClient] = None
) -> list[dict]:
    """
    Get all pull requests for the given GitHub repository.

    :param repo: Repository in the format "owner/repo"
    :param state: State of the pull requests to fetch. Can be "open", "closed", or "all".
    :param page_size: Number of items to fetch per page.
    :param client: Optional GithubClient instance. If not provided, a new instance will be created.
    :return: List of pull requests.
    """
    if state not in ["open", "closed", "all"]:
        raise_with_context(
            None,
            ValueError,
            "Invalid state provided. Must be one of: open, closed, all",
        )

    try:
        logger.debug(f"Fetching all repositories for organization {OWNER}")
        return paginated_get(f"{API_REPOS_URL}/{repo}/pulls?state={state}", page_size=page_size, client=client)
    except Exception as e:
        raise_with_context(e)


def create_pull_request(
    repo: str, source_branch: str, target_branch: str, title: str, body: str, client: Optional[GithubClient] = None
) -> tuple[int, str]:
    """
    Create a pull request in the given GitHub repository.

    :param repo: Repository in the format "owner/repo"
    :param source_branch: Source branch for the pull request
    :param target_branch: Target branch for the pull request
    :param title: Title of the pull request
    :param body: Body of the pull request
    :return: Tuple containing the pull request number and URL
    """
    try:
        if not repo:
            raise_with_context(None, ValueError, "Repository name is required to create a pull request")
        if not source_branch:
            raise_with_context(None, ValueError, "Source branch is required to create a pull request")
        if not target_branch:
            raise_with_context(None, ValueError, "Target branch is required to create a pull request")
        if not title:
            raise_with_context(None, ValueError, "Title is required to create a pull request")

        url = f"{API_REPOS_URL}/{repo}/pulls"
        headers = (client or get_github_client()).get_headers()
        payload = {"title": title, "body": body, "head": source_branch, "base": target_branch}

        logger.debug(f"Creating pull request in repo {repo} using URL {url} and payload {payload}")
        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()

        if response.status_code == 201:
            response_json = response.json()
            pr_number = response_json.get("number")
            pr_url = response_json.get("html_url")
            logger.info(f"Pull request {pr_number} created successfully in repo {repo}")
            return pr_number, pr_url
        else:
            raise_with_context(
                None,
                RuntimeError,
                (
                    f"Failed to create pull request in repo {repo}. HTTP status code {response.status_code}. "
                    f"Response: {response.json()}"
                ),
            )
    except Exception as e:
        raise_with_context(e, RuntimeError, f"Error creating pull request in repo {repo}")


def request_reviewers(
    repo: str,
    pr_number: int,
    reviewers: list,
    team_reviewers: list = None,
    client: Optional[GithubClient] = None,
) -> bool:
    """
    Request reviewers for a pull request.

    :param repo: Repository in the format "owner/repo"
    :param pr_number: The pull request number
    :param reviewers: List of GitHub usernames to request
    :param team_reviewers: List of team slugs (optional, org only)
    :param client: Optional GithubClient instance. If not provided, a new instance will be created.
    :return: True if the request was successful, otherwise False.
    """
    try:
        url = f"{API_REPOS_URL}/{repo}/pulls/{pr_number}/requested_reviewers"
        headers = (client or get_github_client()).get_headers()
        payload = {}

        if not reviewers and not team_reviewers:
            logger.warning("No reviewers provided to request for the pull request. Skipping the request.")
            return

        if reviewers:
            logger.debug(f"Adding individual reviewers {reviewers} to the payload")
            payload["reviewers"] = reviewers
        if team_reviewers:
            logger.debug(f"Adding team reviewers {team_reviewers} to the payload")
            payload["team_reviewers"] = team_reviewers

        logger.debug(f"Requesting reviewers for PR #{pr_number} in repo {repo} from {payload}")
        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()

        if response.status_code == 201:
            logger.info(f"Requested reviewers successfully added for PR #{pr_number}")
            return True
        else:
            logger.error(
                f"Failed to request reviewers. Status code: {response.status_code}. Response: {response.json()}"
            )
            return False
    except Exception as e:
        raise_with_context(e, RuntimeError, f"Error requesting reviewers for PR #{pr_number} in repo {repo}")
        return False


def path_exists_in_repository(repo, path_to_check, client: Optional[GithubClient] = None) -> bool:
    """
    Check if the given path exists in the repository.

    :param repo: Repository in the format "owner/repo"
    :param path_to_check: Path to check in the repository
    :return: True if the path exists in the repository, otherwise False.
    """

    try:
        headers = (client or get_github_client()).get_headers()
        contents_url = f"{API_REPOS_URL}/{repo}/contents/{path_to_check}"
        logger.debug(f"Checking if path {path_to_check} exists in repository {repo}")
        contents_response = requests.get(contents_url, headers=headers)
        contents_response.raise_for_status()

        path_exists = contents_response.status_code == 200
        logger.info(f"Checking if path {path_to_check} exists in repository {repo} returned {path_exists}")
        return path_exists
    except Exception as e:
        raise_with_context(e, RuntimeError, f"Error checking if path {path_to_check} exists in repository {repo}")


def get_all_repositories(page_size: int = 100, client: Optional[GithubClient] = None) -> list[dict]:
    """
    Get all the repositories in the organization.

    :return: List of dictionaries containing the repository details.
    """

    try:
        logger.debug(f"Fetching all repositories for organization {OWNER}")
        return paginated_get(f"{GH_BASE_URL}/orgs/{OWNER}/repos", page_size=page_size, client=client)
    except Exception as e:
        raise_with_context(e)


def get_team_members(team_slug: str, client: Optional[GithubClient] = None) -> list[dict]:
    """
    Get all the members of a team.

    :param team_slug: The slug of the team.
    :return: List of dictionaries containing the team member details.
    """

    try:
        headers = (client or get_github_client()).get_headers()
        members_url = f"{GH_BASE_URL}/orgs/{OWNER}/teams/{team_slug}/members"
        logger.debug(f"Fetching team members from URL: {members_url}")
        response = requests.get(members_url, headers=headers)
        response.raise_for_status()

        if response.status_code == 200:
            logger.debug(f"Found {len(response.json())} members in team {team_slug}")
            return response.json()
        else:
            raise_with_context(
                None, RuntimeError, f"Failed to get team members: Response Code {response.status_code}: {response.text}"
            )
    except requests.exceptions.HTTPError as http_err:
        raise_with_context(http_err, RuntimeError, f"HTTP error occurred in get_team_members: {http_err}")
    except Exception as e:
        raise_with_context(e)


def get_all_users(page_size: int = 100, client: Optional[GithubClient] = None) -> list[dict]:
    """
    Get all the users in the organization.

    :param page_size: Number of items to fetch per page.
    :return: List of dictionaries containing the user details.
    """

    try:
        logger.debug(f"Fetching all users for organization {OWNER}")
        return paginated_get(f"{GH_BASE_URL}/orgs/{OWNER}/members", page_size=page_size, client=client)
    except Exception as e:
        raise_with_context(e)


def get_user_by_username(username: str, client: Optional[GithubClient] = None) -> dict | None:
    """
    Get GitHub user details by username.

    :param username: The GitHub username.
    :param client: Optional GithubClient instance. If not provided, a new instance will be created.
    :return: Dictionary containing user details.
    """
    try:
        if not username:
            raise_with_context(None, ValueError, "Username is required to get user details")
        headers = (client or get_github_client()).get_headers()
        user_url = f"{GH_BASE_URL}/users/{username}"
        logger.debug(f"Fetching user details from URL: {user_url}")
        response = requests.get(user_url, headers=headers)
        response.raise_for_status()

        if response.status_code == 200:
            logger.debug(f"Found user {username}: {response.json()}")
            return response.json()
        else:
            logger.warning(
                f"Failed to get user details for {username}: Response Code {response.status_code}: {response.text}"
            )
            return None
    except requests.exceptions.HTTPError as http_err:
        raise_with_context(http_err, RuntimeError, f"HTTP error occurred in get_user_by_username: {http_err}")
    except Exception as e:
        raise_with_context(e, RuntimeError, f"Error occurred in get_user_by_username: {e}")


def get_github_user_email(username: str) -> str | None:
    """
    Get the email of a GitHub user by their username.

    :param username: The GitHub username of the user.
    :return: The email of the user if available, otherwise None.
    """
    try:
        logger.info(f"Looking up GitHub user by username: {username}")
        user = get_user_by_username(username)
        if user:
            email = user.get("email", None)
            logger.debug(f"Email found for user {username}: {email}")
            return email
        else:
            logger.warning(f"GitHub user not found for username: {username}")
            return None
    except Exception as e:
        raise_with_context(e, RuntimeError, f"Failed to get user {username} from GitHub API")
