"""Admin API endpoints consumed by the Angular operations portal.

Every endpoint requires an active authenticated Django staff session. Mutating
actions are recorded in the application audit log.
"""

from decimal import Decimal
from uuid import uuid4

from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Count, Q, Sum
from django.utils import timezone
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response

from disputes.models import Dispute, DisputeMessage
from escrow.models import Deal, Transaction
from payments.flows import (
    record_refund,
    record_release_and_payout,
    record_split,
)
from payments.models import Payout

from .models import AuditLog, KycSubmission, OtpCode, User
from .views import _is_admin_request


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _require_admin(request):
    if not _is_admin_request(request):
        return Response({"detail": "Not authorized."}, status=status.HTTP_403_FORBIDDEN)
    return None


def _operator(request) -> str:
    user = getattr(request, "user", None)
    if user and user.is_authenticated and user.is_staff:
        return (user.get_full_name() or user.get_username()).strip()[:120]
    return (request.headers.get("X-Admin-Operator") or "legacy-api-key").strip()[:120]


def _client_ip(request):
    xff = request.META.get("HTTP_X_FORWARDED_FOR")
    if xff:
        return xff.split(",")[0].strip()
    return request.META.get("REMOTE_ADDR")


def _audit(request, action: str, obj=None, payload=None):
    ct = ContentType.objects.get_for_model(obj) if obj is not None else None
    AuditLog.objects.create(
        actor=None,
        action=action,
        content_type=ct,
        object_id=str(obj.pk) if obj is not None else None,
        payload={
            "operator": _operator(request),
            **(payload or {}),
        },
        ip_address=_client_ip(request),
    )


def _paginate(request, queryset, serializer, default_page_size=25, max_page_size=200):
    try:
        page = max(1, int(request.query_params.get("page", 1)))
    except (TypeError, ValueError):
        page = 1
    try:
        page_size = int(request.query_params.get("page_size", default_page_size))
    except (TypeError, ValueError):
        page_size = default_page_size
    page_size = max(1, min(page_size, max_page_size))

    total = queryset.count()
    start = (page - 1) * page_size
    end = start + page_size
    results = [serializer(item) for item in queryset[start:end]]
    return {
        "count": total,
        "page": page,
        "page_size": page_size,
        "num_pages": max(1, (total + page_size - 1) // page_size),
        "results": results,
    }


def _reason_required(request):
    reason = str(request.data.get("reason") or "").strip()
    if len(reason) < 4:
        return None, Response(
            {"detail": "reason is required (min 4 chars) for this action."},
            status=status.HTTP_400_BAD_REQUEST,
        )
    return reason, None


# ---------------------------------------------------------------------------
# Serializers (dict form)
# ---------------------------------------------------------------------------


def _serialize_user(user: User):
    return {
        "id": str(user.id),
        "phone": user.phone,
        "name": user.name,
        "email": user.email,
        "role": user.role,
        "kyc_level": user.kyc_level,
        "is_verified": user.is_verified,
        "is_frozen": user.is_frozen,
        "frozen_reason": user.frozen_reason,
        "frozen_at": user.frozen_at,
        "reputation_score": str(user.reputation_score),
        "total_deals": user.total_deals,
        "completed_deals": user.completed_deals,
        "pin_set": bool(user.pin_hash),
        "payout_provider": user.payout_provider,
        "payout_phone": user.payout_phone,
        "created_at": user.created_at,
    }


def _serialize_deal(deal: Deal, request=None):
    images = []
    for img in deal.images.all():
        url = img.image.url if img.image else None
        if url and request is not None:
            url = request.build_absolute_uri(url)
        images.append({"id": str(img.id), "url": url, "is_main": img.is_main})
    return {
        "id": str(deal.id),
        "code": deal.code,
        "title": deal.title,
        "description": deal.description,
        "amount": str(deal.amount),
        "fee": str(deal.fee),
        "net_amount": str(deal.net_amount),
        "status": deal.status,
        "has_dispute": deal.has_dispute,
        "seller": {"id": str(deal.seller_id), "phone": deal.seller.phone, "name": deal.seller.name},
        "buyer": (
            {"id": str(deal.buyer_id), "phone": deal.buyer.phone, "name": deal.buyer.name}
            if deal.buyer_id else None
        ),
        "funded_at": deal.funded_at,
        "shipped_at": deal.shipped_at,
        "confirmed_at": deal.confirmed_at,
        "buyer_confirmed_at": deal.buyer_confirmed_at,
        "seller_confirmed_at": deal.seller_confirmed_at,
        "auto_release_at": deal.auto_release_at,
        "inspection_window_hours": deal.inspection_window_hours,
        "tracking_info": deal.tracking_info,
        "images": images,
    }


def _payout_info(deal: Deal):
    """Surface the outcome of the last disbursement attempt so an admin
    force-release/resolve response doesn't silently hide a failed AzamPay
    payout (record_release_and_payout only marks it FAILED_RETRYABLE and
    leaves the seller's payable balance intact — see payments/flows.py)."""
    payout = Payout.objects.filter(deal=deal).order_by("-created_at").first()
    if payout is None:
        return None
    return {
        "status": payout.status,
        "provider": payout.provider,
        "provider_ref": payout.provider_ref,
        "retry_count": payout.retry_count,
    }


def _serialize_transaction(t: Transaction):
    return {
        "id": str(t.id),
        "deal_id": str(t.deal_id),
        "deal_code": t.deal.code if t.deal_id else None,
        "user_id": str(t.user_id) if t.user_id else None,
        "user_phone": t.user.phone if t.user_id else None,
        "amount": str(t.amount),
        "currency": t.currency,
        "event_type": t.event_type,
        "reference": t.reference,
        "metadata": t.metadata,
        "created_at": t.created_at,
    }


def _serialize_dispute(dispute: Dispute):
    return {
        "id": str(dispute.id),
        "deal_code": dispute.deal.code,
        "deal_id": str(dispute.deal_id),
        "status": dispute.status,
        "reason_category": dispute.reason_category,
        "reason_detail": dispute.reason_detail,
        "tier": dispute.tier,
        "raised_by": {
            "id": str(dispute.raised_by_id),
            "phone": dispute.raised_by.phone,
            "name": dispute.raised_by.name,
        },
        "assigned_to": (
            {"id": str(dispute.assigned_to_id), "phone": dispute.assigned_to.phone,
             "name": dispute.assigned_to.name}
            if dispute.assigned_to_id else None
        ),
        "resolution": dispute.resolution,
        "resolution_type": dispute.resolution_type,
        "resolved_at": dispute.resolved_at,
    }


def _serialize_kyc(kyc: KycSubmission, request):
    nida_url = request.build_absolute_uri(kyc.nida_image.url) if kyc.nida_image else None
    return {
        "id": str(kyc.id),
        "user": {"id": str(kyc.user_id), "phone": kyc.user.phone, "name": kyc.user.name},
        "full_name": kyc.full_name,
        "nin": kyc.nin,
        "nida_image_url": nida_url,
        "status": kyc.status,
        "review_notes": kyc.review_notes,
        "submitted_at": kyc.submitted_at,
        "updated_at": kyc.updated_at,
    }


def _serialize_audit(log: AuditLog):
    ct_label = None
    if log.content_type_id:
        ct = ContentType.objects.get_for_id(log.content_type_id)
        ct_label = f"{ct.app_label}.{ct.model}"
    return {
        "id": str(log.id),
        "action": log.action,
        "target_type": ct_label,
        "target_id": log.object_id,
        "payload": log.payload,
        "ip_address": log.ip_address,
        "created_at": log.created_at,
    }


# ---------------------------------------------------------------------------
# Stats
# ---------------------------------------------------------------------------


@api_view(["GET"])
def admin_stats(request):
    err = _require_admin(request)
    if err:
        return err

    deal_by_status = {row["status"]: row["n"] for row in Deal.objects.values("status").annotate(n=Count("id"))}
    dispute_by_status = {row["status"]: row["n"] for row in Dispute.objects.values("status").annotate(n=Count("id"))}
    kyc_by_status = {row["status"]: row["n"] for row in KycSubmission.objects.values("status").annotate(n=Count("id"))}

    protected_amount = Deal.objects.filter(
        status__in=[
            Deal.Status.FUNDED, Deal.Status.IN_TRANSIT,
            Deal.Status.CONFIRMING, Deal.Status.DISPUTED,
        ]
    ).aggregate(total=Sum("amount"))["total"] or 0
    completed_amount = Deal.objects.filter(status=Deal.Status.COMPLETED).aggregate(total=Sum("net_amount"))["total"] or 0

    return Response(
        {
            "users": {
                "total": User.objects.count(),
                "verified": User.objects.filter(is_verified=True).count(),
                "frozen": User.objects.filter(is_frozen=True).count(),
                "by_role": {row["role"]: row["n"] for row in User.objects.values("role").annotate(n=Count("id"))},
            },
            "deals": {
                "total": Deal.objects.count(),
                "protected_amount": str(protected_amount),
                "completed_amount": str(completed_amount),
                "by_status": deal_by_status,
                "with_dispute": Deal.objects.filter(has_dispute=True).count(),
            },
            "disputes": {
                "total": Dispute.objects.count(),
                "by_status": dispute_by_status,
                "open_or_review": Dispute.objects.filter(
                    status__in=[Dispute.Status.OPEN, Dispute.Status.UNDER_REVIEW]
                ).count(),
            },
            "kyc": {
                "total": KycSubmission.objects.count(),
                "by_status": kyc_by_status,
                "pending": KycSubmission.objects.filter(status=KycSubmission.Status.PENDING).count(),
            },
            "authentication": {
                "total": OtpCode.objects.count(),
                "active": OtpCode.objects.filter(
                    consumed_at__isnull=True,
                    expires_at__gt=timezone.now(),
                ).count(),
            },
            "transactions": {
                "total": Transaction.objects.count(),
                "volume": str(Transaction.objects.aggregate(t=Sum("amount"))["t"] or 0),
            },
            "audit": {"total": AuditLog.objects.count()},
        }
    )


# ---------------------------------------------------------------------------
# Users
# ---------------------------------------------------------------------------


@api_view(["GET"])
def admin_users(request):
    err = _require_admin(request)
    if err:
        return err
    qs = User.objects.all().order_by("-created_at")
    q = request.query_params.get("q")
    if q:
        qs = qs.filter(Q(phone__icontains=q) | Q(name__icontains=q) | Q(email__icontains=q))
    role = request.query_params.get("role")
    if role:
        qs = qs.filter(role=role)
    verified = request.query_params.get("verified")
    if verified in {"true", "false"}:
        qs = qs.filter(is_verified=(verified == "true"))
    frozen = request.query_params.get("frozen")
    if frozen in {"true", "false"}:
        qs = qs.filter(is_frozen=(frozen == "true"))
    return Response(_paginate(request, qs, _serialize_user))


@api_view(["GET"])
def admin_user_detail(request, user_id):
    err = _require_admin(request)
    if err:
        return err
    user = User.objects.filter(id=user_id).first()
    if not user:
        return Response({"detail": "User not found."}, status=status.HTTP_404_NOT_FOUND)

    seller_deals = Deal.objects.filter(seller=user).select_related("buyer")[:20]
    buyer_deals = Deal.objects.filter(buyer=user).select_related("seller")[:20]
    disputes = Dispute.objects.filter(raised_by=user).select_related("deal")[:20]

    return Response({
        **_serialize_user(user),
        "seller_deals": [_serialize_deal(d, request) for d in seller_deals],
        "buyer_deals": [_serialize_deal(d, request) for d in buyer_deals],
        "raised_disputes": [_serialize_dispute(d) for d in disputes],
    })


@api_view(["POST"])
def admin_user_freeze(request, user_id):
    err = _require_admin(request)
    if err:
        return err
    reason, err = _reason_required(request)
    if err:
        return err
    user = User.objects.filter(id=user_id).first()
    if not user:
        return Response({"detail": "User not found."}, status=status.HTTP_404_NOT_FOUND)
    if user.is_frozen:
        return Response({"detail": "User is already frozen."}, status=status.HTTP_400_BAD_REQUEST)

    user.is_frozen = True
    user.frozen_reason = reason
    user.frozen_at = timezone.now()
    user.save(update_fields=["is_frozen", "frozen_reason", "frozen_at", "updated_at"])
    _audit(request, "user.freeze", user, {"reason": reason})
    return Response(_serialize_user(user))


@api_view(["POST"])
def admin_user_unfreeze(request, user_id):
    err = _require_admin(request)
    if err:
        return err
    reason, err = _reason_required(request)
    if err:
        return err
    user = User.objects.filter(id=user_id).first()
    if not user:
        return Response({"detail": "User not found."}, status=status.HTTP_404_NOT_FOUND)
    if not user.is_frozen:
        return Response({"detail": "User is not frozen."}, status=status.HTTP_400_BAD_REQUEST)

    user.is_frozen = False
    user.frozen_reason = None
    user.frozen_at = None
    user.save(update_fields=["is_frozen", "frozen_reason", "frozen_at", "updated_at"])
    _audit(request, "user.unfreeze", user, {"reason": reason})
    return Response(_serialize_user(user))


@api_view(["POST"])
def admin_user_set_role(request, user_id):
    err = _require_admin(request)
    if err:
        return err
    reason, err = _reason_required(request)
    if err:
        return err
    role = str(request.data.get("role") or "").strip().lower()
    if role not in {c[0] for c in User.Role.choices}:
        return Response(
            {"detail": f"role must be one of {[c[0] for c in User.Role.choices]}"},
            status=status.HTTP_400_BAD_REQUEST,
        )
    user = User.objects.filter(id=user_id).first()
    if not user:
        return Response({"detail": "User not found."}, status=status.HTTP_404_NOT_FOUND)
    before = user.role
    user.role = role
    user.save(update_fields=["role", "updated_at"])
    _audit(request, "user.role_change", user, {"reason": reason, "before": before, "after": role})
    return Response(_serialize_user(user))


@api_view(["POST"])
def admin_user_reset_pin(request, user_id):
    err = _require_admin(request)
    if err:
        return err
    reason, err = _reason_required(request)
    if err:
        return err
    user = User.objects.filter(id=user_id).first()
    if not user:
        return Response({"detail": "User not found."}, status=status.HTTP_404_NOT_FOUND)
    user.pin_hash = None
    user.save(update_fields=["pin_hash", "updated_at"])
    _audit(request, "user.pin_reset", user, {"reason": reason})
    return Response(_serialize_user(user))


# ---------------------------------------------------------------------------
# Deals
# ---------------------------------------------------------------------------


@api_view(["GET"])
def admin_deals(request):
    err = _require_admin(request)
    if err:
        return err
    qs = Deal.objects.select_related("seller", "buyer").all().order_by("-id")
    q = request.query_params.get("q")
    if q:
        qs = qs.filter(
            Q(code__icontains=q) | Q(title__icontains=q)
            | Q(seller__phone__icontains=q) | Q(buyer__phone__icontains=q)
        )
    st = request.query_params.get("status")
    if st:
        qs = qs.filter(status=st)
    dispute = request.query_params.get("dispute")
    if dispute in {"true", "false"}:
        qs = qs.filter(has_dispute=(dispute == "true"))
    return Response(_paginate(request, qs, _serialize_deal))


@api_view(["GET"])
def admin_deal_detail(request, deal_id):
    err = _require_admin(request)
    if err:
        return err
    deal = Deal.objects.filter(id=deal_id).select_related("seller", "buyer").first()
    if not deal:
        return Response({"detail": "Deal not found."}, status=status.HTTP_404_NOT_FOUND)
    txns = Transaction.objects.filter(deal=deal).select_related("user").order_by("-created_at")
    disputes = Dispute.objects.filter(deal=deal).select_related("raised_by", "assigned_to").order_by("-id")
    acceptances = deal.agreement_acceptances.select_related("user", "agreement").order_by("role")
    return Response({
        **_serialize_deal(deal, request),
        "transactions": [_serialize_transaction(t) for t in txns],
        "disputes": [_serialize_dispute(d) for d in disputes],
        "agreement_acceptances": [
            {
                "role": a.role,
                "user_phone": a.user.phone,
                "agreement_version": a.agreement.version,
                "accepted_at": a.accepted_at,
                "ip_address": a.ip_address,
            }
            for a in acceptances
        ],
    })


def _record_txn(deal, user, amount, event_type, metadata=None):
    Transaction.objects.create(
        deal=deal,
        user=user,
        amount=amount,
        event_type=event_type,
        reference=f"TXN-{uuid4().hex[:12].upper()}",
        metadata=metadata or {},
    )


@api_view(["POST"])
def admin_deal_cancel(request, deal_id):
    err = _require_admin(request)
    if err:
        return err
    reason, err = _reason_required(request)
    if err:
        return err
    with transaction.atomic():
        deal = Deal.objects.select_for_update().filter(id=deal_id).select_related("seller", "buyer").first()
        if not deal:
            return Response({"detail": "Deal not found."}, status=status.HTTP_404_NOT_FOUND)
        if deal.status in {Deal.Status.COMPLETED, Deal.Status.REFUNDED, Deal.Status.CANCELLED}:
            return Response(
                {"detail": f"Deal cannot be cancelled from status={deal.status}."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        before = deal.status
        deal.status = Deal.Status.CANCELLED
        deal.save(update_fields=["status"])
        _audit(request, "deal.cancel", deal, {"reason": reason, "before": before})
    return Response(_serialize_deal(deal, request))


@api_view(["POST"])
def admin_deal_refund(request, deal_id):
    err = _require_admin(request)
    if err:
        return err
    reason, err = _reason_required(request)
    if err:
        return err
    with transaction.atomic():
        deal = Deal.objects.select_for_update().filter(id=deal_id).select_related("seller", "buyer").first()
        if not deal:
            return Response({"detail": "Deal not found."}, status=status.HTTP_404_NOT_FOUND)
        if deal.status in {Deal.Status.COMPLETED, Deal.Status.REFUNDED}:
            return Response(
                {"detail": f"Deal cannot be refunded from status={deal.status}."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        before = deal.status
        deal.status = Deal.Status.REFUNDED
        deal.has_dispute = False
        deal.save(update_fields=["status", "has_dispute"])
        record_refund(deal, deal.amount, operator=_operator(request), reason=reason)
        _record_txn(deal, deal.buyer, deal.amount, Transaction.EventType.REFUND_ISSUED,
                    {"reason": reason, "operator": _operator(request)})
        _audit(request, "deal.refund", deal, {"reason": reason, "before": before, "amount": str(deal.amount)})
    return Response(_serialize_deal(deal, request))


@api_view(["POST"])
def admin_deal_force_release(request, deal_id):
    err = _require_admin(request)
    if err:
        return err
    reason, err = _reason_required(request)
    if err:
        return err
    with transaction.atomic():
        deal = Deal.objects.select_for_update().filter(id=deal_id).select_related("seller", "buyer").first()
        if not deal:
            return Response({"detail": "Deal not found."}, status=status.HTTP_404_NOT_FOUND)
        if deal.status not in {
            Deal.Status.FUNDED, Deal.Status.IN_TRANSIT,
            Deal.Status.CONFIRMING, Deal.Status.DISPUTED,
        }:
            return Response(
                {"detail": f"Cannot force-release from status={deal.status}."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        before = deal.status
        now = timezone.now()
        deal.status = Deal.Status.COMPLETED
        deal.confirmed_at = deal.confirmed_at or now
        deal.has_dispute = False
        deal.save(update_fields=["status", "confirmed_at", "has_dispute"])
        record_release_and_payout(deal, operator=_operator(request))
        deal.refresh_from_db(fields=["fee", "net_amount"])
        payout = _payout_info(deal)
        _record_txn(deal, deal.seller, deal.net_amount, Transaction.EventType.PAYOUT_RELEASED,
                    {"reason": reason, "operator": _operator(request), "override": True})
        _audit(request, "deal.force_release", deal, {
            "reason": reason, "before": before, "amount": str(deal.net_amount),
            "payout_status": payout["status"] if payout else None,
        })
    return Response({**_serialize_deal(deal, request), "payout": payout})


@api_view(["GET"])
def admin_transactions(request):
    err = _require_admin(request)
    if err:
        return err
    qs = Transaction.objects.select_related("deal", "user").order_by("-created_at")
    event = request.query_params.get("event_type")
    if event:
        qs = qs.filter(event_type=event)
    deal_code = request.query_params.get("deal_code")
    if deal_code:
        qs = qs.filter(deal__code__iexact=deal_code)
    return Response(_paginate(request, qs, _serialize_transaction))


# ---------------------------------------------------------------------------
# Disputes
# ---------------------------------------------------------------------------


@api_view(["GET"])
def admin_disputes(request):
    err = _require_admin(request)
    if err:
        return err
    qs = Dispute.objects.select_related("deal", "raised_by", "assigned_to").order_by("-id")
    st = request.query_params.get("status")
    if st:
        qs = qs.filter(status=st)
    search = str(request.query_params.get("search") or "").strip()
    if search:
        qs = qs.filter(
            Q(deal__code__icontains=search)
            | Q(deal__title__icontains=search)
            | Q(raised_by__phone__icontains=search)
            | Q(raised_by__name__icontains=search)
        )
    return Response(_paginate(request, qs, _serialize_dispute))


@api_view(["GET"])
def admin_dispute_detail(request, dispute_id):
    err = _require_admin(request)
    if err:
        return err
    dispute = (
        Dispute.objects.select_related("deal", "deal__seller", "deal__buyer", "raised_by", "assigned_to")
        .filter(id=dispute_id).first()
    )
    if not dispute:
        return Response({"detail": "Dispute not found."}, status=status.HTTP_404_NOT_FOUND)
    messages = [
        {
            "id": str(m.id),
            "sender": {"id": str(m.sender_id), "phone": m.sender.phone, "name": m.sender.name},
            "message": m.message,
            "created_at": m.created_at,
        }
        for m in DisputeMessage.objects.filter(dispute=dispute).select_related("sender")
    ]
    return Response({**_serialize_dispute(dispute), "messages": messages})


@api_view(["POST"])
def admin_dispute_assign(request, dispute_id):
    err = _require_admin(request)
    if err:
        return err
    assignee_phone = str(request.data.get("assignee_phone") or "").strip()
    if not assignee_phone:
        return Response({"detail": "assignee_phone is required."}, status=status.HTTP_400_BAD_REQUEST)
    assignee = User.objects.filter(phone=assignee_phone).first()
    if not assignee:
        return Response(
            {"detail": f"No user with phone {assignee_phone}. Assignee must have an account."},
            status=status.HTTP_404_NOT_FOUND,
        )
    dispute = Dispute.objects.filter(id=dispute_id).select_related("deal").first()
    if not dispute:
        return Response({"detail": "Dispute not found."}, status=status.HTTP_404_NOT_FOUND)
    dispute.assigned_to = assignee
    if dispute.status == Dispute.Status.OPEN:
        dispute.status = Dispute.Status.UNDER_REVIEW
    dispute.save(update_fields=["assigned_to", "status"])
    _audit(request, "dispute.assign", dispute, {"assignee_phone": assignee_phone})
    return Response(_serialize_dispute(dispute))


@api_view(["POST"])
def admin_dispute_staff_message(request, dispute_id):
    err = _require_admin(request)
    if err:
        return err
    text = str(request.data.get("message") or "").strip()
    if not text:
        return Response({"detail": "message is required."}, status=status.HTTP_400_BAD_REQUEST)
    dispute = Dispute.objects.filter(id=dispute_id).select_related("raised_by").first()
    if not dispute:
        return Response({"detail": "Dispute not found."}, status=status.HTTP_404_NOT_FOUND)

    sender = dispute.assigned_to or dispute.raised_by
    prefixed = f"[STAFF · {_operator(request)}] {text}"
    msg = DisputeMessage.objects.create(dispute=dispute, sender=sender, message=prefixed)
    if dispute.status == Dispute.Status.OPEN:
        dispute.status = Dispute.Status.UNDER_REVIEW
        dispute.save(update_fields=["status"])
    _audit(request, "dispute.staff_message", dispute, {"len": len(text)})
    return Response({
        "id": str(msg.id),
        "sender": {"id": str(sender.id), "phone": sender.phone, "name": sender.name},
        "message": msg.message,
        "created_at": msg.created_at,
    }, status=status.HTTP_201_CREATED)


@api_view(["POST"])
def admin_dispute_resolve(request, dispute_id):
    err = _require_admin(request)
    if err:
        return err
    reason, err = _reason_required(request)
    if err:
        return err
    resolution_type = str(request.data.get("resolution_type") or "").strip().lower()
    valid = {c[0] for c in Dispute.ResolutionType.choices}
    if resolution_type not in valid:
        return Response(
            {"detail": f"resolution_type must be one of {sorted(valid)}"},
            status=status.HTTP_400_BAD_REQUEST,
        )

    with transaction.atomic():
        dispute = (
            Dispute.objects.select_for_update()
            .filter(id=dispute_id)
            .select_related("deal", "deal__seller", "deal__buyer", "raised_by")
            .first()
        )
        if not dispute:
            return Response({"detail": "Dispute not found."}, status=status.HTTP_404_NOT_FOUND)
        if dispute.status in {Dispute.Status.RESOLVED, Dispute.Status.CLOSED}:
            return Response({"detail": "Dispute is already resolved/closed."}, status=status.HTTP_400_BAD_REQUEST)

        now = timezone.now()
        dispute.resolution = reason
        dispute.resolution_type = resolution_type
        dispute.status = Dispute.Status.RESOLVED
        dispute.resolved_at = now
        dispute.save(update_fields=["resolution", "resolution_type", "status", "resolved_at"])

        deal = dispute.deal
        deal.has_dispute = False
        operator = _operator(request)
        payout = None
        if resolution_type == Dispute.ResolutionType.FULL_SELLER:
            deal.status = Deal.Status.COMPLETED
            deal.confirmed_at = deal.confirmed_at or now
            deal.save(update_fields=["status", "has_dispute", "confirmed_at"])
            record_release_and_payout(deal, operator=operator)
            deal.refresh_from_db(fields=["fee", "net_amount"])
            payout = _payout_info(deal)
            _record_txn(deal, deal.seller, deal.net_amount, Transaction.EventType.PAYOUT_RELEASED,
                        {"reason": reason, "dispute_id": str(dispute.id), "operator": operator})
        elif resolution_type == Dispute.ResolutionType.FULL_BUYER:
            deal.status = Deal.Status.REFUNDED
            deal.save(update_fields=["status", "has_dispute"])
            record_refund(deal, deal.amount, operator=operator, reason=reason)
            _record_txn(deal, deal.buyer, deal.amount, Transaction.EventType.REFUND_ISSUED,
                        {"reason": reason, "dispute_id": str(dispute.id), "operator": operator})
        elif resolution_type == Dispute.ResolutionType.SPLIT:
            try:
                buyer_share = Decimal(str(request.data.get("buyer_amount") or "0"))
                seller_share = Decimal(str(request.data.get("seller_amount") or "0"))
            except Exception:
                return Response({"detail": "buyer_amount and seller_amount must be decimals."},
                                status=status.HTTP_400_BAD_REQUEST)
            if buyer_share + seller_share != deal.amount:
                return Response(
                    {"detail": f"buyer_amount + seller_amount must equal deal amount ({deal.amount})."},
                    status=status.HTTP_400_BAD_REQUEST,
                )
            deal.status = Deal.Status.COMPLETED
            deal.confirmed_at = deal.confirmed_at or now
            deal.save(update_fields=["status", "has_dispute", "confirmed_at"])
            record_split(deal, buyer_share, seller_share, operator=operator)
            payout = _payout_info(deal)
            if buyer_share > 0:
                _record_txn(deal, deal.buyer, buyer_share, Transaction.EventType.REFUND_ISSUED,
                            {"reason": reason, "dispute_id": str(dispute.id), "split": True, "operator": operator})
            if seller_share > 0:
                _record_txn(deal, deal.seller, seller_share, Transaction.EventType.PAYOUT_RELEASED,
                            {"reason": reason, "dispute_id": str(dispute.id), "split": True, "operator": operator})
        elif resolution_type == Dispute.ResolutionType.CANCELLED:
            deal.status = Deal.Status.CANCELLED
            deal.save(update_fields=["status", "has_dispute"])

        _audit(request, "dispute.resolve", dispute, {
            "reason": reason, "resolution_type": resolution_type,
            "deal_after": deal.status,
            "payout_status": payout["status"] if payout else None,
        })

    return Response({
        **_serialize_dispute(dispute),
        "deal": _serialize_deal(deal, request),
        "payout": payout,
    })


@api_view(["POST"])
def admin_dispute_close(request, dispute_id):
    err = _require_admin(request)
    if err:
        return err
    reason, err = _reason_required(request)
    if err:
        return err
    dispute = Dispute.objects.filter(id=dispute_id).select_related("deal").first()
    if not dispute:
        return Response({"detail": "Dispute not found."}, status=status.HTTP_404_NOT_FOUND)
    if dispute.status == Dispute.Status.CLOSED:
        return Response({"detail": "Dispute is already closed."}, status=status.HTTP_400_BAD_REQUEST)
    dispute.status = Dispute.Status.CLOSED
    dispute.save(update_fields=["status"])
    dispute.deal.has_dispute = False
    dispute.deal.save(update_fields=["has_dispute"])
    _audit(request, "dispute.close", dispute, {"reason": reason})
    return Response(_serialize_dispute(dispute))


# ---------------------------------------------------------------------------
# KYC
# ---------------------------------------------------------------------------


@api_view(["GET"])
def admin_kyc_list(request):
    err = _require_admin(request)
    if err:
        return err
    qs = KycSubmission.objects.select_related("user").order_by("-submitted_at")
    st = request.query_params.get("status")
    if st:
        qs = qs.filter(status=st)
    search = str(request.query_params.get("search") or "").strip()
    if search:
        qs = qs.filter(
            Q(full_name__icontains=search)
            | Q(nin__icontains=search)
            | Q(user__phone__icontains=search)
        )
    return Response(_paginate(request, qs, lambda k: _serialize_kyc(k, request)))


@api_view(["POST"])
def admin_kyc_review(request, kyc_id):
    err = _require_admin(request)
    if err:
        return err
    submission = KycSubmission.objects.filter(id=kyc_id).select_related("user").first()
    if not submission:
        return Response({"detail": "KYC submission not found."}, status=status.HTTP_404_NOT_FOUND)
    decision = str(request.data.get("decision") or "").lower().strip()
    if decision not in {"approve", "reject"}:
        return Response({"detail": "decision must be 'approve' or 'reject'."}, status=status.HTTP_400_BAD_REQUEST)
    notes = str(request.data.get("review_notes") or "").strip() or None

    if decision == "approve":
        submission.status = KycSubmission.Status.APPROVED
        submission.user.is_verified = True
        submission.user.kyc_level = max(submission.user.kyc_level, 2)
        submission.user.save(update_fields=["is_verified", "kyc_level", "updated_at"])
    else:
        submission.status = KycSubmission.Status.REJECTED
    submission.review_notes = notes
    submission.save(update_fields=["status", "review_notes", "updated_at"])
    _audit(request, f"kyc.{decision}", submission, {"notes": notes})

    return Response(_serialize_kyc(submission, request))


# ---------------------------------------------------------------------------
# Audit log
# ---------------------------------------------------------------------------


@api_view(["GET"])
def admin_audit(request):
    err = _require_admin(request)
    if err:
        return err
    qs = AuditLog.objects.all().order_by("-created_at")
    action = request.query_params.get("action")
    if action:
        qs = qs.filter(action__icontains=action)
    operator = request.query_params.get("operator")
    if operator:
        qs = qs.filter(payload__operator=operator)
    return Response(_paginate(request, qs, _serialize_audit))


# ---------------------------------------------------------------------------
# Dashboard time-series (for charts)
# ---------------------------------------------------------------------------


@api_view(["GET"])
def admin_stats_timeseries(request):
    """Daily activity for the last N days (default 14) to drive dashboard charts.

    Deals have no created_at, so 'deals_created' is derived from the
    DEAL_CREATED transaction event.
    """
    err = _require_admin(request)
    if err:
        return err

    from datetime import timedelta

    try:
        days = int(request.query_params.get("days", 14))
    except (TypeError, ValueError):
        days = 14
    days = max(1, min(days, 90))

    today = timezone.localdate()
    start = today - timedelta(days=days - 1)

    # Build empty buckets keyed by ISO date.
    buckets = {}
    for i in range(days):
        d = start + timedelta(days=i)
        buckets[d.isoformat()] = {
            "date": d.isoformat(),
            "deals_created": 0,
            "payments": 0,
            "payouts": 0,
            "refunds": 0,
            "volume": 0.0,
            "signups": 0,
        }

    txns = Transaction.objects.filter(created_at__date__gte=start)
    for t in txns.only("event_type", "amount", "created_at"):
        key = timezone.localtime(t.created_at).date().isoformat()
        b = buckets.get(key)
        if not b:
            continue
        b["volume"] += float(t.amount)
        if t.event_type == Transaction.EventType.DEAL_CREATED:
            b["deals_created"] += 1
        elif t.event_type == Transaction.EventType.PAYMENT_RECEIVED:
            b["payments"] += 1
        elif t.event_type == Transaction.EventType.PAYOUT_RELEASED:
            b["payouts"] += 1
        elif t.event_type == Transaction.EventType.REFUND_ISSUED:
            b["refunds"] += 1

    for u in User.objects.filter(created_at__date__gte=start).only("created_at"):
        key = timezone.localtime(u.created_at).date().isoformat()
        b = buckets.get(key)
        if b:
            b["signups"] += 1

    series = list(buckets.values())
    return Response({
        "days": days,
        "series": series,
        "totals": {
            "deals_created": sum(b["deals_created"] for b in series),
            "payments": sum(b["payments"] for b in series),
            "payouts": sum(b["payouts"] for b in series),
            "refunds": sum(b["refunds"] for b in series),
            "volume": round(sum(b["volume"] for b in series), 2),
            "signups": sum(b["signups"] for b in series),
        },
    })
