# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Multi-SCM webhook Lambda handler for GitLab, Bitbucket, and Azure DevOps.

This module implements the AWS Lambda function that receives and processes
webhook events from non-GitHub SCM platforms. It supports:
  - GitLab (POST /webhook/gitlab)
  - Bitbucket Cloud (POST /webhook/bitbucket)
  - Azure DevOps (POST /webhook/azure)

Each SCM has its own signature verification, event parsing, and PR/MR handler.
The handler dispatches based on the API Gateway route path.

Architecture:
    Follows the same lazy-initialization pattern as webhook.py (GitHub handler)
    for optimal cold-start performance. Resources are cached in module-level
    globals and reused across warm invocations.

Environment Variables:
    ENVIRONMENT: Deployment environment (dev, staging, prod)
    GITLAB_WEBHOOK_SECRET_ARN: ARN for GitLab webhook token
    GITLAB_ACCESS_TOKEN_ARN: ARN for GitLab PAT/OAuth token
    BITBUCKET_WEBHOOK_SECRET_ARN: ARN for Bitbucket HMAC secret
    BITBUCKET_OAUTH_SECRET_ARN: ARN for Bitbucket OAuth credentials
    AZURE_WEBHOOK_SECRET_ARN: ARN for Azure DevOps HMAC secret
    AZURE_PAT_SECRET_ARN: ARN for Azure DevOps PAT
    EVIDENCE_BUCKET: S3 bucket for storing evidence artifacts
    SIGNING_SECRET_ARN: ARN for evidence signing key
    RATE_LIMITS_TABLE: DynamoDB table for rate limiting
    EVALUATIONS_TABLE: DynamoDB table for storing evaluations
    ORGANIZATIONS_TABLE: DynamoDB table for org lookup
"""

from __future__ import annotations

import json
import logging
import os
from typing import Any

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

# Import mergeguide modules
try:
    from mergeguide.engine.evaluator import PolicyEngine
    from mergeguide.engine.orchestrator import DualLayerOrchestrator
    from mergeguide.evidence.storage import EvidenceStorage
    from mergeguide.policies.defaults import get_default_rules
except ImportError:
    import sys

    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    from mergeguide.engine.evaluator import PolicyEngine
    from mergeguide.engine.orchestrator import DualLayerOrchestrator
    from mergeguide.evidence.storage import EvidenceStorage
    from mergeguide.policies.defaults import get_default_rules

# Import rate limiting
try:
    from mergeguide.rate_limiting import (
        DEFAULT_TIER,
        build_rate_limit_headers,
        check_rate_limit,
    )
except ImportError:
    from rate_limiting import (  # type: ignore[import-not-found,no-redef]
        DEFAULT_TIER,
        build_rate_limit_headers,
        check_rate_limit,
    )

# =============================================================================
# Environment Variables
# =============================================================================
ENVIRONMENT = os.environ.get("ENVIRONMENT", "dev")
EVIDENCE_BUCKET = os.environ.get("EVIDENCE_BUCKET", f"mergeguide-evidence-{ENVIRONMENT}")
SIGNING_SECRET_ARN = os.environ.get("SIGNING_SECRET_ARN", "")
RATE_LIMITS_TABLE = os.environ.get("RATE_LIMITS_TABLE", f"mergeguide-rate-limits-{ENVIRONMENT}")
EVALUATIONS_TABLE = os.environ.get("EVALUATIONS_TABLE", f"mergeguide-evaluations-{ENVIRONMENT}")
ORGANIZATIONS_TABLE = os.environ.get(
    "ORGANIZATIONS_TABLE", f"mergeguide-organizations-{ENVIRONMENT}"
)
AWS_REGION = os.environ.get("AWS_REGION", "us-east-1")

# GitLab
GITLAB_WEBHOOK_SECRET_ARN = os.environ.get("GITLAB_WEBHOOK_SECRET_ARN", "")
GITLAB_ACCESS_TOKEN_ARN = os.environ.get("GITLAB_ACCESS_TOKEN_ARN", "")

# Bitbucket
BITBUCKET_WEBHOOK_SECRET_ARN = os.environ.get("BITBUCKET_WEBHOOK_SECRET_ARN", "")
BITBUCKET_OAUTH_SECRET_ARN = os.environ.get("BITBUCKET_OAUTH_SECRET_ARN", "")

# Azure DevOps
AZURE_WEBHOOK_SECRET_ARN = os.environ.get("AZURE_WEBHOOK_SECRET_ARN", "")
AZURE_PAT_SECRET_ARN = os.environ.get("AZURE_PAT_SECRET_ARN", "")

# =============================================================================
# Lazy-loaded singletons
# =============================================================================
_signing_key: str | None = None
_engine: DualLayerOrchestrator | None = None
_storage: EvidenceStorage | None = None

# Per-SCM cached credentials
_gitlab_webhook_secret: str | None = None
_gitlab_access_token: str | None = None
_bitbucket_webhook_secret: str | None = None
_bitbucket_oauth: dict[str, str] | None = None
_azure_webhook_secret: str | None = None
_azure_pat: str | None = None


# =============================================================================
# Shared helpers (same pattern as webhook.py)
# =============================================================================


def _get_secret(secret_arn: str, key: str) -> str:
    """Fetch a specific key from a JSON secret in Secrets Manager."""
    if not secret_arn:
        return ""
    try:
        import boto3
        from botocore.exceptions import ClientError

        client = boto3.client("secretsmanager", region_name=AWS_REGION)
        try:
            response = client.get_secret_value(SecretId=secret_arn)
        except ClientError as ce:
            if ce.response["Error"]["Code"] == "AccessDeniedException" and ":secret:" in secret_arn:
                secret_name = secret_arn.split(":secret:")[-1]
                response = client.get_secret_value(SecretId=secret_name)
            else:
                raise
        secret = json.loads(response["SecretString"])
        return str(secret.get(key, ""))
    except Exception as e:
        logger.warning(f"Failed to get secret {secret_arn}: {e}")
        return ""


def _get_secret_dict(secret_arn: str) -> dict[str, Any]:
    """Fetch the full JSON dict from a secret in Secrets Manager."""
    if not secret_arn:
        return {}
    try:
        import boto3
        from botocore.exceptions import ClientError

        client = boto3.client("secretsmanager", region_name=AWS_REGION)
        try:
            response = client.get_secret_value(SecretId=secret_arn)
        except ClientError as ce:
            if ce.response["Error"]["Code"] == "AccessDeniedException" and ":secret:" in secret_arn:
                secret_name = secret_arn.split(":secret:")[-1]
                response = client.get_secret_value(SecretId=secret_name)
            else:
                raise
        result: dict[str, Any] = json.loads(response["SecretString"])
        return result
    except Exception as e:
        logger.warning(f"Failed to get secret dict {secret_arn}: {e}")
        return {}


def get_signing_key() -> str:
    """Get the HMAC signing key for evidence artifacts."""
    global _signing_key
    if _signing_key is None:
        _signing_key = _get_secret(SIGNING_SECRET_ARN, "signing_key")
    return _signing_key


def get_engine() -> DualLayerOrchestrator:
    """Get or create the singleton DualLayerOrchestrator."""
    global _engine
    if _engine is None:
        regex_engine = PolicyEngine(rules=get_default_rules())
        _engine = DualLayerOrchestrator(
            regex_engine=regex_engine,
            semgrep_enabled=True,
            semgrep_timeout=int(os.environ.get("SEMGREP_TIMEOUT", "120")),
            parallel=True,
        )
    return _engine


def get_storage() -> EvidenceStorage:
    """Get or create the singleton EvidenceStorage."""
    global _storage
    if _storage is None:
        _storage = EvidenceStorage(
            bucket_name=EVIDENCE_BUCKET,
            region=AWS_REGION,
        )
    return _storage


def _get_org_tier_for_scm(scm_type: str, scm_identifier: str) -> str:
    """Resolve an organization's billing tier from SCM integration data via GSI.

    Queries the organizations table using a dedicated GSI for the SCM platform,
    then returns its billing tier. Falls back to DEFAULT_TIER if no match found
    or on any error.

    Args:
        scm_type: The SCM platform (gitlab, bitbucket, azure_devops).
        scm_identifier: The SCM-specific identifier (project path, workspace, org name).

    Returns:
        The billing tier string.
    """
    if not scm_identifier or scm_identifier == "unknown":
        return DEFAULT_TIER

    # Map SCM type to its GSI index name and partition key attribute
    scm_gsi_map = {
        "gitlab": ("gitlab-group-index", "gitlab_group"),
        "bitbucket": ("bitbucket-workspace-index", "bitbucket_workspace"),
        "azure_devops": ("azure-org-index", "azure_devops_org"),
    }
    gsi_info = scm_gsi_map.get(scm_type)
    if not gsi_info:
        return DEFAULT_TIER

    index_name, key_attr = gsi_info

    try:
        import boto3
        from boto3.dynamodb.conditions import Key

        dynamodb = boto3.resource("dynamodb", region_name=AWS_REGION)
        table = dynamodb.Table(ORGANIZATIONS_TABLE)

        response = table.query(
            IndexName=index_name,
            KeyConditionExpression=Key(key_attr).eq(scm_identifier),
            ProjectionExpression="billing_tier",
            Limit=1,
        )
        items = response.get("Items", [])
        if items and items[0].get("billing_tier"):
            return str(items[0]["billing_tier"])
    except Exception as e:
        logger.warning(f"Failed to look up org tier for {scm_type}/{scm_identifier}: {e}")

    return DEFAULT_TIER


def _response(
    status_code: int,
    body: dict[str, Any],
    headers: dict[str, str] | None = None,
) -> dict[str, Any]:
    """Build an API Gateway HTTP API v2 compatible response."""
    response_headers = {"Content-Type": "application/json"}
    if headers:
        response_headers.update(headers)
    return {
        "statusCode": status_code,
        "body": json.dumps(body),
        "headers": response_headers,
    }


# =============================================================================
# GitLab handler
# =============================================================================


def _get_gitlab_webhook_secret() -> str:
    """Get cached GitLab webhook secret."""
    global _gitlab_webhook_secret
    if _gitlab_webhook_secret is None:
        _gitlab_webhook_secret = _get_secret(GITLAB_WEBHOOK_SECRET_ARN, "webhook_secret")
    return _gitlab_webhook_secret


def _get_gitlab_access_token() -> str:
    """Get cached GitLab access token."""
    global _gitlab_access_token
    if _gitlab_access_token is None:
        _gitlab_access_token = _get_secret(GITLAB_ACCESS_TOKEN_ARN, "access_token")
    return _gitlab_access_token


def _handle_gitlab_event(
    headers: dict[str, str], raw_body: str, body: dict[str, Any]
) -> dict[str, Any]:
    """Handle a GitLab webhook event.

    GitLab uses X-Gitlab-Token (plain string compare, NOT HMAC).
    """
    import asyncio

    from mergeguide.gitlab.api import GitLabAPI
    from mergeguide.gitlab.auth import GitLabAuth
    from mergeguide.gitlab.mr_handler import MRHandler
    from mergeguide.gitlab.webhook import parse_webhook_event, verify_webhook_signature

    # 1. Verify token
    token = headers.get("x-gitlab-token", "")
    secret = _get_gitlab_webhook_secret()

    if not secret:
        logger.error("GitLab webhook secret unavailable")
        return _response(401, {"error": "Webhook verification unavailable"})

    if not verify_webhook_signature(token, secret):
        logger.warning("Invalid GitLab webhook token")
        return _response(401, {"error": "Invalid token"})

    # 2. Parse event
    event = parse_webhook_event(headers, body)
    logger.info(f"GitLab event: {event.event_type} action={event.action}")

    # 3. Rate limit by project using org's actual billing tier
    project_path = event.get_project_path() or "unknown"
    tier = _get_org_tier_for_scm("gitlab", project_path)
    rate_result = check_rate_limit(f"GITLAB#{project_path}", tier)
    if not rate_result.allowed:
        logger.warning(f"Rate limit exceeded for GitLab project {project_path}")
        return _response(
            429,
            {
                "error": "rate_limit_exceeded",
                "retry_after": rate_result.retry_after,
            },
            headers=build_rate_limit_headers(rate_result),
        )

    rate_headers = build_rate_limit_headers(rate_result)

    # 4. Process if MR event
    if event.should_process_mr:
        access_token = _get_gitlab_access_token()
        auth = GitLabAuth(access_token=access_token)
        api = GitLabAPI(auth)
        engine = get_engine()
        storage = get_storage()
        signing_key = get_signing_key()

        handler = MRHandler(
            gitlab_api=api,
            policy_engine=engine,  # type: ignore[arg-type]  # DualLayerOrchestrator wraps PolicyEngine
            evidence_storage=storage,
            signing_key=signing_key,
        )

        result = asyncio.run(handler.handle_merge_request(event))

        return _response(
            200,
            {
                "status": "processed",
                "scm": "gitlab",
                "passed": result.passed,
                "violations_count": result.violations_count,
            },
            headers=rate_headers,
        )

    # Non-MR events: acknowledge
    return _response(
        200,
        {
            "status": "ignored",
            "scm": "gitlab",
            "event": event.event_type,
        },
        headers=rate_headers,
    )


# =============================================================================
# Bitbucket handler
# =============================================================================


def _get_bitbucket_webhook_secret() -> str:
    """Get cached Bitbucket webhook secret."""
    global _bitbucket_webhook_secret
    if _bitbucket_webhook_secret is None:
        _bitbucket_webhook_secret = _get_secret(BITBUCKET_WEBHOOK_SECRET_ARN, "webhook_secret")
    return _bitbucket_webhook_secret


def _get_bitbucket_oauth() -> dict[str, Any]:
    """Get cached Bitbucket OAuth credentials."""
    global _bitbucket_oauth
    if _bitbucket_oauth is None:
        _bitbucket_oauth = _get_secret_dict(BITBUCKET_OAUTH_SECRET_ARN)
    return _bitbucket_oauth


def _handle_bitbucket_event(
    headers: dict[str, str], raw_body: str, body: dict[str, Any]
) -> dict[str, Any]:
    """Handle a Bitbucket webhook event.

    Bitbucket uses X-Hub-Signature HMAC-SHA256 (same format as GitHub: sha256=<hex>).
    """
    import asyncio

    from mergeguide.bitbucket.api import BitbucketAPI
    from mergeguide.bitbucket.auth import BitbucketAuth
    from mergeguide.bitbucket.pr_handler import PRHandler
    from mergeguide.bitbucket.webhook import parse_webhook_event, verify_webhook_signature

    # 1. Verify HMAC signature
    signature = headers.get("x-hub-signature", "")
    secret = _get_bitbucket_webhook_secret()
    body_bytes = raw_body.encode("utf-8") if isinstance(raw_body, str) else raw_body

    if not secret:
        logger.error("Bitbucket webhook secret unavailable")
        return _response(401, {"error": "Webhook verification unavailable"})

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

    # 2. Parse event
    event = parse_webhook_event(headers, body)
    logger.info(f"Bitbucket event: {event.event_key} action={event.action}")

    # 3. Ignore terminal PR events
    if event.event_key in ("pullrequest:fulfilled", "pullrequest:rejected"):
        return _response(
            200,
            {
                "status": "ignored",
                "scm": "bitbucket",
                "event": event.event_key,
            },
        )

    # 4. Rate limit by workspace using org's actual billing tier
    repo_info = event.get_repository_info()
    workspace = repo_info[0] if repo_info else "unknown"
    tier = _get_org_tier_for_scm("bitbucket", workspace)
    rate_result = check_rate_limit(f"BITBUCKET#{workspace}", tier)
    if not rate_result.allowed:
        logger.warning(f"Rate limit exceeded for Bitbucket workspace {workspace}")
        return _response(
            429,
            {
                "error": "rate_limit_exceeded",
                "retry_after": rate_result.retry_after,
            },
            headers=build_rate_limit_headers(rate_result),
        )

    rate_headers = build_rate_limit_headers(rate_result)

    # 5. Process if PR event
    if event.should_process_pr:
        oauth_creds = _get_bitbucket_oauth()
        auth = BitbucketAuth(
            client_id=oauth_creds.get("client_id", ""),
            client_secret=oauth_creds.get("client_secret", ""),
        )
        api = BitbucketAPI(auth)

        engine = get_engine()
        storage = get_storage()
        signing_key = get_signing_key()

        handler = PRHandler(
            api=api,
            policy_engine=engine,  # DualLayerOrchestrator wraps PolicyEngine
            evidence_storage=storage,
            signing_key=signing_key,
            evaluations_table_name=EVALUATIONS_TABLE,
            organizations_table_name=ORGANIZATIONS_TABLE,
            aws_region=AWS_REGION,
        )

        result = asyncio.run(handler.handle_pr_event(event))

        if result is None:
            return _response(
                500,
                {"status": "error", "scm": "bitbucket", "error": "No result from PR handler"},
                headers=rate_headers,
            )

        return _response(
            200,
            {
                "status": "processed",
                "scm": "bitbucket",
                "passed": result.passed,
            },
            headers=rate_headers,
        )

    # Non-PR events: acknowledge
    return _response(
        200,
        {
            "status": "ignored",
            "scm": "bitbucket",
            "event": event.event_key,
        },
        headers=rate_headers,
    )


# =============================================================================
# Azure DevOps handler
# =============================================================================


def _get_azure_webhook_secret() -> str:
    """Get cached Azure DevOps webhook secret."""
    global _azure_webhook_secret
    if _azure_webhook_secret is None:
        _azure_webhook_secret = _get_secret(AZURE_WEBHOOK_SECRET_ARN, "webhook_secret")
    return _azure_webhook_secret


def _get_azure_pat() -> str:
    """Get cached Azure DevOps PAT."""
    global _azure_pat
    if _azure_pat is None:
        _azure_pat = _get_secret(AZURE_PAT_SECRET_ARN, "pat")
    return _azure_pat


def _handle_azure_event(
    headers: dict[str, str], raw_body: str, body: dict[str, Any]
) -> dict[str, Any]:
    """Handle an Azure DevOps webhook event.

    Azure DevOps uses X-Hub-Signature HMAC (fail-closed if secret configured
    and header missing). Event type comes from JSON body eventType field.
    """
    import asyncio

    from mergeguide.azure_devops.api import AzureDevOpsAPI
    from mergeguide.azure_devops.auth import AzureDevOpsAuth
    from mergeguide.azure_devops.pr_handler import AzureDevOpsPRHandler
    from mergeguide.azure_devops.webhook import parse_webhook_event, verify_webhook_signature

    # 1. Verify HMAC signature (fail-closed if secret is configured)
    signature = headers.get("x-hub-signature", "")
    secret = _get_azure_webhook_secret()
    body_bytes = raw_body.encode("utf-8") if isinstance(raw_body, str) else raw_body

    if secret:
        # Secret is configured — enforce signature verification
        if not signature:
            logger.warning("Azure DevOps webhook missing signature header (fail-closed)")
            return _response(401, {"error": "Missing signature"})

        if not verify_webhook_signature(body_bytes, signature, secret):
            logger.warning("Invalid Azure DevOps webhook signature")
            return _response(401, {"error": "Invalid signature"})
    else:
        logger.error("Azure DevOps webhook secret unavailable")
        return _response(401, {"error": "Webhook verification unavailable"})

    # 2. Parse event (event type comes from body, not header)
    event = parse_webhook_event(headers, body)
    logger.info(f"Azure DevOps event: {event.event_type} action={event.action}")

    # 3. Ignore terminal events
    if event.event_type in ("git.pullrequest.merged", "git.pullrequest.abandoned"):
        return _response(
            200,
            {
                "status": "ignored",
                "scm": "azure_devops",
                "event": event.event_type,
            },
        )

    # 4. Rate limit by org
    containers = body.get("resourceContainers", {})
    collection = containers.get("collection", {})
    org_url = collection.get("baseUrl", "").rstrip("/")
    org_name = org_url.split("/")[-1] if org_url else "unknown"

    tier = _get_org_tier_for_scm("azure_devops", org_name)
    rate_result = check_rate_limit(f"AZURE#{org_name}", tier)
    if not rate_result.allowed:
        logger.warning(f"Rate limit exceeded for Azure org {org_name}")
        return _response(
            429,
            {
                "error": "rate_limit_exceeded",
                "retry_after": rate_result.retry_after,
            },
            headers=build_rate_limit_headers(rate_result),
        )

    rate_headers = build_rate_limit_headers(rate_result)

    # 5. Process if PR event
    if event.should_process_pr:
        pat = _get_azure_pat()
        auth = AzureDevOpsAuth(org_url=org_url, pat=pat)
        api = AzureDevOpsAPI(auth)
        engine = get_engine()

        handler = AzureDevOpsPRHandler(
            api=api,
            engine=engine,  # type: ignore[arg-type]  # DualLayerOrchestrator wraps PolicyEngine
        )

        result = asyncio.run(handler.handle_pull_request(event))

        return _response(
            200,
            {
                "status": "processed",
                "scm": "azure_devops",
                "passed": result.passed,
                "evaluation_id": result.evaluation_id,
                "violations_count": result.violations_count,
            },
            headers=rate_headers,
        )

    # Non-PR events: acknowledge
    return _response(
        200,
        {
            "status": "ignored",
            "scm": "azure_devops",
            "event": event.event_type,
        },
        headers=rate_headers,
    )


# =============================================================================
# Main handler — route dispatcher
# =============================================================================


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

    Routes to the appropriate SCM handler based on the API Gateway path:
      /webhook/gitlab    -> _handle_gitlab_event
      /webhook/bitbucket -> _handle_bitbucket_event
      /webhook/azure     -> _handle_azure_event

    Args:
        event: API Gateway HTTP API v2 event dictionary.
        context: AWS Lambda context object (not used).

    Returns:
        dict: API Gateway-compatible response.
    """
    logger.info("SCM webhook received")

    try:
        # Extract path from API Gateway event
        path = event.get("rawPath", "") or event.get("requestContext", {}).get("http", {}).get(
            "path", ""
        )
        headers = {k.lower(): v for k, v in event.get("headers", {}).items()}
        raw_body = event.get("body", "")

        # Handle base64 encoding
        if event.get("isBase64Encoded", False):
            import base64

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

        # Parse body
        body = json.loads(raw_body) if isinstance(raw_body, str) and raw_body else {}

        logger.info(f"SCM webhook path: {path}")

        # Route to appropriate handler
        if "/webhook/gitlab" in path:
            return _handle_gitlab_event(headers, raw_body, body)
        elif "/webhook/bitbucket" in path:
            return _handle_bitbucket_event(headers, raw_body, body)
        elif "/webhook/azure" in path:
            return _handle_azure_event(headers, raw_body, body)
        else:
            logger.warning(f"Unknown webhook path: {path}")
            return _response(404, {"error": f"Unknown webhook path: {path}"})

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

    except Exception as e:
        logger.exception(f"SCM webhook handler error: {e}")
        return _response(500, {"error": "Internal error"})
