# recharge/views/api_views.py
import re
from django.core.cache import cache
from django.utils import timezone
from django.db import transaction
from django.db.models import Q

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
# from rest_framework.permissions import IsAuthenticated, AllowAny
# from rest_framework.authentication import TokenAuthentication

from recharge.models import Operator, Circle, MobileInfoMap
from recharge.services.goterpay_client import get_goterpay_client
from recharge.services.goterpay_api import GoterPayError, GoterPayAPI

MOBILE_NON_DIGITS = re.compile(r"\D+")

def normalize_mobile(raw: str | None) -> str:
    if not raw:
        return ""
    digits = MOBILE_NON_DIGITS.sub("", raw)
    if len(digits) == 12 and digits.startswith("91"):
        digits = digits[2:]
    if len(digits) == 11 and digits.startswith("0"):
        digits = digits[1:]
    return digits

def _norm(s: str | None) -> str | None:
    return str(s).strip().upper() if s else None

class MobileInfoAPI(APIView):
    """
    GET /api/recharge/mobile-info?mobile=9xxxxxxxxx[&refresh=1]
    Returns { status, source, mobile, operator, circle, operator_name, circle_name, operator_image_url, served_from, raw? }
    """
    # authentication_classes = [TokenAuthentication]
    # permission_classes = [IsAuthenticated]
    # ↓ make it public if you prefer:
    # permission_classes = [AllowAny]
    CACHE_SECONDS = 24 * 60 * 60          # 24h – change as you like
    PROVIDER_SOURCE = "Goter"

    def get(self, request):
        raw_mobile = request.query_params.get("mobile") or request.query_params.get("number")
        refresh = (request.query_params.get("refresh") in ("1","true","True"))
        mobile = normalize_mobile(raw_mobile)

        if len(mobile) != 10:
            return Response({"detail":"Provide a valid 10-digit mobile in 'mobile' or 'number'."},
                            status=status.HTTP_400_BAD_REQUEST)

        # 0) Memory cache (unless refresh)
        cache_key = f"mi:{mobile}"
        if not refresh:
            cached = cache.get(cache_key)
            if cached:
                cached["served_from"] = "memory"
                return Response(cached, status=status.HTTP_200_OK)

        # 1) DB cache (unless refresh)
        if not refresh:
            row = MobileInfoMap.objects.filter(mobile=mobile).only(
                "mobile","provider_source","operator_code","circle_code",
                "operator_name","circle_name","last_payload","lookups"
            ).first()
            if row:
                # enrich with image & names from our Operator/Circle tables if available
                op_obj = Operator.objects.filter(source=self.PROVIDER_SOURCE, code=row.operator_code).only("image_url","name").first()
                cir_obj = Circle.objects.filter(source=self.PROVIDER_SOURCE, code=row.circle_code).only("name").first()
                data = {
                    "source": self.PROVIDER_SOURCE,
                    "status": "SUCCESS",
                    "mobile": mobile,
                    "operator": row.operator_code,
                    "circle": row.circle_code,
                    "operator_name": (op_obj.name if op_obj else row.operator_name),
                    "circle_name": (cir_obj.name if cir_obj else row.circle_name),
                    "operator_image_url": (op_obj.image_url if op_obj and op_obj.image_url else None),
                    "served_from": "db",
                }
                cache.set(cache_key, data, self.CACHE_SECONDS)
                # async would be nicer, but cheap enough:
                MobileInfoMap.objects.filter(pk=row.pk).update(lookups=row.lookups+1)
                return Response(data, status=status.HTTP_200_OK)

        # 2) Provider call
        client: GoterPayAPI = get_goterpay_client()
        try:
            resp = client.mobile_info(mobile=mobile)  # should return {"operator":"JO","circle":"15","status":"success",...}
        except GoterPayError as e:
            return Response(
                {"status":"ERROR","message":str(e),"provider_status_code":e.status_code,"provider_payload":e.payload},
                status=status.HTTP_502_BAD_GATEWAY
            )
        except Exception as e:
            return Response({"status":"ERROR","message":"Upstream error contacting provider.","detail":str(e)},
                            status=status.HTTP_502_BAD_GATEWAY)

        status_raw = resp.get("status") or resp.get("STATUS")
        try:
            status_norm = GoterPayAPI.normalize_status(status_raw)
        except Exception:
            status_norm = (status_raw or "").upper() or "UNKNOWN"

        op_code = _norm(resp.get("operator_code") or resp.get("op_code") or resp.get("operator"))
        cir_code = _norm(resp.get("circle_code")   or resp.get("circle"))

        # Resolve friendly names & image
        op_obj = Operator.objects.filter(source=self.PROVIDER_SOURCE).filter(
            Q(code__iexact=op_code) | Q(name__iexact=op_code)
        ).first() if op_code else None
        cir_obj = Circle.objects.filter(source=self.PROVIDER_SOURCE).filter(
            Q(code__iexact=cir_code) | Q(name__iexact=cir_code)
        ).first() if cir_code else None

        operator_code = op_obj.code if op_obj else (op_code or None)
        circle_code   = cir_obj.code if cir_obj else (cir_code or None)
        operator_name = op_obj.name if op_obj else None
        circle_name   = cir_obj.name if cir_obj else None
        operator_image_url = op_obj.image_url if (op_obj and op_obj.image_url) else None

        data = {
            "source": self.PROVIDER_SOURCE,
            "status": status_norm,
            "mobile": mobile,
            "operator": operator_code,
            "circle": circle_code,
            "operator_name": operator_name,
            "circle_name": circle_name,
            "operator_image_url": operator_image_url,
            "message": resp.get("message") or resp.get("msg"),
            "served_from": "provider",
        }

        # 3) Upsert durable mapping
        if operator_code and circle_code:
            with transaction.atomic():
                row, created = MobileInfoMap.objects.select_for_update().get_or_create(
                    mobile=mobile,
                    defaults={
                        "provider_source": self.PROVIDER_SOURCE,
                        "operator_code": operator_code,
                        "circle_code": circle_code,
                        "operator_name": operator_name,
                        "circle_name": circle_name,
                        "last_payload": resp,
                        "lookups": 1,
                    }
                )
                if not created:
                    # Update if changed or just bump counters
                    changed = False
                    if row.operator_code != operator_code:
                        row.operator_code = operator_code; changed = True
                    if row.circle_code != circle_code:
                        row.circle_code = circle_code; changed = True
                    # Keep friendly names if we have them
                    if operator_name and row.operator_name != operator_name:
                        row.operator_name = operator_name; changed = True
                    if circle_name and row.circle_name != circle_name:
                        row.circle_name = circle_name; changed = True
                    # store last payload for audit (optional)
                    row.last_payload = resp
                    row.lookups = (row.lookups or 0) + 1
                    if changed:
                        row.provider_source = self.PROVIDER_SOURCE
                    row.save()

        # 4) Populate memory cache
        cache.set(cache_key, data, self.CACHE_SECONDS)
        return Response(data, status=status.HTTP_200_OK)

from django.conf import settings
from django.db.models import Q
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.authentication import TokenAuthentication

from recharge.models import Operator, Circle
from recharge.services.goterpay_client import get_goterpay_client
from recharge.services.goterpay_api import GoterPayAPI, GoterPayError


def _resolve_provider_source(provider_param: str | None) -> str:
    """
    Resolve a provider 'source' the same way your form view does:
    provider = settings.RECHARGE_PROVIDER by default, then map via PROVIDER_SOURCE_MAP.
    Optionally allow ?provider= in the query to override (useful for testing).
    """
    provider_key = (provider_param or settings.RECHARGE_PROVIDER or "").lower()
    return settings.PROVIDER_SOURCE_MAP.get(provider_key, provider_key)

from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator

@method_decorator(cache_page(60 * 10), name='dispatch')  # 10 minutes
class OperatorsAPI(APIView):
    """
    GET /api/recharge/operators?service_type=Mobile&provider=goterpay&search=air
    Returns operators from DB filtered by service_type + provider source.
    """
    permission_classes = [AllowAny]
    authentication_classes = []

    def get(self, request):
        service_type = request.query_params.get("service_type", "Mobile")  # default like your form
        provider_q = request.query_params.get("provider")
        source = _resolve_provider_source(provider_q)
        search = (request.query_params.get("search") or "").strip()

        qs = Operator.objects.filter(
            service_type__iexact=service_type,
            source__iexact=source,
        )

        if search:
            qs = qs.filter(Q(name__icontains=search) | Q(code__icontains=search))

        # Keep payload light and predictable
        data = [
            {
                "id": op.id,
                "code": op.code,          # code your plans endpoint will need
                "name": op.name,          # display name
                "service_type": op.service_type,
                "source": op.source,
                "imgurl": op.image_url,
                
            }
            for op in qs.order_by("name")
        ]
        return Response({"operators": data}, status=status.HTTP_200_OK)

from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator

@method_decorator(cache_page(60 * 10), name='dispatch')  # 10 minutes
class CirclesAPI(APIView):
    """
    GET /api/recharge/circles?provider=goterpay&search=kar
    Returns circles from DB filtered by provider source.
    """
    permission_classes = [AllowAny]
    authentication_classes = []

    def get(self, request):
        provider_q = request.query_params.get("provider")
        source = _resolve_provider_source(provider_q)
        search = (request.query_params.get("search") or "").strip()

        qs = Circle.objects.filter(source__iexact=source)
        if search:
            qs = qs.filter(Q(name__icontains=search) | Q(code__icontains=search))

        data = [
            {
                "id": c.id,
                "code": c.code,
                "name": c.name,
                "source": c.source,
            }
            for c in qs.order_by("name")
        ]
        return Response({"circles": data}, status=status.HTTP_200_OK)


from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.db import transaction
from django.utils import timezone

from recharge.models import Operator, Circle, PlanCache
# from recharge.integrations.goterpay import GoterPayAPI, GoterPayError  # your existing import
from typing import Any, Dict, Iterable

def lower_keys(d: Dict[str, Any]) -> Dict[str, Any]:
    """Return a shallow copy with lower-cased keys (handles provider case mismatches)."""
    if not isinstance(d, dict):
        return {}
    return { (k or "") .lower(): v for k, v in d.items() }

def any_key(d: Dict[str, Any], keys: Iterable[str], default=None):
    """Get first present key (case-insensitive) from dict."""
    if not isinstance(d, dict):
        return default
    dl = lower_keys(d)
    for k in keys:
        v = dl.get(k.lower())
        if v is not None:
            return v
    return default


def _normalize_provider_payload(raw: dict):
    """
    Returns (status_norm, plans_list, raw_dict) using case-insensitive keys.
    Supports providers returning keys like Status/STATUS/status and Data/DATA/data/records/plans.
    """
    # Case-insensitive wrappers:
    status_raw = any_key(raw, ["status", "STATUS", "Status"])
    # Your existing normalization (assuming it’s resilient to None):
    try:
        status_norm = GoterPayAPI.normalize_status(status_raw)
    except Exception:
        status_norm = (status_raw or "ERROR")

    # Find plans array in common places (case-insensitive)
    plans = any_key(raw, ["plans", "data", "records", "Data", "Records"], default=[])
    if not isinstance(plans, list):
        # Some providers wrap under an object; last resort try to dig a bit
        plans = []

    return status_norm, plans, raw

def _resolve_provider_source(provider_q: str) -> str:
    # your existing helper; fallback to "Goter" if needed
    return (provider_q or "goterpay").strip().lower().replace("goterpay", "Goter").replace("goter", "Goter")

class PlansAPI(APIView):
    # authentication_classes = [TokenAuthentication]
    # permission_classes = [IsAuthenticated]
    permission_classes = []
    authentication_classes = []

    def get(self, request):
        operator_code = request.query_params.get("operator")
        circle_code   = request.query_params.get("circle")
        provider_q    = request.query_params.get("provider")
        refresh_flag  = request.query_params.get("refresh") in ("1", "true", "True")
        source        = _resolve_provider_source(provider_q)

        # Validate required params
        if not operator_code or not circle_code:
            return Response(
                {"detail": "operator_code and circle_code are required"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Validate codes for this provider
        if not Operator.objects.filter(code=operator_code, source__iexact=source).exists():
            return Response({"detail": "Invalid operator_code for this provider"}, status=status.HTTP_400_BAD_REQUEST)
        if not Circle.objects.filter(code=circle_code, source__iexact=source).exists():
            return Response({"detail": "Invalid circle_code for this provider"}, status=status.HTTP_400_BAD_REQUEST)

        # 1) Try cache first (unless refresh requested)
        try:
            cache_obj = PlanCache.objects.get(
                provider_source=source, operator_code=operator_code, circle_code=circle_code
            )
        except PlanCache.DoesNotExist:
            cache_obj = None

        if cache_obj and not refresh_flag:
            # Serve from cache
            return Response({
                "status": cache_obj.status or "SUCCESS",
                "plans": cache_obj.plans or [],
                "raw": cache_obj.raw_payload or {},
                "cache": {
                    "provider_source": cache_obj.provider_source,
                    "operator_code": cache_obj.operator_code,
                    "circle_code": cache_obj.circle_code,
                    "fetched_at": cache_obj.fetched_at,
                    "updated_at": cache_obj.updated_at,
                    "served_from_cache": True,
                }
            }, status=status.HTTP_200_OK)

        # 2) Fetch from provider (or refresh)
        client = get_goterpay_client() #GoterPayAPI()  # or your get_goterpay_client()
        try:
            provider_raw = client.recharge_plan(operator_code, circle_code)  # original provider payload
        except GoterPayError as e:
            # If provider fails but we have cache, serve stale
            if cache_obj:
                return Response({
                    "status": cache_obj.status or "SUCCESS",
                    "plans": cache_obj.plans or [],
                    "raw": cache_obj.raw_payload or {},
                    "cache": {
                        "provider_source": cache_obj.provider_source,
                        "operator_code": cache_obj.operator_code,
                        "circle_code": cache_obj.circle_code,
                        "fetched_at": cache_obj.fetched_at,
                        "updated_at": cache_obj.updated_at,
                        "served_from_cache": True,
                        "warning": "Provider error; served cached data.",
                        "provider_status_code": e.status_code,
                    }
                }, status=status.HTTP_200_OK)

            # No cache: propagate upstream error
            return Response(
                {
                    "status": "ERROR",
                    "message": str(e),
                    "provider_status_code": e.status_code,
                    "provider_payload": e.payload,
                },
                status=status.HTTP_502_BAD_GATEWAY,
            )
        except Exception as e:
            if cache_obj:
                return Response({
                    "status": cache_obj.status or "SUCCESS",
                    "plans": cache_obj.plans or [],
                    "raw": cache_obj.raw_payload or {},
                    "cache": {
                        "provider_source": cache_obj.provider_source,
                        "operator_code": cache_obj.operator_code,
                        "circle_code": cache_obj.circle_code,
                        "fetched_at": cache_obj.fetched_at,
                        "updated_at": cache_obj.updated_at,
                        "served_from_cache": True,
                        "warning": "Provider error; served cached data.",
                    }
                }, status=status.HTTP_200_OK)
            return Response(
                {"status": "ERROR", "message": "Upstream error contacting provider", "detail": str(e)},
                status=status.HTTP_502_BAD_GATEWAY,
            )

        # 3) Normalize with case-insensitive keys (fixes your shown sample)
        status_norm, plans_list, raw_payload = _normalize_provider_payload(provider_raw)

        # 4) Upsert cache
        with transaction.atomic():
            cache_obj, _created = PlanCache.objects.select_for_update().get_or_create(
                provider_source=source, operator_code=operator_code, circle_code=circle_code,
                defaults={
                    "status": status_norm,
                    "plans": plans_list,
                    "raw_payload": raw_payload,
                }
            )
            # Update existing if necessary
            cache_obj.status = status_norm
            cache_obj.plans = plans_list
            cache_obj.raw_payload = raw_payload
            cache_obj.save(update_fields=["status", "plans", "raw_payload", "updated_at"])

        # 5) Return normalized + raw, mark it as freshly fetched
        return Response({
            "status": status_norm,
            "plans": plans_list,
            "raw": raw_payload,
            "cache": {
                "provider_source": source,
                "operator_code": operator_code,
                "circle_code": circle_code,
                "served_from_cache": False,
                "updated_at": cache_obj.updated_at,
            }
        }, status=status.HTTP_200_OK)

from decimal import Decimal, ROUND_HALF_UP, InvalidOperation
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication

from payments.views import get_wallet_balances  # must return (Decimal ext, Decimal vp)

def q2(x: Decimal) -> Decimal:
    return (x or Decimal("0")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

class CheckPaymentOptionsAPI(APIView):
    """
    POST /api/recharge/check_payment_options
    Body:
    {
      "service": "prepaid",   // "prepaid" | "postpaid" (optional for now)
      "mobile": "98XXXXXXXX", // optional here (used later)
      "operator": "AIRTEL",   // optional validation can be added later
      "circle": "AP",         // optional validation can be added later
      "amount": 399
    }

    200:
    {
      "wallets": {
        "external": {"balance":"120.00","can_use": true},
        "viralpe":  {"balance":"50.00","can_use":  true}
      },
      "recommended": {
        "use_external": true,
        "use_viralpe":  true,
        "razorpay_required": true,
        "razorpay_amount": 229
      },
      "splits": { "external": "120.00", "viralpe": "50.00" }  // optional helper
    }
    """
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        amount_in = request.data.get("amount")

        # ---- validate amount (as Decimal) ----
        try:
            if amount_in is None:
                raise InvalidOperation
            amount = q2(Decimal(str(amount_in)))
        except (InvalidOperation, TypeError, ValueError):
            return Response({"error": "Invalid amount"}, status=status.HTTP_400_BAD_REQUEST)

        if amount <= 0:
            return Response({"error": "Amount must be greater than zero"}, status=status.HTTP_400_BAD_REQUEST)

        # ---- balances ----
        ext_balance, vp_balance = get_wallet_balances(request.user)  # expect Decimals
        ext_balance = q2(ext_balance)
        vp_balance  = q2(vp_balance)

        # ---- split (external first, then viralpe) ----
        use_ext = min(ext_balance, amount)
        remaining = amount - use_ext

        use_vp = min(vp_balance, remaining)
        remaining = amount - (use_ext + use_vp)

        # ---- recommendation flags ----
        razorpay_required = remaining > Decimal("0.00")

        # Display-friendly whole-rupee for preview; real charge later will use paise
        razorpay_amount_rupees = int(remaining.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) if razorpay_required else 0

        resp = {
            "wallets": {
                "external": {"balance": str(q2(ext_balance)), "can_use": ext_balance > 0},
                "viralpe":  {"balance": str(q2(vp_balance)),  "can_use": vp_balance  > 0},
            },
            "recommended": {
                "use_external": use_ext > 0,
                "use_viralpe":  use_vp  > 0,
                "razorpay_required": razorpay_required,
                "razorpay_amount": razorpay_amount_rupees
            },
            # helpful to show UI split; remove if you want to keep output minimal
            "splits": {
                "external": str(q2(use_ext)),
                "viralpe":  str(q2(use_vp)),
            }
        }
        return Response(resp, status=status.HTTP_200_OK)


import uuid, razorpay
from decimal import Decimal
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
from payments.models import PaymentTransaction


class RazorpayCreateOrderAPI(APIView):
    """
    POST /api/payments/razorpay/create_order
    { "amount": 229 }   # in INR rupees
    """

    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        try:
            user = request.user
            amount = Decimal(str(request.data.get("amount", "0")))
            number = user.mobile_number

            if amount <= 0:
                return Response({"status": "error", "message": "Invalid amount"}, status=400)

            # 🆔 Internal order ID (track with your own prefix)
            # order_id = f"VP-{user.id}-{uuid.uuid4().hex[:6].upper()}"
            order_id = f"VP-{request.user.id}-{number[-4:]}-{uuid.uuid4().hex[:6].upper()}"

            print(order_id)
            # 🪙 Razorpay wants paise
            razorpay_due = int(amount * 100)

            # 🔑 Create Razorpay order
            client = razorpay.Client(auth=(settings.RAZORPAY_API_KEY, settings.RAZORPAY_API_SECRET))
            rzp_order = client.order.create({
                "amount": razorpay_due,
                "currency": "INR",
                "receipt": order_id,
                "payment_capture": 1,
                "notes": {"purpose": "mobile_recharge"}
            })

            razorpay_order_id = rzp_order["id"]

            # 📝 Log transaction
            PaymentTransaction.objects.create(
                user=user,
                order_id=order_id,
                amount=float(amount),
                razorpay_order_id=razorpay_order_id,
                status="initiated"
            )

            # ✅ Response for Android
            return Response({
                "success": True,
                "order_id": order_id,                 # your internal tracker
                "razorpay_order_id": razorpay_order_id,
                "amount": rzp_order["amount"],        # in paise
                "currency": rzp_order["currency"],
                "key_id": settings.RAZORPAY_API_KEY,  # so frontend can open Razorpay checkout
            })

        except Exception as e:
            return Response({"status": "error", "message": str(e)}, status=500)


# payments/api.py
import razorpay
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication

from payments.models import PaymentTransaction


class RazorpayVerifyPaymentAPI(APIView):
    """
    POST /api/payments/razorpay/verify
    Body (from Android after Razorpay success):
    {
      "order_id": "VP-1-ABC123",                // your internal order id returned by /create_order
      "razorpay_order_id": "order_9A33XWu...",  // from Razorpay SDK
      "razorpay_payment_id": "pay_29QQoU...",   // from Razorpay SDK
      "razorpay_signature": "generated_signature" // from Razorpay SDK
    }


    Response:
    { "verified": true, "order_id": "...", "razorpay_payment_id": "..." }
    """
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        try:
            order_id = request.data.get("order_id")
            rzp_order_id = request.data.get("razorpay_order_id")
            rzp_payment_id = request.data.get("razorpay_payment_id")
            rzp_signature = request.data.get("razorpay_signature")
# {"order_id":"order_R6o2ETWV5rco4z",
# "payment_id":"pay_R6o2JXRB1bkp9D",
# "signature":"bea26704c24f9419bfca2c098b744c0e0dea10ce2141f390bff77d5a57e50ad7",
# "meta":{"service":"prepaid","mobile":"8096248999","operator":"JO","circle":"1","amount":12,"use_external":false,"use_viralpe":false}}

            print(order_id)
            print(rzp_order_id)
            print(rzp_payment_id)
            print(rzp_signature)
            if not (order_id and rzp_order_id and rzp_payment_id and rzp_signature):
                return Response(
                    {"verified": False, "message": "Missing one or more required fields."},
                    status=status.HTTP_400_BAD_REQUEST,
                )

            # 1) verify signature (Razorpay SDK values)
            client = razorpay.Client(auth=(settings.RAZORPAY_API_KEY, settings.RAZORPAY_API_SECRET))
            client.utility.verify_payment_signature({
                "razorpay_order_id": rzp_order_id,
                "razorpay_payment_id": rzp_payment_id,
                "razorpay_signature": rzp_signature,
            })

            # 2) mark transaction as verified/success (idempotent)
            try:
                tx = PaymentTransaction.objects.get(razorpay_order_id=rzp_order_id)
            except PaymentTransaction.DoesNotExist:
                return Response(
                    {"verified": False, "message": "Transaction not found for this razorpay_order_id."},
                    status=status.HTTP_404_NOT_FOUND,
                )

            if tx.status not in ("success", "verified"):
                tx.status = "verified"
                tx.razorpay_payment_id = rzp_payment_id
                tx.razorpay_signature = rzp_signature
                # keep tx.amount/order_id as created in /create_order
                tx.save()

            return Response({
                "verified": True,
                "order_id": order_id,
                "razorpay_payment_id": rzp_payment_id,
            }, status=status.HTTP_200_OK)

        except razorpay.errors.SignatureVerificationError:
            return Response({"verified": False, "message": "Invalid payment signature"}, status=400)
        except Exception as e:
            return Response({"verified": False, "message": str(e)}, status=500)

import uuid
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from django.utils.timezone import now

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication

from payments.models import PaymentTransaction, RechargePaymentSummary
from payments.views import get_wallet_balances, deduct_wallets, perform_wallet_recharge
from payments.ids import generate_unique_provider_txnid


TWOPL = Decimal("0.01")

def _d(x) -> Decimal:
    return Decimal(str(x))

def q2(x: Decimal) -> Decimal:
    # Round HALF_UP to 2-decimal places like money should
    return x.quantize(TWOPL, rounding=ROUND_HALF_UP)

def to_paise(x: Decimal) -> int:
    # Integer paise for gateways/providers
    return int((x * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))


class RechargePerformAPI(APIView):
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        user = request.user
        data = request.data

        # ---- Required inputs ----
        try:
            service  = (data.get("service") or "").lower()
            mobile   = data.get("mobile") or ""
            operator = data.get("operator") or ""
            circle   = data.get("circle") or ""
            amount   = q2(_d(data.get("amount", "0")))   # << keep as Decimal(2dp)

            if not (service and mobile and operator and circle):
                return Response({"status": "FAILED", "message": "Missing required fields"}, status=400)
            if amount <= 0:
                return Response({"status": "FAILED", "message": "Invalid amount"}, status=400)
        except (InvalidOperation, TypeError, ValueError):
            return Response({"status": "FAILED", "message": "Invalid amount format"}, status=400)

        # ---- Wallet usage flags ----
        use_external = bool(data.get("use_external", False))
        use_viralpe  = bool(data.get("use_viralpe", False))

        # ---- Optional Razorpay (hybrid) ----
        rzp_order_id   = data.get("razorpay_order_id") or None
        rzp_payment_id = data.get("razorpay_payment_id") or None
        rzp_signature  = data.get("razorpay_signature") or None
        has_rzp = bool(rzp_order_id and rzp_payment_id and rzp_signature)

        # ----- Determine order_id & razorpay_paid -----
        if has_rzp:
            try:
                tx = PaymentTransaction.objects.get(razorpay_order_id=rzp_order_id, user=user)
            except PaymentTransaction.DoesNotExist:
                return Response({"status": "FAILED", "message": "Unknown Razorpay order"}, status=404)

            if tx.status not in ("verified", "success"):
                return Response({"status": "FAILED", "message": "Payment not verified yet"}, status=400)

            order_id = tx.order_id
            razorpay_paid = q2(_d(tx.amount))  # tx.amount stored in rupees; keep as Decimal(2dp)
        else:
            order_id = f"VP-{user.id}-{mobile[-4:] if len(mobile) >= 4 else 'XXXX'}-{uuid.uuid4().hex[:6].upper()}"
            razorpay_paid = q2(Decimal("0"))

        # ---- Wallet requirement & splits ----
        wallet_required = q2(amount - razorpay_paid)
        if wallet_required < 0:
            wallet_required = Decimal("0.00")

        ext_balance, vp_balance = get_wallet_balances(user)  # MUST return Decimal
        ext_to_use = q2(Decimal("0"))
        vp_to_use  = q2(Decimal("0"))

        remaining = wallet_required
        if remaining > 0 and use_external:
            ext_to_use = q2(min(ext_balance, remaining))
            remaining  = q2(remaining - ext_to_use)

        if remaining > 0 and use_viralpe:
            vp_to_use = q2(min(vp_balance, remaining))
            remaining = q2(remaining - vp_to_use)

        if remaining > 0:
            return Response(
                {"status": "FAILED", "message": f"Insufficient wallet balance; need {q2(remaining)} more"},
                status=400,
            )

        # Sanity: exact conservation
        if q2(ext_to_use + vp_to_use + razorpay_paid) != amount:
            # Correct minor rounding drift (e.g., ₹0.01)
            drift = q2(amount - (ext_to_use + vp_to_use + razorpay_paid))
            # Prefer to add/subtract drift to the last non-zero component
            if vp_to_use > 0:
                vp_to_use = q2(vp_to_use + drift)
            elif ext_to_use > 0:
                ext_to_use = q2(ext_to_use + drift)
            else:
                razorpay_paid = q2(razorpay_paid + drift)

        # ---- Provider/client txn id ----
        client_txnid = generate_unique_provider_txnid(prefix="VP", length=10)

        # ---- Record summary (store as floats only if your model uses FloatField) ----
        RechargePaymentSummary.objects.update_or_create(
            order_id=order_id,
            defaults={
                "user": user,
                "recharge_amount": float(amount),          # or DecimalField in model (recommended)
                "used_external_wallet": float(ext_to_use),
                "used_internal_wallet": float(vp_to_use),
                "paid_via_gateway": float(razorpay_paid),
                "gateway_reference": rzp_payment_id if has_rzp else None,
                "updated_at": now(),
            },
        )

        # ---- Deduct wallets ----
        try:
            if ext_to_use > 0 or vp_to_use > 0:
                # keep Decimal all the way in your wallet functions too
                deduct_wallets(request, amount, ext_to_use, vp_to_use, order_id, client_txnid)
        except Exception as e:
            return Response({"status": "FAILED", "message": f"Wallet deduction failed: {e}"}, status=400)

        # ---- Prepare payload for performer (NO floats) ----
        enriched = {
            "service": service,
            "number":  mobile,
            "operator": operator,
            "circle":   circle,

            # Keep both rupees (string/Decimal-safe) and paise (int) for downstream
            "amount_rupees": str(amount),          # e.g. "1353.46" (never loses cents)
            "amount_paise":  to_paise(amount),     # e.g. 135346

            # Wallet splits (rupees)
            "use_ext_wallet": str(ext_to_use),
            "use_viralpe_wallet": str(vp_to_use),

            # Razorpay refs + amounts
            "razorpay_amount_rupees": str(razorpay_paid),
            "razorpay_amount_paise":  to_paise(razorpay_paid),
            "razorpay_order_id": rzp_order_id,
            "razorpay_payment_id": rzp_payment_id,
            "razorpay_signature": rzp_signature,

            "plan_id": data.get("plan_id"),
        }

        # ---- Perform recharge ----
        try:
            result = perform_wallet_recharge(request, enriched, order_id, client_txnid)
        except Exception as e:
            return Response({"status": "FAILED", "message": f"Provider call failed: {e}"}, status=502)

        # ---- Response ----
        status_out   = (result.get("status") or "").upper() or "PENDING"
        message      = result.get("message") or "Processed"
        provider_ref = result.get("provider_ref") or result.get("opid") or ""
        transaction_id = result.get("transaction_id") or client_txnid

        if status_out in {"FAILED", "ERROR"}:
            return Response({"status": "FAILED", "transaction_id": transaction_id, "message": message}, status=200)

        return Response(
            {
                "status": status_out,
                "transaction_id": transaction_id,
                "provider_ref": provider_ref,
                "message": message,
                "receipt": {
                    "operator": operator,
                    "mobile": mobile,
                    "amount": str(amount),     # return as string to preserve 2dp exactly
                    "time": now().isoformat(),
                },
            },
            status=200,
        )

# recharge/apis/status.py (drop-in)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status as http_status
from django.utils import timezone
from datetime import timedelta
from decimal import Decimal

from recharge.services.goterpay_client import get_goterpay_client
from recharge.models import RechargeTransaction
from payments.models import RechargePaymentSummary
from recharge.services.provider_utils import get_source_value
from recharge.services.recharge_flow import handle_recharge_outcome

import logging
log = logging.getLogger(__name__)

# ---- Tunables ---------------------------------------------------------------
GRACE_SECONDS = 15             # local Pending window before first provider call
FAIL_GRACE_SECONDS = 25        # suppress early provider FAILED while locally Pending & young
NO_LOCAL_TX_HARD_FAIL_AGE = 120  # after this age (s), allow provider FAILED even if no local tx
REQUIRE_CONSECUTIVE_FAILS = True # require 2 provider FAILs before persisting Failure
# ---------------------------------------------------------------------------

GOTER_TO_MODEL_STATUS = {
    "SUCCESS": "Success",
    "FAILED":  "Failure",
    "PENDING": "Pending",
    "ERROR":   "Error",
}
STATUS_RANK = {"Pending": 1, "Error": 2, "Failure": 3, "Success": 4}

def _normalize_status(value: str | None) -> str:
    s = (value or "").strip().lower()
    if s in {"success", "successful", "completed", "ok", "done"}: return "SUCCESS"
    if s in {"failed", "failure", "fail", "declined"}:            return "FAILED"
    if s in {"pending", "processing", "initiated", "inprocess", "queued"}: return "PENDING"
    if s in {"error", "timeout", "timedout", "cancelled", "canceled"}:     return "ERROR"
    return "PENDING"  # unknown => treat as still in progress

def _display_message(normalized: str, res_text: str | None) -> str:
    if normalized == "SUCCESS": return (res_text or "OK").strip() or "OK"
    if normalized == "PENDING": return "Queued"
    if normalized == "ERROR":   return (res_text or "Processing").strip() or "Processing"
    return (res_text or "Failed").strip() or "Failed"

def _is_terminal(normalized: str) -> bool:
    return normalized in {"SUCCESS", "FAILED"}

def _should_update(old_model_status: str | None, new_model_status: str) -> bool:
    old = (old_model_status or "Pending")
    return STATUS_RANK.get(new_model_status, 0) >= STATUS_RANK.get(old, 0)


class RechargeStatusAPI(APIView):
    # authentication_classes = [TokenAuthentication]
    # permission_classes = [IsAuthenticated]
    # log.info(" =============================== Just entered Recharge Status API ==================================")
    def get(self, request):
        txnid = request.query_params.get("transaction_id")
        log.info("txnid %s ", txnid)
        if not txnid:
            return Response({"status": "ERROR", "message": "transaction_id is required"},
                            status=http_status.HTTP_400_BAD_REQUEST)

        now_ts = timezone.now()
        log.info("STATUS %s start ts=%s", txnid, now_ts.isoformat())
        
        # always have something to return
        response_data = ""   # or {} if you prefer dicts

        try:
            # 0) LOCAL SHORT-CIRCUIT
            tx = RechargeTransaction.objects.filter(client_txn_id=txnid).first()
            if tx:
                # prefer echoing last known data
                response_data = tx.response_data if tx.response_data else ""
                ts = getattr(tx, "status_updated_at", None) or getattr(tx, "created_at", None)
                age_sec = (now_ts - ts).total_seconds() if ts else None
                model_status = (tx.status or "Pending")
                display_msg = (tx.status_message or ("Queued" if model_status == "Pending" else model_status))
                log.info("------------------ Response data -----------------")
                log.info(response_data)
                # log.info("STATUS %s local tx: status=%s age=%.1fs created=%s updated=%s",
                #          txnid, model_status, (age_sec or -1),
                #          getattr(tx, "created_at", None), getattr(tx, "status_updated_at", None))

                # Terminal locally? stop polling
                if model_status in {"Success", "Failure"}:
                    api_status = "SUCCESS" if model_status == "Success" else "FAILED"
                    log.info("STATUS %s LOCAL terminal -> %s", txnid, api_status)
                    return Response({"status": api_status, "message": display_msg, "responsedata":response_data, "terminal": True},
                                    status=http_status.HTTP_200_OK)

                # Fresh Pending? hold off provider
                if model_status == "Pending" and (age_sec is None or age_sec < GRACE_SECONDS):
                    log.info("STATUS %s LOCAL pending within grace %ss -> PENDING", txnid, GRACE_SECONDS)
                    return Response({"status": "PENDING", "message": "Queued", "responsedata":response_data, "terminal": False},
                                    status=http_status.HTTP_200_OK)
            else:
                log.info("STATUS %s no local tx row yet", txnid)

            # 1) CALL PROVIDER
            log.info("STATUS %s calling provider…", txnid)
            client = get_goterpay_client()
            raw = client.status(txnid)
            log.info("RAW %s", raw)

            normalized = _normalize_status(raw.get("status") or raw.get("Status") or raw.get("STATUS"))
            model_from_provider = GOTER_TO_MODEL_STATUS.get(normalized, "Error")
            res_text = raw.get("resText") or raw.get("message") or ""
            display_msg = _display_message(normalized, res_text)
            provider_refid = raw.get("RefId") or raw.get("refId") or ""
            goter_order_id = raw.get("orderId") or ""
            provider_commission = raw.get("Comi") or 0
            log.info("STATUS %s provider -> normalized=%s, resText=%r, refId=%s, orderId=%s",
                     txnid, normalized, res_text, provider_refid, goter_order_id)

            # 2) STILL NO LOCAL TX? decide safely
            if not tx:
                # Allow SUCCESS to pass through immediately so UI can stop polling quickly.
                # for transparency you can return the provider payload
                response_data = raw  # or keep "" if you don’t want to expose
                if normalized == "SUCCESS":
                    log.info("STATUS %s no-local -> SUCCESS passthrough", txnid)
                    return Response({"status": "SUCCESS", "message": display_msg, "responsedata":response_data, "terminal": True},
                                    status=http_status.HTTP_200_OK)

                # If provider screams FAILED/ERROR but we have no local row (likely race),
                # treat as PENDING for a while.
                # After NO_LOCAL_TX_HARD_FAIL_AGE seconds we allow provider failure through.
                first_seen_at = raw.get("_first_seen_at")  # we don't actually have this; left for future use
                # conservatively gate FAILED/ERROR early:
                log.info("STATUS %s no-local -> normalized=%s (return PENDING, unless you've decided to hard-fail late)",
                         txnid, normalized)
                return Response({"status": "PENDING", "message": "Queued", "responsedata":response_data, "terminal": False},
                                status=http_status.HTTP_200_OK)

            # 3) FROM HERE, WE HAVE A LOCAL TX
            current_model = (tx.status or "Pending")

            # Age since *last* status change (prefer status_updated_at)
            ts = getattr(tx, "status_updated_at", None) or getattr(tx, "created_at", None)
            age = (now_ts - ts) if ts else None
            young_pending = (current_model == "Pending" and age and age.total_seconds() < FAIL_GRACE_SECONDS)
            log.info("STATUS %s current=%s age=%.1fs young_pending=%s",
                     txnid, current_model, (age.total_seconds() if age else -1), young_pending)

            # 3a) *Guard 1*: early FAILED suppression while young & pending
            if normalized == "FAILED" and young_pending:
                tx.response_data = {**(tx.response_data or {}), "last_status_poll": raw}
                tx.status_updated_at = now_ts
                tx.save(update_fields=["response_data", "status_updated_at"])
                log.info("STATUS %s suppress early FAILED (young pending) -> return PENDING", txnid)
                return Response({"status": "PENDING", "message": "Queued", "responsedata":response_data,  "terminal": False},
                                status=http_status.HTTP_200_OK)

            # 3b) *Guard 2*: require two consecutive FAILs (if enabled)
            fails = int((tx.response_data or {}).get("_fail_count", 0))
            if REQUIRE_CONSECUTIVE_FAILS and normalized == "FAILED" and current_model == "Pending" and not young_pending:
                if fails < 1:
                    tx.response_data = {**(tx.response_data or {}), "_fail_count": fails + 1, "last_status_poll": raw}
                    tx.status_updated_at = now_ts
                    tx.save(update_fields=["response_data", "status_updated_at"])
                    log.info("STATUS %s FIRST provider FAIL seen -> keep PENDING; will require next FAIL to persist",
                             txnid)
                    return Response({"status": "PENDING", "message": "Queued", "responsedata":response_data,  "terminal": False},
                                    status=http_status.HTTP_200_OK)

            # clear the counter on any non-FAILED
            if normalized != "FAILED" and fails:
                tx.response_data = {**(tx.response_data or {}), "_fail_count": 0, "last_status_poll": raw}
                tx.status_updated_at = now_ts
                tx.save(update_fields=["response_data", "status_updated_at"])
                log.info("STATUS %s cleared _fail_count after non-FAILED provider reply (%s)", txnid, normalized)

            # 4) APPLY MONOTONIC UPDATE
            if (current_model.lower() != model_from_provider.lower()) and _should_update(current_model, model_from_provider):
                # Upgrade or lateral move (Pending->Error/Failure/Success; Error->Failure/Success; Failure->Success)
                tx.status = model_from_provider
                tx.status_message = display_msg
                tx.status_updated_at = now_ts
                tx.response_data = {**(tx.response_data or {}), "last_status_poll": raw}
                if provider_refid and not tx.provider_order_id:
                    tx.provider_order_id = provider_refid
                tx.save(update_fields=["status","status_message","status_updated_at","response_data","provider_order_id"])
                log.info("STATUS %s persisted change: %s -> %s", txnid, current_model, model_from_provider)

                # Sync summary too
                summary = RechargePaymentSummary.objects.filter(order_id=tx.order_id).first()
                if summary:
                    summary.recharge_status = model_from_provider
                    summary.status_message = display_msg
                    summary.full_response = {**(summary.full_response or {}), "last_status_poll": raw}
                    summary.updated_at = now_ts
                    if provider_refid and not summary.gateway_reference:
                        summary.gateway_reference = provider_refid
                    summary.save(update_fields=["recharge_status","status_message","full_response","updated_at","gateway_reference"])
                    log.info("STATUS %s summary synced to %s", txnid, model_from_provider)
            else:
                # unchanged (or attempted downgrade) — just stamp the poll meta
                tx.response_data = {**(tx.response_data or {}), "last_status_poll": raw}
                tx.status_updated_at = now_ts
                tx.save(update_fields=["response_data","status_updated_at"])
                log.info("STATUS %s no status change (current=%s, provider=%s)", txnid, current_model, model_from_provider)

            # 5) TERMINAL SIDE EFFECTS (exactly once)
            if _is_terminal(normalized) and getattr(tx, "side_effects_done_at", None) is None:
                vendor_code = get_source_value()
                summary = RechargePaymentSummary.objects.filter(order_id=tx.order_id).first()
                user = getattr(tx, "user", None) or (summary.user if summary else None)
                paid_via_gateway = (summary.paid_via_gateway if summary else Decimal("0.00"))
                gateway_ref = (
                    (summary.gateway_reference if summary else None)
                    or tx.razorpay_payment_id
                    or provider_refid
                    or goter_order_id
                    or txnid
                )
                try:
                    handle_recharge_outcome(
                        user=user,
                        transaction=tx,
                        order_id=tx.order_id,
                        status_title=GOTER_TO_MODEL_STATUS[normalized],  # "Success"/"Failure"
                        status_message=display_msg,
                        amount=tx.amount,
                        operator=(tx.operator.code if getattr(tx, "operator", None) else ""),
                        vendor_code=vendor_code,
                        gateway_ref=gateway_ref,
                        paid_via_gateway=paid_via_gateway,
                        service=getattr(tx, "service_type", None) or "",
                        provider_commission =provider_commission,
                    )
                    tx.side_effects_done_at = now_ts
                    tx.save(update_fields=["side_effects_done_at"])
                    log.info("STATUS %s ran side-effects for terminal=%s", txnid, normalized)
                except Exception as e:
                    log.exception("STATUS %s side-effects error: %s", txnid, e)

            # 6) FINAL RESPONSE TO APP
            final_model = (tx.status or "Pending")
            api_status = (
                "SUCCESS" if final_model == "Success"
                else "FAILED" if final_model == "Failure"
                else "PENDING" if final_model == "Pending"
                else "ERROR"
            )
            log.info("STATUS %s respond -> %s, msg=%r, terminal=%s",
                     txnid, api_status, (tx.status_message or display_msg), api_status in {"SUCCESS","FAILED"})

            return Response(
                {
                    "status": api_status,
                    "message": tx.status_message or display_msg,
                    "responsedata":response_data, 
                    "terminal": api_status in {"SUCCESS", "FAILED"},
                },
                status=http_status.HTTP_200_OK
            )

        except Exception as e:
            log.exception("STATUS %s error: %s", txnid, e)
            return Response({"status": "ERROR", "message": str(e)}, status=http_status.HTTP_500_INTERNAL_SERVER_ERROR)

from django.utils.timezone import localtime
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
from rest_framework import status

from payments.models import RechargePaymentSummary


class RechargeHistoryAPI(APIView):
    """
    GET /api/recharge/history?service=prepaid&limit=10

    Response:
    {
      "items": [
        {"txn_id":"TXN_...","status":"SUCCESS","amount":399,"mobile":"98XXXXXXXX","at":"2025-08-16 18:45"}
      ]
    }

    Note: `service` is accepted for parity but ignored here (no field on model).
    """
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def get(self, request):
        # Optional params
        _ = (request.query_params.get("service") or "").strip()  # not used (no field in model)
        try:
            limit = int(request.query_params.get("limit", 10))
        except ValueError:
            limit = 10
        limit = max(1, min(limit, 50))

        qs = (
            RechargePaymentSummary.objects
            .filter(user=request.user)
            .order_by("-updated_at", "-id")[:limit]
        )

        def fmt_dt(dt):
            return localtime(dt).strftime("%Y-%m-%d %H:%M") if dt else None

        items = []
        for s in qs:
            # txn_id: use your internal order_id (unique) as a stable id
            txn_id = s.order_id

            # status: from recharge_status
            status_val = (s.recharge_status or "pending").upper()

            # amount
            amount = float(s.recharge_amount or 0)

            # mobile best-effort from stored response (common keys tried)
            mobile = ""
            if isinstance(s.full_response, dict):
                fr = s.full_response
                mobile = (
                    fr.get("number")
                    or fr.get("mobile")
                    or fr.get("msisdn")
                    or ""
                )

            items.append({
                "txn_id": txn_id,
                "status": status_val,
                "amount": amount,
                "mobile": mobile,
                "at": fmt_dt(s.updated_at),
            })

        return Response({"items": items}, status=status.HTTP_200_OK)
