"""GitLab source connector — async group enumeration and concurrent clone/pull."""

from __future__ import annotations

import asyncio
import hashlib
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any
from urllib.parse import quote_plus, urlparse

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

from redhat_oracle.core.config import (
    GitLabGroupConfig,
    GitLabInstanceConfig,
    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_group_projects(
    client: httpx.AsyncClient,
    base_url: str,
    group: str,
    token: str | None,
) -> list[dict[str, Any]]:
    """Enumerate all projects in a GitLab group, including subgroups."""
    projects: list[dict[str, Any]] = []
    headers: dict[str, str] = {}
    if token:
        headers["PRIVATE-TOKEN"] = token

    page = 1
    while True:
        resp = await client.get(
            f"{base_url}/api/v4/groups/{group}/projects",
            headers=headers,
            params={"per_page": 100, "page": page, "include_subgroups": "true"},
        )
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        projects.extend(data)
        page += 1

    return projects


def _should_include(project_name: str, group_config: GitLabGroupConfig) -> bool:
    """Check if a project matches include/exclude glob patterns."""
    from fnmatch import fnmatch

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


def _clone_url(project: dict[str, Any], use_ssh: bool) -> str:
    """Get the clone URL for a GitLab project."""
    if use_ssh:
        return str(project["ssh_url_to_repo"])
    return str(project["http_url_to_repo"])


def _resolve_token(
    instance: GitLabInstanceConfig,
    global_token: str | None,
) -> str | None:
    """Resolve the token for a GitLab instance (instance > global)."""
    return instance.token or global_token


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 project, 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, falling back to git CLI for shallow clones."""
    import subprocess as _sp

    repo = Repo(str(dest))
    origin = repo.remotes.origin
    try:
        origin.pull()
    except (AssertionError, Exception):
        _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:
    Repo.clone_from(url, str(dest), **kwargs)


async def sync_gitlab(
    config: OracleConfig,
    progress: Progress | None = None,
) -> dict[str, int]:
    """Sync all configured GitLab instances and groups.

    Returns:
        Dict with counts: synced, skipped, failed.
    """
    gitlab = config.sources.gitlab
    global_token = gitlab.token
    data_dir = get_data_dir(config) / "repos" / "gitlab"
    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 instance in gitlab.instances:
            token = _resolve_token(instance, global_token)
            hostname = urlparse(instance.url).hostname or instance.url

            for group_config in instance.groups:
                projects = await _list_group_projects(
                    client, instance.url, group_config.name, token
                )
                matching = [
                    p for p in projects if _should_include(p["name"], group_config)
                ]
                stats["skipped"] += len(projects) - len(matching)

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

                tasks = []
                for project in matching:
                    url = _clone_url(project, gitlab.use_ssh)
                    namespace = project.get("path_with_namespace", project["name"])
                    dest = data_dir / hostname / namespace
                    tasks.append(
                        _clone_or_pull(
                            url, dest, group_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}"


async def _list_mr_notes(
    client: httpx.AsyncClient,
    base_url: str,
    project_id: str,
    mr_iid: int,
    token: str | None,
) -> list[dict[str, Any]]:
    """Fetch all notes (comments) on a merge request."""
    notes: list[dict[str, Any]] = []
    headers: dict[str, str] = {}
    if token:
        headers["PRIVATE-TOKEN"] = token

    page = 1
    while True:
        resp = await client.get(
            f"{base_url}/api/v4/projects/{quote_plus(project_id)}/merge_requests/{mr_iid}/notes",
            headers=headers,
            params={"per_page": 100, "page": page},
        )
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        notes.extend(data)
        page += 1

    return notes


async def _list_issue_notes(
    client: httpx.AsyncClient,
    base_url: str,
    project_id: str,
    issue_iid: int,
    token: str | None,
) -> list[dict[str, Any]]:
    """Fetch all notes (comments) on an issue."""
    notes: list[dict[str, Any]] = []
    headers: dict[str, str] = {}
    if token:
        headers["PRIVATE-TOKEN"] = token

    page = 1
    while True:
        resp = await client.get(
            f"{base_url}/api/v4/projects/{quote_plus(project_id)}/issues/{issue_iid}/notes",
            headers=headers,
            params={"per_page": 100, "page": page},
        )
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        notes.extend(data)
        page += 1

    return notes


async def _list_project_mrs(
    client: httpx.AsyncClient,
    base_url: str,
    project_id: str,
    token: str | None,
) -> list[dict[str, Any]]:
    """List merge requests for a project."""
    mrs: list[dict[str, Any]] = []
    headers: dict[str, str] = {}
    if token:
        headers["PRIVATE-TOKEN"] = token

    page = 1
    while True:
        resp = await client.get(
            f"{base_url}/api/v4/projects/{quote_plus(project_id)}/merge_requests",
            headers=headers,
            params={"per_page": 100, "page": page, "state": "all", "order_by": "updated_at"},
        )
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        mrs.extend(data)
        page += 1

    return mrs


async def _list_project_issues(
    client: httpx.AsyncClient,
    base_url: str,
    project_id: str,
    token: str | None,
) -> list[dict[str, Any]]:
    """List issues for a project."""
    issues: list[dict[str, Any]] = []
    headers: dict[str, str] = {}
    if token:
        headers["PRIVATE-TOKEN"] = token

    page = 1
    while True:
        resp = await client.get(
            f"{base_url}/api/v4/projects/{quote_plus(project_id)}/issues",
            headers=headers,
            params={"per_page": 100, "page": page, "state": "all", "order_by": "updated_at"},
        )
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        issues.extend(data)
        page += 1

    return issues


async def fetch_gitlab_comments(
    config: OracleConfig,
    store: VectorStore | None = None,
    progress: Progress | None = None,
) -> dict[str, Any]:
    """Fetch MR and issue comments from all configured GitLab instances.

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

    Returns:
        Dict with stats: projects_processed, projects_skipped, chunks.
    """
    from redhat_oracle.core.cache import SyncCache

    gitlab = config.sources.gitlab
    global_token = gitlab.token

    cache = SyncCache(config)
    total_chunks = 0
    projects_processed = 0
    projects_skipped = 0

    async with httpx.AsyncClient(timeout=30.0) as client:
        for instance in gitlab.instances:
            token = _resolve_token(instance, global_token)
            hostname = urlparse(instance.url).hostname or instance.url

            if not token:
                continue

            for group_config in instance.groups:
                projects = await _list_group_projects(
                    client, instance.url, group_config.name, token
                )
                matching = [
                    p for p in projects if _should_include(p["name"], group_config)
                ]

                task_id = None
                if progress:
                    task_id = progress.add_task(
                        f"[cyan]GitLab comments: {hostname}/{group_config.name}",
                        total=len(matching),
                    )

                for project in matching:
                    project_id = str(project.get("id", ""))
                    project_name = project["name"]
                    namespace = project.get("path_with_namespace", project_name)

                    # Skip projects already completed
                    if cache.is_gitlab_project_comments_done(hostname, namespace):
                        projects_skipped += 1
                        if progress and task_id is not None:
                            progress.advance(task_id)
                        continue

                    project_chunks: list[Chunk] = []

                    # MR notes
                    try:
                        mrs = await _list_project_mrs(
                            client, instance.url, project_id, token
                        )
                        for mr in mrs:
                            mr_iid = mr["iid"]
                            notes = await _list_mr_notes(
                                client, instance.url, project_id, mr_iid, token
                            )
                            for note in notes:
                                body = note.get("body", "")
                                if not body.strip() or note.get("system", False):
                                    continue
                                chunk_id = _generate_chunk_id(
                                    f"gitlab/{hostname}/{namespace}/mr/{mr_iid}/note/{note.get('id', '')}",
                                    body,
                                )
                                project_chunks.append(
                                    Chunk(
                                        chunk_id=chunk_id,
                                        text=body,
                                        source="gitlab",
                                        org=hostname,
                                        repo=namespace,
                                        path="",
                                        content_type="pr_comment",
                                        author=(note.get("author") or {}).get(
                                            "username", ""
                                        ),
                                        timestamp=note.get("updated_at", ""),
                                        url=mr.get("web_url", ""),
                                        pr_number=str(mr_iid),
                                    )
                                )
                    except Exception:
                        logger.exception(
                            "Failed to fetch MR notes for %s/%s",
                            hostname,
                            namespace,
                        )

                    # Issue notes
                    try:
                        issues = await _list_project_issues(
                            client, instance.url, project_id, token
                        )
                        for issue in issues:
                            issue_iid = issue["iid"]
                            notes = await _list_issue_notes(
                                client, instance.url, project_id, issue_iid, token
                            )
                            for note in notes:
                                body = note.get("body", "")
                                if not body.strip() or note.get("system", False):
                                    continue
                                chunk_id = _generate_chunk_id(
                                    f"gitlab/{hostname}/{namespace}/issue/{issue_iid}/note/{note.get('id', '')}",
                                    body,
                                )
                                project_chunks.append(
                                    Chunk(
                                        chunk_id=chunk_id,
                                        text=body,
                                        source="gitlab",
                                        org=hostname,
                                        repo=namespace,
                                        path="",
                                        content_type="issue_comment",
                                        author=(note.get("author") or {}).get(
                                            "username", ""
                                        ),
                                        timestamp=note.get("updated_at", ""),
                                        url=issue.get("web_url", ""),
                                        pr_number=str(issue_iid),
                                        labels=issue.get("labels", []) or None,
                                    )
                                )
                    except Exception:
                        logger.exception(
                            "Failed to fetch issue notes for %s/%s",
                            hostname,
                            namespace,
                        )

                    # Upsert this project's chunks
                    if store and project_chunks:
                        store.upsert_chunks(project_chunks)
                    cache.mark_gitlab_project_comments_done(
                        hostname, namespace, len(project_chunks)
                    )
                    total_chunks += len(project_chunks)
                    projects_processed += 1

                    if progress and task_id is not None:
                        progress.advance(task_id)

    logger.info("GitLab comments: %d projects processed, %d skipped, %d chunks",
                projects_processed, projects_skipped, total_chunks)
    return {
        "projects_processed": projects_processed,
        "projects_skipped": projects_skipped,
        "chunks": total_chunks,
    }
