# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""GitLab webhook signature verification and event parsing.

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

1. Secret token verification to ensure webhook authenticity
2. Parsing of webhook headers and payloads into structured events
3. Extraction of relevant information for MR and project events

Security Note:
    GitLab uses a simple token-based verification via the X-Gitlab-Token header.
    Unlike GitHub's HMAC signature, GitLab sends the secret token directly.
    Always verify tokens before processing webhook payloads.

Typical usage:
    >>> from mergeguide.gitlab.webhook import verify_webhook_signature, parse_webhook_event
    >>>
    >>> # Verify the webhook is from GitLab
    >>> if not verify_webhook_signature(request.headers.get("X-Gitlab-Token"), webhook_secret):
    ...     raise ValueError("Invalid webhook token")
    >>>
    >>> # Parse the event
    >>> event = parse_webhook_event(headers, json_body)
    >>> if event.should_process_mr:
    ...     await handler.handle_merge_request(event)
"""

from __future__ import annotations

import hmac
from typing import Any


def verify_webhook_signature(token: str | None, secret: str) -> bool:
    """Verify the token from a GitLab webhook request.

    GitLab sends the webhook secret token in the X-Gitlab-Token header.
    This function verifies that the received token matches the configured secret.

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

    Args:
        token: Value of the X-Gitlab-Token header. May be None if not provided.
        secret: Webhook secret configured in GitLab project settings.

    Returns:
        True if the token matches the secret and the request is authentic.
        False if verification fails or inputs are empty.

    Example:
        >>> token = request.headers.get("X-Gitlab-Token")
        >>> if not verify_webhook_signature(token, WEBHOOK_SECRET):
        ...     return Response("Unauthorized", status=401)
    """
    if not token or not secret:
        return False

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


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

    Extracts event type from the X-Gitlab-Event header and wraps the payload
    in a WebhookEvent for easier handling.

    Args:
        headers: HTTP request headers. Keys are case-insensitive.
            Must include X-Gitlab-Event.
        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_merge_request and event.action == "open":
        ...     print(f"New MR !{event.get_mr_info()['mr_iid']}")
    """
    # 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-gitlab-event", "")
    event_uuid = headers_lower.get("x-gitlab-event-uuid", "")

    # GitLab uses "object_attributes.action" for MR events
    object_attrs = body.get("object_attributes", {})
    action = object_attrs.get("action", "")

    return WebhookEvent(
        event_type=event_type,
        event_uuid=event_uuid,
        action=action,
        payload=body,
    )


class WebhookEvent:
    """Structured representation of a parsed GitLab 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: GitLab event type (e.g., "Merge Request Hook", "Push Hook").
            From the X-Gitlab-Event header.
        event_uuid: Unique ID for this webhook delivery.
            From the X-Gitlab-Event-UUID header. Useful for idempotency.
        action: Event action (e.g., "open", "update", "merge").
            From the payload's "object_attributes.action" field for MR events.
        payload: Full webhook payload as a dict.

    Example:
        >>> event = WebhookEvent(
        ...     event_type="Merge Request Hook",
        ...     event_uuid="abc-123",
        ...     action="open",
        ...     payload={...}
        ... )
        >>> event.should_process_mr
        True
        >>> event.get_mr_info()["mr_iid"]
        42
    """

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

        Args:
            event_type: GitLab event type from X-Gitlab-Event header.
            event_uuid: Unique delivery ID from X-Gitlab-Event-UUID header.
            action: Event action from the payload's "object_attributes.action" field.
            payload: Full webhook payload as a dict.
        """
        self.event_type = event_type
        self.event_uuid = event_uuid
        self.action = action
        self.payload = payload

    @property
    def is_merge_request(self) -> bool:
        """Check if this is a merge request event.

        Returns:
            True if event_type is "Merge Request Hook".
        """
        return self.event_type == "Merge Request Hook"

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

        Returns:
            True if event_type is "Push Hook".
        """
        return self.event_type == "Push Hook"

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

        Returns:
            True if event_type is "Pipeline Hook".
        """
        return self.event_type == "Pipeline Hook"

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

        We evaluate on specific actions that indicate code changes:
        - open: New MR created
        - update: MR was updated (e.g., new commits pushed)
        - reopen: Previously closed MR was reopened

        Other actions (close, merge, approved, etc.) do not require
        re-evaluation since the code hasn't changed or evaluation is moot.

        Returns:
            True if this is an MR event that should trigger evaluation.
        """
        return self.is_merge_request and self.action in ("open", "update", "reopen")

    def get_project_id(self) -> int | None:
        """Extract the GitLab project ID from the event payload.

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

    def get_project_path(self) -> str | None:
        """Extract the project path from the event payload.

        Returns:
            Project path with namespace (e.g., "acme/my-repo"), or None if not available.
        """
        project = self.payload.get("project", {})
        return project.get("path_with_namespace")

    def get_mr_info(self) -> dict[str, Any] | None:
        """Extract merge request metadata from the event payload.

        Extracts commonly needed MR information into a flat dict for
        easy access. Only valid for merge request events.

        Returns:
            Dict containing:
            - mr_iid: MR internal ID (e.g., 42)
            - source_sha: Full SHA of the MR's source branch head
            - source_branch: Branch name of the MR source (e.g., "feature/xyz")
            - target_sha: Full SHA of the target branch
            - target_branch: Branch name of the target (e.g., "main")
            - title: MR title
            - author_username: GitLab username of the MR author
            - state: MR state (opened, closed, merged)

            Returns None if this is not a merge_request event.
        """
        if not self.is_merge_request:
            return None

        obj_attrs = self.payload.get("object_attributes", {})
        last_commit = obj_attrs.get("last_commit", {})
        user = self.payload.get("user", {})

        return {
            "mr_iid": obj_attrs.get("iid"),
            "mr_id": obj_attrs.get("id"),
            "source_sha": last_commit.get("id") or obj_attrs.get("last_commit", {}).get("sha"),
            "source_branch": obj_attrs.get("source_branch"),
            "target_sha": obj_attrs.get(
                "target_branch"
            ),  # GitLab doesn't provide target SHA directly
            "target_branch": obj_attrs.get("target_branch"),
            "title": obj_attrs.get("title", ""),
            "author_username": user.get("username", ""),
            "author_name": user.get("name", ""),
            "state": obj_attrs.get("state"),
            "url": obj_attrs.get("url"),
        }

    def get_push_info(self) -> dict[str, Any] | None:
        """Extract push event metadata from the payload.

        Returns:
            Dict containing push information, or None if not a push event.
        """
        if not self.is_push:
            return None

        return {
            "before": self.payload.get("before"),
            "after": self.payload.get("after"),
            "ref": self.payload.get("ref"),
            "checkout_sha": self.payload.get("checkout_sha"),
            "user_username": self.payload.get("user_username"),
            "user_name": self.payload.get("user_name"),
            "commits_count": self.payload.get("total_commits_count", 0),
        }
