"""GitHub inbound webhook receiver."""

from __future__ import annotations

import hashlib
import hmac
import json
import logging
import os
from datetime import UTC, datetime

import httpx
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request

from peon_mcp.common.dependencies import get_db
from peon_mcp.common.schemas import PaginatedResponse
from peon_mcp.db import get_pool, row_to_dict
from peon_mcp.github_app import get_github_token, _parse_github_owner
from peon_mcp.github_webhooks.dispatcher import dispatcher
from peon_mcp.github_webhooks import pr_closed_handler  # noqa: F401  # registers handler
from peon_mcp.github_webhooks import pr_opened_handler  # noqa: F401  # registers handler
from peon_mcp.github_webhooks.merge_conflict_handler import (
    _build_conflict_description,
    _conflict_task_exists,
    _find_task_for_pr,
)
from peon_mcp.github_webhooks.schemas import (
    GitHubWebhookEventLogResponse,
    GitHubWebhookResponse,
    PRConflictScanResponse,
)

# Register event-specific handlers by importing their modules.
# Each module uses @register_handler decorators at import time.
import peon_mcp.github_webhooks.merge_conflict_handler as _merge_conflict_handler  # noqa: F401
import peon_mcp.github_webhooks.pr_handler as _pr_handler  # noqa: F401
import peon_mcp.github_webhooks.pr_merge_handler as _pr_merge_handler  # noqa: F401

logger = logging.getLogger(__name__)

router = APIRouter(tags=["GitHub Webhooks"])

# NOTE: This endpoint is exempt from the global APIKeyMiddleware because GitHub
# cannot send arbitrary headers. Auth is handled here via query param or HMAC.
_WEBHOOK_PATH = "/api/github/webhook"


def _verify_signature(secret: str, payload: bytes, signature: str | None) -> bool:
    """Return True if X-Hub-Signature-256 matches the HMAC-SHA256 of payload."""
    if not signature:
        return False
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)


@router.post(_WEBHOOK_PATH, response_model=GitHubWebhookResponse)
async def receive_github_webhook(
    request: Request,
    api_key: str | None = Query(default=None),
    x_github_event: str | None = Header(default=None),
    x_github_delivery: str | None = Header(default=None),
    x_hub_signature_256: str | None = Header(default=None),
    db=Depends(get_db),
):
    """Receive inbound webhooks from GitHub.

    **Authentication** (at least one must pass):
    - Valid ``X-Hub-Signature-256`` HMAC (when GITHUB_WEBHOOK_SECRET is set)
    - ``api_key`` query parameter matching PEON_API_KEY
    - ``X-API-Key`` request header matching PEON_API_KEY
    """
    peon_api_key = os.environ.get("PEON_API_KEY")
    github_secret = os.environ.get("GITHUB_WEBHOOK_SECRET")

    # Read the raw body once — used for both signature verification and JSON parsing.
    body = await request.body()

    # --- GitHub HMAC signature check ---
    # Checked first: a valid signature is sufficient authentication.
    hmac_valid = False
    if github_secret:
        if not x_hub_signature_256:
            raise HTTPException(status_code=401, detail="Missing GitHub webhook signature")
        if _verify_signature(github_secret, body, x_hub_signature_256):
            hmac_valid = True
        else:
            raise HTTPException(status_code=401, detail="Invalid GitHub webhook signature")

    # --- Peon API key check ---
    # Skipped when HMAC signature already authenticated the request.
    if peon_api_key and not hmac_valid:
        provided_header = request.headers.get("X-API-Key")
        if api_key != peon_api_key and provided_header != peon_api_key:
            raise HTTPException(status_code=401, detail="Invalid or missing API key")

    event = x_github_event or "unknown"
    delivery = x_github_delivery or "unknown"

    # Parse the JSON payload to extract the action field (if present).
    action = ""
    parsed: dict = {}
    try:
        parsed = json.loads(body) if body else {}
        action = str(parsed.get("action", ""))
    except (json.JSONDecodeError, TypeError):
        pass

    # Dispatch the event through the registry.
    status = await dispatcher.dispatch(event, delivery, parsed)

    # Log the received webhook for auditability (including full payload).
    await db.execute(
        """INSERT INTO github_webhook_events (delivery_id, event_type, action, status, payload)
           VALUES (?, ?, ?, ?, ?::jsonb)""",
        (delivery, event, action, status, json.dumps(parsed) if parsed else None),
    )
    await db.commit()

    logger.info("github webhook: event=%s delivery=%s status=%s", event, delivery, status)

    return GitHubWebhookResponse(ok=True, event=event, delivery=delivery)


@router.get(
    "/api/github/webhook/events",
    response_model=PaginatedResponse[GitHubWebhookEventLogResponse],
)
async def list_github_webhook_events(
    limit: int = Query(default=50, ge=1, le=200),
    offset: int = Query(default=0, ge=0),
    db=Depends(get_db),
):
    """List recent GitHub webhook events (most recent first)."""
    count_rows = await db.execute_fetchall("SELECT COUNT(*) FROM github_webhook_events")
    total = count_rows[0]["count"] if count_rows else 0

    rows = await db.execute_fetchall(
        "SELECT * FROM github_webhook_events ORDER BY received_at DESC LIMIT ? OFFSET ?",
        (limit, offset),
    )
    items = [row_to_dict(r) for r in rows]

    return {
        "items": items,
        "total": total,
        "limit": limit,
        "offset": offset,
        "has_next": (offset + len(items)) < total,
    }


# ---------------------------------------------------------------------------
# Proactive PR conflict scanning
# ---------------------------------------------------------------------------

_GITHUB_API_BASE = "https://api.github.com"


def _extract_repo_full_name(github_url: str) -> str | None:
    """Extract 'owner/repo' from a GitHub URL.

    >>> _extract_repo_full_name("https://github.com/pyrabbit/peon-mcp")
    'pyrabbit/peon-mcp'
    >>> _extract_repo_full_name("https://github.com/pyrabbit/peon-mcp.git")
    'pyrabbit/peon-mcp'
    """
    url = github_url.rstrip("/").removesuffix(".git")
    if "github.com/" not in url:
        return None
    # Take the last two path segments as owner/repo.
    parts = url.split("github.com/", 1)[-1].split("/")
    if len(parts) >= 2:
        return f"{parts[0]}/{parts[1]}"
    return None


@router.post(
    "/api/projects/{owner}/{repo}/scan-pr-conflicts",
    response_model=PRConflictScanResponse,
)
async def scan_pr_conflicts(owner: str, repo: str, db=Depends(get_db)):
    project_id = f"{owner}/{repo}"
    """Proactively scan open PRs for merge conflicts and create fix tasks.

    For each open PR with conflicts, checks if a fix task already exists;
    if not, creates a high-priority fix task.
    """
    # 1. Look up the project's github_url.
    rows = await db.execute_fetchall(
        "SELECT id, github_url FROM projects WHERE id = ?", (project_id,)
    )
    if not rows:
        raise HTTPException(status_code=404, detail=f"Project '{project_id}' not found")

    github_url = rows[0]["github_url"] or ""
    if not github_url:
        raise HTTPException(
            status_code=400,
            detail=f"Project '{project_id}' has no github_url configured",
        )

    repo = _extract_repo_full_name(github_url)
    if not repo:
        raise HTTPException(
            status_code=400,
            detail=f"Could not parse GitHub repo from URL: {github_url}",
        )

    # 2. Generate a GitHub App installation token.
    owner = _parse_github_owner(github_url)
    token = get_github_token(owner=owner)
    if not token:
        raise HTTPException(
            status_code=503,
            detail="GitHub App authentication not configured or token generation failed",
        )

    headers = {
        "Authorization": f"token {token}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }

    # 3. Fetch open PRs.
    scanned = 0
    conflicts_found = 0
    tasks_created = 0
    already_tracked = 0

    async with httpx.AsyncClient(timeout=30.0) as http:
        try:
            resp = await http.get(
                f"{_GITHUB_API_BASE}/repos/{repo}/pulls",
                params={"state": "open", "per_page": 100},
                headers=headers,
            )
            resp.raise_for_status()
            prs = resp.json()
        except httpx.HTTPError as exc:
            logger.error("scan-pr-conflicts: failed to fetch PRs for %s: %s", repo, exc)
            raise HTTPException(
                status_code=502,
                detail=f"GitHub API error fetching PRs: {exc}",
            )

        scanned = len(prs)

        # 4. Check each PR for conflicts.
        pool = get_pool()
        for pr in prs:
            mergeable = pr.get("mergeable")
            mergeable_state = pr.get("mergeable_state", "")

            # If mergeable is null, GitHub hasn't computed it yet.
            # Fetch the individual PR to get the computed value.
            if mergeable is None:
                pr_number = pr.get("number")
                try:
                    detail_resp = await http.get(
                        f"{_GITHUB_API_BASE}/repos/{repo}/pulls/{pr_number}",
                        headers=headers,
                    )
                    detail_resp.raise_for_status()
                    pr_detail = detail_resp.json()
                    mergeable = pr_detail.get("mergeable")
                    mergeable_state = pr_detail.get("mergeable_state", "")
                except httpx.HTTPError:
                    logger.warning(
                        "scan-pr-conflicts: failed to fetch PR #%s detail for %s; skipping",
                        pr_number, repo,
                    )
                    continue

            has_conflicts = mergeable is False or mergeable_state == "dirty"
            if not has_conflicts:
                continue

            conflicts_found += 1

            pr_number = pr.get("number")
            pr_url = pr.get("html_url", "")
            pr_title = pr.get("title", "")
            branch = (pr.get("head") or {}).get("ref", "")
            base_branch = (pr.get("base") or {}).get("ref", "")

            if not pr_number or not pr_url:
                continue

            async with pool.acquire() as conn:
                # Check for existing conflict fix task.
                if await _conflict_task_exists(conn, pr_url):
                    already_tracked += 1
                    continue

                # Find the original task linked to this PR for feature context.
                original_task = await _find_task_for_pr(conn, pr_url)
                feature_id = original_task["feature_id"] if original_task else None

                title = f"Resolve merge conflicts on PR #{pr_number}"
                if len(title) > 200:
                    title = title[:197] + "..."

                description = _build_conflict_description(
                    pr_number, pr_url, pr_title, repo, branch, base_branch,
                )
                now = datetime.now(UTC)

                new_task_id = await conn.fetchval(
                    """INSERT INTO tasks
                           (project_id, feature_id, title, description, priority,
                            status, branch, pr_url, task_type, created_at, updated_at)
                       VALUES ($1, $2, $3, $4, 'high', 'todo', $5, $6, 'fix', $7, $8)
                       RETURNING id""",
                    project_id,
                    feature_id,
                    title,
                    description,
                    branch,
                    pr_url,
                    now,
                    now,
                )

                tasks_created += 1
                logger.info(
                    "scan-pr-conflicts: created fix task #%d for PR #%s on %s",
                    new_task_id, pr_number, repo,
                )

    logger.info(
        "scan-pr-conflicts: project=%s scanned=%d conflicts=%d created=%d tracked=%d",
        project_id, scanned, conflicts_found, tasks_created, already_tracked,
    )

    return PRConflictScanResponse(
        scanned=scanned,
        conflicts_found=conflicts_found,
        tasks_created=tasks_created,
        already_tracked=already_tracked,
    )
