"""Pull request event handler for MergeGuide policy checks.

This module provides the main entry point for processing GitHub pull request
webhooks. It orchestrates the complete PR check workflow including:

1. Posting initial "pending" commit status to GitHub
2. Fetching the PR 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

The module contains two main handlers:
- PRHandler: Processes pull_request events for policy evaluation
- InstallationHandler: Manages GitHub App installation lifecycle events

Typical usage in a Lambda function:
    >>> from mergeguide.github.pr_handler import PRHandler
    >>> from mergeguide.github.api import GitHubAPI
    >>> from mergeguide.engine.evaluator import PolicyEngine
    >>>
    >>> github = GitHubAPI(app_id=123, private_key="...")
    >>> engine = PolicyEngine.from_rules(rules)
    >>> handler = PRHandler(github, engine)
    >>>
    >>> # In webhook handler
    >>> result = await handler.handle_pull_request(event)
    >>> if not result.passed:
    ...     print(f"PR blocked: {result.errors_count} errors")
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from .api import GitHubAPI
from .comments import (
    COMMENT_MARKER,
    format_error_comment,
    format_pr_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 PRCheckResult:
    """Encapsulates the outcome of a pull 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 PR 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.
        comment_id: GitHub comment ID for the bot's PR comment, if posted.

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

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


class PRHandler:
    """Handles pull_request webhook events for security policy evaluation.

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

    The check workflow:
        1. Post "pending" commit status to GitHub
        2. Fetch the PR diff via GitHub 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 PR comment with detailed results

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

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

    Example:
        >>> github = GitHubAPI(app_id=123, private_key=key)
        >>> engine = PolicyEngine.from_rules(rules)
        >>> handler = PRHandler(github, engine)
        >>>
        >>> # Process a webhook event
        >>> result = await handler.handle_pull_request(event)
        >>> print(f"Passed: {result.passed}, Errors: {result.errors_count}")
    """

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

        Args:
            github_api: GitHub API client for status and comment posting.
                Must be configured with valid App credentials.
            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.github = github_api
        self.engine = policy_engine
        self.storage = evidence_storage
        self.signing_key = signing_key

    async def handle_pull_request(self, event: WebhookEvent) -> PRCheckResult:
        """Process a pull_request webhook event and run policy checks.

        This is the main entry point for PR policy evaluation. It executes
        the complete check workflow:
        1. Validates the event and extracts PR metadata
        2. Posts a "pending" commit status
        3. Fetches and evaluates the PR 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 pull_request webhook.
                Must have action in ["opened", "synchronize", "reopened"].

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

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

        # Extract required metadata from the webhook payload
        installation_id = event.get_installation_id()
        repo_info = event.get_repository_info()
        pr_info = event.get_pr_info()

        if not installation_id or not repo_info or not pr_info:
            raise ValueError("Missing required event data (installation, repo, or PR info)")

        owner, repo = repo_info
        pr_number = pr_info["pr_number"]
        head_sha = pr_info["head_sha"]

        logger.info(f"Processing PR #{pr_number} on {owner}/{repo} @ {head_sha[:7]}")

        # Immediately post "pending" status so users see the check is running
        await self.github.post_status(
            installation_id=installation_id,
            owner=owner,
            repo=repo,
            sha=head_sha,
            state="pending",
            description="Evaluating against security policies...",
        )

        try:
            # Fetch the PR diff content for analysis
            diff = await self.github.fetch_pr_diff(
                installation_id=installation_id,
                owner=owner,
                repo=repo,
                pr_number=pr_number,
            )

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

            context = EvaluationContext(
                org_id=owner,
                repo=repo,
                branch=pr_info["head_ref"],
                commit_sha=head_sha,
                author=pr_info["author"],
                pr_number=pr_number,
            )

            # Execute all configured policies against the diff
            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 GitHub status state
            state = "success" if output.passed else "failure"
            description = self._build_status_description(output)

            # Update commit status with final result
            await self.github.post_status(
                installation_id=installation_id,
                owner=owner,
                repo=repo,
                sha=head_sha,
                state=state,
                description=description,
            )

            # Post detailed results as a PR comment (creates or updates existing)
            comment_id = await self._post_or_update_comment(
                installation_id=installation_id,
                owner=owner,
                repo=repo,
                pr_number=pr_number,
                output=output,
            )

            return PRCheckResult(
                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,
                comment_id=comment_id,
            )

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

            # Post "error" status to indicate the check itself failed (not the PR)
            await self.github.post_status(
                installation_id=installation_id,
                owner=owner,
                repo=repo,
                sha=head_sha,
                state="error",
                description="Policy check failed - see PR comment",
            )

            # Post a user-friendly error comment explaining what went wrong
            comment_body = format_error_comment(str(e))
            await self._post_or_update_comment_raw(
                installation_id=installation_id,
                owner=owner,
                repo=repo,
                pr_number=pr_number,
                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 PR 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 PR 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 GitHub commit status.

        GitHub status descriptions are limited to ~140 characters, so this
        method creates a brief summary suitable for display in the PR 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_comment(
        self,
        installation_id: int,
        owner: str,
        repo: str,
        pr_number: int,
        output: PolicyOutput,
    ) -> int | None:
        """Post or update the bot's PR comment with evaluation results.

        Formats the policy output into a Markdown comment and delegates
        to _post_or_update_comment_raw for the actual GitHub API calls.

        Args:
            installation_id: GitHub App installation ID for API auth.
            owner: Repository owner (org or user).
            repo: Repository name.
            pr_number: Pull request number to comment on.
            output: Policy evaluation output to format into the comment.

        Returns:
            The GitHub comment ID if successful, None if the operation failed.
        """
        body = format_pr_comment(output, output.evaluation_id)
        return await self._post_or_update_comment_raw(
            installation_id=installation_id,
            owner=owner,
            repo=repo,
            pr_number=pr_number,
            body=body,
        )

    async def _post_or_update_comment_raw(
        self,
        installation_id: int,
        owner: str,
        repo: str,
        pr_number: int,
        body: str,
    ) -> int | None:
        """Post or update a PR comment with raw body content.

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

        Args:
            installation_id: GitHub App installation ID for API auth.
            owner: Repository owner (org or user).
            repo: Repository name.
            pr_number: Pull request number to comment on.
            body: Raw Markdown body content for the comment.

        Returns:
            The GitHub comment ID (existing or new) if successful,
            None if the operation failed.
        """
        try:
            # Search for existing bot comment to update (avoids duplicates)
            existing_id = await self.github.find_bot_comment(
                installation_id=installation_id,
                owner=owner,
                repo=repo,
                pr_number=pr_number,
                marker=COMMENT_MARKER,
            )

            if existing_id:
                # Update existing comment in place
                await self.github.update_comment(
                    installation_id=installation_id,
                    owner=owner,
                    repo=repo,
                    comment_id=existing_id,
                    body=body,
                )
                return existing_id
            else:
                # Create new comment
                return await self.github.post_comment(
                    installation_id=installation_id,
                    owner=owner,
                    repo=repo,
                    pr_number=pr_number,
                    body=body,
                )

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


class InstallationHandler:
    """Handles GitHub App installation lifecycle events.

    This handler processes installation webhooks to track which organizations
    and repositories have installed the MergeGuide GitHub App. Installation
    state is persisted to DynamoDB for later lookup during PR processing.

    Supported installation actions:
        - created: New installation, store in DynamoDB as active
        - deleted: Mark installation as deleted (soft delete)
        - suspended: Mark installation as suspended (billing/admin action)
        - unsuspended: Reactivate a suspended installation

    DynamoDB table schema:
        - pk: INSTALLATION#{installation_id}
        - sk: CONFIG
        - status: active | suspended | deleted
        - account_login: GitHub org/user login
        - created_at: ISO 8601 timestamp

    Attributes:
        table_name: Name of the DynamoDB table for installation records.
        region: AWS region where the DynamoDB table is located.

    Example:
        >>> handler = InstallationHandler("mergeguide-installations")
        >>> result = handler.handle_installation(event)
        >>> print(result)
        {'action': 'created', 'installation_id': 12345}
    """

    def __init__(self, installations_table_name: str, region: str = "us-east-1") -> None:
        """Initialize the installation handler with DynamoDB configuration.

        Args:
            installations_table_name: Name of the DynamoDB table for storing
                installation records. Table must exist with pk/sk key schema.
            region: AWS region for DynamoDB. Defaults to us-east-1.
        """
        self.table_name = installations_table_name
        self.region = region
        self._table = None

    def _get_table(self):
        """Lazily initialize and return the DynamoDB table resource.

        Uses lazy loading to avoid AWS SDK initialization overhead when
        the handler is constructed but not used (e.g., during import).

        Returns:
            boto3 DynamoDB Table resource.
        """
        if self._table is None:
            import boto3

            dynamodb = boto3.resource("dynamodb", region_name=self.region)
            self._table = dynamodb.Table(self.table_name)
        return self._table

    def handle_installation(self, event: WebhookEvent) -> dict[str, Any]:
        """Route an installation webhook event to the appropriate handler.

        Dispatches to _handle_created, _handle_deleted, _handle_suspended,
        or _handle_unsuspended based on the event action.

        Args:
            event: Parsed WebhookEvent from an installation webhook.

        Returns:
            Dict with 'action' key and 'installation_id' or 'status'.

        Raises:
            ValueError: If the event is missing the installation ID.
        """
        action = event.action
        installation = event.payload.get("installation", {})
        installation_id = installation.get("id")

        if not installation_id:
            raise ValueError("Missing installation ID in event")

        if action == "created":
            return self._handle_created(installation)
        elif action == "deleted":
            return self._handle_deleted(installation)
        elif action == "suspended":
            return self._handle_suspended(installation)
        elif action == "unsuspended":
            return self._handle_unsuspended(installation)
        else:
            logger.info(f"Unhandled installation action: {action}")
            return {"action": action, "status": "ignored"}

    def _handle_created(self, installation: dict) -> dict[str, Any]:
        """Handle a new GitHub App installation.

        Creates a new record in DynamoDB with the installation metadata
        and sets status to 'active'.

        Args:
            installation: Installation object from the webhook payload.

        Returns:
            Dict with action='created' and the installation_id.
        """
        from datetime import UTC, datetime

        table = self._get_table()

        # Build the DynamoDB item with installation metadata
        item = {
            "pk": f"INSTALLATION#{installation['id']}",
            "sk": "CONFIG",
            "installation_id": installation["id"],
            "account_login": installation.get("account", {}).get("login", ""),
            "account_type": installation.get("account", {}).get("type", ""),
            "created_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
            "status": "active",
            "app_id": installation.get("app_id"),
            "target_type": installation.get("target_type", ""),
        }

        table.put_item(Item=item)
        logger.info(f"Created installation {installation['id']}")

        return {"action": "created", "installation_id": installation["id"]}

    def _handle_deleted(self, installation: dict) -> dict[str, Any]:
        """Handle GitHub App uninstallation.

        Performs a soft delete by updating the status to 'deleted' and
        recording the deletion timestamp. The record is retained for
        audit purposes.

        Args:
            installation: Installation object from the webhook payload.

        Returns:
            Dict with action='deleted' and the installation_id.
        """
        table = self._get_table()

        # Soft delete: update status rather than removing the record
        table.update_item(
            Key={
                "pk": f"INSTALLATION#{installation['id']}",
                "sk": "CONFIG",
            },
            UpdateExpression="SET #status = :status, deleted_at = :deleted_at",
            ExpressionAttributeNames={"#status": "status"},
            ExpressionAttributeValues={
                ":status": "deleted",
                ":deleted_at": __import__("datetime").datetime.now(
                    __import__("datetime").UTC
                ).isoformat().replace("+00:00", "Z"),
            },
        )
        logger.info(f"Marked installation {installation['id']} as deleted")

        return {"action": "deleted", "installation_id": installation["id"]}

    def _handle_suspended(self, installation: dict) -> dict[str, Any]:
        """Handle GitHub App installation suspension.

        Suspension typically occurs due to billing issues or admin action.
        Updates the status to 'suspended' to prevent processing webhooks
        for this installation.

        Args:
            installation: Installation object from the webhook payload.

        Returns:
            Dict with action='suspended' and the installation_id.
        """
        table = self._get_table()

        table.update_item(
            Key={
                "pk": f"INSTALLATION#{installation['id']}",
                "sk": "CONFIG",
            },
            UpdateExpression="SET #status = :status",
            ExpressionAttributeNames={"#status": "status"},
            ExpressionAttributeValues={":status": "suspended"},
        )
        logger.info(f"Marked installation {installation['id']} as suspended")

        return {"action": "suspended", "installation_id": installation["id"]}

    def _handle_unsuspended(self, installation: dict) -> dict[str, Any]:
        """Handle GitHub App installation reactivation.

        Restores the installation to 'active' status after a suspension
        has been lifted.

        Args:
            installation: Installation object from the webhook payload.

        Returns:
            Dict with action='unsuspended' and the installation_id.
        """
        table = self._get_table()

        table.update_item(
            Key={
                "pk": f"INSTALLATION#{installation['id']}",
                "sk": "CONFIG",
            },
            UpdateExpression="SET #status = :status",
            ExpressionAttributeNames={"#status": "status"},
            ExpressionAttributeValues={":status": "active"},
        )
        logger.info(f"Marked installation {installation['id']} as active")

        return {"action": "unsuspended", "installation_id": installation["id"]}
