"""Load GitHub repository contents into LangChain Documents for RAG indexing."""

from __future__ import annotations

import base64
import fnmatch
import logging
import re
from dataclasses import dataclass

import requests
from langchain_core.documents import Document

from pinrag.config import get_github_default_branch

logger = logging.getLogger(__name__)

# GitHub URL patterns: github.com/owner/repo, github.com/owner/repo/tree/branch
_GITHUB_REPO_RE = re.compile(
    r"(?:https?://)?(?:www\.)?github\.com/([^/]+)/([^/]+?)(?:/tree/([^/?#]+))?(?:[/?#]|$)",
    re.IGNORECASE,
)

# Default exclude: lock files, caches, binaries, large artifacts
_DEFAULT_EXCLUDE = (
    ".git/",
    "__pycache__/",
    "*.pyc",
    "*.pyo",
    "node_modules/",
    ".venv/",
    "venv/",
    "uv.lock",
    "pnpm-lock.yaml",
    "package-lock.json",
    "yarn.lock",
    "*.png",
    "*.jpg",
    "*.jpeg",
    "*.gif",
    "*.ico",
    "*.pdf",
    "*.mp4",
    "*.webm",
    "*.mp3",
    "*.wav",
    "*.zip",
    "*.tar",
    "*.gz",
    "*.rar",
    "*.woff",
    "*.woff2",
    "*.ttf",
    "*.eot",
)

# Extensions we consider text (include by default if no include pattern)
_TEXT_EXTENSIONS = frozenset(
    {
        ".py",
        ".ts",
        ".tsx",
        ".js",
        ".jsx",
        ".mjs",
        ".cjs",
        ".md",
        ".mdx",
        ".rst",
        ".txt",
        ".adoc",
        ".json",
        ".yaml",
        ".yml",
        ".toml",
        ".ini",
        ".cfg",
        ".conf",
        ".html",
        ".htm",
        ".css",
        ".scss",
        ".sass",
        ".less",
        ".go",
        ".rs",
        ".rb",
        ".java",
        ".kt",
        ".kts",
        ".scala",
        ".c",
        ".h",
        ".cpp",
        ".hpp",
        ".cc",
        ".cxx",
        ".sql",
        ".sh",
        ".bash",
        ".zsh",
        ".fish",
        ".ps1",
        ".vue",
        ".svelte",
        ".astro",
        ".dockerfile",
        ".gitignore",
        ".env.example",
        ".pio",
        ".cmake",
        ".ld",
        ".s",
        ".asm",
    }
)


def _parse_github_url(url: str) -> tuple[str, str, str]:
    """Parse GitHub URL into (owner, repo, branch). Branch defaults to 'main'."""
    s = (url or "").strip()
    m = _GITHUB_REPO_RE.search(s)
    if not m:
        raise ValueError(
            f"Invalid GitHub URL: {url!r}. "
            "Expected https://github.com/owner/repo or github.com/owner/repo/tree/branch"
        )
    owner, repo, branch = m.group(1), m.group(2), m.group(3)
    if not owner or not repo:
        raise ValueError(f"Could not parse owner/repo from GitHub URL: {url!r}")
    # Remove .git suffix if present
    if repo.endswith(".git"):
        repo = repo[:-4]
    return owner, repo, branch or get_github_default_branch()


def _matches_patterns(path: str, include: list[str] | None, exclude: list[str]) -> bool:
    """Return True if path matches include (if set) and does not match exclude."""
    if include is not None and include:
        if not any(fnmatch.fnmatch(path, p) for p in include):
            return False
    parts = path.split("/")
    for p in exclude:
        p_stripped = p.rstrip("/")
        if p.endswith("/"):
            if (
                p_stripped in parts
                or path.startswith(p_stripped + "/")
                or path.startswith(p)
            ):
                return False
        else:
            if fnmatch.fnmatch(parts[-1] if parts else path, p):
                return False
    return True


def _infer_language(file_path: str) -> str:
    """Infer language from file extension."""
    ext = "." + file_path.rsplit(".", 1)[-1].lower() if "." in file_path else ""
    lang_map = {
        ".py": "python",
        ".ts": "typescript",
        ".tsx": "typescript",
        ".js": "javascript",
        ".jsx": "javascript",
        ".mjs": "javascript",
        ".md": "markdown",
        ".mdx": "markdown",
        ".rst": "rst",
        ".json": "json",
        ".yaml": "yaml",
        ".yml": "yaml",
        ".toml": "toml",
        ".html": "html",
        ".css": "css",
        ".go": "go",
        ".rs": "rust",
        ".rb": "ruby",
        ".java": "java",
        ".kt": "kotlin",
        ".scala": "scala",
        ".c": "c",
        ".h": "c",
        ".cpp": "cpp",
        ".hpp": "cpp",
        ".sql": "sql",
        ".sh": "shell",
        ".bash": "shell",
        ".vue": "vue",
        ".svelte": "svelte",
        ".pio": "pio",
        ".cmake": "cmake",
        ".ld": "ld",
        ".s": "asm",
        ".asm": "asm",
    }
    return lang_map.get(ext, "plaintext")


@dataclass(frozen=True)
class GitHubLoadResult:
    """Result of loading a GitHub repo into Documents."""

    owner: str
    repo: str
    branch: str
    documents: list[Document]
    files_loaded: int
    total_chars: int


def load_github_repo_as_documents(
    repo_url: str,
    *,
    token: str | None = None,
    branch: str | None = None,
    include_patterns: list[str] | None = None,
    exclude_patterns: list[str] | None = None,
    max_file_bytes: int = 524288,
) -> GitHubLoadResult:
    """Load a GitHub repository's text files into LangChain Documents.

    Uses GitHub API to list files (recursive tree) and fetch content via Contents API.
    Each file becomes one Document; chunking is done by the indexer.

    Args:
        repo_url: GitHub URL (e.g. https://github.com/owner/repo or github.com/owner/repo/tree/main).
        token: GitHub personal access token (optional for public repos; needed for private).
        branch: Override branch. If None, parsed from URL or defaults to main.
        include_patterns: Glob patterns for files to include (e.g. ["*.md", "src/**/*.py"]).
        exclude_patterns: Glob patterns to exclude. Default excludes lock files, caches, binaries.
        max_file_bytes: Skip files larger than this (default 512 KB).

    Returns:
        GitHubLoadResult with documents, owner, repo, branch, files_loaded, total_chars.

    Raises:
        ValueError: If repo_url is not a valid GitHub URL.
        RuntimeError: If API requests fail (404, 403, etc.).

    """
    owner, repo, default_branch = _parse_github_url(repo_url)
    use_branch = branch if branch else default_branch
    exclude = list(exclude_patterns) if exclude_patterns else list(_DEFAULT_EXCLUDE)

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

    # 1. Get tree SHA from branch
    commit_resp = requests.get(
        f"https://api.github.com/repos/{owner}/{repo}/commits/{use_branch}",
        headers=headers,
        timeout=30,
    )
    if commit_resp.status_code == 404:
        raise RuntimeError(
            f"GitHub repo or branch not found: {owner}/{repo} @ {use_branch}. "
            "Check the URL and branch. For private repos, set GITHUB_TOKEN."
        )
    if commit_resp.status_code == 403:
        raise RuntimeError(
            f"GitHub API rate limit or access denied for {owner}/{repo}. "
            "Set GITHUB_TOKEN for higher limits and private repo access."
        )
    commit_resp.raise_for_status()
    commit_data = commit_resp.json()
    tree_sha = commit_data.get("commit", {}).get("tree", {}).get("sha")
    if not tree_sha:
        raise RuntimeError(f"Could not get tree SHA for {owner}/{repo} @ {use_branch}")

    # 2. Get recursive tree
    tree_resp = requests.get(
        f"https://api.github.com/repos/{owner}/{repo}/git/trees/{tree_sha}",
        headers=headers,
        params={"recursive": "1"},
        timeout=60,
    )
    tree_resp.raise_for_status()
    tree_data = tree_resp.json()
    if tree_data.get("truncated"):
        logger.warning(
            "GitHub tree response truncated for %s/%s @ %s; index may be incomplete.",
            owner,
            repo,
            use_branch,
        )
    items = tree_data.get("tree", [])

    # 3. Filter to blobs (files) that match patterns and are text
    file_paths: list[str] = []
    for item in items:
        if item.get("type") != "blob":
            continue
        path = item.get("path", "")
        if not path:
            continue
        if not _matches_patterns(path, include_patterns, exclude):
            continue
        ext = "." + path.rsplit(".", 1)[-1].lower() if "." in path else ""
        if not include_patterns and ext not in _TEXT_EXTENSIONS:
            continue
        file_paths.append(path)

    # 4. Fetch each file's content
    base_url = f"https://api.github.com/repos/{owner}/{repo}/contents"
    documents: list[Document] = []
    total_chars = 0

    for path in file_paths:
        try:
            file_resp = requests.get(
                f"{base_url}/{path}",
                headers=headers,
                params={"ref": use_branch},
                timeout=30,
            )
            if file_resp.status_code != 200:
                continue
            file_data = file_resp.json()
            if file_data.get("type") != "file":
                continue
            size = file_data.get("size", 0)
            if size > max_file_bytes:
                continue
            content_b64 = file_data.get("content")
            if not content_b64:
                continue
            try:
                raw = base64.b64decode(content_b64).decode("utf-8", errors="replace")
            except Exception:
                logger.debug(
                    "Failed decoding GitHub content for %s/%s:%s", owner, repo, path
                )
                continue
            total_chars += len(raw)
            repo_id = f"{owner}/{repo}"
            source_url = f"https://github.com/{owner}/{repo}/blob/{use_branch}/{path}"
            meta = {
                "document_id": repo_id,
                "document_type": "github",
                "source": source_url,
                "file_path": path,
                "repo": repo_id,
                "branch": use_branch,
                "language": _infer_language(path),
                "doc_bytes": size,
            }
            documents.append(Document(page_content=raw, metadata=meta))
        except Exception:
            logger.debug("Failed fetching GitHub file %s/%s:%s", owner, repo, path)
            continue

    return GitHubLoadResult(
        owner=owner,
        repo=repo,
        branch=use_branch,
        documents=documents,
        files_loaded=len(documents),
        total_chars=total_chars,
    )
