# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Merge request event handler for MergeGuide policy checks.

This module provides the main entry point for processing GitLab merge request
webhooks. It orchestrates the complete MR check workflow including:

1. Posting initial "pending" commit status to GitLab
2. Fetching the MR diff content
3. Evaluating the diff against configured security policies
4. Storing evidence artifacts for compliance/audit purposes
5. Posting the final pass/fail commit status
6. Creating or updating the bot comment with detailed results

Typical usage in a Lambda function:
    >>> from mergeguide.gitlab.mr_handler import MRHandler
    >>> from mergeguide.gitlab.api import GitLabAPI
    >>> from mergeguide.gitlab.auth import GitLabAuth
    >>> from mergeguide.engine.evaluator import PolicyEngine
    >>>
    >>> auth = GitLabAuth(access_token="glpat-xxxx")
    >>> api = GitLabAPI(auth)
    >>> engine = PolicyEngine.from_rules(rules)
    >>> handler = MRHandler(api, engine)
    >>>
    >>> # In webhook handler
    >>> result = await handler.handle_merge_request(event)
    >>> if not result.passed:
    ...     print(f"MR blocked: {result.errors_count} errors")
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING

from .api import GitLabAPI
from .comments import (
    COMMENT_MARKER,
    format_error_comment,
    format_mr_comment,
)
from .webhook import WebhookEvent

if TYPE_CHECKING:
    from mergeguide.engine.evaluator import EvaluationContext, PolicyEngine, PolicyOutput
    from mergeguide.evidence.storage import EvidenceStorage

logger = logging.getLogger(__name__)


@dataclass
class MRCheckResult:
    """Encapsulates the outcome of a merge request policy check.

    This dataclass captures all relevant information about a completed
    policy evaluation, including pass/fail status, violation counts,
    and references to stored evidence.

    Attributes:
        passed: True if the MR passed all policy checks (no error-level violations).
        evaluation_id: Unique identifier for this evaluation run (UUID format).
        violations_count: Total number of violations found across all severities.
        errors_count: Number of error-level violations (blocking).
        warnings_count: Number of warning-level violations (non-blocking).
        evidence_key: S3 key or storage path for the evidence artifact, if stored.
        note_id: GitLab note ID for the bot's MR comment, if posted.

    Example:
        >>> result = MRCheckResult(
        ...     passed=False,
        ...     evaluation_id="a1b2c3d4-...",
        ...     violations_count=3,
        ...     errors_count=1,
        ...     warnings_count=2,
        ...     evidence_key="acme-corp/my-repo/2024-01/a1b2c3d4.json",
        ...     note_id=12345678,
        ... )
        >>> result.passed
        False
    """

    passed: bool
    evaluation_id: str
    violations_count: int
    errors_count: int
    warnings_count: int
    evidence_key: str | None = None
    note_id: int | None = None


class MRHandler:
    """Handles merge request webhook events for security policy evaluation.

    This is the main coordinator for MR-based policy checks. It implements
    the complete workflow from receiving a webhook to posting results back
    to GitLab.

    The check workflow:
        1. Post "pending" commit status to GitLab
        2. Fetch the MR diff via GitLab API
        3. Evaluate the diff against all configured policies
        4. Store an evidence artifact (if storage configured)
        5. Post final commit status (success/failure/error)
        6. Post or update the bot's MR comment with detailed results

    The handler is designed to be stateless and can be reused across
    multiple webhook invocations. All GitLab state is managed through
    the GitLabAPI client.

    Attributes:
        gitlab: GitLabAPI client for all GitLab interactions.
        engine: PolicyEngine instance for running policy evaluations.
        storage: Optional EvidenceStorage for persisting audit artifacts.
        signing_key: Optional key for cryptographically signing evidence.

    Example:
        >>> auth = GitLabAuth(access_token="glpat-xxxx")
        >>> gitlab = GitLabAPI(auth)
        >>> engine = PolicyEngine.from_rules(rules)
        >>> handler = MRHandler(gitlab, engine)
        >>>
        >>> # Process a webhook event
        >>> result = await handler.handle_merge_request(event)
        >>> print(f"Passed: {result.passed}, Errors: {result.errors_count}")
    """

    def __init__(
        self,
        gitlab_api: GitLabAPI,
        policy_engine: PolicyEngine,
        evidence_storage: EvidenceStorage | None = None,
        signing_key: str = "",
    ) -> None:
        """Initialize the MR handler with required dependencies.

        Args:
            gitlab_api: GitLab API client for status and comment posting.
                Must be configured with valid authentication.
            policy_engine: MergeGuide policy engine for diff evaluation.
                Should have rules pre-loaded.
            evidence_storage: Optional storage backend for evidence artifacts.
                If None, evidence artifacts will not be persisted.
            signing_key: Optional HMAC key for signing evidence artifacts.
                Enables tamper detection for stored evidence.
        """
        self.gitlab = gitlab_api
        self.engine = policy_engine
        self.storage = evidence_storage
        self.signing_key = signing_key

    async def handle_merge_request(self, event: WebhookEvent) -> MRCheckResult:
        """Process a merge_request webhook event and run policy checks.

        This is the main entry point for MR policy evaluation. It executes
        the complete check workflow:
        1. Validates the event and extracts MR metadata
        2. Posts a "pending" commit status
        3. Fetches and evaluates the MR diff
        4. Stores evidence (if configured)
        5. Posts final status and comment

        If an error occurs during evaluation, the handler posts an "error"
        status and an error comment, then re-raises the exception.

        Args:
            event: Parsed WebhookEvent from a merge_request webhook.
                Must have action in ["open", "update", "reopen"].

        Returns:
            MRCheckResult containing evaluation outcome, violation counts,
            and references to stored evidence and comments.

        Raises:
            ValueError: If the event is missing required data (project ID,
                or MR info) or should not be processed.
            Exception: Re-raises any exception from evaluation after posting
                error status to GitLab.
        """
        if not event.should_process_mr:
            raise ValueError(f"Event {event.event_type}.{event.action} should not be processed")

        # Extract required metadata from the webhook payload
        project_id = event.get_project_id()
        project_path = event.get_project_path()
        mr_info = event.get_mr_info()

        if not project_id or not mr_info:
            raise ValueError("Missing required event data (project or MR info)")

        mr_iid = mr_info["mr_iid"]
        source_sha = mr_info["source_sha"]

        logger.info(
            f"Processing MR !{mr_iid} on {project_path} @ {source_sha[:7] if source_sha else 'unknown'}"
        )

        # Immediately post "pending" status so users see the check is running
        if source_sha:
            await self.gitlab.post_commit_status(
                project_id=project_id,
                sha=source_sha,
                state="pending",
                description="Evaluating against security policies...",
            )

        try:
            # Fetch the MR diff content for analysis
            diff = await self.gitlab.fetch_mr_diff(
                project_id=project_id,
                mr_iid=mr_iid,
            )

            # Build the evaluation context with MR metadata for policy decisions
            from mergeguide.engine.evaluator import EvaluationContext

            # Parse project path to extract namespace and repo
            namespace = ""
            repo = project_path or ""
            if project_path and "/" in project_path:
                parts = project_path.rsplit("/", 1)
                namespace = parts[0]
                repo = parts[1]

            context = EvaluationContext(
                org_id=namespace,
                repo=repo,
                branch=mr_info["source_branch"],
                commit_sha=source_sha or "",
                author=mr_info["author_username"],
                pr_number=mr_iid,  # Using pr_number field for MR IID
            )

            # Execute all configured policies against the diff
            # Use dual-layer scanning (regex + Semgrep) when orchestrator is available,
            # falling back to regex-only PolicyEngine.evaluate_diff() otherwise.
            from mergeguide.engine.orchestrator import DualLayerOrchestrator

            if isinstance(self.engine, DualLayerOrchestrator):
                output = self.engine.scan_diff(diff, context)
            else:
                output = self.engine.evaluate_diff(diff, context)

            # Persist evidence artifact for compliance/audit trail
            evidence_key = await self._store_evidence(
                output=output,
                context=context,
                diff_size=len(diff),
            )

            # Map evaluation result to GitLab status state
            state = "success" if output.passed else "failed"
            description = self._build_status_description(output)

            # Update commit status with final result
            if source_sha:
                await self.gitlab.post_commit_status(
                    project_id=project_id,
                    sha=source_sha,
                    state=state,
                    description=description,
                )

            # Post detailed results as an MR comment (creates or updates existing)
            note_id = await self._post_or_update_note(
                project_id=project_id,
                mr_iid=mr_iid,
                output=output,
            )

            return MRCheckResult(
                passed=output.passed,
                evaluation_id=output.evaluation_id,
                violations_count=len(output.violations),
                errors_count=sum(1 for v in output.violations if v.severity == "error"),
                warnings_count=sum(1 for v in output.violations if v.severity == "warning"),
                evidence_key=evidence_key,
                note_id=note_id,
            )

        except Exception as e:
            # Log the full exception with traceback for debugging
            logger.exception(f"Error evaluating MR !{mr_iid}: {e}")

            # Post "error" status to indicate the check itself failed (not the MR)
            if source_sha:
                await self.gitlab.post_commit_status(
                    project_id=project_id,
                    sha=source_sha,
                    state="failed",
                    description="Policy check failed - see MR comment",
                )

            # Post a user-friendly error comment explaining what went wrong
            comment_body = format_error_comment(str(e))
            await self._post_or_update_note_raw(
                project_id=project_id,
                mr_iid=mr_iid,
                body=comment_body,
            )

            # Re-raise to let the Lambda handler deal with retries/alerts
            raise

    async def _store_evidence(
        self,
        output: PolicyOutput,
        context: EvaluationContext,
        diff_size: int,
    ) -> str | None:
        """Store an evidence artifact for the evaluation in configured storage.

        Evidence artifacts provide an immutable audit trail of policy evaluations.
        They can be signed for tamper detection and stored in S3 or other backends.

        The artifact includes:
        - Evaluation timestamp and ID
        - All violations found with file/line info
        - Metadata about the MR and author
        - List of policies that were applied

        Note: The full diff content is NOT stored in the artifact for privacy
        and storage efficiency reasons.

        Args:
            output: Policy evaluation output containing violations and metadata.
            context: Evaluation context with MR and repository information.
            diff_size: Size of the diff in bytes (for source metadata).

        Returns:
            Storage key (e.g., S3 object key) if successfully stored,
            None if storage is not configured or if storage fails.
        """
        if not self.storage:
            return None

        try:
            from mergeguide.evidence.artifact import EvidenceArtifact, SourceInfo

            # Build source metadata for the artifact
            source = SourceInfo(
                type="diff",
                location=f"{context.repo}!{context.pr_number}",
                ref=context.commit_sha,
                size_bytes=diff_size,
            )

            # Create the evidence artifact from evaluation output
            artifact = EvidenceArtifact.from_output(
                output=output,
                source=source,
                context=context,
                input_content="",  # Don't store full diff in artifact for privacy
                policies_used=[r.id for r in self.engine.rule_engine.rules],
            )

            # Optionally sign the artifact for tamper detection
            if self.signing_key:
                artifact.sign(self.signing_key)

            # Persist to configured storage backend
            evidence_key = self.storage.store(
                artifact=artifact,
                org_id=context.org_id,
                repo=context.repo or "unknown",
            )

            logger.info(f"Stored evidence at: {evidence_key}")
            return evidence_key

        except Exception as e:
            # Log but don't fail the check if evidence storage fails
            logger.warning(f"Failed to store evidence: {e}")
            return None

    def _build_status_description(self, output: PolicyOutput) -> str:
        """Build a concise description string for the GitLab commit status.

        GitLab status descriptions are limited to ~255 characters, so this
        method creates a brief summary suitable for display in the MR UI.

        Args:
            output: Policy evaluation output to summarize.

        Returns:
            A short description string like "All checks passed (5 files)"
            or "Found 2 errors, 3 warnings".
        """
        if output.passed:
            return f"All checks passed ({len(output.files_checked)} files)"

        # Count violations by severity level
        errors = sum(1 for v in output.violations if v.severity == "error")
        warnings = sum(1 for v in output.violations if v.severity == "warning")

        # Build human-readable summary with proper pluralization
        parts = []
        if errors:
            parts.append(f"{errors} error{'s' if errors != 1 else ''}")
        if warnings:
            parts.append(f"{warnings} warning{'s' if warnings != 1 else ''}")

        return f"Found {', '.join(parts)}"

    async def _post_or_update_note(
        self,
        project_id: int,
        mr_iid: int,
        output: PolicyOutput,
    ) -> int | None:
        """Post or update the bot's MR note with evaluation results.

        Formats the policy output into a Markdown note and delegates
        to _post_or_update_note_raw for the actual GitLab API calls.

        Args:
            project_id: GitLab project ID.
            mr_iid: Merge request internal ID.
            output: Policy evaluation output to format into the note.

        Returns:
            The GitLab note ID if successful, None if the operation failed.
        """
        body = format_mr_comment(output, output.evaluation_id)
        return await self._post_or_update_note_raw(
            project_id=project_id,
            mr_iid=mr_iid,
            body=body,
        )

    async def _post_or_update_note_raw(
        self,
        project_id: int,
        mr_iid: int,
        body: str,
    ) -> int | None:
        """Post or update an MR note with raw body content.

        This method implements idempotent commenting by looking for an existing
        bot note (identified by COMMENT_MARKER) and updating it rather than
        creating duplicates. This keeps the MR clean even after multiple
        force-pushes or re-runs.

        Args:
            project_id: GitLab project ID.
            mr_iid: Merge request internal ID.
            body: Raw Markdown body content for the note.

        Returns:
            The GitLab note ID (existing or new) if successful,
            None if the operation failed.
        """
        try:
            # Search for existing bot note to update (avoids duplicates)
            existing = await self.gitlab.find_bot_note(
                project_id=project_id,
                mr_iid=mr_iid,
                marker=COMMENT_MARKER,
            )

            if existing:
                # Update existing note in place
                note_id = existing.get("id")
                await self.gitlab.update_mr_note(
                    project_id=project_id,
                    mr_iid=mr_iid,
                    note_id=note_id,
                    body=body,
                )
                return note_id
            else:
                # Create new note
                result = await self.gitlab.create_mr_note(
                    project_id=project_id,
                    mr_iid=mr_iid,
                    body=body,
                )
                return result.get("id")

        except Exception as e:
            # Log but don't fail the check if commenting fails
            logger.warning(f"Failed to post/update note: {e}")
            return None
