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

Azure DevOps Service Hooks send webhook payloads for PR events.
This module handles signature verification and event parsing.

Webhook signature verification uses HMAC-SHA256, similar to GitHub webhooks.
The signature is passed in the X-Hub-Signature header.

Example:
    if verify_webhook_signature(payload, signature, secret):
        event = parse_webhook_event(headers, json.loads(payload))
        if event.should_process_pr:
            # Process the PR event
            pr_info = event.get_pr_info()
"""

from __future__ import annotations

import hashlib
import hmac
from dataclasses import dataclass, field
from typing import Any


def verify_webhook_signature(
    payload: bytes | str,
    signature: str,
    secret: str,
) -> bool:
    """Verify the webhook signature using HMAC-SHA256.

    Azure DevOps can be configured to sign webhooks with a shared secret.
    The signature is computed as HMAC-SHA256(secret, payload) and sent
    in the X-Hub-Signature header.

    Args:
        payload: Raw request body (bytes or string).
        signature: Signature from X-Hub-Signature header.
        secret: Shared secret configured in Service Hook.

    Returns:
        True if signature is valid, False otherwise.

    Example:
        >>> verify_webhook_signature(b'{"event": "data"}', "sha256=abc...", "secret")
        True
    """
    if not signature or not secret:
        return False

    # Convert payload to bytes if needed
    if isinstance(payload, str):
        payload = payload.encode()

    # Compute expected signature
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()

    # Handle both "sha256=xxx" and plain "xxx" formats
    actual = signature.removeprefix("sha256=")

    # Constant-time comparison to prevent timing attacks
    return hmac.compare_digest(expected, actual)


@dataclass
class AzureDevOpsWebhookEvent:
    """Parsed Azure DevOps webhook event.

    Represents a Service Hook event from Azure DevOps, containing
    information about the triggering action and associated resources.

    Attributes:
        event_type: Type of event (e.g., "git.pullrequest.created").
        subscription_id: ID of the Service Hook subscription.
        notification_id: Unique notification sequence number.
        resource: Event resource data (PR details, repository, etc.).
        resource_version: API version of the resource data.
        raw_payload: Original unmodified payload dictionary.

    Example:
        >>> event = parse_webhook_event(headers, payload)
        >>> if event.should_process_pr:
        ...     pr_info = event.get_pr_info()
        ...     print(f"PR #{pr_info['pr_id']} on {pr_info['repo_name']}")
    """

    event_type: str
    subscription_id: str
    notification_id: int
    resource: dict[str, Any]
    resource_version: str = "1.0"
    raw_payload: dict[str, Any] = field(default_factory=dict, repr=False)

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

        Only processes PR created and updated events for active PRs.

        Returns:
            True if the event should be processed.
        """
        # Check event type
        if self.event_type not in (
            "git.pullrequest.created",
            "git.pullrequest.updated",
        ):
            return False

        # Check PR is active (not abandoned, completed, etc.)
        pr_status = self.resource.get("status", "").lower()
        if pr_status and pr_status != "active":
            return False

        return True

    def get_repository_info(self) -> tuple[str, str, str]:
        """Extract repository information from the event.

        Returns:
            Tuple of (organization_url, project_name, repository_name).
        """
        repo = self.resource.get("repository", {})
        project = repo.get("project", {})

        # Get organization URL from resourceContainers
        containers = self.raw_payload.get("resourceContainers", {})
        collection = containers.get("collection", {})
        org_url = collection.get("baseUrl", "").rstrip("/")

        return (
            org_url,
            project.get("name", ""),
            repo.get("name", ""),
        )

    def get_pr_info(self) -> dict[str, Any]:
        """Extract pull request information from the event.

        Returns:
            Dictionary with PR details:
            - pr_id: Pull request ID (int)
            - repo_id: Repository GUID
            - repo_name: Repository name
            - project_id: Project GUID
            - project_name: Project name
            - source_branch: Source branch ref (e.g., refs/heads/feature)
            - target_branch: Target branch ref (e.g., refs/heads/main)
            - head_sha: Latest commit SHA on source branch
            - author: PR author email/username
            - title: PR title
        """
        repo = self.resource.get("repository", {})
        project = repo.get("project", {})
        created_by = self.resource.get("createdBy", {})

        # Get head SHA from lastMergeSourceCommit
        merge_commit = self.resource.get("lastMergeSourceCommit", {})
        head_sha = merge_commit.get("commitId", "")

        return {
            "pr_id": self.resource.get("pullRequestId"),
            "repo_id": repo.get("id", ""),
            "repo_name": repo.get("name", ""),
            "project_id": project.get("id", ""),
            "project_name": project.get("name", ""),
            "source_branch": self.resource.get("sourceRefName", ""),
            "target_branch": self.resource.get("targetRefName", ""),
            "head_sha": head_sha,
            "author": created_by.get("uniqueName", created_by.get("displayName", "")),
            "title": self.resource.get("title", ""),
        }

    @property
    def action(self) -> str:
        """Get the action type (created, updated, etc.).

        Returns:
            Action string extracted from event type.
        """
        # Event type format: git.pullrequest.{action}
        parts = self.event_type.split(".")
        if len(parts) >= 3:
            return parts[2]
        return "unknown"


def parse_webhook_event(
    headers: dict[str, str],
    payload: dict[str, Any],
) -> AzureDevOpsWebhookEvent:
    """Parse a webhook event from Azure DevOps Service Hooks.

    Args:
        headers: HTTP request headers (not currently used, for future expansion).
        payload: Parsed JSON payload from request body.

    Returns:
        Parsed AzureDevOpsWebhookEvent instance.

    Raises:
        ValueError: If required fields are missing from payload.

    Example:
        >>> headers = {"Content-Type": "application/json"}
        >>> payload = json.loads(request.body)
        >>> event = parse_webhook_event(headers, payload)
    """
    # Validate required fields
    if "eventType" not in payload:
        raise ValueError("Missing required field: eventType")
    if "resource" not in payload:
        raise ValueError("Missing required field: resource")

    return AzureDevOpsWebhookEvent(
        event_type=payload["eventType"],
        subscription_id=payload.get("subscriptionId", ""),
        notification_id=payload.get("notificationId", 0),
        resource=payload["resource"],
        resource_version=payload.get("resourceVersion", "1.0"),
        raw_payload=payload,
    )
