"""Event-driven drift webhook receiver — Cloud Run Function gen2.

Receives cloud change events, verifies HMAC signature, normalizes,
re-assesses, and triggers alerts on score threshold breach.
"""

from __future__ import annotations

import hashlib
import hmac as hmac_mod
import json
import logging
import time
from datetime import UTC, datetime

import functions_framework
from flask import Request, jsonify

from backend.drift.event_normalizer import normalize_event
from backend.drift.models import DriftDetectionResult, DriftFinding

_log = logging.getLogger(__name__)

_REPLAY_WINDOW_SECONDS = 300  # 5 minutes
_DEFAULT_THRESHOLD = 5
_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}


def _detect_cloud(event_data: dict) -> str:  # type: ignore[type-arg]
    """Detect cloud provider from event payload structure.

    GCP: has "asset" key with "assetType"
    AWS: has "source" == "aws.config" or "detail-type" containing "Config"
    Azure: has "eventType" starting with "Microsoft."
    """
    if (
        "asset" in event_data
        and isinstance(event_data["asset"], dict)
        and "assetType" in event_data["asset"]
    ):
        return "gcp"

    if event_data.get("source") == "aws.config":
        return "aws"
    if "Config" in event_data.get("detail-type", ""):
        return "aws"

    event_type = event_data.get("eventType", "")
    if isinstance(event_type, str) and event_type.startswith("Microsoft."):
        return "azure"

    msg = "Cannot detect cloud provider from event payload"
    raise ValueError(msg)


def _parse_signature_header(header: str) -> tuple[str, str]:
    """Parse X-Drift-Signature header: 't={ts},v1={sig}' → (ts, sig)."""
    parts: dict[str, str] = {}
    for segment in header.split(","):
        if "=" in segment:
            key, _, value = segment.partition("=")
            parts[key.strip()] = value.strip()

    ts = parts.get("t", "")
    sig = parts.get("v1", "")
    if not ts or not sig:
        msg = "Malformed X-Drift-Signature header"
        raise ValueError(msg)
    return ts, sig


@functions_framework.http
def handle_drift_event(request: Request):  # type: ignore[return-value]
    """Event-driven drift webhook receiver.

    POST /v1/drift/event (routed via Cloud Run Function gen2).
    Auth: HMAC-SHA256 signature in X-Drift-Signature header.
    """
    import asyncio

    # 1. Parse signature header
    sig_header = request.headers.get("X-Drift-Signature", "")
    if not sig_header:
        return "", 401

    try:
        timestamp_str, signature = _parse_signature_header(sig_header)
    except ValueError:
        return "", 401

    # 2. Replay protection
    try:
        event_ts = int(timestamp_str)
    except ValueError:
        return "", 401

    if abs(time.time() - event_ts) > _REPLAY_WINDOW_SECONDS:
        return "", 401

    # 3. Parse raw body
    raw_body = request.get_data(as_text=True)
    try:
        event_data = json.loads(raw_body)
    except json.JSONDecodeError, ValueError:
        return jsonify({"error": "Invalid JSON"}), 400

    # 4. Detect cloud and normalize
    try:
        cloud = _detect_cloud(event_data)
    except ValueError:
        return jsonify({"error": "Unknown cloud provider"}), 400

    try:
        normalized = normalize_event(cloud, event_data)
    except Exception:
        _log.exception("Event normalization failed")
        return jsonify({"error": "Normalization failed"}), 400

    # 5. Look up project by cloud_account_id
    loop = asyncio.new_event_loop()
    try:
        result = loop.run_until_complete(
            _process_event(
                normalized=normalized,
                raw_body=raw_body,
                timestamp_str=timestamp_str,
                signature=signature,
            )
        )
        return result
    except Exception:
        _log.exception("Event processing failed")
        return jsonify({"error": "Internal error"}), 500
    finally:
        loop.close()


async def _process_event(
    *,
    normalized: object,
    raw_body: str,
    timestamp_str: str,
    signature: str,
) -> tuple[str, int]:
    """Async event processing after normalization."""
    from backend.drift.models import NormalizedDriftEvent

    assert isinstance(normalized, NormalizedDriftEvent)
    from backend.shared.firestore import get_db

    db = get_db()

    # Query project by cloud_account_id via collection group
    query = (
        db.collection_group("projects")
        .where("cloud_account_id", "==", normalized.cloud_account_id)
        .where("status", "==", "active")
        .limit(1)
    )
    docs = [doc async for doc in query.stream()]
    if not docs:
        return "project not found", 404

    project_doc = docs[0]
    project_id = project_doc.id
    project_data = project_doc.to_dict() or {}

    # Extract customer_id from parent path
    # Path: customers/{customer_id}/projects/{project_id}
    ref_path = project_doc.reference.path
    path_parts = ref_path.split("/")
    customer_id = path_parts[1] if len(path_parts) >= 2 else ""

    # 6. Load webhook secret and verify HMAC
    from google.cloud import secretmanager

    sm_client = secretmanager.SecretManagerServiceClient()
    secret_name = (
        f"projects/complyform-prod/secrets/drift-secret-{project_id}/versions/latest"
    )
    try:
        secret_response = sm_client.access_secret_version(request={"name": secret_name})
        webhook_secret = secret_response.payload.data
    except Exception:
        _log.exception("Failed to load drift secret for project %s", project_id)
        return "", 401

    expected = hmac_mod.new(
        webhook_secret,
        f"{timestamp_str}.{raw_body}".encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac_mod.compare_digest(expected, signature):
        return "", 401

    # 7. Deduplication
    dedup_key = (
        f"{project_id}_{normalized.resource_id}"
        f"_{hashlib.sha256(normalized.event_timestamp.encode()).hexdigest()[:16]}"
    )
    dedup_ref = db.collection("drift_events_processed").document(dedup_key)
    dedup_doc = await dedup_ref.get()
    if dedup_doc.exists:
        return "duplicate", 200

    # Write dedup record with TTL
    now = datetime.now(UTC)
    from datetime import timedelta

    await dedup_ref.set(
        {
            "processed_at": now.isoformat(),
            "project_id": project_id,
            "expires_at": now + timedelta(minutes=5),
        }
    )

    # 8. Load latest findings and re-assess
    latest_scan = project_data.get("latest_scan", {})
    previous_score: float = project_data.get("last_score", 0.0) or 0.0
    findings_uri = latest_scan.get("findings_uri", "")

    # Simplified re-assessment: use existing score as baseline
    # Full re-assessment would parse state and run assessment engine
    new_score = previous_score
    new_failures = 0
    resolved_failures = 0

    if findings_uri:
        try:
            from backend.shared.gcs import download_findings

            findings = await download_findings(findings_uri)
            # Count findings that match the changed resource
            resource_findings = [
                f
                for f in findings
                if f.get("resource_address", "").endswith(normalized.resource_type)
            ]
            if normalized.change_type == "delete":
                resolved_failures = len(resource_findings)
                new_score = min(100.0, previous_score + len(resource_findings) * 0.5)
            elif normalized.change_type == "create":
                new_failures = 1
                new_score = max(0.0, previous_score - 0.5)
        except Exception:
            _log.exception("Failed to load findings for re-assessment")

    score_delta = new_score - previous_score

    # 9. Build top findings
    top_findings: list[DriftFinding] = []
    if new_failures > 0:
        top_findings.append(
            DriftFinding(
                control_id="drift-detected",
                framework=", ".join(project_data.get("frameworks", [])),
                resource_address=normalized.resource_id,
                severity="medium",
            )
        )

    # 10. Upload drift findings
    from backend.shared.gcs import upload_findings

    drift_scan_id = f"drift-{now.isoformat()}"
    drift_findings_uri = await upload_findings(
        customer_id=customer_id,
        project_id=project_id,
        scan_id=drift_scan_id,
        findings=[],
    )

    # 11. Write drift_history
    drift_history_ref = (
        db.collection("customers")
        .document(customer_id)
        .collection("projects")
        .document(project_id)
        .collection("drift_history")
    )

    alert_config = project_data.get("alert_config", {})
    threshold = alert_config.get("alert_threshold", _DEFAULT_THRESHOLD)
    alert_required = abs(score_delta) >= threshold

    drift_doc_data = {
        "detected_at": now.isoformat(),
        "mode": "event_driven",
        "previous_score": previous_score,
        "new_score": new_score,
        "score_delta": score_delta,
        "new_failures": new_failures,
        "resolved_failures": resolved_failures,
        "findings_uri": drift_findings_uri,
        "top_findings": [f.model_dump() for f in top_findings],
        "alert_sent": False,
    }
    drift_history_doc = await drift_history_ref.add(drift_doc_data)

    # 12. Update project doc
    project_ref = (
        db.collection("customers")
        .document(customer_id)
        .collection("projects")
        .document(project_id)
    )
    update_fields = {
        "last_score": new_score,
        "score_delta": score_delta,
        "drift_last_event_at": now.isoformat(),
    }

    # Increment event count
    current_count = project_data.get("drift_event_count_24h", 0)
    update_fields["drift_event_count_24h"] = current_count + 1

    await project_ref.update(update_fields)

    # 13. Send alert if threshold breached
    if alert_required:
        try:
            from backend.drift.alerter import send_drift_alert
            from backend.shared.firestore import get_customer

            customer = await get_customer(customer_id)
            customer_email = (customer or {}).get("email", "")

            drift_result = DriftDetectionResult(
                project_id=project_id,
                project_label=project_data.get("project_label", project_id),
                cloud=normalized.cloud,
                previous_score=previous_score,
                new_score=new_score,
                score_delta=score_delta,
                new_failures=new_failures,
                resolved_failures=resolved_failures,
                top_findings=top_findings[:5],
                findings_uri=drift_findings_uri,
                alert_required=True,
            )

            # Load webhook secret for outgoing alerts
            alert_webhook_secret: bytes | None = None
            secret_name_key = project_data.get("drift_webhook_secret_name")
            if secret_name_key:
                try:
                    resp = sm_client.access_secret_version(
                        request={
                            "name": (
                                f"projects/complyform-prod/secrets"
                                f"/{secret_name_key}/versions/latest"
                            )
                        }
                    )
                    alert_webhook_secret = resp.payload.data
                except Exception:
                    _log.exception("Failed to load alert webhook secret")

            await send_drift_alert(
                result=drift_result,
                alert_config=alert_config,
                customer_email=customer_email,
                webhook_secret=alert_webhook_secret,
            )

            # Mark alert sent
            if isinstance(drift_history_doc, tuple):
                doc_ref = drift_history_doc[1]
            else:
                doc_ref = drift_history_doc
            await doc_ref.update({"alert_sent": True})

        except Exception:
            _log.exception("Alert delivery failed for project %s", project_id)

    return "ok", 200
