import logging

from django.db import transaction
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.utils import timezone
from email.utils import getaddresses

from api.agent.comms import (
    ingest_inbound_message,
    ingest_inbound_webhook_message,
    TwilioSmsAdapter,
    PostmarkEmailAdapter,
    MailgunEmailAdapter,
)
from api.models import (
    CommsChannel,
    PersistentAgent,
    PersistentAgentInboundWebhook,
    PersistentAgentCommsEndpoint,
    OutboundMessageAttempt,
    DeliveryStatus,
    PipedreamConnectSession,
    PersistentAgentPipedreamTriggerSubscription,
)
from api.agent.comms.message_reads import (
    READ_SOURCE_EMAIL_CLICK,
    READ_SOURCE_EMAIL_OPEN,
    mark_postmark_event_message_read,
)
from opentelemetry import trace
import json
import re
from config import settings

from util.analytics import Analytics, AnalyticsEvent, AnalyticsSource
from api.services.email_verification import has_verified_email
from api.services.pipedream_trigger_subscriptions import (
    PipedreamTriggerSignatureError,
    ingest_trigger_delivery,
    verify_pipedream_signature,
)

logger = logging.getLogger(__name__)
tracer = trace.get_tracer("gobii.utils")


def _agent_runs_in_current_environment(agent: PersistentAgent) -> bool:
    return agent.execution_environment == settings.GOBII_RELEASE_ENV


@csrf_exempt
@require_POST
@tracer.start_as_current_span("COMM sms_webhook")
def sms_webhook(request):
    """Handle incoming SMS messages from Twilio"""

    # Get the GET parameter 't' to do our security check
    span = trace.get_current_span()
    api_key = request.GET.get('t', '').strip()

    if not api_key:
        logger.warning("SMS webhook called without 't' parameter; rejecting request.")
        span.add_event('SMS - Missing API KEY', {})
        return HttpResponse(status=400)

    # Validate it matches env var
    if api_key != settings.TWILIO_INCOMING_WEBHOOK_TOKEN:
        logger.warning(f"SMS webhook called with invalid API Key; got: {api_key}")
        span.add_event('SMS - Invalid API KEY', {'api_key': api_key})
        return HttpResponse(status=403)

    try:
        from_number = request.POST.get('From', "Unknown")
        to_number = request.POST.get('To', "Unknown")
        body = request.POST.get('Body', "Empty").strip()

        span.set_attribute("from_number", from_number)
        span.set_attribute("to_number", to_number)
        span.set_attribute("body", body)

        logger.info(f"Received SMS from {from_number} to {to_number}: {body}")

        with tracer.start_as_current_span("COMM sms whitelist check") as whitelist_span:
            try:
                endpoint = PersistentAgentCommsEndpoint.objects.select_related('owner_agent__user').get(
                    channel=CommsChannel.SMS,
                    address__iexact=to_number,
                    owner_agent__is_active=True
                )
                agent = endpoint.owner_agent
            except PersistentAgentCommsEndpoint.DoesNotExist:
                logger.info(f"Discarding SMS to unroutable number: {to_number}")
                whitelist_span.add_event('SMS - Unroutable Number', {'to_number': to_number})
                return HttpResponse(status=200)

            if not agent or not agent.user:
                logger.warning(f"Endpoint {to_number} is not associated with a usable agent/user. Discarding.")
                whitelist_span.add_event('SMS - No Agent/User', {'to_number': to_number})
                return HttpResponse(status=200)

            from api.services.email_verification import has_verified_email
            if not has_verified_email(agent.user):
                logger.info(f"Discarding inbound SMS to agent '{agent.name}' - owner email not verified.")
                whitelist_span.add_event('SMS - Owner Email Not Verified', {
                    'agent_id': str(agent.id),
                    'agent_name': agent.name,
                    'to_number': to_number,
                })
                return HttpResponse(status=200)

            if not agent.is_sender_whitelisted(CommsChannel.SMS, from_number):
                logger.info(
                    f"Discarding SMS from non-whitelisted sender '{from_number}' to agent '{agent.name}'."
                )
                whitelist_span.add_event('SMS - Sender Not Whitelisted', {
                    'from_number': from_number,
                    'agent_id': str(agent.id),
                    'agent_name': agent.name,
                })
                return HttpResponse(status=200)



        # Add message via message service
        parsed_message = TwilioSmsAdapter.parse_request(request)
        ingest_inbound_message(CommsChannel.SMS, parsed_message)

        props = Analytics.with_org_properties(
            {
                'agent_id': str(agent.id),
                'agent_name': agent.name,
                'from_number': from_number,
                'to_number': to_number,
                'message_body': body,
            },
            organization=getattr(agent, "organization", None),
        )
        Analytics.track_event(
            user_id=agent.user.id,
            event=AnalyticsEvent.PERSISTENT_AGENT_SMS_RECEIVED,
            source=AnalyticsSource.SMS,
            properties=props.copy(),
        )

        # Return a 200 OK response to Twilio
        return HttpResponse(status=200)

    except Exception as e:
        logger.error(f"Error processing Twilio SMS webhook: {e}", exc_info=True)
        return HttpResponse(status=500)


@csrf_exempt
@require_POST
@tracer.start_as_current_span("COMM sms_status_webhook")
def sms_status_webhook(request):
    """Handle status callbacks from Twilio for outbound SMS."""

    api_key = request.GET.get("t", "").strip()
    if not api_key:
        logger.warning("SMS status webhook called without 't' parameter; rejecting request.")
        return HttpResponse(status=400)

    if api_key != settings.TWILIO_INCOMING_WEBHOOK_TOKEN:
        logger.warning(f"SMS status webhook called with invalid API Key; got: {api_key}")
        return HttpResponse(status=403)

    message_sid = request.POST.get("MessageSid")
    status = request.POST.get("MessageStatus")
    error_code = request.POST.get("ErrorCode") or ""
    error_message = request.POST.get("ErrorMessage") or ""

    logger.info(
        "Received SMS status update sid=%s status=%s code=%s",
        message_sid,
        status,
        error_code,
    )

    if not message_sid or not status:
        return HttpResponse(status=400)

    try:
        attempt = OutboundMessageAttempt.objects.filter(provider_message_id=message_sid).order_by("-queued_at").first()
        if not attempt:
            logger.warning("No OutboundMessageAttempt found for SID %s", message_sid)
            return HttpResponse(status=200)

        message = attempt.message
        now = timezone.now()

        if status in ["sent", "queued"]:
            attempt.status = DeliveryStatus.SENT
            attempt.sent_at = now
            message.latest_status = DeliveryStatus.SENT
            message.latest_sent_at = now
        elif status == "delivered":
            attempt.status = DeliveryStatus.DELIVERED
            attempt.delivered_at = now
            message.latest_status = DeliveryStatus.DELIVERED
            message.latest_delivered_at = now
            delivered_props = Analytics.with_org_properties(
                {
                    "agent_id": str(message.owner_agent_id),
                    "message_id": str(message.id),
                    "sms_id": message_sid,
                },
                organization=getattr(message.owner_agent, "organization", None),
            )
            Analytics.track_event(
                user_id=message.owner_agent.user.id,
                event=AnalyticsEvent.PERSISTENT_AGENT_SMS_DELIVERED,
                source=AnalyticsSource.AGENT,
                properties=delivered_props.copy(),
            )
        elif status in ["failed", "undelivered"]:
            attempt.status = DeliveryStatus.FAILED
            attempt.error_code = str(error_code)
            attempt.error_message = error_message
            message.latest_status = DeliveryStatus.FAILED
            message.latest_error_code = str(error_code)
            message.latest_error_message = error_message
            failed_props = Analytics.with_org_properties(
                {
                    "agent_id": str(message.owner_agent_id),
                    "message_id": str(message.id),
                    "sms_id": message_sid,
                    "error_code": str(error_code),
                },
                organization=getattr(message.owner_agent, "organization", None),
            )
            Analytics.track_event(
                user_id=message.owner_agent.user.id,
                event=AnalyticsEvent.PERSISTENT_AGENT_SMS_FAILED,
                source=AnalyticsSource.AGENT,
                properties=failed_props.copy(),
            )
        else:
            # Unknown or intermediate status
            return HttpResponse(status=200)

        attempt.save()
        message.save(
            update_fields=[
                "latest_status",
                "latest_sent_at",
                "latest_delivered_at",
                "latest_error_code",
                "latest_error_message",
            ]
        )

        return HttpResponse(status=200)
    except Exception as e:
        logger.error(f"Error processing Twilio status webhook: {e}", exc_info=True)
        return HttpResponse(status=500)


def _normalize_multivalue_mapping(mapping) -> dict[str, object]:
    normalized: dict[str, object] = {}
    for key in mapping.keys():
        values = mapping.getlist(key)
        if not values:
            continue
        normalized[key] = values[0] if len(values) == 1 else values
    return normalized


def _collect_uploaded_files(request) -> tuple[list[object], list[dict[str, object]]]:
    attachments: list[object] = []
    metadata: list[dict[str, object]] = []
    for field_name, files in request.FILES.lists():
        for uploaded in files:
            attachments.append(uploaded)
            metadata.append(
                {
                    "field_name": field_name,
                    "filename": getattr(uploaded, "name", ""),
                    "content_type": getattr(uploaded, "content_type", ""),
                    "size": getattr(uploaded, "size", None),
                }
            )
    return attachments, metadata


def _build_inbound_agent_webhook_body(
    *,
    json_payload=None,
    form_payload: dict[str, object] | None = None,
    text_payload: str = "",
) -> tuple[str, str]:
    if json_payload is not None:
        return json.dumps(json_payload, indent=2, sort_keys=True), "json"
    if form_payload:
        return json.dumps(form_payload, indent=2, sort_keys=True), "form"
    if text_payload.strip():
        return text_payload.strip(), "text"
    return "", "empty"


def _decode_request_text_body(request) -> str:
    return (request.body or b"").decode(request.encoding or "utf-8", errors="replace")


def _parse_inbound_agent_webhook_request(request) -> tuple[str, dict[str, object], list[object]]:
    content_type = ((request.content_type or "").split(";", 1)[0]).strip().lower()
    attachments, attachment_metadata = _collect_uploaded_files(request)
    form_payload: dict[str, object] = {}
    json_payload = None
    text_payload = ""

    if content_type == "application/json":
        raw_text = _decode_request_text_body(request)
        text_payload = raw_text
        if raw_text.strip():
            try:
                json_payload = json.loads(raw_text)
            except json.JSONDecodeError as exc:
                raise ValueError("Invalid JSON payload.") from exc
        else:
            json_payload = {}
    elif content_type in {"multipart/form-data", "application/x-www-form-urlencoded"}:
        form_payload = _normalize_multivalue_mapping(request.POST)
    elif request.POST:
        form_payload = _normalize_multivalue_mapping(request.POST)
    else:
        text_payload = _decode_request_text_body(request)

    query_payload = _normalize_multivalue_mapping(request.GET)
    query_payload.pop("t", None)

    body, payload_kind = _build_inbound_agent_webhook_body(
        json_payload=json_payload,
        form_payload=form_payload,
        text_payload=text_payload,
    )

    raw_payload = {
        "content_type": content_type or "",
        "method": request.method,
        "path": request.path,
        "query_params": query_payload,
        "form_payload": form_payload,
        "json_payload": json_payload,
        "text_payload": text_payload if text_payload.strip() else "",
        "attachments": attachment_metadata,
        "payload_kind": payload_kind,
        "source": "inbound_webhook",
        "source_kind": "webhook",
    }
    return body, raw_payload, attachments


@csrf_exempt
@require_POST
@tracer.start_as_current_span("COMM inbound_agent_webhook")
def inbound_agent_webhook(request, webhook_id):
    secret = request.GET.get("t", "").strip()
    if not secret:
        return JsonResponse({"accepted": False, "error": "Missing webhook secret."}, status=400)

    webhook = (
        PersistentAgentInboundWebhook.objects
        .select_related("agent__user", "agent__organization")
        .filter(id=webhook_id)
        .first()
    )
    if webhook is None:
        return JsonResponse({"accepted": False, "error": "Webhook not found."}, status=404)
    if not _agent_runs_in_current_environment(webhook.agent):
        logger.info(
            "Ignoring inbound webhook %s for agent %s: agent environment %s does not match current environment %s",
            webhook_id,
            webhook.agent_id,
            webhook.agent.execution_environment,
            settings.GOBII_RELEASE_ENV,
        )
        return JsonResponse({"accepted": False, "error": "Webhook not found."}, status=404)
    if not webhook.matches_secret(secret):
        return JsonResponse({"accepted": False, "error": "Invalid webhook secret."}, status=403)
    if not webhook.is_active:
        return JsonResponse({"accepted": False, "error": "Webhook is inactive."}, status=409)
    if not webhook.agent.is_active:
        return JsonResponse({"accepted": False, "error": "Agent is inactive."}, status=409)

    try:
        body, raw_payload, attachments = _parse_inbound_agent_webhook_request(request)
    except ValueError as exc:
        return JsonResponse({"accepted": False, "error": str(exc)}, status=400)
    except Exception:
        logger.exception("Error parsing inbound webhook request %s", webhook_id)
        return JsonResponse({"accepted": False, "error": "Unable to parse webhook request."}, status=400)

    raw_payload["source_label"] = webhook.name
    raw_payload["webhook_id"] = str(webhook.id)
    raw_payload["webhook_name"] = webhook.name

    try:
        info = ingest_inbound_webhook_message(
            webhook,
            body=body,
            raw_payload=raw_payload,
            attachments=attachments,
        )
    except Exception:
        logger.exception("Error ingesting inbound webhook %s", webhook_id)
        return JsonResponse({"accepted": False, "error": "Failed to ingest webhook payload."}, status=500)

    inbound_props = Analytics.with_org_properties(
        {
            'agent_id': str(webhook.agent_id),
            'agent_name': webhook.agent.name,
            'webhook_id': str(webhook.id),
            'webhook_name': webhook.name,
            'message_id': str(info.message.id),
            'payload_kind': str(raw_payload.get("payload_kind") or ""),
            'content_type': str(raw_payload.get("content_type") or ""),
            'attachment_count': len(attachments),
        },
        organization=getattr(webhook.agent, "organization", None),
    )
    Analytics.track_event(
        user_id=webhook.agent.user.id,
        event=AnalyticsEvent.PERSISTENT_AGENT_INBOUND_WEBHOOK_TRIGGERED,
        source=AnalyticsSource.API,
        properties=inbound_props.copy(),
    )

    return JsonResponse(
        {
            "accepted": True,
            "webhookId": str(webhook.id),
            "webhookName": webhook.name,
            "messageId": str(info.message.id),
            "queued": True,
            "receivedAt": info.message.timestamp.isoformat(),
        },
        status=202,
    )


@csrf_exempt
@require_POST
@tracer.start_as_current_span("COMM pipedream_trigger_subscription_webhook")
def pipedream_trigger_subscription_webhook(request, subscription_id):
    secret = request.GET.get("t", "").strip()
    if not secret:
        return JsonResponse({"accepted": False, "error": "Missing webhook secret."}, status=400)

    subscription = (
        PersistentAgentPipedreamTriggerSubscription.objects
        .select_related("agent__user", "agent__organization")
        .filter(id=subscription_id)
        .first()
    )
    if subscription is None:
        return JsonResponse({"accepted": False, "error": "Subscription not found."}, status=404)
    if not _agent_runs_in_current_environment(subscription.agent):
        logger.info(
            "Ignoring Pipedream trigger subscription %s for agent %s: agent environment %s does not match current environment %s",
            subscription_id,
            subscription.agent_id,
            subscription.agent.execution_environment,
            settings.GOBII_RELEASE_ENV,
        )
        return JsonResponse({"accepted": False, "error": "Subscription not found."}, status=404)
    if not subscription.matches_webhook_secret(secret):
        return JsonResponse({"accepted": False, "error": "Invalid webhook secret."}, status=403)
    if subscription.status != PersistentAgentPipedreamTriggerSubscription.Status.ACTIVE:
        return JsonResponse({"accepted": False, "error": "Subscription is inactive."}, status=409)
    if not subscription.agent.is_active:
        return JsonResponse({"accepted": False, "error": "Agent is inactive."}, status=409)

    try:
        verify_pipedream_signature(
            subscription,
            request.headers.get("x-pd-signature", ""),
            request.body or b"",
        )
    except PipedreamTriggerSignatureError as exc:
        return JsonResponse({"accepted": False, "error": str(exc)}, status=403)

    try:
        result = ingest_trigger_delivery(subscription, request.body or b"")
    except ValueError as exc:
        subscription.record_delivery_error(str(exc))
        return JsonResponse({"accepted": False, "error": str(exc)}, status=400)
    except Exception:
        logger.exception("Error ingesting Pipedream trigger subscription %s", subscription_id)
        subscription.record_delivery_error("Failed to ingest Pipedream trigger delivery.")
        return JsonResponse({"accepted": False, "error": "Failed to ingest trigger delivery."}, status=500)

    return JsonResponse(
        {
            "accepted": True,
            "subscriptionId": str(subscription.id),
            "messageId": result.get("message_id", ""),
            "conversationId": result.get("conversation_id", ""),
            "queued": not bool(result.get("ignored")),
            "ignored": bool(result.get("ignored")),
            "outboundEcho": bool(result.get("outbound_echo")),
            "debounced": bool(result.get("debounced")),
            "debounceSeconds": int(result.get("debounce_seconds") or 0),
        },
        status=202,
    )


def _handle_inbound_email(
    request,
    *,
    adapter_cls,
    provider_label: str,
    from_email_raw: str,
    to_emails: list[str],
    cc_emails: list[str],
    bcc_emails: list[str],
    subject: str | None,
):
    """Common processing for inbound email webhooks."""
    all_recipient_addresses: list[str] = []
    all_recipient_addresses.extend(to_emails)
    all_recipient_addresses.extend(cc_emails)
    all_recipient_addresses.extend(bcc_emails)

    unique_recipient_addresses: list[str] = []
    seen_recipients: set[str] = set()
    for raw_address in all_recipient_addresses:
        normalized = (raw_address or "").strip().lower()
        if not normalized or normalized in seen_recipients:
            continue
        seen_recipients.add(normalized)
        unique_recipient_addresses.append(normalized)
    normalized_to_emails = {(address or "").strip().lower() for address in to_emails if (address or "").strip()}
    normalized_cc_emails = {(address or "").strip().lower() for address in cc_emails if (address or "").strip()}
    normalized_bcc_emails = {(address or "").strip().lower() for address in bcc_emails if (address or "").strip()}
    recipient_position = {address: index for index, address in enumerate(unique_recipient_addresses)}

    logger.info(
        "Received %s email from %s to %s, CC: %s, BCC: %s: %s",
        provider_label,
        from_email_raw,
        to_emails,
        cc_emails,
        bcc_emails,
        subject,
    )

    matching_endpoints = []
    matched_agent_ids: set[str] = set()
    with tracer.start_as_current_span("COMM email endpoint lookup") as span:
        candidate_endpoints = list(
            PersistentAgentCommsEndpoint.objects.select_related("owner_agent__user").filter(
                channel=CommsChannel.EMAIL,
                address__in=unique_recipient_addresses,
                owner_agent__is_active=True,
            )
        )
        endpoints_by_address = {
            (endpoint.address or "").strip().lower(): endpoint
            for endpoint in candidate_endpoints
        }
        for address in unique_recipient_addresses:
            if address not in endpoints_by_address:
                logger.debug("No agent endpoint found for address: %s", address)

        def _recipient_rank(address: str) -> int:
            if address in normalized_to_emails:
                return 0
            if address in normalized_cc_emails:
                return 1
            if address in normalized_bcc_emails:
                return 2
            return 3

        def _selection_key(endpoint: PersistentAgentCommsEndpoint) -> tuple[int, int, int, str]:
            normalized_address = (endpoint.address or "").strip().lower()
            return (
                _recipient_rank(normalized_address),
                0 if endpoint.is_primary else 1,
                recipient_position.get(normalized_address, len(unique_recipient_addresses)),
                normalized_address,
            )

        selected_endpoint_by_agent: dict[str, PersistentAgentCommsEndpoint] = {}
        for endpoint in candidate_endpoints:
            normalized_address = (endpoint.address or "").strip().lower()
            if not endpoint.owner_agent or not endpoint.owner_agent.user:
                logger.warning("Endpoint %s is not associated with a usable agent/user.", normalized_address)
                continue

            agent_id = str(endpoint.owner_agent_id)
            existing_endpoint = selected_endpoint_by_agent.get(agent_id)
            if existing_endpoint is None:
                selected_endpoint_by_agent[agent_id] = endpoint
                logger.info("Found agent endpoint for address: %s", normalized_address)
                continue

            if _selection_key(endpoint) < _selection_key(existing_endpoint):
                logger.info(
                    "Selecting preferred inbound recipient for agent %s: %s (replacing %s)",
                    agent_id,
                    normalized_address,
                    (existing_endpoint.address or "").strip().lower(),
                )
                selected_endpoint_by_agent[agent_id] = endpoint
            else:
                logger.info(
                    "Skipping duplicate inbound recipient for agent %s at address %s",
                    agent_id,
                    normalized_address,
                )

        matching_endpoints = sorted(
            selected_endpoint_by_agent.values(),
            key=lambda endpoint: (
                _selection_key(endpoint),
                str(endpoint.owner_agent_id),
            ),
        )
        matched_agent_ids = set(selected_endpoint_by_agent.keys())

        span.set_attribute("total_recipients", len(all_recipient_addresses))
        span.set_attribute("unique_recipients", len(unique_recipient_addresses))
        span.set_attribute("matching_endpoints", len(matching_endpoints))
        span.set_attribute("matched_agents", len(matched_agent_ids))

    if not matching_endpoints:
        logger.info("Discarding email - no routable agent addresses found in To/CC/BCC")
        with tracer.start_as_current_span("COMM email no endpoints") as span:
            span.add_event('Email - No Routable Addresses', {
                'to_emails': to_emails,
                'cc_emails': cc_emails,
                'bcc_emails': bcc_emails
            })
        return HttpResponse(status=200)

    match = re.search(r'<([^>]+)>', from_email_raw or "")
    from_email = (match.group(1) if match else from_email_raw or "").strip()

    processed_agents = []
    for endpoint in matching_endpoints:
        agent = endpoint.owner_agent

        with tracer.start_as_current_span("COMM email whitelist check") as span:
            span.set_attribute("agent_id", str(agent.id))
            span.set_attribute("agent_name", agent.name)
            span.set_attribute("endpoint_address", endpoint.address)

            if not has_verified_email(agent.user):
                logger.info(
                    f"Discarding inbound email to endpoint {endpoint.address} - owner email not verified."
                )
                span.add_event('Email - Owner Email Not Verified', {
                    'agent_id': str(agent.id),
                    'agent_name': agent.name,
                    'endpoint_address': endpoint.address,
                })
                continue

            if not agent.is_sender_whitelisted(CommsChannel.EMAIL, from_email):
                logger.info(
                    f"Discarding email from non-whitelisted sender '{from_email}' to agent '{agent.name}' (endpoint: {endpoint.address})."
                )
                span.add_event('Email - Sender Not Whitelisted', {
                    'from_email': from_email,
                    'agent_id': str(agent.id),
                    'agent_name': agent.name,
                    'endpoint_address': endpoint.address
                })
                continue

        try:
            adapter = adapter_cls()
            parsed_message = adapter.parse_request(request)
            parsed_message.recipient = endpoint.address

            msg_info = ingest_inbound_message(CommsChannel.EMAIL, parsed_message)

            processed_agents.append(agent)
            normalized_endpoint_address = (endpoint.address or "").strip().lower()
            if normalized_endpoint_address in normalized_to_emails:
                recipient_type = "to"
            elif normalized_endpoint_address in normalized_cc_emails:
                recipient_type = "cc"
            elif normalized_endpoint_address in normalized_bcc_emails:
                recipient_type = "bcc"
            else:
                recipient_type = "to"

            email_props = Analytics.with_org_properties(
                {
                    'agent_id': str(agent.id),
                    'agent_name': agent.name,
                    'from_email': from_email_raw,
                    'message_id': str(msg_info.message.id),
                    'endpoint_address': endpoint.address,
                    'recipient_type': recipient_type,
                },
                organization=getattr(agent, "organization", None),
            )
            Analytics.track_event(
                user_id=agent.user.id,
                event=AnalyticsEvent.PERSISTENT_AGENT_EMAIL_RECEIVED,
                source=AnalyticsSource.AGENT,
                properties=email_props.copy(),
            )

            logger.info(
                "Successfully processed %s email for agent '%s' (endpoint: %s)",
                provider_label,
                agent.name,
                endpoint.address,
            )
        except Exception as e:
            logger.error(f"Error processing email for agent '{agent.name}': {e}", exc_info=True)
            continue

    if processed_agents:
        logger.info(
            "Email processed for %d agent(s): %s",
            len(processed_agents),
            [a.name for a in processed_agents],
        )
    else:
        logger.info("Email not processed for any agents due to whitelist restrictions")

    return HttpResponse(status=200)


@csrf_exempt
@require_POST
@tracer.start_as_current_span("COMM email_webhook_postmark")
def email_webhook_postmark(request):
    """Handle incoming Postmark email messages."""

    raw_json = request.body.decode('utf-8')

    api_key = request.GET.get('t', '').strip()

    if not api_key:
        logger.warning("Email webhook called without 't' parameter; rejecting request.")
        return HttpResponse(status=400)

    if api_key != settings.POSTMARK_INCOMING_WEBHOOK_TOKEN:
        logger.warning(f"Email webhook called with invalid API Key; got: {api_key}")
        return HttpResponse(status=403)

    try:
        data = json.loads(raw_json)
        from_email_raw = data.get('From')
        subject = data.get('Subject')

        def extract_emails_from_full_format(email_array):
            if not email_array or not isinstance(email_array, list):
                return []
            return [item.get('Email', '').strip() for item in email_array if item.get('Email', '').strip()]

        to_emails = extract_emails_from_full_format(data.get('ToFull', []))
        cc_emails = extract_emails_from_full_format(data.get('CcFull', []))
        bcc_emails = extract_emails_from_full_format(data.get('BccFull', []))

        return _handle_inbound_email(
            request,
            adapter_cls=PostmarkEmailAdapter,
            provider_label="Postmark",
            from_email_raw=from_email_raw,
            to_emails=to_emails,
            cc_emails=cc_emails,
            bcc_emails=bcc_emails,
            subject=subject,
        )
    except json.JSONDecodeError as e:
        logger.error(f"Invalid JSON in email webhook: {e}", exc_info=True)
        return HttpResponse(status=400)
    except Exception as e:
        logger.error(f"Error processing inbound Postmark email webhook: {e}", exc_info=True)
        return HttpResponse(status=500)


@csrf_exempt
@require_POST
@tracer.start_as_current_span("COMM email_webhook_mailgun")
def email_webhook_mailgun(request):
    """Handle incoming Mailgun email messages."""

    api_key = request.GET.get('t', '').strip()

    if not api_key:
        logger.warning("Mailgun email webhook called without 't' parameter; rejecting request.")
        return HttpResponse(status=400)

    if api_key != settings.MAILGUN_INCOMING_WEBHOOK_TOKEN:
        logger.warning(f"Mailgun email webhook called with invalid API Key; got: {api_key}")
        return HttpResponse(status=403)

    try:
        data = request.POST

        from_email_raw = data.get('from') or data.get('sender') or ''
        subject = data.get('subject')

        def parse_address_list(header_value: str | None) -> list[str]:
            if not header_value:
                return []
            addresses = getaddresses([header_value])
            cleaned = [addr.strip() for _, addr in addresses if addr and addr.strip()]
            return cleaned

        to_emails = parse_address_list(data.get('To') or data.get('to'))
        cc_emails = parse_address_list(data.get('Cc') or data.get('cc'))
        bcc_emails = parse_address_list(data.get('Bcc') or data.get('bcc'))

        recipient_field = data.get('recipient')
        if recipient_field and recipient_field not in to_emails:
            to_emails.append(recipient_field.strip())

        return _handle_inbound_email(
            request,
            adapter_cls=MailgunEmailAdapter,
            provider_label="Mailgun",
            from_email_raw=from_email_raw,
            to_emails=to_emails,
            cc_emails=cc_emails,
            bcc_emails=bcc_emails,
            subject=subject,
        )
    except Exception as e:
        logger.error(f"Error processing inbound Mailgun email webhook: {e}", exc_info=True)
        return HttpResponse(status=500)


@csrf_exempt
@require_POST
@tracer.start_as_current_span("COMM pipedream_connect_webhook")
def pipedream_connect_webhook(request, session_id):
    """
    Handle Pipedream Connect webhook callbacks for a one‑time Connect token session.
    Security: requires query parameter t matching the stored webhook_secret.
    """
    # Validate one‑time secret
    secret = request.GET.get("t", "").strip()
    if not secret:
        logger.warning("PD Connect: webhook missing secret session=%s", session_id)
        return HttpResponse(status=400)

    try:
        session = PipedreamConnectSession.objects.select_related("agent").get(id=session_id)
    except PipedreamConnectSession.DoesNotExist:
        logger.warning("PD Connect: webhook unknown session=%s", session_id)
        return HttpResponse(status=200)

    if secret != session.webhook_secret:
        logger.warning("PD Connect: webhook invalid secret for session=%s", session_id)
        return HttpResponse(status=403)

    # Idempotency: if already finalized, do nothing
    if session.status in (PipedreamConnectSession.Status.SUCCESS, PipedreamConnectSession.Status.ERROR):
        logger.info("PD Connect: webhook idempotent ignore session=%s status=%s", session_id, session.status)
        return HttpResponse(status=200)

    try:
        payload_raw = request.body.decode("utf-8")
        data = json.loads(payload_raw or "{}")
        event = data.get("event")
        connect_token = data.get("connect_token")
        logger.info(
            "PD Connect: webhook received session=%s agent=%s event=%s has_token=%s",
            str(session.id), str(session.agent_id), event, bool(connect_token)
        )

        # Optional: verify connect_token correlates (if we have it)
        if session.connect_token and connect_token and str(connect_token) != session.connect_token:
            logger.warning("PD Connect: webhook token mismatch session=%s", session_id)
            return HttpResponse(status=400)

        if event == "CONNECTION_SUCCESS":
            account = data.get("account") or {}
            account_id = account.get("id") or ""

            session.status = PipedreamConnectSession.Status.SUCCESS
            session.account_id = account_id or ""
            session.save(update_fields=["status", "account_id", "updated_at"])
            from api.services.pipedream_connections import invalidate_pipedream_connected_accounts_cache
            invalidate_pipedream_connected_accounts_cache(session.agent, app_slug=session.app_slug)
            logger.info(
                "PD Connect: connection SUCCESS session=%s app=%s account=%s",
                str(session.id), session.app_slug, account_id or ""
            )

            # Record a system step and trigger processing
            try:
                from api.models import PersistentAgentStep, PersistentAgentSystemStep
                step = PersistentAgentStep.objects.create(
                    agent=session.agent,
                    description=(
                        f"Pipedream connection SUCCESS for app '{session.app_slug}'"
                        + (f"; account={account_id}" if account_id else "")
                        + f". Call search_tools for Pipedream app '{session.app_slug}' to discover and enable its tools."
                    ),
                )
                PersistentAgentSystemStep.objects.create(
                    step=step,
                    code=PersistentAgentSystemStep.Code.CREDENTIALS_PROVIDED,
                    notes=f"pipedream_connect:{session.app_slug}:{account_id}",
                )
                from api.agent.tasks.process_events import process_agent_events_task
                process_agent_events_task.delay(str(session.agent.id))
            except Exception:
                logger.exception("PD Connect: failed to record success step or trigger resume session=%s", str(session.id))

            return HttpResponse(status=200)

        elif event == "CONNECTION_ERROR":
            err = data.get("error") or ""
            session.status = PipedreamConnectSession.Status.ERROR
            session.save(update_fields=["status", "updated_at"])
            logger.info(
                "PD Connect: connection ERROR session=%s app=%s error=%s",
                str(session.id), session.app_slug, err
            )

            try:
                from api.models import PersistentAgentStep, PersistentAgentSystemStep
                step = PersistentAgentStep.objects.create(
                    agent=session.agent,
                    description=f"Pipedream connection ERROR for app '{session.app_slug}'",
                )
                PersistentAgentSystemStep.objects.create(
                    step=step,
                    code=PersistentAgentSystemStep.Code.PROCESS_EVENTS,
                    notes=f"pipedream_connect_error:{session.app_slug}:{err}",
                )
            except Exception:
                logger.exception("PD Connect: failed to record error step session=%s", str(session.id))
            return HttpResponse(status=200)

        else:
            logger.info("PD Connect: webhook unknown/ignored event session=%s event=%s", str(session.id), event)
            return HttpResponse(status=200)

    except Exception as e:
        logger.error("PD Connect: webhook processing failed session=%s error=%s", session_id, e, exc_info=True)
        return HttpResponse(status=500)

@csrf_exempt
@require_POST
@tracer.start_as_current_span("COMM open_and_link_webhook")
def open_and_link_webhook(request):
    """
    Handles open events and link click webhooks from email services.
    """

    # Get the header X-Gobii-Postmark-Key to do our security check with the Postmark token we created. Note that the
    # Postmark api does not support adding headers to the inbound email hook, but it does for this one. Hence the
    # different security check.
    api_key = request.headers.get('x-gobii-postmark-key', '').strip()

    if not api_key:
        logger.warning("Open/link click webhook called without 'X-Gobii-Postmark-Key' header; rejecting request.")
        return HttpResponse(status=400)

    # Validate it matches env var
    if api_key != settings.POSTMARK_INCOMING_WEBHOOK_TOKEN:
        logger.warning(f"Open/link click webhook called with invalid API Key; got: {api_key}")
        return HttpResponse(status=403)

    try:
        # Parse the JSON payload in the request body
        raw_json = request.body.decode('utf-8')
        data = json.loads(raw_json)
        record_type = data.get('RecordType')

        if record_type == 'Open':
            Analytics.track_agent_email_opened(data)
            mark_postmark_event_message_read(data, READ_SOURCE_EMAIL_OPEN)
        elif record_type == 'Click':
            Analytics.track_agent_email_link_clicked(data)
            mark_postmark_event_message_read(data, READ_SOURCE_EMAIL_CLICK)
        else:
            logger.warning(f"Received email event webhook '{record_type}' which is not handled; disregarding it.")

        # Try to attribute the event back to an agent and update last_interaction_at
        try:

            provider_msg_id = data.get('MessageID') or data.get('MessageId')
            agent: PersistentAgent | None = None

            if provider_msg_id:
                attempt = (
                    OutboundMessageAttempt.objects
                    .select_related('message__owner_agent')
                    .filter(provider_message_id=provider_msg_id)
                    .order_by('-queued_at')
                    .first()
                )
                if attempt and attempt.message and attempt.message.owner_agent_id:
                    agent = attempt.message.owner_agent

            if agent is not None:
                with transaction.atomic():
                    locked_agent = PersistentAgent.objects.select_for_update().get(pk=agent.pk)
                    locked_agent.last_interaction_at = timezone.now()
                    locked_agent.save(update_fields=['last_interaction_at'])
            else:
                logger.warning("Email %s event attribution failed: no agent found. Searched for Message Id %s", record_type, provider_msg_id)

        except Exception as attr_err:
            logger.warning("Email %s event attribution failed: %s", record_type, attr_err)

        return HttpResponse(status=200)
    except Exception as e:
        logger.error(f"Error processing link click webhook: {e}", exc_info=True)
        return HttpResponse(status=500)
