"""GitHub webhook signature verification and event parsing.

This module provides security-critical webhook verification and event parsing
for GitHub App webhooks. It handles:

1. HMAC-SHA256 signature verification to ensure webhook authenticity
2. Parsing of webhook headers and payloads into structured events
3. Extraction of relevant information for PR and installation events

Security Note:
    Webhook signature verification is critical for security. Without it,
    attackers could forge webhook requests to trigger policy evaluations
    or manipulate installation state. Always verify signatures before
    processing webhook payloads.

Typical usage:
    >>> from mergeguide.github.webhook import verify_webhook_signature, parse_webhook_event
    >>>
    >>> # Verify the webhook is from GitHub
    >>> if not verify_webhook_signature(raw_body, signature_header, webhook_secret):
    ...     raise ValueError("Invalid webhook signature")
    >>>
    >>> # Parse the event
    >>> event = parse_webhook_event(headers, json_body)
    >>> if event.should_process_pr:
    ...     await handler.handle_pull_request(event)
"""

from __future__ import annotations

import hashlib
import hmac


def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    """Verify the HMAC-SHA256 signature of a GitHub webhook request.

    GitHub computes a signature of the webhook payload using the webhook secret
    and sends it in the X-Hub-Signature-256 header. This function verifies that
    the signature matches, proving the request came from GitHub.

    The verification uses constant-time comparison (hmac.compare_digest) to
    prevent timing attacks that could leak information about the secret.

    Args:
        payload: Raw request body bytes exactly as received.
            Do not decode or modify before verification.
        signature: Value of the X-Hub-Signature-256 header.
            Format: "sha256=<hex digest>" or just "<hex digest>".
        secret: Webhook secret configured in the GitHub App settings.

    Returns:
        True if the signature is valid and the payload is authentic.
        False if verification fails or inputs are empty.

    Example:
        >>> raw_body = request.body  # bytes
        >>> sig = request.headers["X-Hub-Signature-256"]
        >>> if not verify_webhook_signature(raw_body, sig, WEBHOOK_SECRET):
        ...     return Response("Unauthorized", status=401)
    """
    if not signature or not secret:
        return False

    # Remove "sha256=" prefix if present
    if signature.startswith("sha256="):
        signature = signature[7:]

    # Compute expected signature using HMAC-SHA256
    expected = hmac.new(
        secret.encode("utf-8"),
        payload,
        hashlib.sha256,
    ).hexdigest()

    # Use constant-time comparison to prevent timing attacks
    return hmac.compare_digest(expected, signature)


def parse_webhook_event(headers: dict[str, str], body: dict) -> WebhookEvent:
    """Parse GitHub webhook headers and body into a structured event.

    Extracts event type, delivery ID, and action from the webhook request
    and wraps them in a WebhookEvent for easier handling.

    Args:
        headers: HTTP request headers. Keys are case-insensitive.
            Must include X-GitHub-Event. X-GitHub-Delivery is optional.
        body: Parsed JSON body from the webhook request.

    Returns:
        WebhookEvent instance with parsed event details and helper methods.

    Example:
        >>> event = parse_webhook_event(request.headers, request.json())
        >>> if event.is_pull_request and event.action == "opened":
        ...     print(f"New PR #{event.get_pr_info()['pr_number']}")
    """
    # Normalize header keys to lowercase for case-insensitive access
    headers_lower = {k.lower(): v for k, v in headers.items()}

    event_type = headers_lower.get("x-github-event", "")
    delivery_id = headers_lower.get("x-github-delivery", "")

    return WebhookEvent(
        event_type=event_type,
        delivery_id=delivery_id,
        action=body.get("action", ""),
        payload=body,
    )


class WebhookEvent:
    """Structured representation of a parsed GitHub webhook event.

    Wraps the raw webhook payload with convenience methods for accessing
    common event data and determining how to handle the event.

    Attributes:
        event_type: GitHub event type (e.g., "pull_request", "installation").
            From the X-GitHub-Event header.
        delivery_id: Unique ID for this webhook delivery.
            From the X-GitHub-Delivery header. Useful for idempotency.
        action: Event action (e.g., "opened", "synchronize", "created").
            From the payload's "action" field.
        payload: Full webhook payload as a dict.

    Example:
        >>> event = WebhookEvent(
        ...     event_type="pull_request",
        ...     delivery_id="abc-123",
        ...     action="opened",
        ...     payload={...}
        ... )
        >>> event.should_process_pr
        True
        >>> event.get_pr_info()["pr_number"]
        42
    """

    def __init__(
        self,
        event_type: str,
        delivery_id: str,
        action: str,
        payload: dict,
    ) -> None:
        """Initialize a WebhookEvent with parsed data.

        Args:
            event_type: GitHub event type from X-GitHub-Event header.
            delivery_id: Unique delivery ID from X-GitHub-Delivery header.
            action: Event action from the payload's "action" field.
            payload: Full webhook payload as a dict.
        """
        self.event_type = event_type
        self.delivery_id = delivery_id
        self.action = action
        self.payload = payload

    @property
    def is_pull_request(self) -> bool:
        """Check if this is a pull_request event.

        Returns:
            True if event_type is "pull_request".
        """
        return self.event_type == "pull_request"

    @property
    def is_installation(self) -> bool:
        """Check if this is an installation lifecycle event.

        Returns:
            True if event_type is "installation".
        """
        return self.event_type == "installation"

    @property
    def should_process_pr(self) -> bool:
        """Determine if this PR event should trigger policy evaluation.

        We evaluate on specific actions that indicate code changes:
        - opened: New PR created
        - synchronize: New commits pushed to the PR branch
        - reopened: Previously closed PR was reopened

        Other actions (closed, edited, labeled, etc.) do not require
        re-evaluation since the code hasn't changed.

        Returns:
            True if this is a PR event that should trigger evaluation.
        """
        return self.is_pull_request and self.action in ("opened", "synchronize", "reopened")

    def get_installation_id(self) -> int | None:
        """Extract the GitHub App installation ID from the event payload.

        The installation ID is required for authenticating API requests
        to perform actions on behalf of the installation.

        Returns:
            Installation ID as an integer, or None if not present.
        """
        installation = self.payload.get("installation", {})
        return installation.get("id")

    def get_repository_info(self) -> tuple[str, str] | None:
        """Extract repository owner and name from the event payload.

        Parses the full_name field (e.g., "acme/my-repo") to extract
        the owner (org or user) and repository name.

        Returns:
            Tuple of (owner, repo_name), or None if not available.

        Example:
            >>> owner, repo = event.get_repository_info()
            >>> print(f"{owner}/{repo}")
            acme/my-repo
        """
        repo = self.payload.get("repository", {})
        full_name = repo.get("full_name", "")
        if "/" in full_name:
            owner, name = full_name.split("/", 1)
            return (owner, name)
        return None

    def get_pr_info(self) -> dict | None:
        """Extract pull request metadata from the event payload.

        Extracts commonly needed PR information into a flat dict for
        easy access. Only valid for pull_request events.

        Returns:
            Dict containing:
            - pr_number: PR number (e.g., 42)
            - head_sha: Full SHA of the PR's head commit
            - head_ref: Branch name of the PR source (e.g., "feature/xyz")
            - base_sha: Full SHA of the target branch
            - base_ref: Branch name of the target (e.g., "main")
            - title: PR title
            - author: GitHub username of the PR author

            Returns None if this is not a pull_request event.
        """
        if not self.is_pull_request:
            return None

        pr = self.payload.get("pull_request", {})
        head = pr.get("head", {})
        base = pr.get("base", {})

        return {
            "pr_number": pr.get("number"),
            "head_sha": head.get("sha"),
            "head_ref": head.get("ref"),
            "base_sha": base.get("sha"),
            "base_ref": base.get("ref"),
            "title": pr.get("title", ""),
            "author": pr.get("user", {}).get("login", ""),
        }
