from datetime import timedelta, timezone
import re
from django.utils import timezone 
from django.forms import ValidationError
from django.shortcuts import get_object_or_404, render, redirect
from django.views import View
from django.contrib.auth.models import User
from django.contrib import messages,auth
from django.http import Http404, HttpResponse
from django.conf import settings
from django.core.files.storage import default_storage
from django.db.models import Q
from buyers.models import CustomerBuyer
from codesofy.custom_config import rename_file,set_user_profile,get_standard_text_input,set_menu_items,get_privilleges,is_authorized,validate_email
from codesofy.custom_config import validate_telephone,validate_mobile,is_super_admin,validate_password,get_max_image_file_size_in_bytes,get_global_master_details
from codesofy.custom_config import get_local_date,get_local_date_time,get_unique_text,convert_to_decimal,generate_qr_code
from codesofy.master_details import CustomerRegistrationSource, CustomerRegistrationStatus, DensityUnit, JobRole,PerPageSelector, TemperatureUnit,UserAccountStatus
from codesofy.master_details import DateFilter,UserAccountStatus

from typing import Dict, List, Tuple
from django.apps import apps
from django.db import transaction, models
from core.models import AppSettings, DeviceMoistureThreshold, Language
from core.utils import get_api_message
from customermanagement.services import create_customer_from_post, delete_customer_with_related_data
from fieldmanagement.models import CustomerField
from mydevicemanagement.models import CustomerDevice, MyDeviceMoistureThreshold, shareDevice
from notification_engine.services.email_service import send_otp_via_email
from products.models import Product
from results.models import Result
from results.views import _day_bounds, _month_bounds, _year_bounds
from usermanagement.models import SystemRole
from usermanagement.models import UserProfile
from .models import Customer,EmailOTP
from django.utils import timezone as dj_tz
import datetime as dt
from organizationmanagement.models import Country, Branch
from django.db import transaction, IntegrityError
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.decorators import method_decorator
from django.core.paginator import Paginator
from django.db.models import RestrictedError
from django.views.decorators.cache import cache_control
from django.utils.decorators import method_decorator
import math
from constants.date_const import DateFilter
from constants.general_const import ActiveStatus
from binascii import a2b_base64
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework import status, permissions
from rest_framework_simplejwt.tokens import RefreshToken
from django.conf import settings
from django.contrib.auth.hashers import check_password, make_password
from .models import Customer, EmailOTP,PasswordResetToken
from .serializers import (
    PasswordChangeSerializer, ProfileUpdateSerializer, RegisterSerializer, VerifyOTPSerializer, ResendOTPSerializer,
    LoginSerializer, ForgotPasswordSerializer, ResetPasswordWithOtpSerializer,
)
from django.http import JsonResponse
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
from .otp_utils import OTP_EXPIRY_MINUTES, OTP_MAX_ATTEMPTS_PER_CODE, OTP_MAX_GLOBAL_FAILURES, RESEND_MIN_WAIT_SECONDS, RESEND_MAX_PER_HOUR, RESEND_MAX_PER_DAY, default_expiry, fmt_ts, generate_otp, hash_otp, matches_code
import logging
import sentry_sdk
from sentry_sdk import set_user, set_tag, set_context, capture_exception, capture_message

import os
from dotenv import load_dotenv
load_dotenv()

logger = logging.getLogger("customer") 
# Create your views here.

menu_item = "customer_management"


def get_active_counsellors():
    counsellors = UserProfile.objects.filter(user__is_active=True)
    return counsellors

def set_sub_menu_item(sub_menu_item,context):
    context = set_menu_items(menu_item,sub_menu_item,context)
    return context

def get_local_master_details():

    context = get_global_master_details()
    countries = Country.objects.all()
    branches = []

    if (len(countries) > 0):

        first_country = countries[0]
        branches = Branch.objects.filter(country=first_country)

    context["countries"] = countries
    context["branches"] = branches

    return context

def get_local_master_details_for_user_details():
    context = get_local_master_details()

    user_account_statuses = UserAccountStatus.to_list_for_reports()

  
    context["user_account_statuses"] = user_account_statuses

    return context



def get_local_master_details_for_add_update_customer():
    context = get_local_master_details()
    
    registration_status = CustomerRegistrationStatus.to_list()
    customer_status = ActiveStatus.to_list()
    language_list = Language.objects.all()
    
    context["registration_status"] = registration_status
    context["customer_status_list"] = customer_status
    context["language_list"] = language_list
    
    return context

def get_local_master_details_for_customer_details():
    
    context = get_local_master_details()
    registration_status = CustomerRegistrationStatus.to_list_for_reports()
    customer_status = ActiveStatus.to_list_for_reports()
    language_list = Language.objects.all()
    
    context["registration_statuses"] = registration_status
    context["customer_statuses"] = customer_status
    context["language_list"] = language_list
    
    return context
def get_local_master_details_for_device_details():
    
    context = get_local_master_details()
    return context

def validate_inputs(request):
        mobile = request.POST.get('mobile')
 

        if (not mobile):
            messages.error(request,"Backend Validation Failed! Please fill the required fileds")
            return False
        else:
            return True
        
def validate_inputs_for_update(request):
          
        mobile = request.POST.get('mobile')

        if (not mobile):
            messages.error(request,"Backend Validation Failed! Please fill the required fileds")
            return False

        else:
            return True

def get_latest_valid_otp(customer):
    now = dj_tz.now()
    return (EmailOTP.objects
            .filter(customer=customer, is_valid=True, is_utilized=False, expired_At__gt=now)
            .order_by('-created_At')
            .first())

def _record_global_failure(customer):
    customer.global_fail_count = (customer.global_fail_count or 0) + 1
    if customer.global_fail_count >= OTP_MAX_GLOBAL_FAILURES:
        customer.is_reg_blocked = True
    customer.save(update_fields=["global_fail_count", "is_reg_blocked"])

@transaction.atomic
def validate_and_use_otp(customer, submitted_code):
    """
    Validate OTP without auto-resend.
    Returns (otp_instance | None, error_dict | None)
    """
    # Blocked?
    if getattr(customer, "is_reg_blocked", False):
        return None, {
            "detail": "Registration failed. The email is blocked. Please contact the Wile Service Core Administrator.",
            "code": "auth/blocked",
            "http_status": status.HTTP_423_LOCKED,
        }
    if getattr(customer, "is_locked", False):
        return None, {
            "detail": "This account is locked. Please contact the Wile Service Core Administrator.",
            "code": "AUTH_LOCKED",
            "http_status": status.HTTP_423_LOCKED,
        }

    now = dj_tz.now()
    otp = get_latest_valid_otp(customer)

    # No active OTP
    if not otp:
        _record_global_failure(customer)
        return None, {"detail": "OTP validation failed.", "code": "otp/invalid", "http_status": status.HTTP_401_UNAUTHORIZED}

    # Expired
    if otp.expired_At <= now:
        otp.attempts_count += 1
        if otp.attempts_count >= OTP_MAX_ATTEMPTS_PER_CODE:
            otp.is_utilized = True
            otp.is_valid = False
        otp.save(update_fields=["attempts_count", "is_utilized", "is_valid"])
        _record_global_failure(customer)
        return None, {"detail": "OTP expired.", "code": "otp/expired", "http_status": status.HTTP_410_GONE}

    # Already exhausted (guard)
    if otp.attempts_count >= OTP_MAX_ATTEMPTS_PER_CODE:
        otp.is_utilized = True
        otp.is_valid = False
        otp.save(update_fields=["is_utilized", "is_valid"])
        _record_global_failure(customer)
        return None, {"detail": "OTP validation failed. Attempts exhausted.", "code": "otp/invalid-exhausted", "http_status": status.HTTP_401_UNAUTHORIZED}

    # Wrong code
    if not matches_code(submitted_code, otp.otp_code):
        otp.attempts_count += 1
        exhausted = otp.attempts_count >= OTP_MAX_ATTEMPTS_PER_CODE
        otp.is_utilized = exhausted
        otp.is_valid = not exhausted
        otp.save(update_fields=["attempts_count", "is_utilized", "is_valid"])
        _record_global_failure(customer)

        if exhausted:
            # No auto-resend; client must call /resend-otp
            return None, {
                "detail": "OTP validation failed. Attempts exhausted for this code. Please request a new OTP.",
                "code": "otp/invalid-exhausted",
                "http_status": status.HTTP_401_UNAUTHORIZED,
            }

        # Still have tries left
        return None, {"detail": "OTP validation failed.", "code": "otp/invalid", "http_status": status.HTTP_401_UNAUTHORIZED}

    # Success
    otp.is_utilized = True
    otp.is_valid = False
    otp.save(update_fields=["is_utilized", "is_valid"])
    customer.global_fail_count = 0
    customer.save(update_fields=["global_fail_count"])
    return otp, None

def check_resend_limits(customer):
    now = timezone.now()
    last = EmailOTP.objects.filter(customer=customer).order_by('-created_At').first()
    if last and (now - last.created_At).total_seconds() < RESEND_MIN_WAIT_SECONDS:
        return False, {"detail":"Please wait before requesting another code.",
                       "code":"otp/resend-too-soon",
                       "http_status": status.HTTP_429_TOO_MANY_REQUESTS}
    hour_ago = now - timedelta(hours=1)
    day_ago = now - timedelta(days=1)
    if EmailOTP.objects.filter(customer=customer, created_At__gte=hour_ago).count() >= RESEND_MAX_PER_HOUR:
        return False, {"detail":"Resend limit exceeded. Try again later.",
                       "code":"otp/resend-hour-limit",
                       "http_status": status.HTTP_429_TOO_MANY_REQUESTS}
    if EmailOTP.objects.filter(customer=customer, created_At__gte=day_ago).count() >= RESEND_MAX_PER_DAY:
        return False, {"detail":"Daily resend limit exceeded. Try again tomorrow.",
                       "code":"otp/resend-day-limit",
                       "http_status": status.HTTP_429_TOO_MANY_REQUESTS}
    return True, None

class HasCustomerProfile(permissions.BasePermission):
    """
    Allow access only if:
    - request.user is a Customer instance (we set this in CustomerJWTAuthentication), OR
    - request.user has a 'customer' attribute (User -> Customer OneToOne)
    """
    def has_permission(self, request, view):
        user = getattr(request, "user", None)
        if user is None:
            return False

        # If the authentication returned a Customer model instance directly
        if isinstance(user, Customer):
            return True

        # Otherwise, check for OneToOne relation on Django User (user.customer)
        return getattr(user, "customer", None) is not None
    
    
@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class AddCustomerView(View):
    this_feature = "add_customer"
    sub_menu_item = "add_customer"

    def get(self, request):
        # Tag this feature for Sentry
        sentry_sdk.set_tag("feature", self.this_feature)

        context = get_local_master_details_for_add_update_customer()
        user_profile = set_user_profile(request, context)
        if user_profile is None:
            logger.warning("Unauthorized access attempt (no profile)", extra={"view": self.this_feature})
            return redirect('login')

        get_privilleges(user_profile, context)
        if not is_authorized(user_profile, self.this_feature):
            logger.warning("Unauthorized access attempt", extra={"user": user_profile.id, "view": self.this_feature})
            return redirect('unauthorized-access')

        set_sub_menu_item(self.sub_menu_item, context)
        return render(request, 'add-customer.html', context)

    def post(self, request):
        # Tag this feature for Sentry
        sentry_sdk.set_tag("feature", self.this_feature)

        context = get_local_master_details_for_add_update_customer()
        user_profile = set_user_profile(request, context)
        if user_profile is None:
            logger.warning("Unauthorized POST (no profile)", extra={"view": self.this_feature})
            return redirect('login')

        get_privilleges(user_profile, context)
        if not is_authorized(user_profile, self.this_feature):
            logger.warning("Unauthorized POST", extra={"user": getattr(user_profile, 'id', None), "view": self.this_feature})
            return redirect('unauthorized-access')

        set_sub_menu_item(self.sub_menu_item, context)
        context["old_input_field_values"] = request.POST

        if not validate_inputs(request):
            logger.info("Validation failed while adding customer", extra={"user": getattr(user_profile, 'id', None)})
            return render(request, 'add-customer.html', context)

        try:
            customer = create_customer_from_post(request.POST, created_user=user_profile)
            logger.info("Customer created successfully", extra={"customer_id": customer.pk, "user": user_profile.id})
            messages.success(request, "Customer registered successfully")
            return redirect('view-customer', id=customer.pk)

        except ValidationError as ve:
            logger.warning("ValidationError while creating customer",
                           extra={"errors": ve.messages, "user": user_profile.id})
            messages.error(request, "; ".join(ve.messages))
            return render(request, 'add-customer.html', context)

        except IntegrityError as e:
            logger.error("IntegrityError during customer creation", exc_info=True,
                         extra={"user": user_profile.id})
            sentry_sdk.capture_exception(e)
            messages.error(request, "Error occurred in atomic operation. Contact the System Administrator")
            return render(request, 'add-customer.html', context)

@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class ViewCustomerView(View):
    this_feature = "view_customer"
    sub_menu_item = "customer_details"

    def get(self, request, id):
        # Tag this view/feature in Sentry and add a breadcrumb
        sentry_sdk.set_tag("feature", self.this_feature)
        logger.info("customer.view.enter", extra={"feature": self.this_feature, "customer_id": id})

        context = get_local_master_details()

        user_profile = set_user_profile(request, context)
        if user_profile is None:
            logger.warning("customer.view.unauthenticated", extra={"feature": self.this_feature})
            return redirect('login')

        get_privilleges(user_profile, context)
        if not is_authorized(user_profile, self.this_feature):
            logger.warning("customer.view.unauthorized",
                           extra={"feature": self.this_feature, "user_id": getattr(user_profile, "id", None)})
            return redirect('unauthorized-access')

        set_sub_menu_item(self.sub_menu_item, context)

        try:
            customer = get_object_or_404(Customer, pk=id)
        except Http404:
            logger.warning("customer.view.not_found", extra={"customer_id": id})
            # Optional: send a lightweight signal to Sentry for visibility
            sentry_sdk.capture_message(f"Customer not found: id={id}")
            raise  
        try:
            sentry_sdk.set_context("customer", {
                "id": customer.id,
                "email": customer.email,
                "status": customer.customer_status,
                "registration_status": customer.registration_status,
                "language_id": customer.language_id,
            })
        except Exception:
            pass

        context["customer"] = customer
        logger.info("customer.view.render", extra={"customer_id": customer.id})
        return render(request, 'view-customer.html', context)


@method_decorator(cache_control(no_cache=True, must_revalidate=True,no_store=True), name='dispatch')
class UpdateCustomerView(View):
    this_feature = "update_customer"
    sub_menu_item = "customer_details"

    def get(self, request, id):
        sentry_sdk.set_tag("feature", self.this_feature)
        logger.info("customer.update.get.enter", extra={"customer_id": id})

        context = get_local_master_details_for_add_update_customer()
        user_profile = set_user_profile(request, context)

        if user_profile is None:
            logger.warning("customer.update.get.unauthenticated")
            return redirect('login')

        get_privilleges(user_profile, context)

        if not is_authorized(user_profile, self.this_feature):
            logger.warning("customer.update.get.unauthorized", extra={"user_id": getattr(user_profile, "id", None)})
            return redirect('unauthorized-access')

        set_sub_menu_item(self.sub_menu_item, context)

        try:
            customer = Customer.objects.get(pk=id)
            logger.info("customer.update.get.found", extra={"customer_id": customer.id})
            context["customer"] = customer
            context.update({
                "language_list": Language.objects.all(),
                "selected_language_id": customer.language_id
            })
            return render(request, 'update-customer.html', context)
        except Customer.DoesNotExist:
            logger.warning("customer.update.get.not_found", extra={"customer_id": id})
            messages.error(request, "Customer doesn't exist")
            return redirect('customer-details')

    def post(self, request, id):
        sentry_sdk.set_tag("feature", self.this_feature)
        logger.info("customer.update.post.enter", extra={"customer_id": id})

        context = get_local_master_details_for_add_update_customer()
        user_profile = set_user_profile(request, context)
        if user_profile is None:
            logger.warning("customer.update.post.unauthenticated")
            return redirect('login')

        get_privilleges(user_profile, context)
        if not is_authorized(user_profile, self.this_feature):
            logger.warning("customer.update.post.unauthorized", extra={"user_id": getattr(user_profile, "id", None)})
            return redirect('unauthorized-access')

        set_sub_menu_item(self.sub_menu_item, context)
        context["old_input_field_values"] = request.POST

        # Load the customer
        try:
            customer = Customer.objects.get(pk=id)
            logger.info("customer.update.post.found", extra={"customer_id": customer.id})
        except Customer.DoesNotExist:
            logger.warning("customer.update.post.not_found", extra={"customer_id": id})
            messages.error(request, "Customer does not exist.")
            return redirect('customer-details')

        context["customer"] = customer
        context.update({
            "language_list": Language.objects.all(),
            "selected_language_id": customer.language_id,
        })

        if not validate_inputs_for_update(request):
            logger.info("customer.update.post.validation_failed", extra={"customer_id": customer.id})
            return render(request, 'update-customer.html', context)

        email           = request.POST.get("email").strip()
        first_name      = request.POST.get("first_name").strip().upper()
        last_name       = request.POST.get("last_name").strip().upper()
        mobile          = request.POST.get("mobile").strip()
        language_id_raw = request.POST.get("language").strip()
        customer_status = request.POST.get("customer_status").strip()

        language_obj = None
        if language_id_raw:
            try:
                language_obj = Language.objects.get(pk=int(language_id_raw))
            except (ValueError, ObjectDoesNotExist):
                logger.warning("customer.update.post.language_not_found",
                               extra={"customer_id": customer.id, "language_id": language_id_raw})
                messages.error(request, "Selected language not found.")
                context["selected_language_id"] = language_id_raw
                return render(request, 'update-customer.html', context)

        if email and Customer.objects.filter(email=email).exclude(pk=customer.pk).exists():
            logger.warning("customer.update.post.email_conflict",
                           extra={"customer_id": customer.id, "email": email})
            messages.error(request, "Email is already used by another customer.")
            context["selected_language_id"] = language_id_raw 
            return render(request, 'update-customer.html', context)

        try:
            with transaction.atomic():
                customer.email        = email
                customer.first_name   = first_name
                customer.last_name    = last_name
                customer.mobile       = mobile
                customer.language     = language_obj  
                customer.customer_status = customer_status
                customer.updated_user = user_profile
                customer.updated_At   = dj_tz.now()
                customer.save()

                logger.info("customer.update.post.success", extra={"customer_id": customer.id})
                messages.success(request, "Customer details updated successfully.")
                return redirect("view-customer", id=customer.pk)

        except IntegrityError as e:
            logger.error("customer.update.post.integrity_error",
                         extra={"customer_id": customer.id}, exc_info=True)
            sentry_sdk.capture_exception(e)
            messages.error(request, "Database error occurred. Please contact the system administrator.")
            context["selected_language_id"] = language_id_raw or customer.language_id
            return render(request, 'update-customer.html', context)
        except Exception as exp:
            logger.error("customer.update.post.unexpected_error",
                         extra={"customer_id": customer.id}, exc_info=True)
            sentry_sdk.capture_exception(exp)
            messages.error(request, "An unexpected error occurred.")
            messages.error(request, str(exp))
            context["selected_language_id"] = language_id_raw or customer.language_id
            return render(request, 'update-customer.html', context)


@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class DeleteCustomerView(View):
    this_feature = "delete_customer"

    def post(self, request):
        sentry_sdk.set_tag("feature", self.this_feature)
        logger.info("customer.delete.post.enter")

        context = {}
        user_profile = set_user_profile(request, context)

        if user_profile is None:
            logger.warning("customer.delete.unauthenticated")
            return redirect('login')

        get_privilleges(user_profile, context)

        if not is_authorized(user_profile, self.this_feature):
            logger.warning(
                "customer.delete.unauthorized",
                extra={"user_id": getattr(user_profile, "id", None)}
            )
            return redirect('unauthorized-access')

        customer_id = request.POST.get('customer_id')
        logger.info(
            "customer.delete.attempt",
            extra={"customer_id": customer_id, "user_id": getattr(user_profile, "id", None)}
        )

        try:
            customer = Customer.objects.get(id=customer_id)

            delete_customer_with_related_data(customer)

            logger.info("customer.delete.success", extra={"customer_id": customer_id})
            messages.success(request, "Customer deleted successfully.")
            return redirect('customer-details')

        except Customer.DoesNotExist:
            logger.warning("customer.delete.not_found", extra={"customer_id": customer_id})
            messages.error(request, "Customer not found.")
            return redirect('customer-details')

        except RestrictedError:
            logger.warning("customer.delete.restricted", extra={"customer_id": customer_id})
            messages.error(request, "Deletion restricted. This customer is in use.")
            return redirect('view-customer', customer_id)

        except (Exception, IntegrityError) as e:
            logger.error("customer.delete.error", extra={"customer_id": customer_id}, exc_info=True)
            sentry_sdk.capture_exception(e)
            messages.error(request, "Error occurred during delete operation. Contact the System Administrator.")
            return redirect('view-customer', customer_id)

@method_decorator(cache_control(no_cache=True, must_revalidate=True,no_store=True), name='dispatch')
class CustomerDetailsView(View):
    this_feature = "customer_details"
    sub_menu_item = "customer_details"

    def get(self, request):
        sentry_sdk.set_tag("feature", self.this_feature)
        logger.info("customer.details.get.enter")

        context = get_local_master_details_for_customer_details()
        user_profile = set_user_profile(request, context)

        if user_profile is None:
            logger.warning("customer.details.unauthenticated")
            return redirect('login')

        get_privilleges(user_profile, context)

        if not is_authorized(user_profile, self.this_feature):
            logger.warning("customer.details.unauthorized", extra={"user_id": getattr(user_profile, "id", None)})
            return redirect('unauthorized-access')

        set_sub_menu_item(self.sub_menu_item, context)
        
        per_page_count = request.GET.get('per_page_count')
        page_number = request.GET.get('page_number')
        start_index = request.GET.get('start_index')

        if not page_number:
            page_number = 1

        if not per_page_count:
            per_page_count = PerPageSelector.get_default().value

        if start_index:
            page_number = math.ceil(int(start_index)/int(per_page_count))

        context["default_per_page_count"] = per_page_count
        
        customer_status = request.GET.get('customer_status')
        email = request.GET.get('email')
        mobile = request.GET.get('mobile')
        customer_name = request.GET.get('customer_name')
        reg_date_filter = request.GET.get('registration_date_filter', '')
        reg_from_date = request.GET.get('registration_from_date')
        reg_to_date = request.GET.get('registration_to_date')
        registration_status= request.GET.get('registration_status')

        # Filters
        filter_kwargs = {} 
        query = Q()
        query = Q()
        raw = (request.GET.get("customer_name") or "").strip()
        # collapse extra spaces
        customer_name = re.sub(r"\s+", " ", raw)
        context["default_customer_name"] = customer_name

        if email:
            email = email.strip()
            context["default_email"] = email
            filter_kwargs['email__icontains'] = email
            
        if mobile:
            mobile = mobile.strip()
            context["default_mobile"] = mobile
            filter_kwargs['mobile__icontains'] = mobile

        if customer_name:
            tokens = customer_name.split()  # ["ABOOBACKER", "NUSNAN"]
            for tok in tokens:
                query &= (
                    Q(first_name__icontains=tok) |
                    Q(last_name__icontains=tok)
                )
            
        if customer_status:
            if customer_status.lower()!="all":
                context["default_customer_status"] = ActiveStatus(customer_status).value
                filter_kwargs['customer_status']= ActiveStatus(customer_status).value

        if registration_status:
            if registration_status.lower()!="all":
                context["default_registration_status"] = CustomerRegistrationStatus(registration_status).value
                filter_kwargs['registration_status']= CustomerRegistrationStatus(registration_status).value
        # Date Filter Logic
        if reg_date_filter:
            today = get_local_date()  # your helper (local date)

            val = DateFilter(reg_date_filter).value
            context["default_reg_date_filter"] = val
            context["default_reg_from_date"] = reg_from_date
            context["default_reg_to_date"] = reg_to_date

            if val == DateFilter.CURRENT_DATE.value:
                start, end = _day_bounds(today)
                filter_kwargs["created_At__gte"] = start
                filter_kwargs["created_At__lt"]  = end

            elif val == DateFilter.CURRENT_MONTH.value:
                start, end = _month_bounds(today)
                filter_kwargs["created_At__gte"] = start
                filter_kwargs["created_At__lt"]  = end

            elif val == DateFilter.CURRENT_YEAR.value:
                start, end = _year_bounds(today)
                filter_kwargs["created_At__gte"] = start
                filter_kwargs["created_At__lt"]  = end

            elif val == DateFilter.DATE_RANGE.value:
                if reg_from_date and reg_to_date:
                    if isinstance(reg_from_date, str):
                        reg_from_date = dt.date.fromisoformat(reg_from_date)
                    if isinstance(reg_to_date, str):
                        reg_to_date = dt.date.fromisoformat(reg_to_date)
                    start, _ = _day_bounds(reg_from_date)
                    _, end   = _day_bounds(reg_to_date)
                    filter_kwargs["created_At__gte"] = start
                    filter_kwargs["created_At__lt"]  = end
                else:
                    messages.error(request, "Please fill the date range for Registration Date Filters")
                    context["reg_from_date_error"] = True
                    context["reg_to_date_error"] = True

            elif val == DateFilter.UP_TO.value:
                if reg_to_date:
                    if isinstance(reg_to_date, str):
                        reg_to_date = dt.date.fromisoformat(reg_to_date)
                    _, end = _day_bounds(reg_to_date)
                    filter_kwargs["created_At__lt"] = end
                else:
                    messages.error(request, "Please fill the 'To Date' Field for Registration Date Filters")
                    context["reg_to_date_error"] = True

        # Query Data
        customers = Customer.objects.filter(query, **filter_kwargs).order_by('-id')
        context["has_entries"] = customers.exists()

        # Pagination
        paginator = Paginator(customers, per_page_count)
        page = paginator.get_page(page_number)
        page_list = list(paginator.get_elided_page_range(page_number, on_each_side=1))

        context["page"] = page
        context["page_list"] = page_list

        # Summary log (no PII)
        try:
            logger.info(
                "customer.details.render",
                extra={
                    "filters_set": {
                        "email": bool(email),
                        "mobile": bool(mobile),
                        "customer_name": bool(customer_name),
                        "customer_status": customer_status if customer_status else None,
                        "reg_date_filter": reg_date_filter if reg_date_filter else None,
                    },
                    "page_number": page_number,
                    "per_page_count": per_page_count,
                    "result_count": customers.count(),
                },
            )
        except Exception:
            pass

        return render(request, 'customer-details.html', context)

@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class CustomerDeviceDetailsView(View):
    this_feature = "customer_device_details"
    sub_menu_item = "customer_device_details"

    def get(self, request):
        sentry_sdk.set_tag("feature", self.this_feature)
        logger.info("customer.device_details.get.enter")

        # If you have a separate context loader for devices, use that.
        context = get_local_master_details_for_device_details()
        user_profile = set_user_profile(request, context)

        if user_profile is None:
            logger.warning("customer.device_details.unauthenticated")
            return redirect('login')

        get_privilleges(user_profile, context)

        if not is_authorized(user_profile, self.this_feature):
            logger.warning(
                "customer.device_details.unauthorized",
                extra={"user_id": getattr(user_profile, "id", None)},
            )
            return redirect('unauthorized-access')

        set_sub_menu_item(self.sub_menu_item, context)

        # --- pagination params (same pattern as customers) ---
        per_page_count = request.GET.get('per_page_count')
        page_number = request.GET.get('page_number')
        start_index = request.GET.get('start_index')

        if not page_number:
            page_number = 1

        if not per_page_count:
            per_page_count = PerPageSelector.get_default().value

        if start_index:
            page_number = math.ceil(int(start_index) / int(per_page_count))

        context["default_per_page_count"] = per_page_count

        # --- filters ---
             # exact product id
        customer_name = request.GET.get('customer_name')   # contains
        product_name = request.GET.get('product_name')           # contains
        serial_number = request.GET.get('serial_number')

        reg_date_filter = request.GET.get('registration_date_filter', '')
        reg_from_date = request.GET.get('registration_from_date')
        reg_to_date = request.GET.get('registration_to_date')

        # Build query
        filter_kwargs = {}
        query = Q()
        
        if customer_name:
            customer_name = customer_name.strip()
            context["default_customer_name"] = customer_name
            filter_kwargs['customer__email__icontains'] = customer_name

        # Text filters
        if serial_number:
            serial_number = serial_number.strip()
            context["default_serial_number"] = serial_number
            filter_kwargs['serial_number__icontains'] = serial_number
        # Product filters


        if product_name:
            product_name = product_name.strip()
            context["default_product_name"] = product_name
            query &= Q(product__name__icontains=product_name)



        # Date filter logic (created_at)
        if reg_date_filter:
            today = get_local_date()
            val = DateFilter(reg_date_filter).value
            context["default_reg_date_filter"] = val
            context["default_reg_from_date"] = reg_from_date
            context["default_reg_to_date"] = reg_to_date

            if val == DateFilter.CURRENT_DATE.value:
                start, end = _day_bounds(today)
                filter_kwargs["created_at__gte"] = start
                filter_kwargs["created_at__lt"] = end

            elif val == DateFilter.CURRENT_MONTH.value:
                start, end = _month_bounds(today)
                filter_kwargs["created_at__gte"] = start
                filter_kwargs["created_at__lt"] = end

            elif val == DateFilter.CURRENT_YEAR.value:
                start, end = _year_bounds(today)
                filter_kwargs["created_at__gte"] = start
                filter_kwargs["created_at__lt"] = end

            elif val == DateFilter.DATE_RANGE.value:
                if reg_from_date and reg_to_date:
                    if isinstance(reg_from_date, str):
                        reg_from_date = dt.date.fromisoformat(reg_from_date)
                    if isinstance(reg_to_date, str):
                        reg_to_date = dt.date.fromisoformat(reg_to_date)
                    start, _ = _day_bounds(reg_from_date)
                    _, end = _day_bounds(reg_to_date)
                    filter_kwargs["created_at__gte"] = start
                    filter_kwargs["created_at__lt"] = end
                else:
                    messages.error(request, "Please fill the date range for Registration Date Filters")
                    context["reg_from_date_error"] = True
                    context["reg_to_date_error"] = True

            elif val == DateFilter.UP_TO.value:
                if reg_to_date:
                    if isinstance(reg_to_date, str):
                        reg_to_date = dt.date.fromisoformat(reg_to_date)
                    _, end = _day_bounds(reg_to_date)
                    filter_kwargs["created_at__lt"] = end
                else:
                    messages.error(request, "Please fill the 'To Date' Field for Registration Date Filters")
                    context["reg_to_date_error"] = True

        # --- Query & order ---
        devices_qs = (
            CustomerDevice.objects
            .filter(query, **filter_kwargs)
            .select_related("product", "customer")
            .order_by("-id")
        )

        context["has_entries"] = devices_qs.exists()

        # --- Pagination ---
        paginator = Paginator(devices_qs, per_page_count)
        page = paginator.get_page(page_number)
        page_list = list(paginator.get_elided_page_range(page_number, on_each_side=1))

        context["page"] = page
        context["page_list"] = page_list

        # Useful lookups for filters (optional)
        context["products"] = Product.objects.only("id", "name").order_by("name")
        context["temperature_units"] = [tu.value for tu in TemperatureUnit]
        context["density_units"] = [du.value for du in DensityUnit]

        # --- Summary log (no PII) ---
        try:
            logger.info(
                "customer.device_details.render",
                extra={
                    "filters_set": {
                        "custmer_name": bool(customer_name),
                        "product_name": bool(product_name),
                        "serial_number": bool(serial_number),
                        "reg_date_filter": reg_date_filter if reg_date_filter else None,
                    },
                    "page_number": page_number,
                    "per_page_count": per_page_count,
                    "result_count": devices_qs.count(),
                },
            )
        except Exception:
            pass

        return render(request, "customer-device-details.html", context)
    
@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class ToggleCustomerLockView(View):
    
    this_feature = "toggle_customer_lock"
    sub_menu_item = "customer_details"

    def post(self, request, customer_id):
        context = {}
        user_profile = set_user_profile(request, context)

        if user_profile is None:
            return redirect('login')

        get_privilleges(user_profile, context)

        if not is_authorized(user_profile, self.this_feature):
            return redirect('unauthorized-access')

        customer = get_object_or_404(Customer, id=customer_id)

        # Toggle logic
        if customer.is_locked:
            customer.is_locked = False
            customer.locked_At = None
            messages.success(request, "Customer unlocked successfully.")
        else:
            customer.is_locked = True
            customer.locked_At = timezone.now()
            messages.success(request, "Customer locked successfully.")

        customer.save()

        return redirect('view-customer', customer_id)
@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name="dispatch")
class RegisterView(APIView):
    permission_classes = [permissions.AllowAny]

    def post(self, request):
        sentry_sdk.set_tag("feature", "register_customer")
        logger.info("customer.register.post.enter")

        s = RegisterSerializer(data=request.data)

        if not s.is_valid():
            # If serializer returned language_id errors like ["LANGUAGE_INVALID"]
            # you can keep errors as-is; message comes from VALIDATION_ERROR
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("VALIDATION_ERROR", request),
                    "code": "VALIDATION_ERROR",
                    "errors": s.errors,
                },
                status=400,
            )

        try:
            customer, otp_code, expires_at, label = s.save()

        except serializers.ValidationError as e:
            detail = e.detail if isinstance(e.detail, dict) else {"detail": e.detail}

            # If serializer provided a top-level code
            code = detail.get("code") if isinstance(detail, dict) else None
            if not code:
                code = "VALIDATION_ERROR"

            # If serializer nested errors
            errors = detail.get("errors") if isinstance(detail, dict) else detail

            http_status = 400
            if code == "AUTH_EMAIL_ALREADY_REGISTERED":
                http_status = 409
            elif code in ["AUTH_BLOCKED", "AUTH_LOCKED"]:
                http_status = status.HTTP_423_LOCKED

            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message(code, request),
                    "code": code,
                    "errors": errors,
                },
                status=http_status,
            )

        ts = fmt_ts(expires_at) if "fmt_ts" in globals() else expires_at.isoformat()

        # TEST MODE
        if str(getattr(settings, "TEST_OTP", "")).lower() == "enabled":
            return JsonResponse(
                {
                    "success": True,
                    "message": get_api_message("REGISTRATION_TEST_MODE", request),
                    "code": "REGISTRATION_TEST_MODE",
                    "data": {"registered": True, "email": customer.email, "expires_at": ts},
                },
                status=201,
            )

        # NORMAL MODE
        if otp_code:
            try:
                send_otp_via_email(
                        otp_number=otp_code,
                        reg_email=customer.email,
                        template_code="CUSTOMER_REGISTER_OTP",
                        request=request,
                    )
            except Exception as ex:
                logger.error("customer.register.otp_failed", exc_info=True)
                sentry_sdk.capture_exception(ex)

        return JsonResponse(
            {
                "success": True,
                "message": get_api_message("REGISTRATION_OTP_SENT", request),
                "code": "REGISTRATION_OTP_SENT",
                "data": {"registered": True, "email": customer.email, "expires_at": ts},
            },
            status=201,
        )

@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name="dispatch")
class VerifyOTPView(APIView):
    permission_classes = [permissions.AllowAny]

    def post(self, request):
        sentry_sdk.set_tag("feature", "verify_otp")
        logger.info("otp.verify.post.enter")

        # Validate input
        try:
            s = VerifyOTPSerializer(data=request.data)
            s.is_valid(raise_exception=True)
        except serializers.ValidationError as e:
            detail = e.detail if isinstance(e.detail, dict) else {"non_field_errors": e.detail}
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("VALIDATION_ERROR", request),
                    "code": "VALIDATION_ERROR",
                    "errors": detail,
                },
                status=status.HTTP_400_BAD_REQUEST,
            )

        email = s.validated_data["email"].lower()
        code = s.validated_data["code"]

        # --- find customer ---
        customer = Customer.objects.filter(email__iexact=email).first()
        if not customer:
            logger.warning("otp.verify.no_customer", extra={"email": email})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_NO_CUSTOMER", request),
                    "code": "AUTH_NO_CUSTOMER",
                    "errors": {"email": ["No account found for this email."]},
                },
                status=status.HTTP_400_BAD_REQUEST,
            )
        if customer.is_locked:
            logger.warning("otp.verify.locked_customer", extra={"customer_id": customer.id, "email": customer.email})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_LOCKED", request),
                    "code": "AUTH_LOCKED",
                    "errors": {"email": ["This account is locked. Contact support."]},
                },
                status=status.HTTP_423_LOCKED,
            )
        # --- TEST OTP MODE ---
        if getattr(settings, "TEST_OTP", None) == "enabled":
            test_code = str(getattr(settings, "TEST_OTP_CODE", ""))
            if str(code) != test_code:
                logger.warning("otp.verify.test_mode.invalid_code", extra={"email": email})
                return JsonResponse(
                    {
                        "success": False,
                        "message": get_api_message("OTP_INVALID", request),
                        "code": "OTP_INVALID",
                        "errors": {"code": ["The code you entered is invalid or expired."]},
                    },
                    status=status.HTTP_400_BAD_REQUEST,
                )

            customer.registration_status = CustomerRegistrationStatus.APPROVED.value
            customer.customer_status = ActiveStatus.ACTIVE.value
            customer.is_email_verified = True
            customer.created_by = CustomerRegistrationSource.CUSTOMER.value
            customer.save(update_fields=["registration_status", "customer_status", "is_email_verified", "created_by"])

            logger.info("otp.verify.test_mode.success", extra={"customer_id": customer.id, "email": customer.email})
            return JsonResponse(
                {
                    "success": True,
                    "message": get_api_message("OTP_VERIFIED", request),
                    "code": "OTP_VERIFIED",
                    "data": {"verified": True},
                },
                status=status.HTTP_200_OK,
            )

        # --- NORMAL MODE ---
        otp, err = validate_and_use_otp(customer, code)
        if err:
            logger.warning("otp.verify.failed", extra={"email": email, "code": err.get("code")})

            # Capture notable errors
            if err.get("code") in {"otp/invalid-exhausted", "auth/blocked", "otp/expired"}:
                try:
                    sentry_sdk.capture_message(f"OTP verify failure: {err.get('code')} for {email}")
                except Exception:
                    pass

            # Unified error response
            response_data = {
                "success": False,
                "message": get_api_message(err.get("code", "OTP_ERROR"), request),
                "code": err.get("code", "OTP_ERROR"),
                "errors": {
                    "detail": [
                        err.get(
                            "detail",
                            get_api_message("OTP_ERROR", request),
                        )
                    ]
                },
            }

            # Include extra fields if present
            extras = {}
            for key in ("resent", "expires_at", "resend_blocked", "resend_error"):
                if key in err:
                    extras[key] = err[key]
            if extras:
                response_data["data"] = extras

            return JsonResponse(
                response_data,
                status=err.get("http_status", status.HTTP_400_BAD_REQUEST),
            )

        # --- SUCCESS ---
        customer.registration_status = CustomerRegistrationStatus.APPROVED.value
        customer.is_email_verified = True
        customer.customer_status = ActiveStatus.ACTIVE.value
        customer.created_by = CustomerRegistrationSource.CUSTOMER.value
        customer.save(update_fields=["registration_status", "customer_status", "is_email_verified", "created_by"])

        logger.info("otp.verify.success", extra={"customer_id": customer.id, "email": customer.email})
        return JsonResponse(
            {
                "success": True,
                "message": get_api_message("OTP_VERIFIED", request),
                "code": "OTP_VERIFIED",
                "data": {"verified": True},
            },
            status=status.HTTP_200_OK,
        )

@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class ResendOTPView(APIView):
    permission_classes = [permissions.AllowAny]

    def post(self, request):
        sentry_sdk.set_tag("feature", "resend_otp")
        logger.info("otp.resend.post.enter")

        # --- Validate payload ---
        try:
            s = ResendOTPSerializer(data=request.data)
            s.is_valid(raise_exception=True)
        except serializers.ValidationError as e:
            detail = e.detail if isinstance(e.detail, dict) else {"non_field_errors": e.detail}
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("VALIDATION_ERROR", request),
                    "code": "VALIDATION_ERROR",
                    "errors": detail,
                },
                status=status.HTTP_400_BAD_REQUEST,
            )

        email = s.validated_data['email'].lower()
        customer = Customer.objects.filter(email__iexact=email).first()
        if not customer:
            logger.warning("otp.resend.no_customer", extra={"email": email})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_NO_CUSTOMER", request),
                    "code": "AUTH_NO_CUSTOMER",
                    "errors": {"email": ["No account found for this email."]},
                },
                status=status.HTTP_400_BAD_REQUEST,
            )
        if customer.is_locked:
            logger.warning("otp.resend.locked_customer", extra={"customer_id": customer.id, "email": customer.email})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_LOCKED", request),
                    "code": "AUTH_LOCKED",
                    "errors": {"email": ["This account is locked. Contact support."]},
                },
                status=status.HTTP_423_LOCKED,
            )
        # Blocked? -> 423
        if getattr(customer, "is_reg_blocked", False):
            logger.warning("otp.resend.blocked", extra={"customer_id": customer.id, "email": customer.email})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_BLOCKED", request),
                    "code": "AUTH_BLOCKED",
                    "errors": {"email": ["This email has been blocked. Contact support."]},
                },
                status=status.HTTP_423_LOCKED,
            )

        # Rate limits (60s / 5 per hour / 10 per day)
        ok, err = check_resend_limits(customer)
        if not ok:
            logger.warning("otp.resend.rate_limited", extra={"customer_id": customer.id, "code": err.get("code")})
            if err.get("code") in {"otp/resend-too-soon", "otp/resend-hour-limit", "otp/resend-day-limit"}:
                sentry_sdk.capture_message(f"Resend OTP blocked: {err.get('code')} for {customer.email}")
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message(err.get("code", "OTP_ERROR"), request),
                    "code": err["code"],
                    "errors": {"detail": [err["detail"]]},
                },
                status=err.get("http_status", status.HTTP_429_TOO_MANY_REQUESTS),
            )

        #  Invalidate any active OTPs
        EmailOTP.objects.filter(customer=customer, is_valid=True, is_utilized=False).update(
            is_valid=False, is_utilized=True
        )
        logger.info("otp.resend.invalidate_previous", extra={"customer_id": customer.id})

        # Generate & store new OTP
        label = "New verification OTP"
        if email == os.getenv("DEFAULT_USER_EMAIL"):
            plain = os.getenv("DEFAULT_OTP")  # from env
        else:
            plain = generate_otp()
        expires_at = default_expiry(OTP_EXPIRY_MINUTES)
        EmailOTP.objects.create(
            customer=customer,
            otp_code=hash_otp(plain),
            created_At=dj_tz.now(),
            expired_At=expires_at,
            is_utilized=False,
            attempts_count=0,
            is_valid=True,
        )

        # Send via email
        try:
            send_otp_via_email(
                    otp_number=plain,
                    reg_email=customer.email,
                    template_code="CUSTOMER_RESEND_OTP",
                    request=request,
                )
            logger.info("otp.resend.success", extra={"customer_id": customer.id, "email": customer.email})
        except Exception as e:
            logger.error(
                "otp.resend.email_failed",
                extra={"customer_id": customer.id, "email": customer.email},
                exc_info=True
            )
            sentry_sdk.capture_exception(e)
            # still success since OTP is generated and stored
            return JsonResponse(
                {
                    "success": True,
                    "message": get_api_message("OTP_RESENT_FAILED", request),
                    "code": "OTP_RESENT_FAILED",
                    "data": {"resent": True, "expires_at": fmt_ts(expires_at)},
                },
                status=status.HTTP_200_OK,
            )

        #Success
        return JsonResponse(
            {
                "success": True,
                "message": get_api_message("OTP_RESENT", request),
                "code": "OTP_RESENT",
                "data": {"resent": True, "expires_at": fmt_ts(expires_at)},
            },
            status=status.HTTP_200_OK,
        )


@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class LoginView(APIView):
    permission_classes = [permissions.AllowAny]

    def post(self, request):
        sentry_sdk.set_tag("feature", "login")
        logger.info("auth.login.post.enter")

        # --- 0) SHORT-CIRCUIT: custom UX for missing/blank ---
        raw_email = (request.data.get("email") or "").strip()
        raw_password = request.data.get("password")
        if not raw_email or raw_password in (None, ""):
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_MISSING_CREDENTIALS", request),
                    "code": "AUTH_MISSING_CREDENTIALS",
                },
                status=401,
            )

        email = raw_email.lower()
        password = raw_password
        remember_me = bool(request.data.get("remember_me", False))

        # --- 1) Validate via serializer (NO raise_exception) ---
        s = LoginSerializer(data={"email": email, "password": password, "remember_me": remember_me})
        if not s.is_valid():
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_INVALID_INPUT", request),
                    "code": "AUTH_INVALID_INPUT",
                    "errors": s.errors,
                },
                status=401,
            )

        # --- 2) Lookup & password check ---
        customer = Customer.objects.filter(email__iexact=email).first()
        if not customer or not customer.password or not check_password(password, customer.password):
            logger.warning("auth.login.invalid_credentials", extra={"email": email})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_INVALID_CREDENTIALS", request),
                    "code": "AUTH_INVALID_CREDENTIALS",
                },
                status=401,
            )
        if customer.is_locked or customer.global_fail_count == OTP_MAX_GLOBAL_FAILURES:
            logger.warning("auth.login.account_locked", extra={"customer_id": customer.id, "email": customer.email})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_LOCKED", request),
                    "code": "AUTH_LOCKED",
                },
                status=status.HTTP_403_FORBIDDEN,
            )
        # --- 3) OTP required cases ---
        if (
            customer.created_by == CustomerRegistrationSource.SYSTEM.value
            or customer.registration_status == CustomerRegistrationStatus.PENDING.value
            or customer.customer_status == ActiveStatus.INACTIVE.value
            or not customer.is_email_verified
        ):
            logger.info("auth.login.otp_required", extra={"customer_id": customer.id, "email": customer.email})

            EmailOTP.objects.filter(
                customer=customer, is_valid=True, is_utilized=False, expired_At__gte=dj_tz.now()
            ).update(is_valid=False, is_utilized=True)

            if email == os.getenv("DEFAULT_USER_EMAIL"):
                plain = os.getenv("DEFAULT_OTP")  # from env
            else:
                plain = generate_otp()
            expires_at = default_expiry(OTP_EXPIRY_MINUTES)
            EmailOTP.objects.create(
                customer=customer,
                otp_code=hash_otp(plain),
                created_At=dj_tz.now(),
                expired_At=expires_at,
                is_utilized=False,
                attempts_count=0,
                is_valid=True,
            )
            try:
                send_otp_via_email(plain, customer.email)
                logger.info("auth.login.otp_sent", extra={"customer_id": customer.id, "email": customer.email})
            except Exception as e:
                logger.error(
                    "auth.login.otp_email_failed",
                    extra={"customer_id": customer.id, "email": customer.email},
                    exc_info=True
                )
                sentry_sdk.capture_exception(e)

            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_OTP_REQUIRED", request),
                    "code": "AUTH_OTP_REQUIRED",
                    "data": {"otp_sent": True},
                },
                status=status.HTTP_403_FORBIDDEN,
            )

        # --- 4) Locked account ---
        if customer.is_locked or customer.global_fail_count == OTP_MAX_GLOBAL_FAILURES:
            logger.warning("auth.login.account_locked", extra={"customer_id": customer.id, "email": customer.email})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_LOCKED", request),
                    "code": "AUTH_LOCKED",
                },
                status=status.HTTP_403_FORBIDDEN,
            )

        # --- 5) Success: JWT ---
        refresh = RefreshToken()
        refresh["sub"] = f"customer:{customer.id}"
        access = refresh.access_token

        if remember_me:
            access.set_exp(lifetime=timedelta(days=settings.REMEMBER_ACCESS_DAYS))
            refresh.set_exp(lifetime=timedelta(days=settings.REMEMBER_REFRESH_DAYS))

        logger.info(
            "auth.login.success",
            extra={"customer_id": customer.id, "email": customer.email, "remember_me": remember_me},
        )

        return JsonResponse(
            {
                "success": True,
                "message": get_api_message("AUTH_LOGIN_SUCCESS", request),
                "code": "AUTH_LOGIN_SUCCESS",
                "data": {
                    "access": str(access),
                    "refresh": str(refresh),
                    "customer": {
                        "id": customer.id,
                        "email": customer.email,
                        "first_name": customer.first_name,
                        "last_name": customer.last_name,
                    },
                },
            },
            status=status.HTTP_200_OK,
        )


@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class ForgotPasswordOTPView(APIView):
    permission_classes = [permissions.AllowAny]

    def post(self, request):
        sentry_sdk.set_tag("feature", "forgot_password_otp")
        logger.info("auth.forgot_password_otp.post.enter")

        # --- 0) Handle empty email early ---
        raw_email = (request.data.get("email") or "").strip()
        if not raw_email:
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_EMAIL_REQUIRED", request),
                    "code": "AUTH_EMAIL_REQUIRED",
                },
                status=status.HTTP_400_BAD_REQUEST,
            )

        email = raw_email.lower()

        # --- 1) Validate email format ---
        s = ForgotPasswordSerializer(data={"email": email})
        if not s.is_valid():
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_EMAIL_INVALID", request),
                    "code": "AUTH_EMAIL_INVALID",
                    "errors": s.errors,
                },
                status=status.HTTP_400_BAD_REQUEST,
            )

        # --- 2) Find customer ---
        customer = Customer.objects.filter(email__iexact=email).first()
        if not customer:
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_NO_CUSTOMER", request),
                    "code": "AUTH_NO_CUSTOMER",
                },
                status=status.HTTP_404_NOT_FOUND,
            )

        if not customer.is_email_verified:
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_EMAIL_NOT_VERIFIED", request),
                    "code": "AUTH_EMAIL_NOT_VERIFIED",
                },
                status=status.HTTP_400_BAD_REQUEST,
            )

        # --- 3) Invalidate previous OTPs ---
        EmailOTP.objects.filter(
            customer=customer,
            is_valid=True,
            is_utilized=False,
            expired_At__gte=dj_tz.now(),
        ).update(is_valid=False, is_utilized=True)

        # --- 4) Generate and send OTP ---
        if email == os.getenv("DEFAULT_USER_EMAIL"):
            plain = os.getenv("DEFAULT_OTP")  # from env
        else:
            plain = generate_otp()
        expires_at = default_expiry(OTP_EXPIRY_MINUTES)
        EmailOTP.objects.create(
            customer=customer,
            otp_code=hash_otp(plain),
            created_At=dj_tz.now(),
            expired_At=expires_at,
            is_utilized=False,
            attempts_count=0,
            is_valid=True,
        )

        # label = "Wile forgot password"
        try:
            send_otp_via_email(
                    otp_number=plain,
                    reg_email=customer.email,
                    template_code="CUSTOMER_FORGOT_PASSWORD_OTP",
                    request=request,
                )
            logger.info("auth.forgot_password_otp.email_sent", extra={"email": customer.email})
        except Exception as e:
            logger.error("auth.forgot_password_otp.email_failed", exc_info=True)
            sentry_sdk.capture_exception(e)
            print(e)
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_OTP_SEND_FAILED", request),
                    "code": "AUTH_OTP_SEND_FAILED",
                },
                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
            )

        ts = fmt_ts(expires_at) if 'fmt_ts' in globals() else expires_at.isoformat()
        return JsonResponse(
            {
                "success": True,
                "message": get_api_message("AUTH_OTP_SENT", request),
                "code": "AUTH_OTP_SENT",
                "data": {"sent": True, "expires_at": ts},
            },
            status=status.HTTP_200_OK,
        )


@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class ResetPasswordWithOtpView(APIView):
    """
    POST /customers/auth/reset-password/
    Body: { "email": "user@example.com", "password": "NewPassword123" }
    - Works only after OTP has been verified.
    """
    permission_classes = [permissions.AllowAny]

    def post(self, request):
        sentry_sdk.set_tag("feature", "reset_password_after_otp")
        logger.info("auth.reset_password_after_otp.post.enter")

        # Validate payload
        try:
            s = ResetPasswordWithOtpSerializer(data=request.data)
            s.is_valid(raise_exception=True)
        except serializers.ValidationError as e:
            detail = e.detail if isinstance(e.detail, dict) else {"non_field_errors": e.detail}
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("VALIDATION_ERROR", request),
                    "code": "VALIDATION_ERROR",
                    "errors": detail,
                },
                status=status.HTTP_400_BAD_REQUEST,
            )

        email = s.validated_data["email"].lower()
        new_pw = s.validated_data["password"]

        # Find customer
        customer = Customer.objects.filter(email__iexact=email).first()
        if not customer:
            logger.warning("auth.reset_password_after_otp.no_customer", extra={"email": email})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_NO_CUSTOMER", request),
                    "code": "AUTH_NO_CUSTOMER",
                    "errors": {"email": ["No account found for this email."]},
                },
                status=status.HTTP_404_NOT_FOUND,
            )

        # Ensure OTP verification step was completed
        if not customer.is_email_verified:
            logger.warning("auth.reset_password_after_otp.not_verified", extra={"customer_id": customer.id})
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("AUTH_OTP_NOT_VERIFIED", request),
                    "code": "AUTH_OTP_NOT_VERIFIED",
                    "errors": {"detail": ["Please verify the OTP before resetting the password."]},
                },
                status=status.HTTP_403_FORBIDDEN,
            )

        # Reset password
        try:
            customer.password = make_password(new_pw)
            customer.updated_At = timezone.now()
            customer.save(update_fields=["password", "updated_At"])
            logger.info("auth.reset_password_after_otp.success", extra={"customer_id": customer.id})
        except Exception as e:
            logger.error(
                "auth.reset_password_after_otp.error",
                extra={"customer_id": getattr(customer, "id", None)},
                exc_info=True,
            )
            sentry_sdk.capture_exception(e)
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("PASSWORD_RESET_FAILED", request),
                    "code": "PASSWORD_RESET_FAILED",
                    "errors": {"exception": [str(e)]},
                },
                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
            )

        # Success
        return JsonResponse(
            {
                "success": True,
                "message": get_api_message("PASSWORD_RESET", request),
                "code": "PASSWORD_RESET",
                "data": {"reset": True},
            },
            status=status.HTTP_200_OK,
        )


@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name="dispatch")
class CurrentCustomerView(APIView):
    permission_classes = [IsAuthenticated, HasCustomerProfile]

    def get(self, request):
        sentry_sdk.set_tag("feature", "current_customer")
        logger.info("customer.me.get.enter")

        user = request.user

        # Handle both direct and related customer models
        if isinstance(user, Customer):
            customer = user
        else:
            customer = getattr(user, "customer", None)

        if not customer:
            logger.warning("customer.me.get.not_found")
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("CUSTOMER_NOT_FOUND", request),
                    "code": "CUSTOMER_NOT_FOUND",
                    "errors": {"detail": ["Customer record does not exist for this user."]},
                },
                status=404,
            )

        # Optional: Sentry context
        try:
            sentry_sdk.set_user({"id": f"customer:{customer.id}", "email": customer.email})
            sentry_sdk.set_context(
                "customer",
                {
                    "id": customer.id,
                    "email": customer.email,
                    "status": customer.customer_status,
                    "registration_status": customer.registration_status,
                    "language_id": customer.language_id,
                },
            )
        except Exception:
            pass

        data = {
            "id": customer.id,
            "email": customer.email,
            "first_name": customer.first_name,
            "last_name": customer.last_name,
            "mobile": customer.mobile,
            "language": {
                "id": customer.language.id,
                "code": customer.language.code,
                "name": customer.language.name,
            }
            if getattr(customer, "language", None)
            else None,
            "registration_status": customer.registration_status,
            "customer_status": customer.customer_status,
            "created_at": customer.created_At,
            "updated_at": customer.updated_At,
            "is_locked": customer.is_locked,
            "privacy_policy_version": customer.privacy_policy_version,
            "privacy_policy_accepted": customer.privacy_policy_accepted,
        }

        logger.info("customer.me.get.success", extra={"customer_id": customer.id})

        return JsonResponse(
            {
                "success": True,
                "message": get_api_message("CUSTOMER_PROFILE", request),
                "code": "CUSTOMER_PROFILE",
                "data": data,
            },
            status=200,
        )


@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name="dispatch")
class ChangePasswordAPIView(APIView):
    """
    POST /api/v1/customers/change-password/
    Body: { "current_password": "...", "new_password": "..." }
    """
    permission_classes = [permissions.IsAuthenticated, HasCustomerProfile]

    def post(self, request):
        sentry_sdk.set_tag("feature", "change_password")
        logger.info("customer.password.change.enter")

        serializer = PasswordChangeSerializer(data=request.data, context={"request": request})

        try:
            serializer.is_valid(raise_exception=True)
        except serializers.ValidationError as e:
            err = e.detail

            raw_code = err.get("code", "VALIDATION_ERROR")

            if isinstance(raw_code, list):
                code = raw_code[0]
            else:
                code = raw_code

            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message(code, request),
                    "code": code,
                    "errors": err.get("errors", {}),
                },
                status=status.HTTP_401_UNAUTHORIZED,
            )
        # Resolve customer
        user = request.user
        customer = user if isinstance(user, Customer) else getattr(user, "customer", None)
        if not customer:
            logger.warning("customer.password.change.not_found_user")
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("CUSTOMER_NOT_FOUND", request),
                    "code": "CUSTOMER_NOT_FOUND",
                    "errors": {"detail": ["Customer record does not exist for this user."]},
                },
                status=status.HTTP_404_NOT_FOUND,
            )

        # Update password
        new_password = serializer.validated_data["new_password"]
        customer.password = make_password(new_password)
        customer.updated_At = timezone.now()
        customer.save(update_fields=["password", "updated_At"])

        logger.info("customer.password.change.success", extra={"customer_id": customer.id})
        return JsonResponse(
            {
                "success": True,
                "message": get_api_message("PASSWORD_CHANGED", request),
                "code": "PASSWORD_CHANGED",
                "data": {"changed": True},
            },
            status=status.HTTP_200_OK,
        )


@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name="dispatch")
class UpdateProfileAPIView(APIView):
    """
    PUT/PATCH /api/v1/customers/profile/
    Body (any of): { "first_name": "...", "last_name": "...", "mobile": "..." }
    """
    permission_classes = [permissions.IsAuthenticated, HasCustomerProfile]

    def put(self, request):
        return self._update(request, partial=False)

    def patch(self, request):
        return self._update(request, partial=True)

    def _update(self, request, partial=False):
        sentry_sdk.set_tag("feature", "update_profile")
        logger.info("customer.profile.update.enter", extra={"method": "PATCH" if partial else "PUT"})

        # Validate payload
        try:
            serializer = ProfileUpdateSerializer(data=request.data, partial=partial, context={"request": request})
            serializer.is_valid(raise_exception=True)
        except serializers.ValidationError as e:
            detail = e.detail if isinstance(e.detail, dict) else {"non_field_errors": e.detail}
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("VALIDATION_ERROR", request),
                    "code": "VALIDATION_ERROR",
                    "errors": detail,
                },
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Resolve customer
        user = request.user
        customer = user if isinstance(user, Customer) else getattr(user, "customer", None)
        if not customer:
            logger.warning("customer.profile.update.not_found_user")
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("CUSTOMER_NOT_FOUND", request),
                    "code": "CUSTOMER_NOT_FOUND",
                    "errors": {"detail": ["Customer record does not exist for this user."]},
                },
                status=status.HTTP_404_NOT_FOUND,
            )

        data = serializer.validated_data

        try:
            with transaction.atomic():
                changed_fields = []
                if "first_name" in data and data["first_name"] != customer.first_name:
                    customer.first_name = data["first_name"]; changed_fields.append("first_name")
                if "last_name" in data and data["last_name"] != customer.last_name:
                    customer.last_name = data["last_name"]; changed_fields.append("last_name")
                if "mobile" in data and data["mobile"] != customer.mobile:
                    customer.mobile = data["mobile"]; changed_fields.append("mobile")

                if changed_fields:
                    customer.updated_At = timezone.now()
                    changed_fields.append("updated_At")
                    customer.save(update_fields=changed_fields)

            payload = {
                "id": customer.id,
                "first_name": customer.first_name,
                "last_name": customer.last_name,
                "mobile": customer.mobile,
                "email": customer.email,
                "language_id": customer.language_id,
            }

            logger.info(
                "customer.profile.update.success",
                extra={"customer_id": customer.id, "changed": bool(changed_fields)}
            )

            return JsonResponse(
                {
                    "success": True,
                    "message": get_api_message("PROFILE_UPDATED", request),
                    "code": "PROFILE_UPDATED",
                    "data": payload,
                },
                status=status.HTTP_200_OK,
            )

        except Exception as e:
            logger.error("customer.profile.update.error", extra={"customer_id": getattr(customer, "id", None)}, exc_info=True)
            sentry_sdk.capture_exception(e)
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("PROFILE_UPDATE_FAILED", request),
                    "code": "PROFILE_UPDATE_FAILED",
                    "errors": {"exception": [str(e)]},
                },
                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
            )


@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name="dispatch")
class DeleteMyAccountAPIView(APIView):
    """
    DELETE all data owned by the authenticated customer (atomic).
    Method: DELETE
    Endpoint: /api/v1/customers/me/delete/?dry_run=true|false
    Auth: Authorization: Bearer <JWT>
    """
    permission_classes = [permissions.IsAuthenticated, HasCustomerProfile]

    def delete(self, request):
        try:
            dry_run = str(request.query_params.get("dry_run", "false")).lower() == "true"
            customer: Customer = request.user.customer

            # ---- Explicit dependents (known relations & cross-refs) ----
            devices_qs = CustomerDevice.objects.filter(customer=customer)
            fields_qs  = CustomerField.objects.filter(customer=customer)
            buyers_qs  = CustomerBuyer.objects.filter(customer=customer)

            # Build results queryset with ORs
            results_qs = Result.objects.filter(
                Q(device__customer=customer) |
                Q(field__customer=customer) |
                Q(buyer__customer=customer)
            )

            app_settings_qs = AppSettings.objects.filter(customer_user=customer)

            share_qs = shareDevice.objects.filter(shared_with=customer) if shareDevice else None
            c_thresh_qs = DeviceMoistureThreshold.objects.filter(
                customer_device__customer=customer
            ) if DeviceMoistureThreshold else None
            m_thresh_qs = MyDeviceMoistureThreshold.objects.filter(
                device__customer=customer
            ) if MyDeviceMoistureThreshold else None

            # If your OTP/reset tables FK to Customer, keep as-is; if they FK to User, switch to user=request.user
            otp_qs = EmailOTP.objects.filter(customer=customer) if EmailOTP else None
            reset_qs = PasswordResetToken.objects.filter(customer=customer) if PasswordResetToken else None

            # ---- Generic sweep: any model with FK to Customer ----
            generic_qsets = []
            for model in apps.get_models():
                for field in model._meta.get_fields():
                    if isinstance(field, models.ForeignKey) and getattr(field.remote_field, "model", None) is Customer:
                        try:
                            q = model.objects.filter(**{field.name: customer})
                            if q.exists():
                                generic_qsets.append((f"{model._meta.label}({field.name})", q))
                        except Exception:
                            pass

            # ---- Counts (for dry_run + final response) ----
            def _count(qs): return int(qs.count()) if qs is not None else 0
            counts = {
                "results": _count(results_qs),
                "devices": _count(devices_qs),
                "fields": _count(fields_qs),
                "buyers": _count(buyers_qs),
                "app_settings": _count(app_settings_qs),
                "device_shares": _count(share_qs),
                "core_device_moisture_thresholds": _count(c_thresh_qs),
                "mydevice_moisture_thresholds": _count(m_thresh_qs),
                "email_otp": _count(otp_qs),
                "password_resettoken": _count(reset_qs),
                "generic_fk_to_customer": sum(_count(q) for _, q in generic_qsets),
                "customer": 1,
            }

            if dry_run:
                generic_breakdown = {label: _count(qs) for label, qs in generic_qsets if _count(qs) > 0}
                return JsonResponse(
                    {
                        "success": True,
                        "message": get_api_message("ACCOUNT_DELETE_DRY_RUN", request),
                        "code": "ACCOUNT_DELETE_DRY_RUN",
                        "data": {
                            "will_delete": counts,
                            "generic_breakdown": generic_breakdown,
                        },
                    },
                    status=status.HTTP_200_OK,
                )

            # ---- Hard delete (one transaction) ----
            with transaction.atomic():
                # 1) Leaf dependents first
                if c_thresh_qs is not None:
                    c_thresh_qs.delete()
                if m_thresh_qs is not None:
                    m_thresh_qs.delete()

                # 2) Results
                results_qs.delete()

                # 3) Shares
                if share_qs is not None:
                    share_qs.delete()

                # 4) App settings
                app_settings_qs.delete()

                # 5) OTP/reset tokens
                if otp_qs is not None:
                    otp_qs.delete()
                if reset_qs is not None:
                    reset_qs.delete()

                # 6) Generic FK sweep (retry a few times in case of PROTECT chains)
                for _ in range(3):
                    remaining = []
                    for label, qs in generic_qsets:
                        try:
                            qs.delete()
                        except Exception:
                            remaining.append((label, qs))
                    if not remaining:
                        break
                    generic_qsets = remaining

                # 7) Primary children
                buyers_qs.delete()
                fields_qs.delete()
                devices_qs.delete()

                # 8) Finally the customer
                customer.delete()

            return JsonResponse(
                {
                    "success": True,
                    "message": get_api_message("ACCOUNT_DELETED", request),
                    "code": "ACCOUNT_DELETED",
                    "data": {"deleted": counts},
                },
                status=status.HTTP_200_OK,
            )

        except Exception as e:
            return JsonResponse(
                {
                    "success": False,
                    "message": get_api_message("ACCOUNT_DELETE_FAILED", request),
                    "code": "ACCOUNT_DELETE_FAILED",
                    "errors": {"exception": [str(e)]},
                },
                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
            )