from datetime import datetime
from typing import Optional

import github
import requests
from pydantic.v1 import BaseModel

from prowler.lib.logger import logger
from prowler.providers.github.lib.service.service import GithubService


class Repository(GithubService):
    def __init__(self, provider):
        super().__init__(__class__.__name__, provider)
        self.repositories = self._list_repositories()

    def _file_exists(self, repo, filename):
        """Check if a file exists in the repository. Returns True if exists, False if not, None if error."""
        try:
            return repo.get_contents(filename) is not None
        except Exception as error:
            if "404" in str(error):
                return False
            else:
                logger.error(
                    f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
                )
                return None

    def _validate_repository_format(self, repo_name: str) -> bool:
        """
        Validate repository name format.

        Args:
            repo_name: Repository name to validate

        Returns:
            bool: True if format is valid, False otherwise
        """
        if not repo_name or "/" not in repo_name:
            return False

        parts = repo_name.split("/")
        if len(parts) != 2:
            return False

        owner, repo = parts
        # Ensure both owner and repo names are non-empty
        if not owner.strip() or not repo.strip():
            return False

        return True

    def _get_accessible_repos_graphql(self) -> list[str]:
        """
        Use the GitHub GraphQL API to list all repositories that the authentication token has access to.
        This works with high-granularity (fine-grained) PATs.
        """
        graphql_url = "https://api.github.com/graphql"
        token = self.provider.session.token
        headers = {
            "Authorization": f"bearer {token}",
            "Content-Type": "application/json",
        }
        query = """
        {
          viewer {
            repositories(first: 100, affiliations: [OWNER, ORGANIZATION_MEMBER]) {
              nodes {
                nameWithOwner
              }
            }
          }
        }
        """

        try:
            response = requests.post(
                graphql_url, json={"query": query}, headers=headers
            )
            response.raise_for_status()
            data = response.json()

            if "errors" in data:
                logger.error(f"Error in GraphQL query: {data['errors']}")
                return []

            repo_nodes = (
                data.get("data", {})
                .get("viewer", {})
                .get("repositories", {})
                .get("nodes", [])
            )
            return [repo["nameWithOwner"] for repo in repo_nodes]

        except requests.exceptions.RequestException as error:
            logger.error(
                f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
            )
            return []

    def _list_repositories(self):
        """
        List repositories based on provider scoping configuration.
        """
        logger.info("Repository - Listing Repositories...")
        repos = {}
        try:
            for client in self.clients:
                if self.provider.repositories or self.provider.organizations:
                    if self.provider.repositories:
                        logger.info(
                            f"Filtering for specific repositories: {self.provider.repositories}"
                        )
                        for repo_name in self.provider.repositories:
                            if not self._validate_repository_format(repo_name):
                                logger.warning(
                                    f"Repository name '{repo_name}' should be in 'owner/repo-name' format. Skipping."
                                )
                                continue
                            try:
                                repo = client.get_repo(repo_name)
                                self._process_repository(repo, repos)
                            except Exception as error:
                                self._handle_github_api_error(
                                    error, "accessing repository", repo_name
                                )

                    if self.provider.organizations:
                        logger.info(
                            f"Filtering for repositories in organizations: {self.provider.organizations}"
                        )
                        for org_name in self.provider.organizations:
                            try:
                                repos_list, _ = self._get_repositories_from_owner(
                                    client, org_name
                                )
                                for repo in repos_list:
                                    self._process_repository(repo, repos)
                            except Exception as error:
                                self._handle_github_api_error(
                                    error, "processing organization", org_name
                                )
                else:
                    logger.info(
                        "No repository or organization specified, discovering accessible repositories via GraphQL API..."
                    )
                    accessible_repo_names = self._get_accessible_repos_graphql()

                    if not accessible_repo_names:
                        logger.warning(
                            "Could not find any accessible repositories with the provided token."
                        )

                    for repo_name in accessible_repo_names:
                        try:
                            repo = client.get_repo(repo_name)
                            logger.info(
                                f"Processing repository found via GraphQL: {repo.full_name}"
                            )
                            self._process_repository(repo, repos)
                        except Exception as error:
                            if hasattr(self, "_handle_github_api_error"):
                                self._handle_github_api_error(
                                    error,
                                    "accessing repository discovered via GraphQL",
                                    repo_name,
                                )
                            else:
                                logger.error(
                                    f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
                                )

        except github.RateLimitExceededException as error:
            logger.error(f"GitHub API rate limit exceeded: {error}")
            raise
        except github.GithubException as error:
            logger.error(f"GitHub API error while listing repositories: {error}")
        except Exception as error:
            logger.error(
                f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
            )
        return repos

    def _process_repository(self, repo, repos):
        """Process a single repository and extract all its information."""
        try:
            default_branch = repo.default_branch
            securitymd_exists = self._file_exists(repo, "SECURITY.md")
            # CODEOWNERS file can be in .github/, root, or docs/
            # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location
            codeowners_paths = [
                ".github/CODEOWNERS",
                "CODEOWNERS",
                "docs/CODEOWNERS",
            ]
            codeowners_files = [
                self._file_exists(repo, path) for path in codeowners_paths
            ]
            if True in codeowners_files:
                codeowners_exists = True
            elif all(file is None for file in codeowners_files):
                codeowners_exists = None
            else:
                codeowners_exists = False
            delete_branch_on_merge = (
                repo.delete_branch_on_merge
                if repo.delete_branch_on_merge is not None
                else False
            )

            require_pr = False
            approval_cnt = 0
            branch_protection = False
            required_linear_history = False
            allow_force_pushes = True
            branch_deletion = True
            require_code_owner_reviews = False
            require_signed_commits = False
            status_checks = False
            enforce_admins = False
            conversation_resolution = False
            try:
                branch = repo.get_branch(default_branch)
                if branch.protected:
                    protection = branch.get_protection()
                    if protection:
                        require_pr = (
                            protection.required_pull_request_reviews is not None
                        )
                        approval_cnt = (
                            protection.required_pull_request_reviews.required_approving_review_count
                            if require_pr
                            else 0
                        )
                        required_linear_history = protection.required_linear_history
                        allow_force_pushes = protection.allow_force_pushes
                        branch_deletion = protection.allow_deletions
                        status_checks = protection.required_status_checks is not None
                        enforce_admins = protection.enforce_admins
                        conversation_resolution = (
                            protection.required_conversation_resolution
                        )
                        branch_protection = True
                        require_code_owner_reviews = (
                            protection.required_pull_request_reviews.require_code_owner_reviews
                            if require_pr
                            else False
                        )
                        require_signed_commits = branch.get_required_signatures()
            except Exception as error:
                # If the branch is not found, it is not protected
                if "404" in str(error):
                    logger.warning(
                        f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
                    )
                # Any other error, we cannot know if the branch is protected or not
                else:
                    require_pr = None
                    approval_cnt = None
                    branch_protection = None
                    required_linear_history = None
                    allow_force_pushes = None
                    branch_deletion = None
                    require_code_owner_reviews = None
                    require_signed_commits = None
                    status_checks = None
                    enforce_admins = None
                    conversation_resolution = None
                    logger.error(
                        f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
                    )

            secret_scanning_enabled = False
            dependabot_alerts_enabled = False
            try:
                if (
                    repo.security_and_analysis
                    and repo.security_and_analysis.secret_scanning
                ):
                    secret_scanning_enabled = (
                        repo.security_and_analysis.secret_scanning.status == "enabled"
                    )
                try:
                    # Use get_dependabot_alerts to check if Dependabot alerts are enabled
                    repo.get_dependabot_alerts().totalCount
                    # If the call succeeds, Dependabot is enabled (even if no alerts)
                    dependabot_alerts_enabled = True
                except Exception as error:
                    error_str = str(error)
                    if (
                        "403" in error_str
                        and "Dependabot alerts are disabled for this repository."
                        in error_str
                    ):
                        dependabot_alerts_enabled = False
                    else:
                        logger.error(
                            f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
                        )
                        dependabot_alerts_enabled = None
            except Exception as error:
                logger.error(
                    f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
                )
                secret_scanning_enabled = None
                dependabot_alerts_enabled = None
            repos[repo.id] = Repo(
                id=repo.id,
                name=repo.name,
                owner=repo.owner.login,
                full_name=repo.full_name,
                default_branch=Branch(
                    name=default_branch,
                    protected=branch_protection,
                    default_branch=True,
                    require_pull_request=require_pr,
                    approval_count=approval_cnt,
                    required_linear_history=required_linear_history,
                    allow_force_pushes=allow_force_pushes,
                    branch_deletion=branch_deletion,
                    status_checks=status_checks,
                    enforce_admins=enforce_admins,
                    conversation_resolution=conversation_resolution,
                    require_code_owner_reviews=require_code_owner_reviews,
                    require_signed_commits=require_signed_commits,
                ),
                private=repo.private,
                archived=repo.archived,
                pushed_at=repo.pushed_at,
                securitymd=securitymd_exists,
                codeowners_exists=codeowners_exists,
                secret_scanning_enabled=secret_scanning_enabled,
                dependabot_alerts_enabled=dependabot_alerts_enabled,
                delete_branch_on_merge=delete_branch_on_merge,
            )
        except Exception as error:
            logger.error(
                f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
            )


class Branch(BaseModel):
    """Model for Github Branch"""

    name: str
    protected: Optional[bool]
    default_branch: bool
    require_pull_request: Optional[bool]
    approval_count: Optional[int]
    required_linear_history: Optional[bool]
    allow_force_pushes: Optional[bool]
    branch_deletion: Optional[bool]
    status_checks: Optional[bool]
    enforce_admins: Optional[bool]
    require_code_owner_reviews: Optional[bool]
    require_signed_commits: Optional[bool]
    conversation_resolution: Optional[bool]


class Repo(BaseModel):
    """Model for Github Repository"""

    id: int
    name: str
    owner: str
    full_name: str
    default_branch: Branch
    private: bool
    archived: bool
    pushed_at: datetime
    securitymd: Optional[bool]
    codeowners_exists: Optional[bool]
    secret_scanning_enabled: Optional[bool]
    dependabot_alerts_enabled: Optional[bool]
    delete_branch_on_merge: Optional[bool]
