# utilities\views.py
import uuid
from typing import Any, Dict, Optional  # <-- add Optional here
from .serializers import (
    ProvidersQuery, BillValidateReq, PerformBillPayReq, StatusQuery
)

# your static schemas for the app UI
from utilities.provider_catalog import CATALOG
from utilities.field_mapping import map_fields

# your existing client/wrapper
from recharge.services.goterpay_client import get_goterpay_client
from recharge.services.goterpay_api import GoterPayAPI, GoterPayError
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status as http_status
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication

from .serializers import ProvidersQuery
from utilities.provider_fields import build_fields_for_operator

from recharge.models import Operator  # your model

from utilities.provider_catalog import CATALOG  # keep your static as a fallback

class BillsProvidersAPI(APIView):
    """
    GET /api/bills/providers/?service=fastag
    Filters by source='Goter' (as you noted) and service_type=<service>.
    """
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def get(self, request):
        q = ProvidersQuery(data=request.query_params)
        q.is_valid(raise_exception=True)
        service = q.validated_data["service"]  # normalized & validated

        qs = (
            Operator.objects
            .filter(source__iexact="Goter", service_type__iexact=service)
            .order_by("name")
            .only("name", "code", "image_url", "number_label", "optional1_label", "service_type")  # projection hint
        )

        providers = []
        for op in qs:
            providers.append({
                "code": op.code,                       # <- used by Android to call validate/pay
                "name": op.name,
                "logoUrl": op.image_url,
                "fields": build_fields_for_operator(service, op),
            })

        if not providers:
            providers = CATALOG.get(service, [])

        return Response({"service": service, "providers": providers}, status=200)


from typing import Any, Dict, Optional  # <-- add Optional here
import uuid

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status as http_status

# if you temporarily disabled auth, fine; add them back later
# from rest_framework.permissions import IsAuthenticated
# from rest_framework.authentication import TokenAuthentication

from .serializers import BillValidateReq
from utilities.field_mapping import map_fields
from recharge.services.goterpay_client import get_goterpay_client

# utilities/views.py
from typing import Any, Dict, Optional
import uuid

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status as http_status
# auth lines OK to keep disabled for testing

from .serializers import BillValidateReq
from utilities.field_mapping import map_fields
from utilities.ids import make_provider_txn_id
from recharge.services.goterpay_client import get_goterpay_client
from recharge.services.goterpay_api import GoterPayError

class BillValidateAPI_without_dth(APIView):
    def post(self, request):
        try:
            s = BillValidateReq(data=request.data)
            s.is_valid(raise_exception=True)
            data: Dict[str, Any] = s.validated_data

            service: str = data["service"]
            provider: str = data["provider"]
            fields: Dict[str, str] = data.get("fields") or {}

            number: str = map_fields(service, fields)
            if not number:
                return Response({"detail": "Missing required fields for this service"}, status=400)

            # <=10 chars txn id
            client_txn_id = make_provider_txn_id(prefix="VP", max_len=10)

            api = get_goterpay_client()
            try:
                res = api.bill_fetch(
                    number=number,
                    operator_code=provider,
                    txnid=client_txn_id,
                    optional1=None,  # ignoring optional by design
                ) or {}
            except GoterPayError as e:
                return Response(
                    {"status": "FAILED", "message": str(e), "payload": getattr(e, "payload", None)},
                    status=getattr(e, "status_code", None) or http_status.HTTP_502_BAD_GATEWAY
                )

            # ---- helpers ----
            def pick(d: Dict[str, Any], *keys):
                for k in keys:
                    if k in d and d[k] not in (None, ""):
                        return d[k]
                lower = {str(k).lower(): v for k, v in d.items()}
                for k in keys:
                    v = lower.get(k.lower())
                    if v not in (None, ""):
                        return v
                return None

            def s_(val: Any) -> Optional[str]:
                if val is None:
                    return None
                t = str(val).strip()
                return t or None

            # ---- pass-through + normalized fields ----
            operator_name = s_(pick(res, "operator")) or provider
            status_raw    = s_(pick(res, "status", "Status", "STATUS")) or ""
            status_norm   = status_raw.upper() if status_raw else ""
            message       = s_(pick(res, "message", "msg", "remark", "desc")) or ""

            customer_name = s_(pick(res, "customerName", "customername", "customer_name", "name", "username", "consumername"))
            amount        = s_(pick(res, "amount", "billamount", "bill_amount", "dueAmount", "total_amount", "amt", "payable"))
            due_date      = s_(pick(res, "dueDate", "duedate", "due_date", "bill_due_date", "due"))
            ref_id        = s_(pick(res, "refId", "refid", "reference", "ref"))

            # details map for UI
            details: Dict[str, str] = {}
            def add(label: str, *keys):
                v = pick(res, *keys)
                if v not in (None, ""):
                    details[label] = str(v)

            add("billNumber", "billNumber", "billnumber", "bill_no", "billno")
            add("billDate",   "billDate", "billdate")
            add("billPeriod", "billPeriod", "bill_period")
            add("vehicleNo",  "vehicle_number", "vehicleNumber", "vehicleno", "vehicle_no")
            add("accountId",  "account_id", "accountid", "account", "usn", "ca_number")
            if ref_id:
                details["refId"] = ref_id

            # editable: pass through if present; otherwise infer (true when no amount)
            editable = bool(res.get("editable")) if "editable" in res else (amount in (None, ""))

            # valid: SUCCESS|PENDING or any meaningful data
            valid = bool(
                amount or customer_name or (status_norm in ("SUCCESS", "PENDING"))
            )

            return Response({
                # normalized fields your app already uses
                "valid": valid,
                "customerName": customer_name,
                "amount": amount,        # will be "1.0" for your sample
                "dueDate": due_date,
                "details": details,

                # pass-throughs to “maintain” provider response semantics
                "status": status_norm or ("SUCCESS" if valid else "FAILED"),
                "message": message,
                "operator": operator_name,
                "refId": ref_id,
                "editable": editable,

                # full raw for visibility/debug (optional but handy)
                "raw": res
            }, status=200)

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


class BillValidateAPI(APIView):
    def post(self, request):
        try:
            s = BillValidateReq(data=request.data)
            s.is_valid(raise_exception=True)
            data: Dict[str, Any] = s.validated_data
            print(data)    
            service: str = data["service"]
            provider: str = data["provider"]
            fields: Dict[str, str] = data.get("fields") or {}
            print("====================== BillValidate ======================")
            print(service)
            print(provider)
            print(fields)
            number: str = map_fields(service, fields)
            print(number)
            if not number:
                return Response({"detail": "Missing required fields for this service"}, status=400)

            api = get_goterpay_client()
            is_dth = str(service).strip().lower() in {"dth", "dth_tv", "dthinfo"}

            try:
                if is_dth:
                    # Use DTH info lookup (no txnid needed)
                    res = api.dth_info(operator_code=provider, number=number) or {}
                    print(res)
                else:
                    # BillFetch (electricity/water/loan/insurance/etc.)
                    client_txn_id = make_provider_txn_id(prefix="VP", max_len=10)  # <=10 chars
                    res = api.bill_fetch(
                        number=number,
                        operator_code=provider,
                        txnid=client_txn_id,
                        optional1=None,  # keep ignoring optional for now
                    ) or {}
            except GoterPayError as e:
                return Response(
                    {"status": "FAILED", "message": str(e), "payload": getattr(e, "payload", None)},
                    status=getattr(e, "status_code", None) or http_status.HTTP_502_BAD_GATEWAY
                )

            # ---- helpers (unchanged) ----
            def pick(d: Dict[str, Any], *keys):
                for k in keys:
                    if k in d and d[k] not in (None, ""):
                        return d[k]
                lower = {str(k).lower(): v for k, v in d.items()}
                for k in keys:
                    v = lower.get(k.lower())
                    if v not in (None, ""):
                        return v
                return None

            def s_(val: Any) -> Optional[str]:
                if val is None:
                    return None
                t = str(val).strip()
                return t or None

            # ---- normalized fields (works for both BillFetch and DTH info) ----
            operator_name = s_(pick(res, "operator")) or provider
            status_raw    = s_(pick(res, "status", "Status", "STATUS")) or ""
            status_norm   = status_raw.upper() if status_raw else ""
            message       = s_(pick(res, "message", "msg", "remark", "desc")) or ""

            # For DTH, many providers return "name"/"customer"/"account_name"
            customer_name = s_(pick(
                res, "customerName", "customername", "customer_name", "name", "username", "consumername", "customer"
            ))

            amount   = s_(pick(res, "amount", "billamount", "bill_amount", "dueAmount", "total_amount", "amt", "payable"))
            due_date = s_(pick(res, "dueDate", "duedate", "due_date", "bill_due_date", "due"))
            ref_id   = s_(pick(res, "refId", "refid", "reference", "ref"))

            # details for UI (extend with common DTH keys)
            details: Dict[str, str] = {}
            def add(label: str, *keys):
                v = pick(res, *keys)
                if v not in (None, ""):
                    details[label] = str(v)

            add("billNumber", "billNumber", "billnumber", "bill_no", "billno")
            add("billDate",   "billDate", "billdate")
            add("billPeriod", "billPeriod", "bill_period")
            add("vehicleNo",  "vehicle_number", "vehicleNumber", "vehicleno", "vehicle_no")
            add("accountId",  "account_id", "accountid", "account", "usn", "ca_number", "subscriberId", "vc_no", "card_no")
            if ref_id:
                details["refId"] = ref_id

            # editable remains consistent with your original rule:
            # explicit "editable" from provider or inferred true when no amount
            editable = bool(res.get("editable")) if "editable" in res else (amount in (None, ""))

            # valid for both flows: we got an amount or a name, or status success/pending
            valid = bool(amount or customer_name or (status_norm in ("SUCCESS", "PENDING")))

            return Response({
                "valid": valid,
                "customerName": customer_name,
                "amount": amount,          # DTH typically None → editable True (enter top-up)
                "dueDate": due_date,
                "details": details,

                "status": status_norm or ("SUCCESS" if valid else "FAILED"),
                "message": message,
                "operator": operator_name,
                "refId": ref_id,
                "editable": editable,

                "raw": res,                # keep raw for visibility/debug
            }, status=200)

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


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

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

from recharge.models import Operator, ROfferCache, DTHInfoMap  # add if you created the models above
from recharge.services.goterpay_client import get_goterpay_client   # :contentReference[oaicite:2]{index=2}
from recharge.services.goterpay_api import GoterPayError, GoterPayAPI # :contentReference[oaicite:3]{index=3}

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

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

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 _resolve_operator(op_code: str | None):
    """Try to enrich operator code with friendly name/image from our Operator table."""
    if not op_code:
        return None, None, None
    op = Operator.objects.filter(source=PROVIDER_SOURCE).filter(
        Q(code__iexact=op_code) | Q(name__iexact=op_code)
    ).only("code", "name", "image_url").first()
    if op:
        return op.code, op.name, op.image_url
    return op_code, None, None


class ROfferAPI(APIView):
    """
    GET /api/recharge/r-offer?operator=AT&number=9xxxxxxxxx[&refresh=1][&include_raw=1]
    Returns: { status, source, served_from, operator, operator_name, operator_image_url, number, offers?, message?, raw? }
    """
    CACHE_SECONDS = 6 * 60 * 60  # 6h

    def get(self, request):
        operator = _norm(request.query_params.get("operator"))
        raw_number = request.query_params.get("number") or request.query_params.get("mobile")
        refresh = request.query_params.get("refresh") in ("1", "true", "True")
        include_raw = request.query_params.get("include_raw") in ("1", "true", "True")

        if not operator:
            return Response({"detail": "Provide ?operator= (e.g., AT or VI)."}, status=status.HTTP_400_BAD_REQUEST)

        # ROffer is for mobile numbers → normalize + enforce 10 digits
        number = normalize_mobile(raw_number)
        if len(number) != 10:
            return Response({"detail": "Provide a valid 10-digit mobile in ?number= or ?mobile=."},
                            status=status.HTTP_400_BAD_REQUEST)

        # 0) Memory cache
        ckey = f"roffer:{operator}:{number}"
        if not refresh:
            cached = cache.get(ckey)
            if cached:
                cached["served_from"] = "memory"
                return Response(cached, status=status.HTTP_200_OK)

        # 1) DB cache
        row = None
        if not refresh and 'ROfferCache' in globals():
            row = ROfferCache.objects.filter(operator_code=operator, number=number).only(
                "operator_code", "number", "offers", "last_payload", "lookups"
            ).first()
            if row:
                op_code, op_name, op_img = _resolve_operator(row.operator_code)
                data = {
                    "source": PROVIDER_SOURCE,
                    "status": "SUCCESS",  # cached hit ⇒ previously succeeded
                    "operator": op_code,
                    "operator_name": op_name,
                    "operator_image_url": op_img,
                    "number": number,
                    "offers": row.offers,   # whatever we normalized earlier
                    "served_from": "db",
                }
                cache.set(ckey, data, self.CACHE_SECONDS)
                ROfferCache.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()  # raises ImproperlyConfigured if settings missing
        try:
            resp = client.r_offer(operator_code=operator, number=number)
        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_norm = GoterPayAPI.normalize_status(resp.get("status") or resp.get("STATUS"))
        op_code, op_name, op_img = _resolve_operator(operator)

        # Normalize “offers” field (providers vary; capture common keys or fall back)
        offers = resp.get("offers") or resp.get("data") or resp.get("plans") or None

        data = {
            "source": PROVIDER_SOURCE,
            "status": status_norm,
            "operator": op_code,
            "operator_name": op_name,
            "operator_image_url": op_img,
            "number": number,
            "offers": offers,
            "message": resp.get("message") or resp.get("msg"),
            "served_from": "provider",
        }
        if include_raw:
            data["raw"] = resp

        # 3) Upsert DB cache (optional)
        if 'ROfferCache' in globals() and status_norm in ("SUCCESS", "PENDING"):
            with transaction.atomic():
                row, created = ROfferCache.objects.select_for_update().get_or_create(
                    operator_code=operator, number=number,
                    defaults={"offers": offers, "last_payload": resp, "lookups": 1}
                )
                if not created:
                    row.offers = offers
                    row.last_payload = resp
                    row.lookups = (row.lookups or 0) + 1
                    row.save()

        # 4) Memory cache
        cache.set(ckey, data, self.CACHE_SECONDS)
        return Response(data, status=status.HTTP_200_OK)


class DTHInfoAPI(APIView):
    """
    GET /api/recharge/dth-info?operator=AD&number=3003451289[&refresh=1][&include_raw=1]
    Returns: { status, source, served_from, operator, operator_name, operator_image_url, number, account_name?, message?, details/raw? }
    """
    CACHE_SECONDS = 12 * 60 * 60  # 12h

    def get(self, request):
        operator = _norm(request.query_params.get("operator"))
        raw_number = request.query_params.get("number")
        refresh = request.query_params.get("refresh") in ("1", "true", "True")
        include_raw = request.query_params.get("include_raw") in ("1", "true", "True")

        if not operator:
            return Response({"detail": "Provide ?operator= (e.g., AD for Airtel Digital)."},
                            status=status.HTTP_400_BAD_REQUEST)
        if not raw_number:
            return Response({"detail": "Provide ?number= (DTH subscriber/card/VC number)."},
                            status=status.HTTP_400_BAD_REQUEST)

        # For DTH, just strip non-digits; length varies by provider.
        number = MOBILE_NON_DIGITS.sub("", raw_number)
        if len(number) < 5 or len(number) > 20:
            return Response({"detail": "Provide a valid DTH number (5–20 digits)."},
                            status=status.HTTP_400_BAD_REQUEST)

        # 0) Memory cache
        ckey = f"dthinfo:{operator}:{number}"
        if not refresh:
            cached = cache.get(ckey)
            if cached:
                cached["served_from"] = "memory"
                return Response(cached, status=status.HTTP_200_OK)

        # 1) DB cache
        row = None
        if not refresh and 'DTHInfoMap' in globals():
            row = DTHInfoMap.objects.filter(operator_code=operator, number=number).only(
                "operator_code", "number", "account_name", "status_message", "last_payload", "lookups"
            ).first()
            if row:
                op_code, op_name, op_img = _resolve_operator(row.operator_code)
                data = {
                    "source": PROVIDER_SOURCE,
                    "status": "SUCCESS",
                    "operator": op_code,
                    "operator_name": op_name,
                    "operator_image_url": op_img,
                    "number": number,
                    "account_name": row.account_name,
                    "message": row.status_message,
                    "details": row.last_payload,  # safe to expose cached details
                    "served_from": "db",
                }
                cache.set(ckey, data, self.CACHE_SECONDS)
                DTHInfoMap.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.dth_info(operator_code=operator, number=number)
        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_norm = GoterPayAPI.normalize_status(resp.get("status") or resp.get("STATUS"))
        op_code, op_name, op_img = _resolve_operator(operator)

        # Best-effort extraction (providers differ)
        account_name = resp.get("name") or resp.get("account_name") or resp.get("customer") or None
        msg = resp.get("message") or resp.get("msg")

        data = {
            "source": PROVIDER_SOURCE,
            "status": status_norm,
            "operator": op_code,
            "operator_name": op_name,
            "operator_image_url": op_img,
            "number": number,
            "account_name": account_name,
            "message": msg,
            "details": resp if include_raw else None,
            "served_from": "provider",
        }
        if data["details"] is None:
            data.pop("details")

        # 3) Upsert DB cache (optional)
        if 'DTHInfoMap' in globals() and status_norm in ("SUCCESS", "PENDING"):
            with transaction.atomic():
                row, created = DTHInfoMap.objects.select_for_update().get_or_create(
                    operator_code=operator, number=number,
                    defaults={
                        "account_name": account_name,
                        "status_message": msg,
                        "last_payload": resp,
                        "lookups": 1,
                    }
                )
                if not created:
                    changed = False
                    if account_name and row.account_name != account_name:
                        row.account_name = account_name; changed = True
                    if msg and row.status_message != msg:
                        row.status_message = msg; changed = True
                    row.last_payload = resp
                    row.lookups = (row.lookups or 0) + 1
                    if changed:
                        row.provider_source = PROVIDER_SOURCE
                    row.save()

        # 4) Memory cache
        cache.set(ckey, data, self.CACHE_SECONDS)
        return Response(data, status=status.HTTP_200_OK)

# NOTE:
# You already have PaymentPerformAPI + RechargeStatusAPI implemented with wallets, threading, etc.
# Keep those. Just ensure your PaymentPerformAPI accepts `service` values like
# "postpaid", "fastag", "electricity", etc., and routes them to a Goter bill pay.
#
# Below is a **minimal patch** you can drop into your existing code:
#
# 1) Add this helper to perform the provider payment (called inside your background thread).
def _perform_goter_bill_payment(*, service: str, provider: str, fields: Dict[str, str],
                                amount: int, client_txn_id: str) -> Dict[str, Any]:
    """
    Calls Goter BillPay and returns:
      {
        'status': 'SUCCESS|PENDING|FAILED|ERROR',
        'provider_ref': '<TxnId/Ref/...>',
        'raw': {...}    # full provider payload
      }
    """
    # map_fields should return the primary number string for this service
    number: str = map_fields(service, fields)
    if not number or amount <= 0:
        raise ValueError("Invalid number/amount")

    api = get_goterpay_client()
    res = api.bill_pay(
        txnid=client_txn_id,      # so /Status can find it later
        number=number,
        amount=amount,
        operator_code=provider,
        # optional1 intentionally omitted per your note
    ) or {}

    status_norm = GoterPayAPI.normalize_status(res.get("status") or res.get("Status"))
    provider_ref = (
        res.get("TxnId") or res.get("txnid") or
        res.get("ref")   or res.get("RefId") or res.get("reference") or res.get("Reference") or
        ""
    )
    return {"status": status_norm, "provider_ref": provider_ref, "raw": res}
