# accounts/admin_views.py
from decimal import Decimal
from typing import Any, Dict, List, Tuple
from django.db import transaction, models
from django.utils import timezone
from datetime import datetime, timezone
from django.utils.timezone import localtime
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAdminUser
from rest_framework import status as http_status

from payments.models import (
    PaymentTransaction,
    RechargePaymentSummary,
    ViralPeWallet,
    ViralPeWalletUsage,
)
from recharge.models import RechargeTransaction
from recharge.services.goterpay_client import get_goterpay_client

# ------------- helpers -----------------

def _as_bool(v) -> bool:
    return str(v).strip().lower() in {"1", "true", "yes", "y", "on"}

def _safe(obj, name, default=None):
    return getattr(obj, name, default) if obj is not None else default

def _dt(v):
    # render datetimes as local iso strings
    if not v:
        return None
    try:
        return localtime(v).strftime("%Y-%m-%d %H:%M:%S")
    except Exception:
        return str(v)

def _val_to_jsonable(v):
    """Make any field Excel/JSON friendly (keeps precision for decimals)."""
    if v is None:
        return None
    if isinstance(v, Decimal):
        # string keeps exact 2dp; if you want numeric, cast to float
        return f"{v:.2f}"
    if isinstance(v, (dict, list, tuple)):
        return v
    if isinstance(v, (timezone.datetime, )):
        return _dt(v)
    return v

def _model_to_dict_all(instance) -> Dict[str, Any]:
    """
    Dump ALL concrete fields from a model instance.
    - For FK: include both '<name>_id' and '<name>_str'
    - Datetimes stringified in local time
    - Decimals stringified with 2dp
    """
    if instance is None:
        return None
    data: Dict[str, Any] = {}
    model = instance.__class__
    for f in model._meta.get_fields():
        if not getattr(f, "concrete", False) or getattr(f, "many_to_many", False) or f.auto_created:
            continue

        if isinstance(f, models.ForeignKey):
            data[f.attname] = _val_to_jsonable(getattr(instance, f.attname, None))  # <name>_id
            try:
                data[f"{f.name}_str"] = str(getattr(instance, f.name)) if getattr(instance, f.name, None) else None
            except Exception:
                data[f"{f.name}_str"] = None
        else:
            val = getattr(instance, f.name, None)
            if isinstance(f, (models.DateTimeField,)):
                data[f.name] = _dt(val)
            else:
                data[f.name] = _val_to_jsonable(val)
    return data

def _qs_to_list_all(qs) -> List[Dict[str, Any]]:
    return [_model_to_dict_all(obj) for obj in qs]

def _get_by_col_lax(model, col: str, value: str, for_update: bool = False):
    """
    Safer single-row fetch that tolerates collation issues.
    """
    qs = model.objects.all()
    if for_update:
        qs = qs.select_for_update()
    try:
        return qs.get(**{col: value})
    except model.DoesNotExist:
        return None

# status precedence (optional, for UI usage)
STATUS_RANK = {"Pending": 1, "Error": 2, "Failure": 3, "Success": 4}

# ------------- SNAPSHOT -----------------

class AdminRechargeSnapshotAPI(APIView):
    """
    GET /api/recharge/admin/snapshot/?order_id=... | ?transaction_id=...
        [&verify_provider=1]
    Returns a full snapshot in this SEQUENCE:
      1) payment_transaction
      2) summary
      3) recharge_transaction
      4) wallet_usages (all with same order_id)
      5) wallet (of the owning user)
      6) provider_status (optional)
    """
    permission_classes = [IsAdminUser]

    def get(self, request):
        p = request.query_params
        order_id = (p.get("order_id") or "").strip()
        txnid    = (p.get("transaction_id") or "").strip()
        verify   = _as_bool(p.get("verify_provider"))

        # Resolve order_id if needed (try payment, summary, then recharge)
        pay = None
        if order_id:
            pay = PaymentTransaction.objects.filter(order_id=order_id).first()
        elif txnid:
            # find via RechargeTransaction.client_txn_id
            rtx = RechargeTransaction.objects.filter(client_txn_id=txnid).first()
            if rtx:
                order_id = rtx.order_id
                pay = PaymentTransaction.objects.filter(order_id=order_id).first()

        # If still not found, fallback to summary or recharge by order_id provided
        if not pay and order_id:
            pay = PaymentTransaction.objects.filter(order_id=order_id).first()

        if not (order_id or pay):
            return Response({"error": "order_id or transaction_id is required"}, status=http_status.HTTP_400_BAD_REQUEST)

        # Fetch all related rows
        summ = RechargePaymentSummary.objects.filter(order_id=order_id).first() if order_id else None

        rtx  = None
        if order_id:
            rtx = RechargeTransaction.objects.filter(order_id=order_id).first()
        if not rtx and txnid:
            rtx = RechargeTransaction.objects.filter(client_txn_id=txnid).first()

        # Wallet usages (all rows for this order)
        usages_qs = ViralPeWalletUsage.objects.none()
        if order_id:
            usages_qs = ViralPeWalletUsage.objects.filter(order_id=order_id).order_by("used_on")

        # Wallet (user from payment, else summary, else recharge)
        the_user = _safe(pay, "user") or _safe(summ, "user") or _safe(rtx, "user")
        wallet = ViralPeWallet.objects.filter(user=the_user).first() if the_user else None

        # Optional provider status check
        provider_status = None
        if verify and rtx and rtx.client_txn_id:
            try:
                provider_status = get_goterpay_client().status(rtx.client_txn_id)
            except Exception as e:
                provider_status = {"error": str(e)}

        # Build response in the sequence you asked for
        resp = {
            "order_id": order_id or None,
            "transaction_id": txnid or None,
            "payment_transaction": _model_to_dict_all(pay),              # 1
            "summary":              _model_to_dict_all(summ),             # 2
            "recharge_transaction": _model_to_dict_all(rtx),              # 3
            "wallet_usages":        _qs_to_list_all(usages_qs),           # 4
            "wallet":               _model_to_dict_all(wallet),           # 5
            "provider_status":      provider_status,                      # 6
        }
        return Response(resp, status=http_status.HTTP_200_OK)

# ------------- REFUND -----------------

from decimal import Decimal
from django.db import transaction, models
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAdminUser
from rest_framework import status as http_status

from payments.models import PaymentTransaction, RechargePaymentSummary
from recharge.models import RechargeTransaction
from accounts.views_refund import process_recharge_refund, process_recharge_refund_new  # your idempotent routine

# helpers reused
def _as_bool(v) -> bool:
    if isinstance(v, bool):
        return v
    return str(v).strip().lower() in {"1", "true", "yes", "y", "on"}

def _model_to_dict_all(instance):
    if instance is None: return None
    data = {}
    model = instance.__class__
    for f in model._meta.get_fields():
        if not getattr(f, "concrete", False) or getattr(f, "many_to_many", False) or f.auto_created:
            continue
        if isinstance(f, models.ForeignKey):
            data[f.attname] = getattr(instance, f.attname, None)                 # <name>_id
            v = getattr(instance, f.name, None)
            data[f"{f.name}_str"] = str(v) if v else None
        else:
            val = getattr(instance, f.name, None)
            if isinstance(f, models.DateTimeField) and val is not None:
                from django.utils.timezone import localtime
                data[f.name] = localtime(val).strftime("%Y-%m-%d %H:%M:%S")
            elif isinstance(val, Decimal):
                data[f.name] = f"{val:.2f}"
            else:
                data[f.name] = val
    return data

def _get_by_col_lax(model, col: str, value: str, for_update: bool = False):
    qs = model.objects.all()
    if for_update:
        qs = qs.select_for_update()
    try:
        return qs.get(**{col: value})
    except model.DoesNotExist:
        return None


class AdminRechargeRefundAPI(APIView):
    """
    POST /api/recharge/admin/refund/
    JSON:
    {
      "order_id": "VPXXXX",           // OR "transaction_id": "clientTxnId"
      "reason": "Manual refund",      // optional
      "dry_run": true                 // optional (default: false)
    }

    - If dry_run=true: returns computed split without applying the refund.
    - Else: runs your idempotent refund routine and returns the result.
    """
    permission_classes = [IsAdminUser]

    @transaction.atomic
    def post(self, request):
        p = request.data or {}
        order_id = (p.get("order_id") or "").strip()
        txnid    = (p.get("transaction_id") or "").strip()
        reason   = (p.get("reason") or "").strip() or "Manual refund"
        dry_run  = _as_bool(p.get("dry_run", False))

        # Resolve order_id from transaction_id if necessary
        if not order_id and txnid:
            tx_lookup = _get_by_col_lax(RechargeTransaction, "client_txn_id", txnid, for_update=False)
            if tx_lookup:
                order_id = tx_lookup.order_id

        if not order_id:
            return Response({"error": "order_id (or transaction_id) is required"},
                            status=http_status.HTTP_400_BAD_REQUEST)

        # Load rows (lock summary for consistency during refund)
        tx   = _get_by_col_lax(RechargeTransaction,    "order_id", order_id, for_update=False)
        pay  = _get_by_col_lax(PaymentTransaction,     "order_id", order_id, for_update=False)
        summ = _get_by_col_lax(RechargePaymentSummary, "order_id", order_id, for_update=True)

        if not (summ or tx or pay):
            return Response({"error": "order not found"}, status=http_status.HTTP_404_NOT_FOUND)

        if tx and (tx.status or "").lower() == "success":
            return Response({"error": "Cannot refund a successful recharge"},
                            status=http_status.HTTP_409_CONFLICT)

        if not summ:
            return Response({"error": "RechargePaymentSummary missing; cannot compute safe refund"},
                            status=http_status.HTTP_409_CONFLICT)

        # Totals
        ext = Decimal(str(summ.used_external_wallet or 0))
        vp  = Decimal(str(summ.used_internal_wallet or 0))
        pg  = Decimal(str(summ.paid_via_gateway    or 0))
        total = ext + vp + pg

        if dry_run:
            return Response({
                "ok": True,
                "dry_run": True,
                "order_id": order_id,
                "breakdown": {
                    "external": f"{ext:.2f}",
                    "internal": f"{vp:.2f}",
                    "gateway":  f"{pg:.2f}",
                },
                "would_refund": f"{total:.2f}",
                "payment_transaction": _model_to_dict_all(pay),
                "summary": _model_to_dict_all(summ),
                "recharge_transaction": _model_to_dict_all(tx),
            }, status=http_status.HTTP_200_OK)

        # Execute idempotent refund
        result = process_recharge_refund(summ)  # implement inside accounts.views_refund

        # Reason stamping (first time)
        if not getattr(summ, "refund_reason", None):
            summ.refund_reason = reason
            summ.save(update_fields=["refund_reason"])

        # If not success, mark as Failure and append reason
        if tx and (tx.status or "").lower() != "success":
            msg = (tx.status_message or "")
            if reason and reason not in msg:
                tx.status_message = (msg + f" | Manual refund: {reason}").strip(" |")
            tx.status = "Failure"
            tx.save(update_fields=["status", "status_message"])

        return Response({
            "ok": True,
            "order_id": order_id,
            "refunded_total": f"{total:.2f}",
            "result": result,
            # "payment_transaction": _model_to_dict_all(pay),
            # "summary": _model_to_dict_all(summ),
            # "recharge_transaction": _model_to_dict_all(tx),
        }, status=http_status.HTTP_200_OK)





# recharge/admin_views_v2.py
from decimal import Decimal
from typing import Any, Dict, List, Optional
from django.db import transaction, models
from django.shortcuts import render
from django.utils import timezone
from django.utils.timezone import localtime
from django.contrib.admin.views.decorators import staff_member_required
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAdminUser
from rest_framework import status as http_status
from django.db.models import Subquery,Q

from payments.models import PaymentTransaction, RechargePaymentSummary, ViralPeWallet, ViralPeWalletUsage
from recharge.models import RechargeTransaction
from recharge.services.goterpay_client import get_goterpay_client
from accounts.views_refund import process_recharge_refund  # your existing idempotent refund

# ---- helpers (mirrored from legacy, with tiny additions) ----
def _as_bool(v) -> bool:
    return str(v).strip().lower() in {"1","true","yes","y","on"}

def _dt(v):
    if not v: return None
    try: return localtime(v).strftime("%Y-%m-%d %H:%M:%S")
    except: return str(v)

def _to_jsonable(v):
    if v is None: return None
    if isinstance(v, Decimal): return f"{v:.2f}"
    if isinstance(v, timezone.datetime): return _dt(v)
    return v

def _model_to_dict_all(instance) -> Optional[Dict[str, Any]]:
    if instance is None: return None
    data: Dict[str, Any] = {}
    m = instance.__class__
    for f in m._meta.get_fields():
        if not getattr(f,"concrete",False) or getattr(f,"many_to_many",False) or f.auto_created:
            continue
        if isinstance(f, models.ForeignKey):
            data[f.attname] = getattr(instance, f.attname, None)
            v = getattr(instance, f.name, None)
            data[f"{f.name}_str"] = str(v) if v else None
        else:
            val = getattr(instance, f.name, None)
            if isinstance(f, models.DateTimeField):
                data[f.name] = _dt(val)
            else:
                data[f.name] = _to_jsonable(val)
    return data

def _norm(s: Optional[str]) -> str:
    if not s: return "PENDING"
    s = s.strip().upper()
    if s in {"SUCCESS"}: return "SUCCESS"
    if s in {"FAIL","FAILED","FAILURE"}: return "FAILURE"
    if s in {"ERROR","ERR"}: return "ERROR"
    if s in {"PENDING","INITIATED","PROCESSING","INPROGRESS"}: return "PENDING"
    return s

# ---- decision matrix (exact rules from your message) ----
def _decide(snapshot: Dict[str, Any]) -> Dict[str, Any]:
    prov = snapshot.get("provider_status") or {}
    summ = snapshot.get("summary") or {}
    rtx  = snapshot.get("recharge_transaction") or {}
    pay  = snapshot.get("payment_transaction") or {}

    provider_status = _norm(prov.get("status"))
    summary_status  = _norm(summ.get("recharge_status") or summ.get("status"))
    recharge_status = _norm(rtx.get("status"))

    gateway_amt = Decimal(str(summ.get("paid_via_gateway") or 0))
    wallet_amt  = Decimal(str(summ.get("used_external_wallet") or 0)) + Decimal(str(summ.get("used_internal_wallet") or 0))
    gateway_used = gateway_amt > 0
    wallet_used  = wallet_amt > 0

    gateway_state = (pay.get("status") or "").strip().upper()        # gateway status (verified/initiated/notrequired)
    wallet_state  = (pay.get("wallet_status") or "").strip().upper() # wallet status (success/initiated/notrequired)
    needs_payment_review = gateway_used and gateway_state not in {"VERIFIED","CAPTURED","SUCCESS","NOTREQUIRED"}

    actions = {
        "enable_update_to_success": False,   # “set Summary & Recharge = SUCCESS”
        "enable_refund": False,              # refund/mark-failed button
        "disable_all": False,                # fully success path
    }
    story = []

    # All SUCCESS → disable action
    if provider_status=="SUCCESS" and summary_status=="SUCCESS" and recharge_status=="SUCCESS":
        actions["disable_all"] = True
        story.append("All sections show SUCCESS → no action required; buttons disabled.")
        return {"actions": actions, "note": " ".join(story)}

    # Provider SUCCESS overrides others → allow mark-to-success (payment untouched)
    if provider_status=="SUCCESS":
        actions["enable_update_to_success"] = True
        story.append("Provider SUCCESS while Summary/Recharge not SUCCESS → enable 'Update to SUCCESS' (payment untouched).")
        return {"actions": actions, "note": " ".join(story)}

    # Provider FAILURE/ERROR ⇒ refund path (after verifying payment as needed)
    if provider_status in {"FAILURE","ERROR"}:
        actions["enable_refund"] = True
        msg = "Provider reports FAILURE/ERROR → Summary set to FAILURE (if fail/pending), Recharge set to FAILURE (if fail/pending/error), then refund."
        if needs_payment_review:
            msg += " ⚠ Gateway initiated/not-verified; review before refund."
        story.append(msg)
        return {"actions": actions, "note": " ".join(story)}

    # Provider PENDING ⇒ wait
    if provider_status=="PENDING":
        story.append("Provider PENDING → no action; wait & recheck.")
        if needs_payment_review:
            story.append(" ⚠ Gateway initiated/not-verified; monitor payment tab.")
        return {"actions": actions, "note": " ".join(story)}

    # Default mixed
    story.append("Mixed/unknown → review tabs.")
    return {"actions": actions, "note": " ".join(story)}

# ---- V2 Snapshot (clone + reason_options + decision) ----
class AdminRechargeSnapshotV2API(APIView):
    permission_classes = [IsAdminUser]
    def get(self, request):
        p = request.query_params
        order_id = (p.get("order_id") or "").strip()
        verify   = _as_bool(p.get("verify_provider"))

        if not order_id:
            return Response({"error":"order_id is required"}, status=400)

        pay  = PaymentTransaction.objects.filter(order_id=order_id).first()
        summ = RechargePaymentSummary.objects.filter(order_id=order_id).first()
        rtx  = RechargeTransaction.objects.filter(order_id=order_id).first()

        usages_qs = ViralPeWalletUsage.objects.filter(order_id=order_id).order_by("used_on") if order_id else ViralPeWalletUsage.objects.none()
        the_user = (pay.user if pay else None) or (summ.user if summ else None) or (rtx.user if rtx else None)
        wallet = ViralPeWallet.objects.filter(user=the_user).first() if the_user else None

        provider_status = None
        if verify and rtx and rtx.client_txn_id:
            try:
                provider_status = get_goterpay_client().status(rtx.client_txn_id)
            except Exception as e:
                provider_status = {"status":"ERROR","resText":str(e)}

        # reason options from resText in recharge/summary/provider (+ last_status_poll)
        reasons: List[str] = []
        def _add_reason(x):
            if not x: return
            t = str(x).strip()
            if t and t.upper() not in {"NULL","NA"} and t not in reasons:
                reasons.append(t)
        if rtx and isinstance(getattr(rtx,"response_data",None), dict):
            rd = rtx.response_data
            _add_reason(rd.get("resText"))
            lp = rd.get("last_status_poll")
            if isinstance(lp, dict): _add_reason(lp.get("resText"))
        if summ and isinstance(getattr(summ,"full_response",None), dict):
            _add_reason(summ.full_response.get("resText"))
        if isinstance(provider_status, dict):
            _add_reason(provider_status.get("resText"))

        snapshot = {
            "order_id": order_id,
            "payment_transaction": _model_to_dict_all(pay),
            "summary":              _model_to_dict_all(summ),
            "recharge_transaction": _model_to_dict_all(rtx),
            "wallet_usages":        [_model_to_dict_all(u) for u in usages_qs],
            "wallet":               _model_to_dict_all(wallet),
            "provider_status":      provider_status or {"status":"PENDING"},
            "reason_options":       reasons[:20],
        }
        snapshot["decision"] = _decide(snapshot)
        return Response(snapshot, status=200)


# ---- V2 Snapshot (clone + reason_options + decision) ----
class AdminRechargeSnapshotV2cacheAPI(APIView):
    permission_classes = []
    #permission_classes = [IsAdminUser]
    def get(self, request):
        p = request.query_params
        order_id = (p.get("order_id") or "").strip()
        verify   = _as_bool(p.get("verify_provider"))

        if not order_id:
            return Response({"error":"order_id is required"}, status=400)

        pay  = PaymentTransaction.objects.filter(order_id=order_id).first()
        summ = RechargePaymentSummary.objects.filter(order_id=order_id).first()
        rtx  = RechargeTransaction.objects.filter(order_id=order_id).first()

        usages_qs = ViralPeWalletUsage.objects.filter(order_id=order_id).order_by("used_on") if order_id else ViralPeWalletUsage.objects.none()
        the_user = (pay.user if pay else None) or (summ.user if summ else None) or (rtx.user if rtx else None)
        wallet = ViralPeWallet.objects.filter(user=the_user).first() if the_user else None

        provider_status = None
        if verify and rtx and rtx.client_txn_id:
            try:
                provider_status = get_goterpay_client().status(rtx.client_txn_id)
            except Exception as e:
                provider_status = {"status":"ERROR","resText":str(e)}

        # reason options from resText in recharge/summary/provider (+ last_status_poll)
        reasons: List[str] = []
        def _add_reason(x):
            if not x: return
            t = str(x).strip()
            if t and t.upper() not in {"NULL","NA"} and t not in reasons:
                reasons.append(t)
        if rtx and isinstance(getattr(rtx,"response_data",None), dict):
            rd = rtx.response_data
            _add_reason(rd.get("resText"))
            lp = rd.get("last_status_poll")
            if isinstance(lp, dict): _add_reason(lp.get("resText"))
        if summ and isinstance(getattr(summ,"full_response",None), dict):
            _add_reason(summ.full_response.get("resText"))
        if isinstance(provider_status, dict):
            _add_reason(provider_status.get("resText"))

        snapshot = {
            "order_id": order_id,
            "payment_transaction": _model_to_dict_all(pay),
            "summary":              _model_to_dict_all(summ),
            "recharge_transaction": _model_to_dict_all(rtx),
            "wallet_usages":        [_model_to_dict_all(u) for u in usages_qs],
            "wallet":               _model_to_dict_all(wallet),
            "provider_status":      provider_status or {"status":"PENDING"},
            "reason_options":       reasons[:20],
        }
        snapshot["decision"] = _decide(snapshot)
        return Response(snapshot, status=200)




def _ts(dt):
    # normalize to a sortable datetime (never None)
    return dt if isinstance(dt, datetime) else datetime.min.replace(tzinfo=timezone.utc)

from django.core.cache import cache
cache.clear()


# ---- V2 Order dropdown + filters (for instant list) ----
class AdminRechargeOrderListV2API(APIView):
    permission_classes = [IsAdminUser]
    
    def get(self, request):
        status_q  = (request.query_params.get("status") or "any").strip().upper()
        refund    = (request.query_params.get("refund") or "any").strip().lower()
        date_from = (request.query_params.get("date_from") or "").strip()
        date_to   = (request.query_params.get("date_to") or "").strip()

        # 1) order_ids windowed by RechargeTransaction.created_at
        rtx_qs = RechargeTransaction.objects.all()
        if date_from:
            rtx_qs = rtx_qs.filter(created_at__date__gte=date_from)
        if date_to:
            rtx_qs = rtx_qs.filter(created_at__date__lte=date_to)

        order_ids = list(rtx_qs.values_list("order_id", flat=True).distinct())

        # 2) summaries matching the window (or all if no date filter)
        summ_qs = (RechargePaymentSummary.objects.filter(order_id__in=order_ids)
                   if (date_from or date_to) else RechargePaymentSummary.objects.all())

        out = []

        # 3) build from summaries
        for s in summ_qs[:1000]:
            rtx = RechargeTransaction.objects.filter(order_id=s.order_id).first()
            sz = _norm(getattr(s, "recharge_status", None))
            rz = _norm(getattr(rtx, "status", None) if rtx else None)
            is_ref = bool(
                getattr(s, "is_refunded", False)
                or ((getattr(rtx, "refund_status", "") or "").lower() in {"processed", "requested"})
            )
            if refund == "not_refunded" and is_ref: continue
            if refund == "refunded" and not is_ref: continue
            combined = sz or rz
            if status_q != "ANY" and combined != status_q: continue

            out.append({
                "order_id": s.order_id,
                "summary_status": sz,
                "recharge_status": rz,
                "created_at": _dt(getattr(rtx, "created_at", None)),
                "updated_at": _dt(getattr(s, "updated_at", None)),
                "is_refunded": is_ref,
                "_ts": _ts(getattr(rtx, "created_at", None)) or _ts(getattr(s, "updated_at", None)),
            })

        # 4) optional fallback: tx without summary
        have_ids = {row["order_id"] for row in out}
        if date_from or date_to:
            missing_ids = [oid for oid in order_ids if oid not in have_ids]
        else:
            missing_ids = list(
                RechargeTransaction.objects.exclude(order_id__in=have_ids)
                .order_by("-created_at")
                .values_list("order_id", flat=True)[:200]
            )

        for oid in missing_ids:
            rtx = RechargeTransaction.objects.filter(order_id=oid).first()
            if not rtx:
                continue
            sz = "PENDING"
            rz = _norm(getattr(rtx, "status", None))
            is_ref = (getattr(rtx, "refund_status", "") or "").lower() in {"processed", "requested"}
            if refund == "not_refunded" and is_ref: continue
            if refund == "refunded" and not is_ref: continue
            combined = sz or rz
            if status_q != "ANY" and combined != status_q: continue

            out.append({
                "order_id": oid,
                "summary_status": sz,
                "recharge_status": rz,
                "created_at": _dt(getattr(rtx, "created_at", None)),
                "updated_at": None,
                "is_refunded": is_ref,
                "_ts": _ts(getattr(rtx, "created_at", None)),
            })

        # ---- final: newest → oldest, then strip private key
        out.sort(key=lambda r: r.get("_ts", datetime.min.replace(tzinfo=timezone.utc)), reverse=True)
        for r in out:
            r.pop("_ts", None)

        return Response({"orders": out}, status=200)


# ---- V2: status-set (Provider SUCCESS -> set Summary/Recharge SUCCESS; payment untouched) ----
class AdminRechargeSetStatusV2API(APIView):
    permission_classes = [IsAdminUser]
    @transaction.atomic
    def post(self, request):
        order_id = (request.data or {}).get("order_id","").strip()
        if not order_id: return Response({"error":"order_id required"}, status=400)
        tx   = RechargeTransaction.objects.select_for_update().filter(order_id=order_id).first()
        summ = RechargePaymentSummary.objects.select_for_update().filter(order_id=order_id).first()
        if not (tx or summ): return Response({"error":"order not found"}, status=404)
        if summ: 
            summ.recharge_status = "Success"
            summ.save(update_fields=["recharge_status"])
        if tx:
            tx.status = "Success"
            tx.save(update_fields=["status"])
        return Response({"ok":True,"order_id":order_id}, status=200)

# ---- V2 Refund (idempotent + stamps provider + sets flags on both models) ----
# class AdminRechargeRefundV2API(APIView):
#     permission_classes = [IsAdminUser]
#     @transaction.atomic
#     def post(self, request):
#         p = request.data or {}
#         order_id = (p.get("order_id") or "").strip()
#         reason   = (p.get("reason") or "").strip() or "Manual refund"
#         dry_run  = _as_bool(p.get("dry_run", False))
#         if not order_id: return Response({"error":"order_id required"}, status=400)

#         tx   = RechargeTransaction.objects.select_for_update().filter(order_id=order_id).first()
#         summ = RechargePaymentSummary.objects.select_for_update().filter(order_id=order_id).first()
#         pay  = PaymentTransaction.objects.filter(order_id=order_id).first()
#         if not (tx or summ or pay): return Response({"error":"order not found"}, status=404)
#         if not summ: return Response({"error":"summary missing; cannot compute refund"}, status=409)

#         ext = Decimal(str(summ.used_external_wallet or 0))
#         vp  = Decimal(str(summ.used_internal_wallet or 0))
#         pg  = Decimal(str(summ.paid_via_gateway    or 0))
#         total = ext + vp + pg

#         # provider snapshot (audit)
#         prov = None
#         try:
#             if tx and tx.client_txn_id:
#                 prov = get_goterpay_client().status(tx.client_txn_id)
#         except Exception as e:
#             prov = {"status":"ERROR","resText":str(e)}

#         # stamp provider json into both models
#         if summ and isinstance(getattr(summ,"full_response",None), dict):
#             fr = dict(summ.full_response)
#             fr["goter_status"] = prov
#             summ.full_response = fr
#             summ.save(update_fields=["full_response"])
#         if tx and isinstance(getattr(tx,"response_data",None), dict):
#             rd = dict(tx.response_data)
#             rd["goter_status"] = prov
#             tx.response_data = rd
#             tx.save(update_fields=["response_data"])

#         if dry_run:
#             return Response({
#                 "ok": True, "dry_run": True, "order_id": order_id,
#                 "breakdown": {"external": f"{ext:.2f}", "internal": f"{vp:.2f}", "gateway": f"{pg:.2f}"},
#                 "would_refund": f"{total:.2f}",
#             }, status=200)

#         # run idempotent refund
#         result = process_recharge_refund(summ)

#         # set flags on Summary & Recharge
#         if summ:
#             if not getattr(summ,"refund_reason",None):
#                 summ.refund_reason = reason
#             summ.is_refunded = True
#             summ.save(update_fields=["refund_reason","is_refunded"])
#         if tx and (tx.status or "").upper() != "SUCCESS":
#             msg = (tx.status_message or "")
#             if reason and reason not in msg:
#                 tx.status_message = (msg + f" | Manual refund: {reason}").strip(" |")
#             tx.refund_status = "processed"
#             tx.status = "FAILURE"
#             tx.save(update_fields=["status","status_message","refund_status"])

#         return Response({"ok":True,"order_id":order_id,"refunded_total":f"{total:.2f}","result":result}, status=200)



# ---- V2 Refund (idempotent + stamps provider + sets flags on both models) ----
from decimal import Decimal
from django.db import transaction
from rest_framework.views import APIView
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response

def _as_bool(v, default=False):
    s = str(v).strip().lower()
    if s in {"1","true","yes","y","on"}:  return True
    if s in {"0","false","no","n","off"}: return False
    return default

# def _norm(s: str) -> str:
#     return (s or "").strip().lower()

class AdminRechargeRefundV2API(APIView):
    permission_classes = [IsAdminUser]

    @transaction.atomic
    def post(self, request):
        p = request.data or {}
        order_id = (p.get("order_id") or "").strip()
        reason   = (p.get("reason") or "").strip() or "Manual refund"
        dry_run  = _as_bool(p.get("dry_run", False))
        if not order_id:
            return Response({"error": "order_id required"}, status=400)

        tx   = RechargeTransaction.objects.select_for_update().filter(order_id=order_id).first()
        summ = RechargePaymentSummary.objects.select_for_update().filter(order_id=order_id).first()
        pay  = PaymentTransaction.objects.filter(order_id=order_id).first()
        if not (tx or summ or pay):
            return Response({"error": "order not found"}, status=404)
        if not summ:
            return Response({"error": "summary missing; cannot compute refund"}, status=409)

        ext = Decimal(str(summ.used_external_wallet or 0))
        vp  = Decimal(str(summ.used_internal_wallet or 0))
        pg  = Decimal(str(summ.paid_via_gateway    or 0))
        total = ext + vp + pg

        # provider snapshot (audit)
        prov = None
        try:
            if tx and tx.client_txn_id:
                prov = get_goterpay_client().status(tx.client_txn_id)
        except Exception as e:
            prov = {"status":"ERROR","resText":str(e)}

        # ----- STAMP provider json into both models (safe/hardened) -----
        if summ:
            fr = dict(getattr(summ, "full_response", {}) or {})
            fr["goter_status"] = prov
            summ.full_response = fr
            summ.save(update_fields=["full_response"])

        if tx:
            rd = dict(getattr(tx, "response_data", {}) or {})
            rd["goter_status"] = prov
            tx.response_data = rd
            tx.save(update_fields=["response_data"])

        # ----- Decide effective recharge outcome based on provider -----
        prov_status = _norm((prov or {}).get("status"))
        # treat these as hard-failure indicators
        failed_like = {"failed", "error", "errors", "failure"}
        pending_like = {"pending","queued", "initiated"}

        summ_status_now = _norm(summ.recharge_status)
        should_force_fail = False
        if summ_status_now.lower() in pending_like and prov_status.lower() in failed_like:
            # Coerce Summary to "failure" so idempotent refund runs
            summ.recharge_status = "failure"
            # (optional) set a helpful message snapshot
            if not getattr(summ, "status_message", None):
                summ.status_message = (prov or {}).get("resText") or "Provider shows FAILED"
            summ.save(update_fields=["recharge_status","status_message"])
            should_force_fail = True

            # Mirror on RechargeTransaction when not success
            if tx and (tx.status or "").upper() != "SUCCESS":
                msg = (tx.status_message or "")
                add = (prov or {}).get("resText") or "Provider shows FAILED"
                if add and add not in msg:
                    tx.status_message = (msg + f" | {add}").strip(" |")
                tx.status = "FAILURE"
                # don't set refund_status here; that comes after refund is done below
                tx.save(update_fields=["status","status_message"])

        if dry_run:
            return Response({
                "ok": True,
                "dry_run": True,
                "order_id": order_id,
                "breakdown": {"external": f"{ext:.2f}", "internal": f"{vp:.2f}", "gateway": f"{pg:.2f}"},
                "would_refund": f"{total:.2f}",
                "summary_status_before": summ_status_now,
                "provider_status": prov_status,
                "would_force_fail": should_force_fail,
            }, status=200)

        # ----- Run idempotent refund (respects already-refunded rows) -----
        result = process_recharge_refund_new(summ, force_fail=should_force_fail)

        # ----- Set flags on Summary & Recharge -----
        if summ:
            if not getattr(summ, "refund_reason", None):
                summ.refund_reason = reason
            summ.is_refunded = True
            summ.save(update_fields=["refund_reason","is_refunded"])

        if tx and (tx.status or "").upper() != "SUCCESS":
            msg = (tx.status_message or "")
            if reason and reason not in msg:
                tx.status_message = (msg + f" | Manual refund: {reason}").strip(" |")
            tx.refund_status = "processed"
            tx.status = "FAILURE"
            tx.save(update_fields=["status","status_message","refund_status"])

        return Response({
            "ok": True,
            "order_id": order_id,
            "refunded_total": f"{total:.2f}",
            "result": result,
            "provider_status": prov_status
        }, status=200)



class AdminRechargeRefundV2APIfromexcel(APIView):
    permission_classes = []

    @transaction.atomic
    def post(self, request):
        p = request.data or {}
        order_id = (p.get("order_id") or "").strip()
        reason   = (p.get("reason") or "").strip() or "Manual refund"
        dry_run  = _as_bool(p.get("dry_run", False))
        if not order_id:
            return Response({"error": "order_id required"}, status=400)

        tx   = RechargeTransaction.objects.select_for_update().filter(order_id=order_id).first()
        summ = RechargePaymentSummary.objects.select_for_update().filter(order_id=order_id).first()
        pay  = PaymentTransaction.objects.filter(order_id=order_id).first()
        if not (tx or summ or pay):
            return Response({"error": "order not found"}, status=404)
        if not summ:
            return Response({"error": "summary missing; cannot compute refund"}, status=409)

        ext = Decimal(str(summ.used_external_wallet or 0))
        vp  = Decimal(str(summ.used_internal_wallet or 0))
        pg  = Decimal(str(summ.paid_via_gateway    or 0))
        total = ext + vp + pg

        # provider snapshot (audit)
        prov = None
        try:
            if tx and tx.client_txn_id:
                prov = get_goterpay_client().status(tx.client_txn_id)
        except Exception as e:
            prov = {"status":"ERROR","resText":str(e)}

        # ----- STAMP provider json into both models (safe/hardened) -----
        if summ:
            fr = dict(getattr(summ, "full_response", {}) or {})
            fr["goter_status"] = prov
            summ.full_response = fr
            summ.save(update_fields=["full_response"])

        if tx:
            rd = dict(getattr(tx, "response_data", {}) or {})
            rd["goter_status"] = prov
            tx.response_data = rd
            tx.save(update_fields=["response_data"])

        # ----- Decide effective recharge outcome based on provider -----
        prov_status = _norm((prov or {}).get("status"))
        # treat these as hard-failure indicators
        failed_like = {"failed", "error", "errors", "failure"}
        pending_like = {"pending","queued", "initiated"}

        summ_status_now = _norm(summ.recharge_status)
        should_force_fail = False
        if summ_status_now.lower() in pending_like and prov_status.lower() in failed_like:
            # Coerce Summary to "failure" so idempotent refund runs
            summ.recharge_status = "failure"
            # (optional) set a helpful message snapshot
            if not getattr(summ, "status_message", None):
                summ.status_message = (prov or {}).get("resText") or "Provider shows FAILED"
            summ.save(update_fields=["recharge_status","status_message"])
            should_force_fail = True

            # Mirror on RechargeTransaction when not success
            if tx and (tx.status or "").upper() != "SUCCESS":
                msg = (tx.status_message or "")
                add = (prov or {}).get("resText") or "Provider shows FAILED"
                if add and add not in msg:
                    tx.status_message = (msg + f" | {add}").strip(" |")
                tx.status = "FAILURE"
                # don't set refund_status here; that comes after refund is done below
                tx.save(update_fields=["status","status_message"])

        if dry_run:
            return Response({
                "ok": True,
                "dry_run": True,
                "order_id": order_id,
                "breakdown": {"external": f"{ext:.2f}", "internal": f"{vp:.2f}", "gateway": f"{pg:.2f}"},
                "would_refund": f"{total:.2f}",
                "summary_status_before": summ_status_now,
                "provider_status": prov_status,
                "would_force_fail": should_force_fail,
            }, status=200)

        # ----- Run idempotent refund (respects already-refunded rows) -----
        result = process_recharge_refund_new(summ, force_fail=should_force_fail)

        # ----- Set flags on Summary & Recharge -----
        if summ:
            if not getattr(summ, "refund_reason", None):
                summ.refund_reason = reason
            summ.is_refunded = True
            summ.save(update_fields=["refund_reason","is_refunded"])

        if tx and (tx.status or "").upper() != "SUCCESS":
            msg = (tx.status_message or "")
            if reason and reason not in msg:
                tx.status_message = (msg + f" | Manual refund: {reason}").strip(" |")
            tx.refund_status = "processed"
            tx.status = "FAILURE"
            tx.save(update_fields=["status","status_message","refund_status"])

        return Response({
            "ok": True,
            "order_id": order_id,
            "refunded_total": f"{total:.2f}",
            "result": result,
            "provider_status": prov_status
        }, status=200)


