"""GitHub source connector — async org enumeration and concurrent clone/pull."""

from __future__ import annotations

import asyncio
import hashlib
import logging
import random
from pathlib import Path
from typing import TYPE_CHECKING, Any

import httpx
from git import Repo
from rich.progress import Progress, TaskID

from redhat_oracle.core.config import GitHubOrgConfig, OracleConfig, get_data_dir
from redhat_oracle.core.vectordb.chunker import Chunk

if TYPE_CHECKING:
    from redhat_oracle.core.vectordb.store import VectorStore

logger = logging.getLogger(__name__)


async def _list_org_repos(
    client: httpx.AsyncClient,
    org: str,
    token: str | None,
) -> list[dict[str, Any]]:
    """Enumerate all repos in a GitHub org, paginating through results."""
    repos: list[dict[str, Any]] = []
    headers: dict[str, str] = {}
    if token:
        headers["Authorization"] = f"Bearer {token}"

    page = 1
    while True:
        resp = await client.get(
            f"https://api.github.com/orgs/{org}/repos",
            headers=headers,
            params={"per_page": 100, "page": page, "type": "all"},
        )
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        repos.extend(data)
        page += 1

    return repos


def _should_include(repo_name: str, org_config: GitHubOrgConfig) -> bool:
    """Check if a repo matches include/exclude glob patterns."""
    from fnmatch import fnmatch

    if org_config.exclude:
        for pattern in org_config.exclude:
            if fnmatch(repo_name, pattern):
                return False
    if org_config.include:
        return any(fnmatch(repo_name, p) for p in org_config.include)
    return True


def _clone_url(repo: dict[str, Any], use_ssh: bool) -> str:
    """Get the clone URL for a repo."""
    if use_ssh:
        return str(repo["ssh_url"])
    return str(repo["clone_url"])


async def _clone_or_pull(
    repo_url: str,
    dest: Path,
    depth: int | None,
    semaphore: asyncio.Semaphore,
    progress: Progress | None = None,
    task_id: TaskID | None = None,
) -> bool:
    """Clone or pull a single repo, respecting the concurrency semaphore."""
    async with semaphore:
        try:
            if dest.exists() and (dest / ".git").exists():
                await asyncio.to_thread(_git_pull, dest)
            else:
                dest.parent.mkdir(parents=True, exist_ok=True)
                kwargs: dict[str, Any] = {}
                if depth is not None:
                    kwargs["depth"] = depth
                await asyncio.to_thread(_git_clone, repo_url, dest, kwargs)
            if progress and task_id is not None:
                progress.advance(task_id)
            return True
        except Exception:
            logger.exception("Failed to sync %s", repo_url)
            if progress and task_id is not None:
                progress.advance(task_id)
            return False


def _git_pull(dest: Path) -> None:
    """Pull latest changes in an existing repo.

    For shallow clones (depth=1), GitPython's pull/fetch both require a
    refspec. Uses subprocess directly when the refspec is missing.
    """
    import subprocess as _sp

    repo = Repo(str(dest))
    origin = repo.remotes.origin
    try:
        origin.pull()
    except (AssertionError, Exception):
        # Shallow clone without refspec — use git CLI directly
        _sp.run(
            ["git", "fetch", "--depth=1", "origin"],
            cwd=str(dest),
            capture_output=True,
            timeout=120,
        )


def _git_clone(url: str, dest: Path, kwargs: dict[str, Any]) -> None:
    """Clone a repo to disk."""
    Repo.clone_from(url, str(dest), **kwargs)


async def sync_github(
    config: OracleConfig,
    progress: Progress | None = None,
) -> dict[str, int]:
    """Sync all configured GitHub orgs.

    Returns:
        Dict with counts: synced, skipped, failed.
    """
    github = config.sources.github
    token = github.token
    data_dir = get_data_dir(config) / "repos" / "github"
    semaphore = asyncio.Semaphore(config.sources.max_concurrent_clones)

    stats = {"synced": 0, "skipped": 0, "failed": 0}

    async with httpx.AsyncClient(timeout=30.0) as client:
        for org_config in github.orgs:
            repos = await _list_org_repos(client, org_config.name, token)
            matching = [r for r in repos if _should_include(r["name"], org_config)]
            stats["skipped"] += len(repos) - len(matching)

            task_id = None
            if progress:
                task_id = progress.add_task(
                    f"[cyan]{org_config.name}", total=len(matching)
                )

            tasks = []
            for repo in matching:
                url = _clone_url(repo, github.use_ssh)
                dest = data_dir / org_config.name / repo["name"]
                tasks.append(
                    _clone_or_pull(
                        url, dest, org_config.depth, semaphore, progress, task_id
                    )
                )

            results = await asyncio.gather(*tasks)
            stats["synced"] += sum(1 for r in results if r)
            stats["failed"] += sum(1 for r in results if not r)

    return stats


def _generate_chunk_id(prefix: str, content: str) -> str:
    """Generate a deterministic chunk ID."""
    content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
    return f"{prefix}::{content_hash}"


# GitHub API hard limit: page 300 max (30,000 items at per_page=100)
_GH_MAX_PAGES = 300


async def _gh_paginate(
    client: httpx.AsyncClient,
    url: str,
    token: str | None,
    max_pages: int = _GH_MAX_PAGES,
) -> list[dict[str, Any]]:
    """Paginate a GitHub API list endpoint with rate-limit delay.

    Caps at max_pages to avoid GitHub's 422 error on page > 300.
    Adds a 0.5-1s delay between requests to avoid timeouts.
    """
    items: list[dict[str, Any]] = []
    headers: dict[str, str] = {}
    if token:
        headers["Authorization"] = f"Bearer {token}"

    page = 1
    while page <= max_pages:
        await asyncio.sleep(random.uniform(0.5, 1.0))
        resp = await client.get(
            url,
            headers=headers,
            params={"per_page": 100, "page": page, "sort": "updated", "direction": "desc"},
        )
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        items.extend(data)
        page += 1

    return items


async def _list_repo_pull_comments(
    client: httpx.AsyncClient,
    org: str,
    repo: str,
    token: str | None,
) -> list[dict[str, Any]]:
    """Fetch PR review comments for a repo (capped at 30k)."""
    return await _gh_paginate(
        client,
        f"https://api.github.com/repos/{org}/{repo}/pulls/comments",
        token,
    )


async def _list_repo_issue_comments(
    client: httpx.AsyncClient,
    org: str,
    repo: str,
    token: str | None,
) -> list[dict[str, Any]]:
    """Fetch issue comments for a repo (capped at 30k)."""
    return await _gh_paginate(
        client,
        f"https://api.github.com/repos/{org}/{repo}/issues/comments",
        token,
    )


async def fetch_github_comments(
    config: OracleConfig,
    store: VectorStore | None = None,
    progress: Progress | None = None,
) -> dict[str, Any]:
    """Fetch PR and issue comments from all configured GitHub orgs.

    When store is provided, upserts chunks per-repo for incremental
    persistence. Skips repos already completed via SyncCache.

    Returns:
        Dict with stats: repos_processed, repos_skipped, chunks.
    """
    from redhat_oracle.core.cache import SyncCache

    github = config.sources.github
    token = github.token

    if not token:
        logger.warning("No GitHub token configured, skipping comment fetch")
        return {"repos_processed": 0, "repos_skipped": 0, "chunks": 0}

    cache = SyncCache(config)
    total_chunks = 0
    repos_processed = 0
    repos_skipped = 0

    async with httpx.AsyncClient(timeout=60.0) as client:
        for org_config in github.orgs:
            repos = await _list_org_repos(client, org_config.name, token)
            matching = [r for r in repos if _should_include(r["name"], org_config)]

            task_id = None
            if progress:
                task_id = progress.add_task(
                    f"[cyan]GitHub comments: {org_config.name}",
                    total=len(matching),
                )

            for i, repo in enumerate(matching):
                repo_name = repo["name"]

                # Skip repos already completed
                if cache.is_github_repo_comments_done(org_config.name, repo_name):
                    repos_skipped += 1
                    if progress and task_id is not None:
                        progress.advance(task_id)
                    continue

                repo_chunks: list[Chunk] = []

                # PR review comments
                try:
                    pr_comments = await _list_repo_pull_comments(
                        client, org_config.name, repo_name, token
                    )
                    for c in pr_comments:
                        text = c.get("body", "")
                        if not text.strip():
                            continue
                        pr_url = c.get("pull_request_url", "")
                        pr_num = pr_url.rstrip("/").split("/")[-1] if pr_url else ""
                        chunk_id = _generate_chunk_id(
                            f"github/{org_config.name}/{repo_name}/pr_comment/{c.get('id', '')}",
                            text,
                        )
                        repo_chunks.append(
                            Chunk(
                                chunk_id=chunk_id,
                                text=text,
                                source="github",
                                org=org_config.name,
                                repo=repo_name,
                                path=c.get("path", ""),
                                content_type="pr_comment",
                                author=(c.get("user") or {}).get("login", ""),
                                timestamp=c.get("updated_at", ""),
                                url=c.get("html_url", ""),
                                pr_number=pr_num,
                            )
                        )
                except Exception:
                    logger.exception(
                        "Failed to fetch PR comments for %s/%s",
                        org_config.name,
                        repo_name,
                    )

                # Issue comments
                try:
                    issue_comments = await _list_repo_issue_comments(
                        client, org_config.name, repo_name, token
                    )
                    for c in issue_comments:
                        text = c.get("body", "")
                        if not text.strip():
                            continue
                        issue_url = c.get("issue_url", "")
                        issue_num = issue_url.rstrip("/").split("/")[-1] if issue_url else ""
                        chunk_id = _generate_chunk_id(
                            f"github/{org_config.name}/{repo_name}/issue_comment/{c.get('id', '')}",
                            text,
                        )
                        repo_chunks.append(
                            Chunk(
                                chunk_id=chunk_id,
                                text=text,
                                source="github",
                                org=org_config.name,
                                repo=repo_name,
                                path="",
                                content_type="issue_comment",
                                author=(c.get("user") or {}).get("login", ""),
                                timestamp=c.get("updated_at", ""),
                                url=c.get("html_url", ""),
                                pr_number=issue_num,
                            )
                        )
                except Exception:
                    logger.exception(
                        "Failed to fetch issue comments for %s/%s",
                        org_config.name,
                        repo_name,
                    )

                # Upsert this repo's chunks
                if store and repo_chunks:
                    store.upsert_chunks(repo_chunks)
                cache.mark_github_repo_comments_done(
                    org_config.name, repo_name, len(repo_chunks)
                )
                total_chunks += len(repo_chunks)
                repos_processed += 1

                if progress and task_id is not None:
                    progress.update(
                        task_id,
                        description=(
                            f"[cyan]GitHub comments: {org_config.name}/{repo_name} "
                            f"(repo {i + 1}/{len(matching)}, {total_chunks} chunks)"
                        ),
                    )
                    progress.advance(task_id)

    logger.info("GitHub comments: %d repos processed, %d skipped, %d chunks",
                repos_processed, repos_skipped, total_chunks)
    return {
        "repos_processed": repos_processed,
        "repos_skipped": repos_skipped,
        "chunks": total_chunks,
    }
