"""Lambda handler for GitHub webhook events.

This module implements the AWS Lambda function that receives and processes
GitHub webhook events. It serves as the entry point for MergeGuide's GitHub
App integration, enabling automated policy evaluation on pull requests.

Triggers:
    - API Gateway HTTP API: POST /webhook
    - GitHub App webhook delivery

Supported Events:
    - pull_request.opened: New PR opened - triggers full evaluation
    - pull_request.synchronize: New commits pushed - triggers re-evaluation
    - pull_request.reopened: PR reopened - triggers re-evaluation
    - installation.created: App installed on repository/organization
    - installation.deleted: App uninstalled
    - installation_repositories.added: Repos added to installation
    - installation_repositories.removed: Repos removed from installation

Event Format (API Gateway HTTP API v2):
    {
        "headers": {
            "x-github-event": "pull_request",
            "x-github-delivery": "uuid",
            "x-hub-signature-256": "sha256=..."
        },
        "body": "{...}",  # JSON string of webhook payload
        "isBase64Encoded": false,
        "requestContext": {...}
    }

Response Format:
    {
        "statusCode": 200,
        "body": "{\"status\": \"processed\", ...}",
        "headers": {"Content-Type": "application/json"}
    }

Environment Variables:
    ENVIRONMENT: Deployment environment (dev, staging, prod)
    GITHUB_APP_SECRET_ARN: ARN for GitHub App credentials in Secrets Manager
    WEBHOOK_SECRET_ARN: ARN for webhook signature verification secret
    EVIDENCE_BUCKET: S3 bucket for storing evidence artifacts
    SIGNING_SECRET_ARN: ARN for evidence signing key
    INSTALLATIONS_TABLE: DynamoDB table for tracking GitHub App installations
    AWS_REGION: AWS region for service clients

Security:
    All incoming webhooks are verified using HMAC-SHA256 signature validation
    before processing. Invalid signatures result in 401 Unauthorized responses.

Architecture:
    This Lambda uses lazy initialization for expensive resources (GitHub auth,
    PolicyEngine, EvidenceStorage) to optimize cold start times. Resources are
    cached in module-level globals and reused across warm invocations.
"""

from __future__ import annotations

import asyncio
import json
import logging
import os
from typing import Any

# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Import mergeguide modules - these will be bundled with the Lambda
try:
    from mergeguide.engine.evaluator import PolicyEngine
    from mergeguide.evidence.storage import EvidenceStorage
    from mergeguide.github import (
        GitHubAPI,
        GitHubAuth,
        InstallationHandler,
        PRHandler,
        parse_webhook_event,
        verify_webhook_signature,
    )
    from mergeguide.policies.defaults import get_default_rules
except ImportError:
    # Running locally or in test mode
    import sys

    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    from mergeguide.engine.evaluator import PolicyEngine
    from mergeguide.evidence.storage import EvidenceStorage
    from mergeguide.github import (
        GitHubAPI,
        GitHubAuth,
        InstallationHandler,
        PRHandler,
        parse_webhook_event,
        verify_webhook_signature,
    )
    from mergeguide.policies.defaults import get_default_rules


# Environment variables
ENVIRONMENT = os.environ.get("ENVIRONMENT", "dev")
GITHUB_APP_SECRET_ARN = os.environ.get("GITHUB_APP_SECRET_ARN", "")
WEBHOOK_SECRET_ARN = os.environ.get("WEBHOOK_SECRET_ARN", "")
EVIDENCE_BUCKET = os.environ.get("EVIDENCE_BUCKET", f"sentinel-evidence-{ENVIRONMENT}")
SIGNING_SECRET_ARN = os.environ.get("SIGNING_SECRET_ARN", "")
INSTALLATIONS_TABLE = os.environ.get(
    "INSTALLATIONS_TABLE", f"sentinel-installations-{ENVIRONMENT}"
)
AWS_REGION = os.environ.get("AWS_REGION", "us-east-1")

# Lazy-loaded resources
_github_auth: GitHubAuth | None = None
_webhook_secret: str | None = None
_signing_key: str | None = None
_engine: PolicyEngine | None = None
_storage: EvidenceStorage | None = None


def _get_secret(secret_arn: str, key: str) -> str:
    """Fetch a specific key from a JSON secret stored in AWS Secrets Manager.

    This is a utility function for retrieving individual values from secrets
    that store multiple key-value pairs as a JSON object.

    Args:
        secret_arn: The ARN of the secret in Secrets Manager.
        key: The JSON key to extract from the secret's value.

    Returns:
        str: The value associated with the key, or empty string if:
            - secret_arn is empty or None
            - The secret cannot be retrieved (network/permission errors)
            - The key doesn't exist in the secret

    Note:
        Failures are logged as warnings but do not raise exceptions,
        allowing the caller to handle missing secrets gracefully.
    """
    if not secret_arn:
        return ""
    try:
        import boto3

        client = boto3.client("secretsmanager", region_name=AWS_REGION)
        response = client.get_secret_value(SecretId=secret_arn)
        # Parse the JSON-formatted secret string
        secret = json.loads(response["SecretString"])
        return secret.get(key, "")
    except Exception as e:
        logger.warning(f"Failed to get secret {secret_arn}: {e}")
        return ""


def get_github_auth() -> GitHubAuth:
    """Get or create the singleton GitHubAuth instance for API authentication.

    Retrieves GitHub App credentials (app_id and private_key) from AWS Secrets
    Manager and creates a GitHubAuth instance capable of generating installation
    access tokens for the GitHub API.

    The auth instance is cached to avoid repeated Secrets Manager calls and
    to reuse the JWT signing configuration across requests.

    Returns:
        GitHubAuth: Configured authentication instance for GitHub API calls.

    Raises:
        KeyError: If the secret is missing required keys (app_id, private_key).
        botocore.exceptions.ClientError: If Secrets Manager access fails.
    """
    global _github_auth
    if _github_auth is None:
        import boto3

        # Fetch GitHub App credentials from Secrets Manager
        client = boto3.client("secretsmanager", region_name=AWS_REGION)
        response = client.get_secret_value(SecretId=GITHUB_APP_SECRET_ARN)
        secret = json.loads(response["SecretString"])

        # Initialize auth with App ID and RSA private key for JWT signing
        _github_auth = GitHubAuth(
            app_id=secret["app_id"],
            private_key=secret["private_key"],
        )
    return _github_auth


def get_webhook_secret() -> str:
    """Get the webhook secret for GitHub signature verification.

    The webhook secret is configured in the GitHub App settings and used
    to compute HMAC-SHA256 signatures. GitHub signs every webhook payload
    with this secret, allowing us to verify the request authenticity.

    Returns:
        str: The webhook secret string, or empty string if unavailable.

    Note:
        An empty return value will cause signature verification to be skipped,
        which should only occur in development/testing environments.
    """
    global _webhook_secret
    if _webhook_secret is None:
        _webhook_secret = _get_secret(WEBHOOK_SECRET_ARN, "webhook_secret")
    return _webhook_secret


def get_signing_key() -> str:
    """Get the HMAC signing key for evidence artifact signatures.

    The signing key is used to create cryptographic signatures on evidence
    artifacts, enabling downstream verification of artifact integrity.

    Returns:
        str: The signing key string, or empty string if unavailable.

    Note:
        Evidence artifacts created without a signing key will be unsigned
        but still stored. This is acceptable for non-production environments.
    """
    global _signing_key
    if _signing_key is None:
        _signing_key = _get_secret(SIGNING_SECRET_ARN, "signing_key")
    return _signing_key


def get_engine() -> PolicyEngine:
    """Get or create the singleton PolicyEngine instance.

    The engine is initialized with default security rules for evaluating
    pull request diffs. It is cached to avoid re-initialization overhead
    on warm Lambda invocations.

    Returns:
        PolicyEngine: Configured policy engine ready for PR evaluations.
    """
    global _engine
    if _engine is None:
        _engine = PolicyEngine(rules=get_default_rules())
    return _engine


def get_storage() -> EvidenceStorage:
    """Get or create the singleton EvidenceStorage instance.

    The storage client is used to persist evaluation evidence artifacts
    to S3 for compliance and audit purposes.

    Returns:
        EvidenceStorage: Configured storage client for the evidence bucket.
    """
    global _storage
    if _storage is None:
        _storage = EvidenceStorage(
            bucket_name=EVIDENCE_BUCKET,
            region=AWS_REGION,
        )
    return _storage


def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
    """AWS Lambda entry point for GitHub webhook events.

    This is the main handler function invoked by AWS Lambda when GitHub
    delivers a webhook event. It processes the complete webhook lifecycle:
    1. Extract and decode the request body from API Gateway format
    2. Verify the webhook signature for security
    3. Parse the GitHub event type and payload
    4. Route to the appropriate event handler (PR or installation)
    5. Return an appropriate HTTP response

    The handler supports all GitHub webhook events but only actively processes:
    - Pull request events (opened, synchronize, reopened)
    - Installation lifecycle events (created, deleted, repos added/removed)

    All other events are acknowledged with a 200 response but not processed.

    Args:
        event: API Gateway HTTP API v2 event dictionary containing:
            - headers (dict): HTTP headers including GitHub-specific headers:
                - x-github-event: Event type (e.g., "pull_request")
                - x-github-delivery: Unique delivery UUID
                - x-hub-signature-256: HMAC-SHA256 signature for verification
            - body (str): JSON-encoded webhook payload (may be base64 encoded)
            - isBase64Encoded (bool): Whether body is base64 encoded
            - requestContext (dict): API Gateway context information
        context: AWS Lambda context object (not currently used).

    Returns:
        dict: API Gateway-compatible response with:
            - statusCode (int): HTTP status code
                - 200: Event processed or ignored successfully
                - 400: Invalid JSON body
                - 401: Invalid webhook signature
                - 500: Internal processing error
            - body (str): JSON-encoded response payload
            - headers (dict): Response headers

    Example Response (PR processed):
        {
            "statusCode": 200,
            "body": "{\"status\": \"processed\", \"passed\": true, ...}",
            "headers": {"Content-Type": "application/json"}
        }

    Security:
        All requests are verified using GitHub's HMAC-SHA256 signature before
        processing. Requests with invalid or missing signatures (when webhook
        secret is configured) are rejected with 401 Unauthorized.
    """
    logger.info("Received webhook event")

    try:
        # Step 1: Extract headers and body from API Gateway event format
        headers = event.get("headers", {})
        raw_body = event.get("body", "")

        # Step 2: Handle base64 encoding (API Gateway may encode binary bodies)
        if event.get("isBase64Encoded", False):
            import base64

            raw_body = base64.b64decode(raw_body).decode("utf-8")

        # Convert to bytes for signature verification
        body_bytes = raw_body.encode("utf-8") if isinstance(raw_body, str) else raw_body

        # Step 3: Verify webhook signature for security
        # GitHub signs all webhooks with HMAC-SHA256 using the webhook secret
        signature = headers.get("x-hub-signature-256", "")
        webhook_secret = get_webhook_secret()

        if webhook_secret and not verify_webhook_signature(body_bytes, signature, webhook_secret):
            logger.warning("Invalid webhook signature")
            return _response(401, {"error": "Invalid signature"})

        # Step 4: Parse the webhook payload and extract event metadata
        body = json.loads(raw_body) if isinstance(raw_body, str) else raw_body
        webhook_event = parse_webhook_event(headers, body)

        logger.info(
            f"Processing {webhook_event.event_type}.{webhook_event.action} "
            f"(delivery: {webhook_event.delivery_id})"
        )

        # Step 5: Route to the appropriate handler based on event type
        if webhook_event.should_process_pr:
            # Pull request events that trigger policy evaluation
            result = _handle_pr_event(webhook_event)
            return _response(200, result)

        elif webhook_event.is_installation:
            # Installation lifecycle events for tracking app installations
            result = _handle_installation_event(webhook_event)
            return _response(200, result)

        else:
            # Acknowledge but don't process other event types
            logger.info(f"Ignoring event type: {webhook_event.event_type}.{webhook_event.action}")
            return _response(200, {"status": "ignored", "event": webhook_event.event_type})

    except json.JSONDecodeError as e:
        # Malformed JSON in request body
        logger.error(f"Invalid JSON body: {e}")
        return _response(400, {"error": "Invalid JSON body"})

    except Exception as e:
        # Unexpected errors - log full stack trace for debugging
        logger.exception(f"Webhook handler error: {e}")
        return _response(500, {"error": "Internal error"})


def _handle_pr_event(event) -> dict[str, Any]:
    """Handle pull request events by running policy evaluation.

    This function orchestrates the complete PR evaluation workflow:
    1. Initialize GitHub API client with installation authentication
    2. Fetch the PR diff from GitHub
    3. Run policy evaluation against the diff
    4. Post check run results back to GitHub
    5. Store evidence artifact in S3

    Args:
        event: Parsed webhook event object containing PR metadata:
            - installation_id: GitHub App installation ID
            - repository: Repository full name (owner/repo)
            - pull_request: PR details (number, head SHA, etc.)

    Returns:
        dict: Processing result with:
            - status (str): "processed"
            - passed (bool): Whether the PR passed all policy checks
            - evaluation_id (str): Unique evaluation identifier
            - violations_count (int): Number of policy violations found
            - evidence_key (str): S3 key where evidence artifact is stored
    """
    # Initialize all required components for PR processing
    auth = get_github_auth()
    github_api = GitHubAPI(auth)
    engine = get_engine()
    storage = get_storage()
    signing_key = get_signing_key()

    # Create the PR handler with all dependencies
    pr_handler = PRHandler(
        github_api=github_api,
        policy_engine=engine,
        evidence_storage=storage,
        signing_key=signing_key,
    )

    # Run the async PR handler synchronously
    # Note: Lambda doesn't have a running event loop, so we create one
    result = asyncio.get_event_loop().run_until_complete(
        pr_handler.handle_pull_request(event)
    )

    return {
        "status": "processed",
        "passed": result.passed,
        "evaluation_id": result.evaluation_id,
        "violations_count": result.violations_count,
        "evidence_key": result.evidence_key,
    }


def _handle_installation_event(event) -> dict[str, Any]:
    """Handle GitHub App installation lifecycle events.

    Processes events related to the installation and configuration of the
    MergeGuide GitHub App on repositories and organizations. This tracks:
    - New installations (app installed)
    - Uninstallations (app removed)
    - Repository additions (repos added to existing installation)
    - Repository removals (repos removed from installation)

    Installation records are stored in DynamoDB for tracking which
    repositories are covered by MergeGuide.

    Args:
        event: Parsed webhook event object containing installation metadata:
            - installation_id: GitHub App installation ID
            - action: Lifecycle action (created, deleted, etc.)
            - repositories: List of affected repositories

    Returns:
        dict: Result from InstallationHandler with processing status.
    """
    # Create handler with DynamoDB table for persistence
    handler = InstallationHandler(
        installations_table_name=INSTALLATIONS_TABLE,
        region=AWS_REGION,
    )
    return handler.handle_installation(event)


def _response(status_code: int, body: dict[str, Any]) -> dict[str, Any]:
    """Build an API Gateway HTTP API v2 compatible response.

    Constructs a properly formatted response dictionary that API Gateway
    can proxy to the HTTP client.

    Args:
        status_code: HTTP status code (e.g., 200, 400, 401, 500).
        body: Response payload dictionary to be JSON-encoded.

    Returns:
        dict: API Gateway response with:
            - statusCode (int): The HTTP status code
            - body (str): JSON-encoded response body
            - headers (dict): Response headers with Content-Type
    """
    return {
        "statusCode": status_code,
        "body": json.dumps(body),
        "headers": {
            "Content-Type": "application/json",
        },
    }


# For local testing
if __name__ == "__main__":
    # Test with a sample PR event
    test_event = {
        "headers": {
            "x-github-event": "pull_request",
            "x-github-delivery": "test-delivery-123",
            "x-hub-signature-256": "",  # Skip verification for local test
        },
        "body": json.dumps({
            "action": "opened",
            "installation": {"id": 12345},
            "repository": {"full_name": "test-org/test-repo"},
            "pull_request": {
                "number": 1,
                "head": {
                    "sha": "abc123",
                    "ref": "feature-branch",
                },
                "base": {
                    "sha": "def456",
                    "ref": "main",
                },
                "title": "Test PR",
                "user": {"login": "test-user"},
            },
        }),
    }

    # Note: This won't work without real GitHub credentials
    print("Test event parsed successfully")
    print("Event type: pull_request.opened")
