"""GitHub webhook handler: auto-assign reviewers when a pull request is opened.

On ``pull_request.opened`` events this handler:
1. Fetches the list of changed files for the PR via the GitHub API.
2. Matches each file path against the ``GITHUB_REVIEWER_OWNERSHIP_MAP`` env var
   (a JSON object mapping path prefixes to lists of reviewer logins).
3. Falls back to recent git-blame history (last N committers per file via the
   GitHub Commits API) when no ownership map is configured.
4. Requests reviews from the matched set of users, excluding the PR author.

Configuration
-------------
``GITHUB_REVIEWER_OWNERSHIP_MAP`` (optional)
    JSON string mapping path-prefix strings to reviewer login lists.
    Example::

        {"peon_mcp/": ["alice", "bob"], "ui/": ["carol"]}

    Single-string values are also accepted (e.g. ``"alice"``).

GitHub App authentication is used for API calls (configured via
``GITHUB_APP_ID`` and ``GITHUB_APP_PRIVATE_KEY_PATH``).
"""

from __future__ import annotations

import json
import logging
import os
from collections import Counter
from typing import Any

import httpx

from peon_mcp.github_webhooks.dispatcher import register_handler

logger = logging.getLogger(__name__)

_GITHUB_API_BASE = "https://api.github.com"
_MAX_BLAME_FILES = 10   # cap files sampled for blame to stay within rate limits
_MAX_BLAME_REVIEWERS = 3  # maximum reviewers to derive from blame history


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _get_token(owner: str = "") -> str:
    """Return a GitHub App installation token.

    Args:
        owner: GitHub org or user login for multi-installation resolution.
    """
    from peon_mcp.github_app import get_github_token

    return get_github_token(owner=owner or None)


def _get_ownership_map() -> dict[str, list[str]]:
    """Parse ``GITHUB_REVIEWER_OWNERSHIP_MAP`` into a path-prefix → logins mapping.

    Returns an empty dict when the env var is absent or malformed.
    """
    raw = os.environ.get("GITHUB_REVIEWER_OWNERSHIP_MAP", "").strip()
    if not raw:
        return {}
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        logger.warning("pr_reviewer: GITHUB_REVIEWER_OWNERSHIP_MAP is not valid JSON; ignoring")
        return {}

    if not isinstance(data, dict):
        logger.warning("pr_reviewer: GITHUB_REVIEWER_OWNERSHIP_MAP must be a JSON object; ignoring")
        return {}

    result: dict[str, list[str]] = {}
    for prefix, reviewers in data.items():
        if isinstance(reviewers, str):
            result[prefix] = [reviewers]
        elif isinstance(reviewers, list):
            result[prefix] = [r for r in reviewers if isinstance(r, str)]
    return result


def _api_headers(token: str) -> dict[str, str]:
    return {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }


def _match_reviewers(
    changed_files: list[str],
    ownership_map: dict[str, list[str]],
) -> set[str]:
    """Return the union of reviewer logins for all files matched by the ownership map."""
    reviewers: set[str] = set()
    for file_path in changed_files:
        for prefix, owners in ownership_map.items():
            if file_path.startswith(prefix):
                reviewers.update(owners)
    return reviewers


# ---------------------------------------------------------------------------
# GitHub API calls
# ---------------------------------------------------------------------------

async def _get_pr_files(token: str, repo: str, pr_number: int) -> list[str]:
    """Return a list of changed file paths for the given PR (paginated)."""
    url = f"{_GITHUB_API_BASE}/repos/{repo}/pulls/{pr_number}/files"
    file_paths: list[str] = []

    async with httpx.AsyncClient(timeout=15.0) as client:
        page = 1
        while True:
            resp = await client.get(
                url,
                headers=_api_headers(token),
                params={"per_page": 100, "page": page},
            )
            if resp.status_code != 200:
                logger.warning(
                    "pr_reviewer: GET %s returned %d: %s",
                    url, resp.status_code, resp.text[:200],
                )
                break

            items: list[dict] = resp.json()
            if not items:
                break
            file_paths.extend(item["filename"] for item in items)
            if len(items) < 100:
                break
            page += 1

    return file_paths


async def _reviewers_from_blame(
    token: str,
    repo: str,
    changed_files: list[str],
    pr_author: str,
) -> set[str]:
    """Derive reviewer candidates from recent commit history for the changed files.

    Queries the GitHub Commits API for each file (capped at ``_MAX_BLAME_FILES``)
    and ranks users by commit frequency, returning up to ``_MAX_BLAME_REVIEWERS``
    unique logins (excluding the PR author).
    """
    counter: Counter[str] = Counter()
    url_template = f"{_GITHUB_API_BASE}/repos/{repo}/commits"

    async with httpx.AsyncClient(timeout=15.0) as client:
        for file_path in changed_files[:_MAX_BLAME_FILES]:
            resp = await client.get(
                url_template,
                headers=_api_headers(token),
                params={"path": file_path, "per_page": 10},
            )
            if resp.status_code != 200:
                continue
            for commit in resp.json():
                login = (commit.get("author") or {}).get("login", "")
                if login and login != pr_author:
                    counter[login] += 1

    return {login for login, _ in counter.most_common(_MAX_BLAME_REVIEWERS)}


async def _request_reviews(token: str, repo: str, pr_number: int, reviewers: set[str]) -> None:
    """POST a review-request for *reviewers* to the GitHub API."""
    if not reviewers:
        return

    url = f"{_GITHUB_API_BASE}/repos/{repo}/pulls/{pr_number}/requested_reviewers"
    async with httpx.AsyncClient(timeout=15.0) as client:
        resp = await client.post(
            url,
            headers=_api_headers(token),
            json={"reviewers": sorted(reviewers)},
        )
    if resp.status_code not in (200, 201):
        logger.warning(
            "pr_reviewer: failed to request reviewers for %s#%d — %d: %s",
            repo, pr_number, resp.status_code, resp.text[:300],
        )
    else:
        logger.info(
            "pr_reviewer: requested reviews from %s for %s#%d",
            sorted(reviewers), repo, pr_number,
        )


# ---------------------------------------------------------------------------
# Webhook handler
# ---------------------------------------------------------------------------

@register_handler("pull_request")
async def handle_pull_request_opened(delivery_id: str, payload: dict[str, Any]) -> None:
    """Handle ``pull_request.opened`` events: auto-assign reviewers.

    Ignores all actions other than ``"opened"``.
    """
    action = payload.get("action")
    if action != "opened":
        return

    pr = payload.get("pull_request") or {}
    pr_number: int | None = pr.get("number")
    pr_author: str = (pr.get("user") or {}).get("login", "")
    repo: str = (payload.get("repository") or {}).get("full_name", "")

    if not pr_number or not repo:
        logger.warning(
            "pr_reviewer: delivery %s — missing PR number or repository in payload",
            delivery_id,
        )
        return

    owner = repo.split("/")[0] if "/" in repo else ""
    token = _get_token(owner=owner)
    if not token:
        logger.warning(
            "pr_reviewer: delivery %s — no GitHub App token available; "
            "skipping reviewer assignment",
            delivery_id,
        )
        return

    changed_files = await _get_pr_files(token, repo, pr_number)
    if not changed_files:
        logger.info(
            "pr_reviewer: delivery %s — %s#%d has no changed files; skipping",
            delivery_id, repo, pr_number,
        )
        return

    ownership_map = _get_ownership_map()
    if ownership_map:
        reviewers = _match_reviewers(changed_files, ownership_map)
    else:
        logger.info(
            "pr_reviewer: delivery %s — no GITHUB_REVIEWER_OWNERSHIP_MAP; "
            "falling back to blame history",
            delivery_id,
        )
        reviewers = await _reviewers_from_blame(token, repo, changed_files, pr_author)

    # Never ask the PR author to review their own PR.
    reviewers.discard(pr_author)

    if not reviewers:
        logger.info(
            "pr_reviewer: delivery %s — no reviewers resolved for %s#%d; "
            "checked %d file(s)",
            delivery_id, repo, pr_number, len(changed_files),
        )
        return

    await _request_reviews(token, repo, pr_number, reviewers)
