from django.shortcuts import render

# Create your views here.
# payments/views.py
from decimal import Decimal, InvalidOperation
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.db.models import Q, F, Sum, Case, When, DecimalField, Value as V
from django.db import transaction
from django.http import JsonResponse, HttpResponseBadRequest, HttpResponseNotAllowed
from django.shortcuts import render, get_object_or_404
from django.utils.dateparse import parse_date
from accounts.models import User
from payments.models import ViralPeWallet, ViralPeWalletUsage
# stdlib
# (optional, cleaner null-handling)
from django.db.models.functions import Coalesce

from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime, time, timedelta
from django.utils import timezone

from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render

def fmt2(value):
    """Return a string with exactly 2 decimals, safe for None/Decimal/float."""
    if value is None:
        value = Decimal('0')
    if not isinstance(value, Decimal):
        value = Decimal(str(value))
    return str(value.quantize(Decimal('0.00'), rounding=ROUND_HALF_UP))

def parse_date_any(s: str):
    if not s:
        return None
    for fmt in ("%d-%m-%Y", "%Y-%m-%d"):
        try:
            return datetime.strptime(s, fmt).date()
        except ValueError:
            continue
    return None


# ---------- PAGE ----------
@login_required
@permission_required('payments.view_viralpewalletusage', raise_exception=True)
def wallet_usage(request):
    page_title = "ViralPe Wallet Usage"
    return render(request, 'payments/wallet_usage.html', {
        'pagetitle': page_title,
    })

# ---------- LIST API (DataTables) ----------
@login_required
@permission_required('payments.view_viralpewalletusage', raise_exception=True)
def api_wallet_usage_list(request):
    if request.method != 'GET':
        return HttpResponseNotAllowed(['GET'])

    # DataTables params
    draw = int(request.GET.get('draw', '1') or 1)
    start = int(request.GET.get('start', '0') or 0)
    length = int(request.GET.get('length', '10') or 10)
    search_val = (request.GET.get('search[value]') or '').strip()

    # Custom filters
    mobile = (request.GET.get('mobile') or '').strip()
    # date_from = parse_date(request.GET.get('date_from') or '')
    # date_to = parse_date(request.GET.get('date_to') or '')
    # date_exact = parse_date(request.GET.get('date_exact') or '')

    # AFTER (accepts both formats)
    date_from  = parse_date_any(request.GET.get('date_from') or '')
    date_to    = parse_date_any(request.GET.get('date_to') or '')
    date_exact = parse_date_any(request.GET.get('date_exact') or '')

    print(date_exact)

    qs = ViralPeWalletUsage.objects.select_related('user', 'reference_user')
    tz = timezone.get_default_timezone()  # ensure this is IST in your 
    
    def local_day_bounds(d):
        """Return (start_utc, end_utc_exclusive) for the local day d."""
        start_local = datetime.combine(d, time.min)
        next_local  = datetime.combine(d + timedelta(days=1), time.min)
        start_aw    = timezone.make_aware(start_local, tz)
        next_aw     = timezone.make_aware(next_local, tz)
        return start_aw.astimezone(timezone.utc), next_aw.astimezone(timezone.utc)

    if mobile:
        qs = qs.filter(user__mobile_number=mobile)

    if date_exact:
        start_utc, next_utc = local_day_bounds(date_exact)
        qs = qs.filter(used_on__gte=start_utc, used_on__lt=next_utc)
    else:
        if date_from:
            start_utc, _ = local_day_bounds(date_from)
            qs = qs.filter(used_on__gte=start_utc)
        if date_to:
            _, next_utc = local_day_bounds(date_to)
            qs = qs.filter(used_on__lt=next_utc)

    # if date_exact:
    #     qs = qs.filter(used_on__date=date_exact)
    # else:
    #     if date_from:
    #         qs = qs.filter(used_on__date__gte=date_from)
    #     if date_to:
    #         qs = qs.filter(used_on__date__lte=date_to)

    if search_val:
        qs = qs.filter(
            Q(user__mobile_number__icontains=search_val) |
            Q(purpose__icontains=search_val) |
            Q(purpose_note__icontains=search_val) |
            Q(order_id__icontains=search_val)
        )

    records_total = ViralPeWalletUsage.objects.count()
    records_filtered = qs.count()

    # Ordering
    # map DataTables column index -> model field
    order_map = {
        0: 'used_on',          # Date/Time
        1: 'user__mobile_number',
        2: 'transaction_type',
        3: 'amount_used',
        4: 'purpose',
        5: 'purpose_note',
        6: 'order_id',
        7: 'reference_user__first_name',
    }
    order_col = int(request.GET.get('order[0][column]', '0') or 0)
    order_dir = request.GET.get('order[0][dir]', 'desc')
    order_field = order_map.get(order_col, 'used_on')
    if order_dir == 'desc':
        order_field = '-' + order_field
    qs = qs.order_by(order_field)

    # Pagination
    rows = []
    for u in qs[start:start+length]:
        rows.append({
            "id": u.id,
            "used_on": u.used_on.strftime("%Y-%m-%d %H:%M:%S"),
            "mobile": u.user.mobile_number,
            "type": u.transaction_type,
            "amount": str(u.amount_used),
            "purpose": u.purpose,
            "purpose_code": u.purpose_code,
            "purpose_note": u.purpose_note,
            "order_id": u.order_id or "",
            "by": (u.reference_user.first_name if u.reference_user else "") or "",
        })


    # Aggregates for the filtered result set
    sum_credit = qs.filter(transaction_type='credit').aggregate(
        s=Sum('amount_used'))['s'] or Decimal('0')
    sum_debit  = qs.filter(transaction_type='debit').aggregate(
        s=Sum('amount_used'))['s'] or Decimal('0')

    # If one mobile provided, also return current wallet balance
    wallet_balance = None
    if mobile:
        try:
            u = User.objects.get(mobile_number=mobile)
            wallet = ViralPeWallet.objects.filter(user=u).first()
            # wallet_balance = str(wallet.balance if wallet else Decimal('0.00'))
            wallet_balance = fmt2(wallet.balance if wallet else Decimal('0'))
        except User.DoesNotExist:
            wallet_balance = None

    return JsonResponse({
        "draw": draw,
        "recordsTotal": records_total,
        "recordsFiltered": records_filtered,
        "data": rows,
        "summary": {
            "wallet_balance": wallet_balance,        # already 2 decimals or null
            "sum_credit": fmt2(sum_credit),          # ← 2 decimals
            "sum_debit": fmt2(sum_debit),            # ← 2 decimals
        }
    })

# ---------- DETAIL API (GET + PATCH/POST to edit) ----------
from django.views.decorators.http import require_http_methods


# # @permission_required('payments.change_viralpewalletusage', raise_exception=True)
# @login_required
# @permission_required('payments.delete_viralpewalletusage', raise_exception=True)
# @require_http_methods(["GET", "POST", "DELETE"])
# def api_wallet_usage_detail(request, pk: int):
#     usage = get_object_or_404(ViralPeWalletUsage.objects.select_related('user'), pk=pk)

#     if request.method == 'GET':
#         return JsonResponse({
#             "id": usage.id,
#             "mobile": usage.user.mobile_number,
#             "transaction_type": usage.transaction_type,
#             "amount_used": str(usage.amount_used),
#             "purpose": usage.purpose,
#             "purpose_code": usage.purpose_code,
#             "purpose_note": usage.purpose_note,
#             "order_id": usage.order_id or "",
#             "used_on": usage.used_on.strftime("%Y-%m-%d %H:%M:%S"),
#         })

#     if request.method not in ('POST', 'PATCH'):
#         return HttpResponseNotAllowed(['GET', 'POST', 'PATCH'])

#     # Parse incoming fields
#     tx_type = (request.POST.get('transaction_type') or request.GET.get('transaction_type') or '').strip()
#     amount_raw = (request.POST.get('amount_used') or request.GET.get('amount_used') or '').strip()
#     purpose_code = (request.POST.get('purpose_code') or request.GET.get('purpose_code') or '').strip() or usage.purpose_code
#     purpose_note = (request.POST.get('purpose_note') or request.GET.get('purpose_note') or '').strip()
#     order_id = (request.POST.get('order_id') or request.GET.get('order_id') or '').strip()

#     # Optional: allow editing human-readable 'purpose' else keep in sync with code+note
#     purpose = (request.POST.get('purpose') or request.GET.get('purpose') or '').strip()

#     try:
#         if tx_type and tx_type not in dict(ViralPeWalletUsage.TRANSACTION_TYPE_CHOICES):
#             return HttpResponseBadRequest("Invalid transaction_type")

#         if amount_raw:
#             try:
#                 new_amount = Decimal(amount_raw)
#             except (InvalidOperation, TypeError):
#                 return HttpResponseBadRequest("Invalid amount_used")
#             if new_amount <= 0:
#                 return HttpResponseBadRequest("amount_used must be > 0")
#         else:
#             new_amount = usage.amount_used

#         new_tx_type = tx_type or usage.transaction_type

#         # Build new purpose string if not given explicitly
#         PURPOSE_MAP = dict(ViralPeWalletUsage.PURPOSE_CHOICES)
#         base = PURPOSE_MAP.get(purpose_code, usage.purpose_code)
#         new_purpose = purpose or (f"{base}" + (f" | {purpose_note}" if purpose_note else ""))

#         # Compute wallet delta: signed amounts before vs after
#         old_signed = usage.amount_used if usage.transaction_type == 'credit' else (usage.amount_used * Decimal('-1'))
#         new_signed = new_amount if new_tx_type == 'credit' else (new_amount * Decimal('-1'))
#         delta = new_signed - old_signed

#         with transaction.atomic():
#             # Update usage record
#             usage.transaction_type = new_tx_type
#             usage.amount_used = new_amount
#             usage.purpose_code = purpose_code
#             usage.purpose_note = purpose_note
#             usage.purpose = new_purpose
#             usage.order_id = order_id or usage.order_id
#             usage.save()

#             # Adjust wallet balance if needed
#             if delta != 0:
#                 wallet, _ = ViralPeWallet.objects.select_for_update().get_or_create(user=usage.user)
#                 wallet.balance = F('balance') + delta
#                 wallet.save()

#         return JsonResponse({"ok": True})
#     except Exception as e:
#         return JsonResponse({"ok": False, "error": str(e)}, status=400)


from decimal import Decimal, InvalidOperation

from django.contrib.auth.decorators import login_required, permission_required
from django.db import transaction
from django.db.models import F
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_http_methods

# adjust these imports to your app structure if different
from payments.models import ViralPeWallet, ViralPeWalletUsage


def _to_decimal(val: str) -> Decimal:
    if val is None or val == "":
        raise InvalidOperation("Amount is required")
    d = Decimal(str(val))
    if d < 0:
        raise InvalidOperation("Amount cannot be negative")
    return d


def _signed_amount(amount: Decimal, tx_type: str) -> Decimal:
    """
    credit  -> +amount  (wallet increases)
    debit   -> -amount  (wallet decreases)
    """
    return amount if tx_type == "credit" else (amount * Decimal("-1"))


@login_required
@require_http_methods(["GET", "POST", "DELETE"])
def api_wallet_usage_detail(request, pk: int):
    """
    GET     /api/wallet/usage/<pk>/            -> return usage JSON
    POST    /api/wallet/usage/<pk>/            -> edit usage (amount/transaction_type/purpose/note)
              (supports POST with _method=DELETE as override)
    DELETE  /api/wallet/usage/<pk>/            -> delete usage AND reverse its wallet effect
    """
    # Method override for browsers that only do POST
    effective_method = request.method
    if request.method == "POST" and (request.POST.get("_method") or "").upper() == "DELETE":
        effective_method = "DELETE"

    usage = get_object_or_404(
        ViralPeWalletUsage.objects.select_related("user"), pk=pk
    )

    # ---- GET: return usage payload ----
    if effective_method == "GET":
        data = {
            "id": usage.id,
            "user_id": usage.user_id,
            "user_str": getattr(usage.user, "get_full_name", lambda: "")() or getattr(usage.user, "username", ""),
            "amount_used": str(usage.amount_used),
            "transaction_type": usage.transaction_type,  # "credit" | "debit"
            "purpose": getattr(usage, "purpose", None),
            "note": getattr(usage, "note", None),
            "created_at": usage.created_at.isoformat() if hasattr(usage, "created_at") and usage.created_at else None,
            "updated_at": usage.updated_at.isoformat() if hasattr(usage, "updated_at") and usage.updated_at else None,
        }
        return JsonResponse({"ok": True, "usage": data})

    # ---- DELETE: reverse effect and remove usage ----
    if effective_method == "DELETE":
        if not request.user.has_perm("payments.delete_viralpewalletusage"):
            return JsonResponse({"ok": False, "error": "Permission denied."}, status=403)

        try:
            amt = Decimal(usage.amount_used)
            signed = _signed_amount(amt, usage.transaction_type)
            reverse_delta = signed * Decimal("-1")  # reverse the original impact

            with transaction.atomic():
                wallet, _ = ViralPeWallet.objects.select_for_update().get_or_create(user=usage.user)
                wallet.balance = F("balance") + reverse_delta
                wallet.save(update_fields=["balance"])
                usage.delete()

            # Return fresh balance
            wallet.refresh_from_db(fields=["balance"])
            return JsonResponse({"ok": True, "wallet_balance": str(wallet.balance)})
        except Exception as e:
            return JsonResponse({"ok": False, "error": str(e)}, status=400)

    # ---- POST: edit usage (and adjust wallet by the delta) ----
    # Fields you can send (all optional except at least one must change):
    # - amount_used
    # - transaction_type  ("credit" | "debit")
    # - purpose
    # - note
    if effective_method == "POST":
        if not request.user.has_perm("payments.change_viralpewalletusage"):
            return JsonResponse({"ok": False, "error": "Permission denied."}, status=403)

        # capture old state
        old_amount = Decimal(usage.amount_used)
        old_type = usage.transaction_type
        old_signed = _signed_amount(old_amount, old_type)

        # read incoming fields
        new_amount = None
        new_type = None
        new_purpose = None
        new_note = None

        if "amount_used" in request.POST:
            try:
                new_amount = _to_decimal(request.POST.get("amount_used"))
            except InvalidOperation as e:
                return JsonResponse({"ok": False, "error": str(e)}, status=400)

        if "transaction_type" in request.POST:
            tx = (request.POST.get("transaction_type") or "").strip().lower()
            if tx not in ("credit", "debit"):
                return JsonResponse({"ok": False, "error": "Invalid transaction_type. Use 'credit' or 'debit'."}, status=400)
            new_type = tx

        if "purpose" in request.POST:
            new_purpose = request.POST.get("purpose")

        if "note" in request.POST:
            new_note = request.POST.get("note")

        # If nothing to change, short-circuit
        if new_amount is None and new_type is None and new_purpose is None and new_note is None:
            return JsonResponse({"ok": True, "message": "No changes."})

        # compute new signed for delta
        amount_for_calc = new_amount if new_amount is not None else old_amount
        type_for_calc = new_type if new_type is not None else old_type
        new_signed = _signed_amount(amount_for_calc, type_for_calc)
        delta = new_signed - old_signed  # apply this on wallet

        try:
            with transaction.atomic():
                # lock wallet row
                wallet, _ = ViralPeWallet.objects.select_for_update().get_or_create(user=usage.user)
                # update usage fields
                if new_amount is not None:
                    usage.amount_used = new_amount
                if new_type is not None:
                    usage.transaction_type = new_type
                if new_purpose is not None and hasattr(usage, "purpose"):
                    usage.purpose = new_purpose
                if new_note is not None and hasattr(usage, "note"):
                    usage.note = new_note
                usage.save()

                # apply wallet delta
                if delta != 0:
                    wallet.balance = F("balance") + delta
                    wallet.save(update_fields=["balance"])

            # respond with fresh values
            usage.refresh_from_db()
            wallet.refresh_from_db(fields=["balance"])
            return JsonResponse({
                "ok": True,
                "usage": {
                    "id": usage.id,
                    "amount_used": str(usage.amount_used),
                    "transaction_type": usage.transaction_type,
                    "purpose": getattr(usage, "purpose", None),
                    "note": getattr(usage, "note", None),
                },
                "wallet_balance": str(wallet.balance),
            })
        except Exception as e:
            return JsonResponse({"ok": False, "error": str(e)}, status=400)

    # should not reach here
    return JsonResponse({"ok": False, "error": "Unsupported method."}, status=405)


# payments/views.py
from payments.utils import generate_wallet_credit_order_id
from notifications.services import send_notification

@login_required
@permission_required('payments.add_viralpewalletusage', raise_exception=True)
def api_wallet_quick_adjust(request):
    if request.method != 'POST':
        return HttpResponseNotAllowed(['POST'])

    mobile = (request.POST.get('mobile') or '').strip()
    tx_type = (request.POST.get('transaction_type') or '').strip()  # 'credit' | 'debit'
    amount_raw = (request.POST.get('amount') or '').strip()
    purpose_code = (request.POST.get('purpose_code') or 'manual_admin').strip()
    purpose_note = (request.POST.get('purpose_note') or '').strip()

    try:
        if not mobile: raise ValueError("Mobile is required")
        if tx_type not in ('credit','debit'): raise ValueError("Invalid transaction_type")

        try:
            amount = Decimal(amount_raw)
        except (InvalidOperation, TypeError):
            raise ValueError("Invalid amount")
        if amount <= 0: raise ValueError("Amount must be greater than zero")

        user = User.objects.get(mobile_number=mobile)
        wallet, _ = ViralPeWallet.objects.select_for_update().get_or_create(user=user)

        PURPOSE_MAP = dict(ViralPeWalletUsage.PURPOSE_CHOICES)
        base = PURPOSE_MAP.get(purpose_code, 'Manual Top-Up (Admin)')
        purpose_str = base + (f" | {purpose_note}" if purpose_note else "")

        with transaction.atomic():
            # Adjust wallet
            sign = Decimal('1') if tx_type == 'credit' else Decimal('-1')
            wallet.balance = F('balance') + (sign * amount)
            wallet.save()

            # Audit row
            order_id = generate_wallet_credit_order_id()
            usage = ViralPeWalletUsage.objects.create(
                user=user,
                amount_used=amount,
                transaction_type=tx_type,
                purpose=purpose_str,
                purpose_code=purpose_code,
                purpose_note=purpose_note,
                order_id=order_id,
                reference_user=request.user,
            )

        # fresh balance
        wallet.refresh_from_db()

        # Notify only for credits (match your existing behavior)
        if tx_type == 'credit':
            send_notification(
                user=user,
                type_key="wallet_topup_credit",
                context={
                    "user": user,
                    "amount": amount,
                    "order_id": usage.order_id,
                    "balance": wallet.balance,
                    "purpose": usage.purpose,
                },
                message=f"INR {amount} added to your ViralPe Wallet. Order {usage.order_id}.",
            )

        return JsonResponse({"ok": True, "wallet_balance": fmt2(wallet.balance)})
    except User.DoesNotExist:
        return JsonResponse({"ok": False, "error": f"User with mobile {mobile} not found"}, status=400)
    except Exception as e:
        return JsonResponse({"ok": False, "error": str(e)}, status=400)

import csv, re
from io import TextIOWrapper, StringIO
from decimal import Decimal, InvalidOperation

from django.db import transaction
from django.db.models import F
from django.http import JsonResponse, HttpResponseNotAllowed
from django.contrib.auth.decorators import login_required, permission_required

from openpyxl import load_workbook

from accounts.models import User
from payments.models import ViralPeWallet, ViralPeWalletUsage
from payments.utils import generate_wallet_credit_order_id


def _norm(s: str) -> str:
    # trim -> lower -> replace spaces/dots/dashes with '_' -> keep a-z0-9_
    s = (s or "").strip().lower()
    s = s.replace(".", "_").replace(" ", "_").replace("-", "_")
    s = re.sub(r"[^a-z0-9_]+", "", s)
    return s


def _xlsx_rows(fileobj):
    wb = load_workbook(fileobj, data_only=True)
    ws = wb.active

    # read header (first non-empty row)
    headers = None
    for row in ws.iter_rows(values_only=True):
        if any(v is not None and str(v).strip() for v in row):
            headers = [str(v or "") for v in row]
            break
    if not headers:
        return []

    mapped = [_norm(h) for h in headers]

    out = []
    for r in ws.iter_rows(min_row=2, values_only=True):
        if all(v in (None, "") for v in r):
            continue
        vals = [("" if v is None else str(v).strip()) for v in r]
        d = {}
        for k, v in zip(mapped, vals):
            if k:
                d[k] = v
        out.append(d)
    return out


def _csv_rows(django_file):
    # decode with BOM-tolerant UTF-8 and fall back to replace
    wrapper = TextIOWrapper(getattr(django_file, "file", django_file), encoding="utf-8-sig", errors="replace")
    reader = csv.DictReader(wrapper)
    rows = []
    for row in reader:
        # normalize keys just like XLSX path
        rows.append({_norm(k): (v.strip() if isinstance(v, str) else v) for k, v in (row or {}).items()})
    return rows


def _process_rows(rows, request_user):
    successes, errors = 0, []
    for i, row in enumerate(rows, start=2):  # header is row 1
        mobile       = (row.get("mobile_number") or "").strip()
        amount_raw   = (row.get("amount") or "").strip()
        tx_type      = (row.get("transaction_type") or "credit").strip().lower()
        purpose_code = (row.get("purpose_code") or "manual_admin").strip()
        purpose_note = (row.get("purpose_note") or "").strip()
        order_id     = (row.get("order_id") or "").strip()
        try:
            if not mobile:
                raise ValueError("mobile_number missing")
            if tx_type not in ("credit", "debit"):
                raise ValueError("transaction_type invalid")

            try:
                amount = Decimal(amount_raw)
            except (InvalidOperation, TypeError):
                raise ValueError("amount invalid")
            if amount <= 0:
                raise ValueError("amount must be > 0")

            user = User.objects.get(mobile_number=mobile)
            PURPOSE_MAP = dict(ViralPeWalletUsage.PURPOSE_CHOICES)
            base = PURPOSE_MAP.get(purpose_code, "Manual Top-Up (Admin)")
            purpose_str = base #+ (f" | {purpose_note}" if purpose_note else "")

            with transaction.atomic():
                wallet, _ = ViralPeWallet.objects.select_for_update().get_or_create(user=user)
                sign = Decimal("1") if tx_type == "credit" else Decimal("-1")
                wallet.balance = F("balance") + (sign * amount)
                wallet.save()

                ViralPeWalletUsage.objects.create(
                    user=user,
                    amount_used=amount,
                    transaction_type=tx_type,
                    purpose=purpose_str,
                    purpose_code=purpose_code,
                    purpose_note=purpose_note,
                    order_id=order_id or generate_wallet_credit_order_id(),
                    reference_user=request_user,
                )
            successes += 1
        except Exception as e:
            errors.append({"row": i, "mobile": mobile, "error": str(e)})
    return successes, errors


@login_required
@permission_required("payments.add_viralpewalletusage", raise_exception=True)
def api_wallet_bulk_upload(request):
    if request.method != "POST":
        return HttpResponseNotAllowed(["POST"])

    f = request.FILES.get("file")
    if not f:
        return JsonResponse({"ok": False, "error": "File is required"}, status=400)

    name = (f.name or "").lower()
    if name.endswith((".xlsx", ".xlsm")):
        rows = _xlsx_rows(f)
    else:
        rows = _csv_rows(f)

    successes, errors = _process_rows(rows, request.user)
    return JsonResponse({"ok": True, "successes": successes, "errors": errors})



# admin/views.py
from django.contrib.auth.decorators import login_required, user_passes_test
from django.shortcuts import render

def is_admin(u):
    return u.is_staff or u.is_superuser

@login_required
@user_passes_test(is_admin)
def admin_users_list(request):
    return render(request, "users_list.html", {
        "pagetitle": "Users",
    })



# admin/views.py
from decimal import Decimal, ROUND_HALF_UP
import json
from datetime import datetime, date, timedelta

from django.contrib.auth.decorators import login_required, user_passes_test
from django.db.models import Sum, Count, Q
from django.http import JsonResponse, HttpResponseNotAllowed
from django.shortcuts import render
from django.utils.timezone import now, localdate

# ADJUST these imports to your actual app/module names
from accounts.models import User  # your CustomUser model
from payments.models import ViralPeWallet
# If RechargeTransaction lives elsewhere, change this import accordingly.
try:
    from recharge.models import RechargeTransaction  # must have a JSON-like response_data field
except Exception:
    RechargeTransaction = None


def is_admin(u):
    return u.is_staff or u.is_superuser


def fmt2(v) -> str:
    """Format any number-like/Decimal as 2-decimals string."""
    if v is None:
        v = Decimal('0')
    if not isinstance(v, Decimal):
        v = Decimal(str(v))
    return str(v.quantize(Decimal('0.00'), rounding=ROUND_HALF_UP))


def get_provider_balance() -> str:
    """
    Look through recent RechargeTransaction rows (latest first) to find a response_data
    containing {"bal": "..."} and return it rounded to 2 decimals.
    """
    if not RechargeTransaction:
        return fmt2(0)

    # Search a reasonable window (e.g., last 200 rows) for a 'bal' field
    qs = RechargeTransaction.objects.order_by('-id').values('response_data')[:200]
    for row in qs:
        data = row.get('response_data')
        try:
            if isinstance(data, str):
                data = json.loads(data)
            if isinstance(data, dict) and 'bal' in data:
                return fmt2(data.get('bal'))
        except Exception:
            continue
    return fmt2(0)


def get_wallets_summary():
    total_viralpe = ViralPeWallet.objects.aggregate(s=Sum('balance'))['s'] or Decimal('0')
    return {
        "viralpe_total": fmt2(total_viralpe),
        "oneapp_total": "0.00",  # placeholder (n/a) – to be wired to external API
    }


def get_user_counts():
    today = localdate()
    total = User.objects.count()
    today_regs = User.objects.filter(date_joined__date=today).count()
    active = User.objects.filter(is_active=True).count()
    inactive = User.objects.filter(is_active=False).count()
    return {
        "total": total,
        "today": today_regs,
        "active": active,
        "inactive": inactive,
    }


# ----- Dummy providers (replace internals later; the view/HTML already support clicking to details) -----
def get_today_transactions_summary(_range='today'):
    # Return dummy counts grouped
    return {
        "success": {
            "Mobile": 12,
            "Postpaid": 3,
            "Electricity": 4,
        },
        "failed": 2,
        "from_wallet": 11,
        "from_upi_netbanking": 8,
        "range": _range,
    }


def get_ledger_summary(_range='today'):
    return {
        "total_amount": fmt2(123456.78),
        "wallet_amount": fmt2(65432.10),
        "razorpay_amount": fmt2(58024.68),
        "distributed": {
            "cashback": fmt2(1200),
            "referral": fmt2(800),
            "pincode_share": fmt2(600),
            "district_share": fmt2(300),
        },
        "commission": {
            "api1": fmt2(2300.55),
            "api2": fmt2(1780.25),
        },
        "range": _range,
    }


def get_pincode_summary(_range='today'):
    return {
        "total_pincodes": 450,
        "allocated": 220,
        "active": 198,
        "range": _range,
    }


@login_required
@user_passes_test(is_admin)
def admin_dashboard(request):
    ctx = {}

    # 1) Provider balance (from latest RechargeTransaction with 'bal' in response_data)
    ctx['provider_balance'] = get_provider_balance()

    # 2) Wallets
    ctx['wallets'] = get_wallets_summary()

    # 3) Users
    ctx['users'] = get_user_counts()

    # 4) Today Transactions (placeholder)
    ctx['tx'] = get_today_transactions_summary('today')

    # 5) Ledger (placeholder)
    ctx['ledger'] = get_ledger_summary('today')

    # 6) Pincodes (placeholder)
    ctx['pincodes'] = get_pincode_summary('today')

    ctx['pagetitle'] = "Admin Dashboard"
    return render(request, "vadmin/dashboard.html", ctx)


@login_required
@user_passes_test(is_admin)
def admin_dashboard_today_users(request):
    """
    Returns a JSON suitable for DataTables: today regs grouped by (state, district, city) + counts.
    Later we can accept ?range=today|yesterday|7d|30d|90d|custom&start=&end=
    """
    if request.method != 'GET':
        return HttpResponseNotAllowed(['GET'])

    # Default range: today
    range_key = (request.GET.get('range') or 'today').lower()
    start = end = None
    today = localdate()

    if range_key == 'today':
        start, end = today, today
    elif range_key == 'yesterday':
        y = today - timedelta(days=1)
        start, end = y, y
    elif range_key in ('7d', '7days'):
        start, end = today - timedelta(days=6), today
    elif range_key in ('30d', 'month'):
        start, end = today - timedelta(days=29), today
    elif range_key in ('90d',):
        start, end = today - timedelta(days=89), today
    else:
        # custom dates e.g., ?start=YYYY-MM-DD&end=YYYY-MM-DD
        try:
            start = date.fromisoformat(request.GET.get('start'))
            end = date.fromisoformat(request.GET.get('end'))
        except Exception:
            start, end = today, today

    qs = User.objects.filter(date_joined__date__gte=start, date_joined__date__lte=end)

    # Group by (state, district, city) – they’re strings on User in your setup
    agg = qs.values('state', 'district', 'city').annotate(
        total=Count('id'),
        active=Count('id', filter=Q(is_active=True)),
        inactive=Count('id', filter=Q(is_active=False)),
    ).order_by('state', 'district', 'city')

    rows = []
    for r in agg:
        rows.append({
            "state": r.get('state') or '',
            "district": r.get('district') or '',
            "city": r.get('city') or '',
            "total": r.get('total') or 0,
            "active": r.get('active') or 0,
            "inactive": r.get('inactive') or 0,
        })

    return JsonResponse({
        "range": range_key,
        "start": str(start),
        "end": str(end),
        "data": rows,
        "summary": {
            "total": sum(x['total'] for x in rows),
            "active": sum(x['active'] for x in rows),
            "inactive": sum(x['inactive'] for x in rows),
        }
    })


# vadmin/views.py
from decimal import Decimal, ROUND_HALF_UP
from datetime import date, timedelta
from django.db.models import OuterRef, Subquery, Value, F, Q
from django.db.models.functions import Coalesce, Concat
from django.shortcuts import render
from django.utils.timezone import localdate
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.pagination import PageNumberPagination

# Models (adjust import paths if different)
from payments.models import PaymentTransaction, RechargePaymentSummary
from recharge.models import RechargeTransaction

# Helpers
def q2(x) -> Decimal:
    try:
        return (Decimal(str(x or "0"))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    except Exception:
        return Decimal("0.00")

def fmt2(x) -> str:
    return format(q2(x), ".2f")

def _todayrange():
    t = localdate()
    return t, t

def _normalize_bool(s):
    if s is None: return None
    s = str(s).strip().lower()
    if s in {"1","true","yes"}: return True
    if s in {"0","false","no"}: return False
    return None

class _Pager(PageNumberPagination):
    page_size = 25
    page_size_query_param = "page_size"
    max_page_size = 200

def recharge_failures_page(request):
    return render(request, "vadmin/recharge_failures.html", {"pagetitle": "Failure Analysis / Refunds"})


class AdminFailureListAPI(APIView):
    """
    GET /api/recharge/admin/failures/?date_from=YYYY-MM-DD&date_to=YYYY-MM-DD
                                   &status=any|failed|error|mixed|successall
                                   &mobile=...&order_id=...
    Returns paginated rows for DataTables:
      - order_id, user_mobile, amount, pay_status, rchg_status, summ_status, is_refunded, updated_at
      - classification: success_all / failed_any / error_any / mixed
      - counters in 'meta'
    """
    permission_classes = [IsAuthenticated]
    from django.db.models import Q

    def get(self, request):
        p = request.query_params
        # Range (defaults: today)
        df = p.get("date_from")
        dt = p.get("date_to")
        if not (df and dt):
            t0, t1 = _todayrange()
            df, dt = str(t0), str(t1)

        # Base filter per-table (date on their updated/created fields)
        # summ_fail = RechargePaymentSummary.objects.filter(
        #     updated_at__date__gte=df, updated_at__date__lte=dt,
        #     recharge_status__iregex=r"^(failed?|error)$"
        # ).values_list("order_id", flat=True)

        # pay_fail  = PaymentTransaction.objects.filter(
        #     updated_at__date__gte=df, updated_at__date__lte=dt,
        #     status__iregex=r"^(failed?|error)$"
        # ).values_list("order_id", flat=True)

        # rchg_fail = RechargeTransaction.objects.filter(
        #     status_updated_at__date__gte=df, status_updated_at__date__lte=dt,
        #     status__iregex=r"^(failed?|error)$"
        # ).values_list("order_id", flat=True)

        # ---- AFTER (portable across MySQL/MariaDB)
        status_fail_q = (Q(status__iexact='fail') |
                        Q(status__iexact='failed') |
                        Q(status__iexact='failure') |
                        Q(status__iexact='error'))

        summ_fail = RechargePaymentSummary.objects.filter(
            updated_at__date__gte=df, updated_at__date__lte=dt
        ).filter(
            Q(recharge_status__iexact='fail') |
            Q(recharge_status__iexact='failed') |
            Q(recharge_status__iexact='failure') |
            Q(recharge_status__iexact='error')
        ).values_list("order_id", flat=True)

        pay_fail = PaymentTransaction.objects.filter(
            updated_at__date__gte=df, updated_at__date__lte=dt
        ).filter(status_fail_q).values_list("order_id", flat=True)

        rchg_fail = RechargeTransaction.objects.filter(
            status_updated_at__date__gte=df, status_updated_at__date__lte=dt
        ).filter(status_fail_q).values_list("order_id", flat=True)


        # Unique order_id universe for the grid (problem space = union of any failure/error)
        ids = set(summ_fail) | set(pay_fail) | set(rchg_fail)

        # Optional filters to narrow:
        if p.get("order_id"):
            oid = p.get("order_id")
            ids = {i for i in ids if oid.lower() in str(i).lower()}
        if p.get("mobile"):
            # We'll filter later when we annotate user_mobile; keep all ids for now
            pass

        if not ids:
            return Response({"count": 0, "results": [], "meta": {
                "date_from": df, "date_to": dt,
                "counters": {"success_all": 0, "failed_any": 0, "error_any": 0, "mixed": 0}
            }})

        # Build an annotated queryset from SUMMARY as the anchor (includes user + amounts).
        # If a summary is missing, fall back to PaymentTransaction anchor.
        base_qs = RechargePaymentSummary.objects.filter(order_id__in=ids)

        # Subqueries to peek into other tables:
        pay_qs  = PaymentTransaction.objects.filter(order_id=OuterRef("order_id"))
        rchg_qs = RechargeTransaction.objects.filter(order_id=OuterRef("order_id"))

        q = base_qs.annotate(
            user_mobile=F("user__mobile_number"),
            pay_status=Subquery(pay_qs.values("status")[:1]),
            pay_amount=Subquery(pay_qs.values("amount")[:1]),
            rchg_status=Subquery(rchg_qs.values("status")[:1]),
            rchg_amount=Subquery(rchg_qs.values("amount")[:1]),
            updated_any=Coalesce(F("updated_at"), Subquery(rchg_qs.values("status_updated_at")[:1])),
        ).values(
            "order_id", "user_mobile",
            "recharge_amount", "recharge_status", "is_refunded",
            "pay_status", "pay_amount",
            "rchg_status", "rchg_amount",
            "updated_any",
        )

        # Fallback rows with no summary (rare): pull from PaymentTransaction only
        missing_in_summary = [oid for oid in ids if oid not in {row["order_id"] for row in q}]
        if missing_in_summary:
            pay_only = PaymentTransaction.objects.filter(order_id__in=missing_in_summary).values(
                "order_id", user_mobile=F("user__mobile_number"),
                recharge_amount=Value(None), recharge_status=Value(None), is_refunded=Value(False),
                pay_status=F("status"), pay_amount=F("amount"),
                rchg_status=Value(None), rchg_amount=Value(None),
                updated_any=F("updated_at"),
            )
            # glue them in memory
            q = list(q) + list(pay_only)
        else:
            q = list(q)

        # Optional mobile filter
        mobile = p.get("mobile")
        if mobile:
            q = [row for row in q if str(row.get("user_mobile") or "").strip() == str(mobile).strip()]

        # Classify rows
        def norm(v): return (v or "").strip().lower()
        def classify(row):
            s1, s2, s3 = norm(row.get("recharge_status")), norm(row.get("pay_status")), norm(row.get("rchg_status"))
            statuses = {s for s in (s1, s2, s3) if s}
            if statuses == {"success"}:
                return "success_all"
            if "failed" in statuses or "failure" in statuses:
                return "failed_any"
            if "error" in statuses:
                # if both failed+error present, still treat as error_any bucket? keep error_any separate:
                return "error_any"
            # mixed non-terminal / odd combos
            return "mixed"

        for row in q:
            row["classification"] = classify(row)
            # compact money
            row["amount"] = fmt2(row.get("recharge_amount") or row.get("pay_amount") or row.get("rchg_amount") or 0)
            # prettify statuses
            row["pay_status"]   = (row["pay_status"] or "").title()
            row["rchg_status"]  = (row["rchg_status"] or "").title()
            row["summ_status"]  = (row["recharge_status"] or "").title()
            row["updated_at"]   = row.pop("updated_any", None)

        # Filter by classification if requested
        want = (p.get("status") or "").strip().lower()
        if want in {"any", "failed", "error", "mixed", "successall"}:
            mapping = {
                "successall": "success_all",
                "failed": "failed_any",
                "error": "error_any",
                "mixed": "mixed",
            }
            key = mapping.get(want)
            if key:
                q = [r for r in q if r["classification"] == key]

        # Counters (from the full universe, not only current page)
        counters = {"success_all": 0, "failed_any": 0, "error_any": 0, "mixed": 0}
        for r in q:
            counters[r["classification"]] += 1

        # Manual pagination compatible with DataTables (start/length)
        start = int(p.get("start", 0))
        length = int(p.get("length", 25))
        page_rows = q[start:start+length]

        return Response({
            "count": len(q),
            "results": page_rows,
            "meta": {
                "date_from": df, "date_to": dt,
                "counters": counters
            }
        })



# vadmin/views.py
from io import BytesIO
from datetime import datetime
from decimal import Decimal
import json

from django.http import HttpResponse
from django.contrib.auth.decorators import login_required, user_passes_test
from django.utils.timezone import localtime

from openpyxl import Workbook
from openpyxl.utils import get_column_letter
from openpyxl.styles import Font

# import your models (adjust paths as needed)
from payments.models import PaymentTransaction, RechargePaymentSummary, ViralPeWalletUsage
from recharge.models import RechargeTransaction

def _is_admin(u):
    return u.is_staff or u.is_superuser


from django.db import models
import uuid
from datetime import datetime
from decimal import Decimal
import json

def _coerce_cell(v):
    """Convert any field value to something Excel can store."""
    if v is None:
        return ""
    if isinstance(v, Decimal):
        try:
            return float(v)
        except Exception:
            return str(v)
    if isinstance(v, (datetime, )):
        from django.utils.timezone import localtime
        return localtime(v).strftime("%Y-%m-%d %H:%M:%S")
    if isinstance(v, (dict, list, tuple)):
        try:
            return json.dumps(v, ensure_ascii=False)
        except Exception:
            return str(v)
    if isinstance(v, (uuid.UUID, )):
        return str(v)
    if isinstance(v, (bytes, bytearray)):
        try:
            return v.decode("utf-8", "replace")
        except Exception:
            return str(v)
    # If somehow a model instance slips through, stringify it
    if isinstance(v, models.Model):
        return str(v)
    return v


def _export_fields(model):
    """
    Build a list of columns to export:
    - For FK: use '<name>_id' (database column) and an extra '<name>_str' for readability.
    - For normal concrete fields: use field.name.
    """
    cols = []
    fk_pairs = []  # (id_attr, str_label, accessor_name)
    for f in model._meta.get_fields():
        # skip M2M and reverse relations
        if not getattr(f, "concrete", False) or getattr(f, "many_to_many", False) or f.auto_created:
            continue

        if isinstance(f, models.ForeignKey):
            # Use the DB column (e.g. 'user_id') and add a human string column
            cols.append(f.attname)  # '<name>_id'
            # later we'll also add '<name>_str' using getattr(obj, f.name)
            fk_pairs.append((f.attname, f"{f.name}_str", f.name))
        else:
            cols.append(f.name)

    # insert the human-readable columns right after each FK id column
    final_cols = []
    for c in cols:
        final_cols.append(c)
        # see if this c matches any fk id; if so, append its _str column after
        for (id_attr, str_label, accessor_name) in fk_pairs:
            if c == id_attr:
                final_cols.append(str_label)
                break
    return final_cols, fk_pairs


def _write_qs_to_sheet(ws, qs, field_names=None, header_title=None):
    """
    Write queryset to sheet. If field_names is None, we auto-build with FK handling.
    """
    model = qs.model
    if field_names is None:
        field_names, fk_pairs = _export_fields(model)
    else:
        fk_pairs = []

    # Header title (optional)
    if header_title:
        ws.append([header_title])
        ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(field_names))
        hcell = ws.cell(row=1, column=1)
        from openpyxl.styles import Font
        hcell.font = Font(bold=True, size=12)
        ws.append([])

    # Column headers
    header_row_idx = ws.max_row + 1
    ws.append(field_names)
    from openpyxl.styles import Font
    for c in range(1, len(field_names) + 1):
        ws.cell(row=header_row_idx, column=c).font = Font(bold=True)

    # Rows
    for obj in qs.iterator(chunk_size=2000):
        row = []
        for col in field_names:
            # If this is an FK readable column like '<name>_str', compute from accessor
            if col.endswith("_str"):
                base = col[:-4]  # remove _str
                # find the accessor for this base from fk_pairs
                accessor = None
                for (id_attr, str_label, accessor_name) in fk_pairs:
                    if str_label == col:
                        accessor = accessor_name
                        break
                val = getattr(obj, accessor, None)
                row.append(_coerce_cell(val))
            else:
                # Normal attribute (including FK id via attname)
                val = getattr(obj, col, None)
                row.append(_coerce_cell(val))
        ws.append(row)

    _auto_columns(ws)



def _auto_columns(ws):
    """Auto-size columns based on max length in each column."""
    for column_cells in ws.columns:
        length = 0
        col = column_cells[0].column if hasattr(column_cells[0], 'column') else column_cells[0].column_letter
        for cell in column_cells:
            try:
                val = str(cell.value) if cell.value is not None else ""
            except Exception:
                val = ""
            length = max(length, len(val))
        ws.column_dimensions[get_column_letter(col)].width = min(max(10, length + 2), 60)


# def _write_qs_to_sheet(ws, qs, field_names=None, header_title=None):
#     """Write a queryset to a sheet with headers. If field_names is None, dumps all concrete fields."""
#     if field_names is None:
#         # all concrete fields (no m2m)
#         field_names = [f.name for f in qs.model._meta.get_fields() if getattr(f, 'concrete', False) and not f.many_to_many]

#     # Header
#     header_row = [header_title] if header_title else None
#     if header_row:
#         ws.append([header_title])
#         ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(field_names))
#         hcell = ws.cell(row=1, column=1)
#         hcell.font = Font(bold=True, size=12)
#         ws.append([])  # empty spacer row

#     header_idx = ws.max_row + 1
#     ws.append(field_names)
#     for c in range(1, len(field_names) + 1):
#         ws.cell(row=header_idx, column=c).font = Font(bold=True)

#     # Rows (iterate without loading entire table in memory)
#     for obj in qs.iterator(chunk_size=2000):
#         row = []
#         for f in field_names:
#             v = getattr(obj, f, None)
#             row.append(_coerce_cell(v))
#         ws.append(row)

#     _auto_columns(ws)


@login_required
@user_passes_test(_is_admin)
def export_all_data_xlsx(request):
    """
    GET /admin/api/export/all-data.xlsx
    -> returns a workbook with 4 sheets:
       - RechargePaymentSummary
       - PaymentTransaction
       - RechargeTransaction
       - ViralPeWalletUsage
    """
    wb = Workbook()
    # The first sheet openpyxl creates by default – reuse for the first model.
    ws1 = wb.active
    ws1.title = "RechargePaymentSummary"

    # ---- 1) RechargePaymentSummary ----
    rps_qs = RechargePaymentSummary.objects.all().order_by("id")
    _write_qs_to_sheet(
        ws1, rps_qs,
        header_title="RechargePaymentSummary (all rows)"
    )

    # ---- 2) PaymentTransaction ----
    ws2 = wb.create_sheet(title="PaymentTransaction")
    pt_qs = PaymentTransaction.objects.all().order_by("id")
    _write_qs_to_sheet(
        ws2, pt_qs,
        header_title="PaymentTransaction (all rows)"
    )

    # ---- 3) RechargeTransaction ----
    ws3 = wb.create_sheet(title="RechargeTransaction")
    rt_qs = RechargeTransaction.objects.all().order_by("id")
    _write_qs_to_sheet(
        ws3, rt_qs,
        header_title="RechargeTransaction (all rows)"
    )

    # ---- 4) ViralPeWalletUsage ----
    ws4 = wb.create_sheet(title="ViralPeWalletUsage")
    wu_qs = ViralPeWalletUsage.objects.all().order_by("id")
    _write_qs_to_sheet(
        ws4, wu_qs,
        header_title="ViralPeWalletUsage (all rows)"
    )

    # Serialize workbook to memory and return
    bio = BytesIO()
    wb.save(bio)
    bio.seek(0)

    now_str = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"export_all_{now_str}.xlsx"

    resp = HttpResponse(
        bio.getvalue(),
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    resp["Content-Disposition"] = f'attachment; filename="{filename}"'
    return resp




# ---------------- Page ----------------

@staff_member_required
def recharge_snapshot_page(request):
    return render(request, "recharge_snapshot.html", {"pagetitle": "Recharge Snapshot & Refund"})

# ---- V2 Page ----
from django.views.decorators.csrf import ensure_csrf_cookie
@staff_member_required
@ensure_csrf_cookie
def recharge_snapshot_v2_page(request):
    return render(request, "recharge_snapshot_v2.html", {"pagetitle": "Recharge Snapshot (V2)"})


# vadmin/views.py
from io import BytesIO
from decimal import Decimal
from django.http import HttpResponse
from rest_framework.views import APIView
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework import status as http_status

from openpyxl import Workbook
from openpyxl.utils import get_column_letter

from payments.models import PaymentTransaction, RechargePaymentSummary
from recharge.models import RechargeTransaction


def _fmt2(x):
    if x is None:
        return ""
    try:
        if isinstance(x, Decimal):
            return f"{x:.2f}"
        return f"{Decimal(str(x)):.2f}"
    except Exception:
        return str(x)


class ExportOrdersCombinedXLSXAPI(APIView):
    """
    GET /admin/api/export/orders.xlsx
      (optional) ?date_from=YYYY-MM-DD&date_to=YYYY-MM-DD
        - filters by Summary.updated_at OR RechargeTransaction.status_updated_at

    One row per order_id found in either RechargePaymentSummary or RechargeTransaction,
    with the two statuses + attached PaymentTransaction fields (if any).
    """
    permission_classes = [IsAdminUser]

    def get(self, request):
        # ---- optional filters
        df = (request.query_params.get("date_from") or "").strip()
        dt = (request.query_params.get("date_to") or "").strip()
        date_filter = (df, dt) if (df and dt) else None

        # ---- gather order ids from the two sources
        summ_qs = RechargePaymentSummary.objects.all()
        rtx_qs  = RechargeTransaction.objects.all()

        if date_filter:
            df_, dt_ = date_filter
            summ_qs = summ_qs.filter(updated_at__date__gte=df_, updated_at__date__lte=dt_)
            rtx_qs  = rtx_qs.filter(status_updated_at__date__gte=df_, status_updated_at__date__lte=dt_)

        # pull minimal fields
        summ_vals = summ_qs.values_list("order_id", "recharge_status", "is_refunded")
        rtx_vals  = rtx_qs.values_list("order_id", "status")

        # Build maps
        order_ids = set()
        summary_status_by_oid = {}
        summary_refunded_by_oid = {}
        recharge_status_by_oid = {}

        for oid, s, is_ref in summ_vals:
            if oid:
                order_ids.add(oid)
                summary_status_by_oid[oid] = s
                summary_refunded_by_oid[oid] = bool(is_ref)

        for oid, s in rtx_vals:
            if oid:
                order_ids.add(oid)
                recharge_status_by_oid[oid] = s

        if not order_ids:
            return Response({"ok": True, "rows": 0, "message": "No orders found for given filters."},
                            status=http_status.HTTP_200_OK)

        # ---- fetch payment transactions in bulk
        pay_map = {
            p.order_id: p
            for p in PaymentTransaction.objects.filter(order_id__in=order_ids)
        }

        # ---- build workbook
        wb = Workbook()
        ws = wb.active
        ws.title = "Orders"

        HEADER = [
            "order_id",
            "summary.recharge_status",
            "summary.is_refunded",
            "recharge.status",

            # payment fields (if present)
            "payment.amount",
            "payment.wallet_amount",
            "payment.gateway_amount",
            "payment.razorpay_order_id",
            "payment.razorpay_payment_id",
            "payment.razorpay_signature",
            "payment.status",
            "payment.wallet_status",
            "payment.user_id",
            "payment.updated_at",
            "payment.created_at",
        ]
        ws.append(HEADER)

        # rows
        for oid in sorted(order_ids):
            pay = pay_map.get(oid)
            row = [
                oid,
                (summary_status_by_oid.get(oid) or ""),
                "YES" if summary_refunded_by_oid.get(oid) else "NO",
                (recharge_status_by_oid.get(oid) or ""),
            ]

            if pay:
                row.extend([
                    _fmt2(pay.amount),
                    _fmt2(pay.wallet_amount),
                    _fmt2(pay.gateway_amount),
                    pay.razorpay_order_id or "",
                    pay.razorpay_payment_id or "",
                    pay.razorpay_signature or "",
                    pay.status or "",
                    pay.wallet_status or "",
                    getattr(pay, "user_id", "") or "",
                    (pay.updated_at.strftime("%Y-%m-%d %H:%M:%S") if pay.updated_at else ""),
                    (pay.created_at.strftime("%Y-%m-%d %H:%M:%S") if pay.created_at else ""),
                ])
            else:
                # blanks for payment columns (total cols - 4 non-payment cols)
                row.extend([""] * (len(HEADER) - 4))

            ws.append(row)

        # autosize
        for col_idx in range(1, len(HEADER) + 1):
            ws.column_dimensions[get_column_letter(col_idx)].width = 22

        # response
        bio = BytesIO()
        wb.save(bio)
        bio.seek(0)

        fname = "orders_combined.xlsx"
        if date_filter:
            fname = f"orders_combined_{df}_to_{dt}.xlsx"

        resp = HttpResponse(
            bio.getvalue(),
            content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        )
        resp["Content-Disposition"] = f'attachment; filename="{fname}"'
        return resp
