import logging

from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response

from core.notify import push_notification
from escrow.models import Deal, Transaction as DealTransaction

from .flows import _post_payout_journal, record_funding
from .models import PaymentAttempt, Payout, ProviderWebhookEvent
from .providers.azampay import AzamPayProvider

logger = logging.getLogger(__name__)


def _webhook_event_id(payload: dict) -> str:
    reference = payload.get("reference") or payload.get("transid") or ""
    utilityref = payload.get("utilityref") or ""
    event_id = f"{utilityref}:{reference}".strip(":")
    if event_id:
        return event_id[:160]
    # Last resort so we never write a NULL/duplicate event_id for a
    # malformed callback — still lets AzamPay's retries dedupe on payload.
    return f"unmatched:{hash(frozenset(payload.items()) if _is_hashable(payload) else str(payload))}"


def _is_hashable(payload: dict) -> bool:
    return all(not isinstance(v, (dict, list)) for v in payload.values())


@api_view(["POST"])
def azampay_callback(request):
    """AzamPay MNO checkout callback (V2 §16).

    Registered with AzamPay as .../payments/azampay/callback?token=<secret>
    (see AzamPay adapter's `verify_callback` — the sandbox payload itself is
    not cryptographically signed, so authenticity relies on that shared
    secret). Always returns 200 once the payload is durably recorded, even if
    it can't be matched to a PaymentAttempt, so AzamPay doesn't retry forever.
    """
    provider = AzamPayProvider()
    if not provider.verify_callback(request):
        return Response({"detail": "Invalid callback token."}, status=status.HTTP_403_FORBIDDEN)

    payload = request.data if isinstance(request.data, dict) else {}
    logger.info("AzamPay callback received", extra={"payload": payload})

    with transaction.atomic():
        event, created = ProviderWebhookEvent.objects.get_or_create(
            provider="azampay",
            event_id=_webhook_event_id(payload),
            defaults={"event_type": "mno.checkout.callback", "payload": payload, "signature_valid": True},
        )
        if not created and event.processed:
            return Response({"detail": "Already processed."}, status=status.HTTP_200_OK)

        result = provider.normalize_callback(payload)
        _apply_collection_result(result)

        event.processed = True
        event.processed_at = timezone.now()
        event.save(update_fields=["processed", "processed_at"])

    return Response({"detail": "Callback received."}, status=status.HTTP_200_OK)


def _apply_collection_result(result):
    attempt = None
    if result.provider_ref:
        attempt = PaymentAttempt.objects.filter(provider_ref=result.provider_ref).select_related("deal").first()
    if attempt is None and result.reference and result.reference.startswith("deal:"):
        # Fallback: AzamPay's "utilityref" may instead echo the externalId we
        # sent at checkout time (`deal:<uuid>`) rather than our provider_ref.
        deal_id = result.reference[len("deal:"):]
        try:
            attempt = (
                PaymentAttempt.objects.filter(deal_id=deal_id, status=PaymentAttempt.Status.PENDING)
                .select_related("deal")
                .order_by("-created_at")
                .first()
            )
        except (ValueError, ValidationError):
            attempt = None

    if attempt is None:
        logger.warning("AzamPay callback did not match any PaymentAttempt: %s", result.raw)
        return
    if attempt.status != PaymentAttempt.Status.PENDING:
        return  # already resolved (duplicate callback) — nothing to do

    deal = attempt.deal
    if result.status == "succeeded":
        attempt.status = PaymentAttempt.Status.SUCCEEDED
        if result.provider_ref:
            attempt.provider_ref = result.provider_ref
        attempt.save(update_fields=["status", "provider_ref", "updated_at"])

        if deal.status == Deal.Status.PROCESSING_PAYMENT:
            deal.status = Deal.Status.FUNDED
            deal.funded_at = timezone.now()
            deal.save(update_fields=["status", "funded_at"])
            DealTransaction.objects.create(
                deal=deal,
                user=deal.buyer,
                amount=deal.amount,
                event_type=DealTransaction.EventType.PAYMENT_RECEIVED,
                reference=f"TXN-{attempt.id.hex[:12].upper()}",
            )
            record_funding(deal, operator="azampay:callback", provider="azampay", provider_ref=attempt.provider_ref)
            push_notification(
                deal.seller,
                "Deal funded — ship now",
                f"Deal {deal.code} is funded and protected in escrow. You can ship the item.",
                deal=deal,
            )
    elif result.status == "failed":
        attempt.status = PaymentAttempt.Status.FAILED
        attempt.save(update_fields=["status", "updated_at"])
        if deal.status == Deal.Status.PROCESSING_PAYMENT:
            deal.status = Deal.Status.AWAITING_PAYMENT
            deal.save(update_fields=["status"])
            push_notification(
                deal.buyer,
                "Payment failed",
                f"Your payment for deal {deal.code} did not go through. Please try again.",
                deal=deal,
            )
    # "pending" callbacks (rare) are recorded via the webhook event but don't
    # change PaymentAttempt/Deal state yet.


@api_view(["POST"])
def azampay_disbursement_callback(request):
    """AzamPay disbursement callback — SEPARATE from the checkout callback
    above: different URL, different payload shape ({initiatorReferenceId,
    fspReferenceId, pgReferenceId, amount, status, message, operator} per
    AzamPay's disbursement docs), confirming a payout submitted by
    payments.flows._disburse_seller_payout (which only got an async
    "processing" ack, not a final result, at submission time).

    Registered with AzamPay as
    .../payments/azampay/disbursement-callback?token=<AZAMPAY_CALLBACK_SECRET>
    — reuses the same shared-secret check as the checkout callback.
    """
    provider = AzamPayProvider()
    if not provider.verify_callback(request):
        return Response({"detail": "Invalid callback token."}, status=status.HTTP_403_FORBIDDEN)

    payload = request.data if isinstance(request.data, dict) else {}
    logger.info("AzamPay disbursement callback received", extra={"payload": payload})

    with transaction.atomic():
        event_id = f"disbursement:{payload.get('pgReferenceId') or payload.get('initiatorReferenceId') or ''}"
        if event_id == "disbursement:":
            event_id = f"disbursement:unmatched:{hash(str(payload))}"
        event, created = ProviderWebhookEvent.objects.get_or_create(
            provider="azampay",
            event_id=event_id[:160],
            defaults={"event_type": "disbursement.callback", "payload": payload, "signature_valid": True},
        )
        if not created and event.processed:
            return Response({"detail": "Already processed."}, status=status.HTTP_200_OK)

        result = provider.normalize_disbursement_callback(payload)
        _apply_disbursement_result(result)

        event.processed = True
        event.processed_at = timezone.now()
        event.save(update_fields=["processed", "processed_at"])

    return Response({"detail": "Callback received."}, status=status.HTTP_200_OK)


def _apply_disbursement_result(result):
    payout = None
    if result.provider_ref:
        payout = Payout.objects.filter(provider_ref=result.provider_ref).select_related("deal", "seller").first()

    if payout is None:
        logger.warning("AzamPay disbursement callback did not match any Payout: %s", result.raw)
        return
    if payout.status != Payout.Status.PROCESSING:
        return  # already resolved (duplicate callback) — nothing to do

    deal = payout.deal
    if result.status == "succeeded":
        _post_payout_journal(
            deal, payout.amount, operator="azampay:disbursement-callback",
            journal_reference=payout.journal_reference, idempotency_key=payout.idempotency_key,
        )
        payout.status = Payout.Status.SUCCEEDED
        payout.save(update_fields=["status", "updated_at"])
        push_notification(
            deal.seller,
            "Payout sent",
            f"{payout.amount} for deal {deal.code} has been sent to your mobile money account.",
            deal=deal,
        )
    else:
        payout.retry_count += 1
        payout.status = Payout.Status.FAILED_RETRYABLE
        payout.save(update_fields=["status", "retry_count", "updated_at"])
