"""Handler for pull_request.opened GitHub webhook events.

Registers a handler on the module-level dispatcher singleton. Import this
module to activate the handler — it self-registers on import.

On PR opened:
  1. Extracts PR metadata (title, author, files changed, lines, PR URL).
  2. Tries to fetch the changed file list from the GitHub API to derive affected
     top-level directories.  Falls back gracefully if the API is unreachable or
     requires authentication (private repo).
  3. Queries discord_configs / discord_channel_mappings for channels subscribed
     to the 'pr.opened' event.
  4. Sends a rich Discord embed summary to each subscribed channel.
"""

from __future__ import annotations

import json
import logging
import os
from typing import Any

import httpx

from peon_mcp.db import get_pool
from peon_mcp.grukk.discord.embeds import _embed, _field
from peon_mcp.grukk.discord.embeds import COLOR_GREEN
from peon_mcp.grukk.discord.sender import send_to_channel
from peon_mcp.github_webhooks.dispatcher import register_handler

logger = logging.getLogger(__name__)

# Event key used in discord_channel_mappings.subscribed_events JSON arrays.
SUBSCRIPTION_EVENT = "pr.opened"

# GitHub API base — no auth required for public repos.
_GH_API_BASE = "https://api.github.com"


# ---------------------------------------------------------------------------
# Handler
# ---------------------------------------------------------------------------


@register_handler("pull_request")
async def handle_pr_opened(delivery_id: str, payload: dict) -> None:
    """Send a Discord PR summary when a pull request is opened."""
    action = payload.get("action")
    if action != "opened":
        return

    pr = payload.get("pull_request", {})
    repo = payload.get("repository", {})

    pr_url = pr.get("html_url", "")
    pr_number = pr.get("number", 0)
    pr_title = pr.get("title", "")
    author = (pr.get("user") or {}).get("login", "unknown")
    changed_files: int = pr.get("changed_files", 0)
    additions: int = pr.get("additions", 0)
    deletions: int = pr.get("deletions", 0)
    head_branch: str = (pr.get("head") or {}).get("ref", "")
    base_branch: str = (pr.get("base") or {}).get("ref", "main")
    repo_full_name: str = repo.get("full_name", "")

    logger.info(
        "PR opened: #%d '%s' in %s by %s (+%d/-%d, %d file(s))",
        pr_number,
        pr_title,
        repo_full_name,
        author,
        additions,
        deletions,
        changed_files,
    )

    # Best-effort: fetch the list of changed files from GitHub API.
    affected_dirs = await _fetch_affected_dirs(
        repo_full_name=repo_full_name,
        pr_number=pr_number,
    )

    embed = _build_pr_opened_embed(
        pr_title=pr_title,
        pr_url=pr_url,
        pr_number=pr_number,
        author=author,
        changed_files=changed_files,
        additions=additions,
        deletions=deletions,
        head_branch=head_branch,
        base_branch=base_branch,
        repo_full_name=repo_full_name,
        affected_dirs=affected_dirs,
    )

    pool = get_pool()
    async with pool.acquire() as conn:
        await _notify_subscribed_channels(conn, embed)


# ---------------------------------------------------------------------------
# GitHub API helper — fetches file paths and derives top-level directories
# ---------------------------------------------------------------------------


async def _fetch_affected_dirs(repo_full_name: str, pr_number: int) -> list[str]:
    """Return unique top-level directory paths changed in the PR.

    Uses a GitHub App installation token for private repositories.
    Returns an empty list on any error so the caller always gets a usable result.
    """
    if not repo_full_name or not pr_number:
        return []

    url = f"{_GH_API_BASE}/repos/{repo_full_name}/pulls/{pr_number}/files"
    headers: dict[str, str] = {
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }
    from peon_mcp.github_app import get_github_token

    owner = repo_full_name.split("/")[0] if "/" in repo_full_name else None
    token = get_github_token(owner=owner)
    if token:
        headers["Authorization"] = f"Bearer {token}"

    try:
        async with httpx.AsyncClient(timeout=8.0) as client:
            response = await client.get(url, headers=headers)
            if response.status_code != 200:
                logger.debug(
                    "pr_opened_handler: GitHub API returned %d for %s, skipping file list",
                    response.status_code,
                    url,
                )
                return []
            files: list[dict[str, Any]] = response.json()
    except Exception as exc:
        logger.debug("pr_opened_handler: GitHub API call failed: %s", exc)
        return []

    seen: set[str] = set()
    dirs: list[str] = []
    for f in files:
        filename: str = f.get("filename", "") if isinstance(f, dict) else ""
        if not filename:
            continue
        top = filename.split("/")[0]
        if top and top not in seen:
            seen.add(top)
            dirs.append(top)

    return dirs


# ---------------------------------------------------------------------------
# Embed builder
# ---------------------------------------------------------------------------


def _build_pr_opened_embed(
    *,
    pr_title: str,
    pr_url: str,
    pr_number: int,
    author: str,
    changed_files: int,
    additions: int,
    deletions: int,
    head_branch: str,
    base_branch: str,
    repo_full_name: str,
    affected_dirs: list[str],
) -> dict:
    """Build a Discord embed dict summarising the opened PR."""
    description_parts = []
    if pr_url:
        description_parts.append(f"**[#{pr_number}: {pr_title}]({pr_url})**")
    else:
        description_parts.append(f"**#{pr_number}: {pr_title}**")

    fields = [
        _field("Author", f"`{author}`", inline=True),
        _field("Branch", f"`{head_branch}` → `{base_branch}`", inline=True),
        _field("Changes", f"+{additions} / -{deletions}", inline=True),
        _field("Files changed", str(changed_files), inline=True),
    ]

    if affected_dirs:
        # Show up to 10 top-level dirs to keep the embed tidy.
        shown = affected_dirs[:10]
        value = "\n".join(f"• `{d}`" for d in shown)
        if len(affected_dirs) > 10:
            value += f"\n…and {len(affected_dirs) - 10} more"
        fields.append(_field("Affected areas", value, inline=False))

    return _embed(
        title="PR Opened",
        description="\n".join(description_parts),
        color=COLOR_GREEN,
        project_id=repo_full_name or "github",
        fields=fields,
    )


# ---------------------------------------------------------------------------
# Discord delivery
# ---------------------------------------------------------------------------


async def _notify_subscribed_channels(conn, embed: dict) -> None:
    """Find all channel mappings subscribed to 'pr.opened' and send the embed."""
    # Fetch all enabled discord configs and their channel mappings.
    # We filter in Python because JSON functions aren't guaranteed to be
    # available on all builds (subscribed_events is a JSON array stored as TEXT).
    rows = await conn.fetch(
        """SELECT dc.id AS config_id, dc.bot_token,
                  dcm.channel_id, dcm.subscribed_events
           FROM discord_configs dc
           JOIN discord_channel_mappings dcm ON dcm.discord_config_id = dc.id
           WHERE dc.enabled = true AND dcm.enabled = true
             AND dc.bot_token != ''""",
    )

    sent_count = 0
    for row in rows:
        config_id = row["config_id"]
        bot_token = row["bot_token"]
        channel_id = row["channel_id"]
        subscribed_events_raw = row["subscribed_events"]

        # Deserialise subscribed_events — it's stored as a JSON array string.
        try:
            subscribed: list = json.loads(subscribed_events_raw or "[]")
        except (json.JSONDecodeError, TypeError):
            subscribed = []

        if SUBSCRIPTION_EVENT not in subscribed:
            continue

        gateway = {"bot_token": bot_token}
        ok = await send_to_channel(gateway, channel_id, embed)
        if ok:
            sent_count += 1
            logger.info(
                "pr_opened_handler: sent PR summary to channel %s (config %d)",
                channel_id,
                config_id,
            )
        else:
            logger.warning(
                "pr_opened_handler: failed to send to channel %s (config %d)",
                channel_id,
                config_id,
            )

    logger.info("pr_opened_handler: notified %d channel(s)", sent_count)
