"""GitHub API collector - maintainer info, issues, sponsors status."""

import logging
import os
import re
import time
from dataclasses import dataclass, field
from typing import Any, Optional

import httpx

from ossuary.collectors.base import BaseCollector

logger = logging.getLogger(__name__)


@dataclass
class IssueData:
    """Extracted issue/PR data."""

    number: int
    title: str
    body: str
    state: str
    is_pull_request: bool
    author_login: str
    created_at: str
    updated_at: str
    closed_at: Optional[str]
    comments: list[dict[str, Any]] = field(default_factory=list)


@dataclass
class GitHubData:
    """Data collected from GitHub API."""

    # Repository info
    owner: str = ""
    repo: str = ""
    owner_type: str = ""  # User or Organization

    # Maintainer info
    maintainer_username: str = ""
    maintainer_public_repos: int = 0
    maintainer_total_stars: int = 0
    maintainer_account_created: str = ""  # ISO date string
    maintainer_repos: list[dict] = field(default_factory=list)  # Full repo data for reputation
    maintainer_sponsor_count: int = 0
    maintainer_orgs: list[str] = field(default_factory=list)
    is_tier1_maintainer: bool = False  # Deprecated, use reputation scorer
    has_github_sponsors: bool = False

    # Organization info
    is_org_owned: bool = False
    org_admin_count: int = 0

    # CII badge
    cii_badge_level: str = "none"

    # Issues and PRs
    issues: list[IssueData] = field(default_factory=list)


class GitHubCollector(BaseCollector):
    """Collector for GitHub API data."""

    API_BASE = "https://api.github.com"
    GRAPHQL_URL = "https://api.github.com/graphql"

    # Rate limiting — with token: 5000 req/hr (~1.4/sec), without: 60/hr
    REQUEST_DELAY = 0.1
    REQUEST_DELAY_UNAUTHENTICATED = 1.0
    RATE_LIMIT_PAUSE = 60

    # Tier-1 thresholds
    TIER1_REPOS = 500
    TIER1_STARS = 100_000

    def __init__(self, token: Optional[str] = None):
        """
        Initialize GitHub collector.

        Args:
            token: GitHub personal access token. Defaults to GITHUB_TOKEN env var.
                   Multiple tokens can be provided via GITHUB_TOKEN, GITHUB_TOKEN_SUSE, etc.
        """
        self.tokens = self._collect_tokens(token)
        self.token_index = 0
        self.token = self.tokens[0] if self.tokens else None
        self.client = httpx.AsyncClient(timeout=30.0, follow_redirects=True)

        if self.token:
            self.client.headers["Authorization"] = f"Bearer {self.token}"
        self.client.headers["Accept"] = "application/vnd.github.v3+json"

    @staticmethod
    def _collect_tokens(explicit_token: Optional[str] = None) -> list[str]:
        """Collect all available GitHub tokens from env vars."""
        tokens = []
        if explicit_token:
            tokens.append(explicit_token)
        else:
            for key, val in os.environ.items():
                if key.startswith("GITHUB_TOKEN") and val:
                    tokens.append(val)
        return tokens

    def _rotate_token(self) -> bool:
        """Switch to next available token. Returns True if rotated, False if no more tokens."""
        if len(self.tokens) <= 1:
            return False
        self.token_index = (self.token_index + 1) % len(self.tokens)
        self.token = self.tokens[self.token_index]
        self.client.headers["Authorization"] = f"Bearer {self.token}"
        logger.info(f"Rotated to GitHub token {self.token_index + 1}/{len(self.tokens)}")
        return True

    def is_available(self) -> bool:
        """Check if GitHub token is available."""
        return bool(self.token)

    @staticmethod
    def parse_repo_url(repo_url: str) -> tuple[Optional[str], Optional[str]]:
        """
        Parse owner and repo from GitHub URL.

        Args:
            repo_url: GitHub repository URL

        Returns:
            Tuple of (owner, repo) or (None, None) if parsing fails
        """
        patterns = [
            r"github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?/?$",
            r"github\.com[:/]([^/]+)/([^/]+)",
        ]

        for pattern in patterns:
            match = re.search(pattern, repo_url)
            if match:
                return match.group(1), match.group(2).replace(".git", "")

        return None, None

    async def _request(self, method: str, url: str, _rotated: bool = False, **kwargs) -> Optional[dict]:
        """Make a rate-limit-aware request."""
        delay = self.REQUEST_DELAY if self.token else self.REQUEST_DELAY_UNAUTHENTICATED
        time.sleep(delay)

        try:
            response = await self.client.request(method, url, **kwargs)

            # Check rate limit
            remaining = int(response.headers.get("X-RateLimit-Remaining", 1))
            if remaining == 0:
                reset_time = int(response.headers.get("X-RateLimit-Reset", 0))
                wait_time = max(reset_time - time.time(), self.RATE_LIMIT_PAUSE)
                if not _rotated and self._rotate_token():
                    logger.warning("Rate limited. Rotated token, retrying immediately.")
                    return await self._request(method, url, _rotated=True, **kwargs)
                logger.warning(f"Rate limited. Waiting {wait_time:.0f} seconds...")
                time.sleep(wait_time)
                return await self._request(method, url, **kwargs)

            if response.status_code == 404:
                return None

            response.raise_for_status()
            return response.json()

        except httpx.HTTPError as e:
            logger.error(f"GitHub API error: {e}")
            return None

    async def _get(self, endpoint: str, params: Optional[dict] = None) -> Optional[dict]:
        """GET request to GitHub REST API."""
        url = f"{self.API_BASE}{endpoint}" if not endpoint.startswith("http") else endpoint
        return await self._request("GET", url, params=params)

    async def _graphql(self, query: str, variables: Optional[dict] = None,
                       _rotated: bool = False) -> Optional[dict]:
        """Execute GraphQL query."""
        payload = {"query": query}
        if variables:
            payload["variables"] = variables

        time.sleep(self.REQUEST_DELAY)

        try:
            response = await self.client.post(self.GRAPHQL_URL, json=payload)

            if response.status_code == 403 and not _rotated and self._rotate_token():
                logger.warning("GraphQL rate limited. Rotated token, retrying.")
                return await self._graphql(query, variables, _rotated=True)

            response.raise_for_status()
            data = response.json()

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

            return data.get("data")

        except httpx.HTTPError as e:
            if not _rotated and self._rotate_token():
                logger.warning("GraphQL error, rotated token, retrying.")
                return await self._graphql(query, variables, _rotated=True)
            logger.error(f"GraphQL error: {e}")
            return None

    async def get_user(self, username: str) -> Optional[dict]:
        """Get GitHub user profile."""
        return await self._get(f"/users/{username}")

    async def get_user_repos(self, username: str, max_pages: int = 3) -> list[dict]:
        """Get all public repos for a user."""
        repos = []
        page = 1

        while page <= max_pages:
            data = await self._get(
                f"/users/{username}/repos",
                params={"per_page": 100, "page": page, "type": "owner"},
            )

            if not data:
                break

            repos.extend(data)

            if len(data) < 100:
                break

            page += 1

        return repos

    async def get_maintainer_reputation(self, username: str) -> dict:
        """Get maintainer reputation metrics."""
        user = await self.get_user(username)
        if not user:
            return {"public_repos": 0, "total_stars": 0, "is_tier1": False}

        public_repos = user.get("public_repos", 0)

        # Calculate total stars
        repos = await self.get_user_repos(username)
        total_stars = sum(r.get("stargazers_count", 0) for r in repos)

        is_tier1 = public_repos > self.TIER1_REPOS or total_stars > self.TIER1_STARS

        return {
            "public_repos": public_repos,
            "total_stars": total_stars,
            "is_tier1": is_tier1,
        }

    async def get_sponsors_status(self, username: str) -> bool:
        """Check if user has GitHub Sponsors enabled."""
        query = """
        query($login: String!) {
            user(login: $login) {
                hasSponsorsListing
            }
        }
        """

        data = await self._graphql(query, {"login": username})
        if not data:
            return False

        user = data.get("user", {})
        return user.get("hasSponsorsListing", False)

    async def get_sponsor_count(self, username: str) -> int:
        """Get count of sponsors for a user."""
        query = """
        query($login: String!) {
            user(login: $login) {
                sponsors {
                    totalCount
                }
            }
        }
        """

        data = await self._graphql(query, {"login": username})
        if not data:
            return 0

        user = data.get("user", {})
        sponsors = user.get("sponsors", {})
        return sponsors.get("totalCount", 0)

    async def get_user_orgs(self, username: str) -> list[str]:
        """Get list of organizations a user belongs to."""
        orgs_data = await self._get(f"/users/{username}/orgs")
        if not orgs_data or not isinstance(orgs_data, list):
            return []
        return [org.get("login", "") for org in orgs_data if org.get("login")]

    async def search_user_by_email(self, email: str) -> Optional[str]:
        """
        Search for GitHub username by email address.

        Args:
            email: Email address to search for

        Returns:
            GitHub username if found, None otherwise
        """
        if not email:
            return None

        # GitHub search API for users by email
        result = await self._get("/search/users", params={"q": f"{email} in:email"})
        if result and result.get("total_count", 0) > 0:
            items = result.get("items", [])
            if items:
                return items[0].get("login")

        return None

    async def get_repo_contributors(self, owner: str, repo: str, limit: int = 10) -> list[dict]:
        """
        Get top contributors for a repository.

        Args:
            owner: Repository owner
            repo: Repository name
            limit: Maximum number of contributors to return

        Returns:
            List of contributor dicts with login, contributions count
        """
        contributors = await self._get(
            f"/repos/{owner}/{repo}/contributors",
            params={"per_page": limit}
        )
        if not contributors or not isinstance(contributors, list):
            return []
        return contributors

    async def get_repo_info(self, owner: str, repo: str) -> Optional[dict]:
        """Get repository information."""
        return await self._get(f"/repos/{owner}/{repo}")

    async def get_org_admins(self, owner: str, repo: str) -> dict:
        """Check if repo is org-owned and estimate admin count."""
        repo_data = await self.get_repo_info(owner, repo)
        if not repo_data:
            return {"is_org": False, "admin_count": 0}

        owner_type = repo_data.get("owner", {}).get("type")

        if owner_type != "Organization":
            return {"is_org": False, "admin_count": 0}

        # Try to get org members (may require permissions)
        members = await self._get(f"/orgs/{owner}/members", params={"role": "admin"})
        admin_count = len(members) if isinstance(members, list) else 1

        return {"is_org": True, "admin_count": max(admin_count, 1)}

    async def get_issues(
        self,
        owner: str,
        repo: str,
        state: str = "all",
        per_page: int = 30,
        max_comment_fetches: int = 10,
    ) -> list[IssueData]:
        """
        Get issues and PRs from a repository.

        Args:
            owner: Repository owner
            repo: Repository name
            state: Issue state filter (all, open, closed)
            per_page: Number of issues to fetch
            max_comment_fetches: Max issues to fetch comments for (API call each)

        Returns:
            List of IssueData objects
        """
        issues_data = await self._get(
            f"/repos/{owner}/{repo}/issues",
            params={"state": state, "per_page": per_page, "sort": "updated"},
        )

        if not issues_data:
            return []

        issues = []
        comment_fetches = 0
        for issue in issues_data:
            issue_obj = IssueData(
                number=issue.get("number"),
                title=issue.get("title", ""),
                body=issue.get("body", "") or "",
                state=issue.get("state", ""),
                is_pull_request="pull_request" in issue,
                author_login=issue.get("user", {}).get("login", ""),
                created_at=issue.get("created_at", ""),
                updated_at=issue.get("updated_at", ""),
                closed_at=issue.get("closed_at"),
            )

            # Fetch comments for a limited number of issues (each is an API call)
            if comment_fetches < max_comment_fetches and issue.get("comments", 0) > 0:
                comments = await self._get(f"/repos/{owner}/{repo}/issues/{issue['number']}/comments")
                if comments:
                    issue_obj.comments = [
                        {
                            "id": c.get("id"),
                            "author": c.get("user", {}).get("login", ""),
                            "body": c.get("body", ""),
                            "created_at": c.get("created_at", ""),
                        }
                        for c in comments
                    ]
                comment_fetches += 1

            issues.append(issue_obj)

        return issues

    async def collect(
        self,
        repo_url: str,
        top_contributor_username: str = None,
        top_contributor_email: str = None,
    ) -> GitHubData:
        """
        Collect all GitHub data for a repository.

        Args:
            repo_url: GitHub repository URL
            top_contributor_username: Override maintainer username (e.g., from git history)
            top_contributor_email: Top contributor's email for GitHub lookup

        Returns:
            GitHubData with all collected information
        """
        owner, repo = self.parse_repo_url(repo_url)
        if not owner or not repo:
            logger.error(f"Could not parse repository URL: {repo_url}")
            return GitHubData()

        data = GitHubData(owner=owner, repo=repo)

        # Get repo info (follows 301 redirects for renamed/transferred repos)
        repo_info = await self.get_repo_info(owner, repo)
        if repo_info:
            data.owner_type = repo_info.get("owner", {}).get("type", "")
            # Update owner/repo to canonical name (handles renames/transfers)
            canonical_owner = repo_info.get("owner", {}).get("login")
            canonical_repo = repo_info.get("name")
            if canonical_owner and canonical_repo:
                if canonical_owner != owner or canonical_repo != repo:
                    logger.info(f"Repo redirected: {owner}/{repo} -> {canonical_owner}/{canonical_repo}")
                owner = canonical_owner
                repo = canonical_repo
                data.owner = owner
                data.repo = repo

        # Determine the actual maintainer username
        maintainer_username = None

        # Priority 1: Provided username
        if top_contributor_username:
            maintainer_username = top_contributor_username
            logger.info(f"Using provided top contributor: {maintainer_username}")

        # Priority 2: For orgs, get top contributor from GitHub API
        if not maintainer_username and data.owner_type == "Organization":
            logger.info(f"Repo is org-owned, finding top contributor...")
            contributors = await self.get_repo_contributors(owner, repo, limit=1)
            if contributors:
                maintainer_username = contributors[0].get("login")
                logger.info(f"Top contributor from GitHub API: {maintainer_username}")

        # Priority 3: Search by email
        if not maintainer_username and top_contributor_email:
            logger.info(f"Searching GitHub for user with email: {top_contributor_email}")
            maintainer_username = await self.search_user_by_email(top_contributor_email)
            if maintainer_username:
                logger.info(f"Found user by email: {maintainer_username}")

        # Priority 4: Fall back to repo owner (if it's a user, not org)
        if not maintainer_username:
            if data.owner_type != "Organization":
                maintainer_username = owner
                logger.info(f"Using repo owner as maintainer: {maintainer_username}")
            else:
                # Last resort for orgs: try to get from repo info
                maintainer_username = repo_info.get("owner", {}).get("login", owner) if repo_info else owner
                logger.warning(f"Could not determine maintainer for org repo, using: {maintainer_username}")

        data.maintainer_username = maintainer_username
        logger.info(f"Final maintainer: {data.maintainer_username}")

        # Get user profile (for account age)
        user_profile = await self.get_user(data.maintainer_username)
        if user_profile:
            data.maintainer_account_created = user_profile.get("created_at", "")
            data.maintainer_public_repos = user_profile.get("public_repos", 0)

        # Get full repo list for reputation scoring
        logger.info(f"Fetching repos for {data.maintainer_username}...")
        repos = await self.get_user_repos(data.maintainer_username)
        data.maintainer_repos = repos
        data.maintainer_total_stars = sum(r.get("stargazers_count", 0) for r in repos)

        # Check sponsors status and count (skip bot accounts)
        if data.maintainer_username and "[bot]" not in data.maintainer_username:
            logger.info(f"Checking sponsors for {data.maintainer_username}...")
            data.has_github_sponsors = await self.get_sponsors_status(data.maintainer_username)
            if data.has_github_sponsors:
                data.maintainer_sponsor_count = await self.get_sponsor_count(data.maintainer_username)

        # Get user's organizations
        logger.info(f"Fetching orgs for {data.maintainer_username}...")
        data.maintainer_orgs = await self.get_user_orgs(data.maintainer_username)

        # Legacy tier-1 check (deprecated, use ReputationScorer instead)
        data.is_tier1_maintainer = (
            data.maintainer_public_repos > self.TIER1_REPOS
            or data.maintainer_total_stars > self.TIER1_STARS
        )

        # Check organization ownership of repo
        logger.info(f"Checking organization status for {owner}/{repo}...")
        org_info = await self.get_org_admins(owner, repo)
        data.is_org_owned = org_info["is_org"]
        data.org_admin_count = org_info["admin_count"]

        # Get issues
        logger.info(f"Fetching issues for {owner}/{repo}...")
        data.issues = await self.get_issues(owner, repo)

        return data

    async def close(self):
        """Close the HTTP client."""
        await self.client.aclose()
