# from django.http import JsonResponse
# from django.utils import timezone
# import math
# from django.contrib import messages
# from django.db import IntegrityError,transaction
# from django.views import View
# from rest_framework.views import APIView
# from rest_framework.response import Response
# from rest_framework.permissions import AllowAny
# from rest_framework import status
# from django.shortcuts import get_object_or_404, redirect, render
# from django.core.paginator import Paginator
# from django.db.models import RestrictedError
# from django.core.exceptions import ObjectDoesNotExist
# from codesofy.custom_config import get_global_master_details, get_privilleges, is_authorized, set_menu_items, set_user_profile
# from codesofy.master_details import PerPageSelector, ProductType, UserAccountStatus
# from constants.general_const import ActiveStatus
# from core.utils import get_api_message
# from cropmanagement.models import Crop
# from usermanagement.models import UserProfile
# from decimal import Decimal, InvalidOperation
# from .models import ProductCropMapping
# from products.models import Product
# from .serializers import ProductCropMappingLiteSerializer
# from django.views.decorators.cache import cache_control
# from django.utils.decorators import method_decorator


# menu_item = "cropmapping_registry"


# 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()
#     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_cropmapping():
    
#     context = get_local_master_details()
#     product_list = Product.objects.all()
#     crop_list = Crop.objects.all()
#     mapping_status = ActiveStatus.to_list()
#     context["mapping_status_list"] = mapping_status
#     context['product_list'] = product_list
#     context['crop_list'] = crop_list
    
#     return context

# def get_local_master_details_for_cropmapping_details():
    
#     context = get_local_master_details()
#     product_list = Product.objects.all()
#     mapping_status = ActiveStatus.to_list_for_reports()
#     context["mapping_status_list"] = mapping_status
#     context['product_list'] = product_list
    
#     return context
# def find_reverse_dependencies(instance, sample_per_relation: int = 3):
#     """
#     Return a dict of {relation_label: [pk,...]} for reverse related objects
#     that currently exist referencing `instance`. Uses model metadata, so you
#     don't have to hardcode table names.
#     """
#     blockers = {}

#     # Walk all reverse relations declared on the model
#     for rel in instance._meta.related_objects:
#         # rel is a ManyToOneRel / ManyToManyRel / OneToOneRel (reverse side)
#         accessor = rel.get_accessor_name()  # e.g. "order_items", "results"
#         manager = getattr(instance, accessor, None)
#         if manager is None:
#             continue

#         # For O2O, manager is a single object accessor; handle safely
#         try:
#             if rel.one_to_one:
#                 try:
#                     obj = manager
#                     # If it exists, accessing .pk will not raise
#                     _ = obj.pk
#                     blockers[rel.related_model._meta.label] = [obj.pk]
#                 except rel.related_model.DoesNotExist:
#                     pass
#                 continue
#         except AttributeError:
#             # Older Django or unusual relation – skip
#             continue

#         # For reverse FK/M2M, manager is a RelatedManager
#         try:
#             qs = manager.all()
#         except Exception:
#             continue

#         # Only flag if there are rows
#         if qs.exists():
#             # Sample a few PKs for the message
#             blockers[rel.related_model._meta.label] = list(qs.values_list("pk", flat=True)[:sample_per_relation])

#     return blockers
# @method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
# class AddCropMappingView(View):
#     this_feature = "add_cropmapping"
#     sub_menu_item = "add_cropmapping"

#     def get(self, request):
#         context = get_local_master_details_for_add_update_cropmapping()

#         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")

#         set_sub_menu_item(self.sub_menu_item, context)
#         return render(request, "add-cropmapping.html", context)

#     def post(self, request):
#         context = get_local_master_details_for_add_update_cropmapping()

#         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")

#         set_sub_menu_item(self.sub_menu_item, context)

#         # ---- Raw inputs (IDs for FKs expected from the form) ----
#         product_id = (request.POST.get("product") or "").strip()
#         crop_id = (request.POST.get("crop") or "").strip()
#         scale_order_number = (request.POST.get("scale_order_number") or "").strip()
#         universal_moisture_threshold = (request.POST.get("universal_moisture_threshold") or "").strip()

#         # Keep what the user typed to re-fill the form on error
#         def keep_form_values():
#             context["form_values"] = {
#                 "product": product_id,
#                 "crop": crop_id,
#                 "scale_order_number": scale_order_number,
#                 "universal_moisture_threshold": universal_moisture_threshold,
#             }

#         # ---- Required fields check ----
#         if not product_id or not crop_id:
#             messages.error(request, "Product and crop are required.")
#             keep_form_values()
#             return render(request, "add-cropmapping.html", context)

#         # ---- Resolve FKs ----
#         try:
#             product = Product.objects.get(pk=product_id)
#             crop = Crop.objects.get(pk=crop_id)
#         except ObjectDoesNotExist:
#             messages.error(request, "Invalid product or crop selection.")
#             keep_form_values()
#             return render(request, "add-cropmapping.html", context)

#         # ---- Parse optional numeric field (no range validation) ----
#         umt = None
#         if universal_moisture_threshold:
#             try:
#                 # Use Decimal to match DecimalField and avoid float quirks
#                 umt = Decimal(universal_moisture_threshold)
#             except (InvalidOperation, ValueError):
#                 messages.error(request, "Moisture threshold must be a numeric value.")
#                 keep_form_values()
#                 return render(request, "add-cropmapping.html", context)

#         # ---- Create with race-safety & unique-pair handling ----
#         try:
#             with transaction.atomic():
#                 mapping = ProductCropMapping.objects.create(
#                     product=product,
#                     crop=crop,
#                     scale_order_number=scale_order_number,
#                     universal_moisture_threshold=umt,
#                     created_user=user_profile,
#                     updated_user=user_profile,
#                 )
#         except IntegrityError:
#             # UniqueConstraint(fields=['product','crop']) hit
#             messages.error(request, "That product is already mapped to this crop.")
#             keep_form_values()
#             return render(request, "add-cropmapping.html", context)

#         messages.success(
#             request, f'Mapping created: "{mapping.product.name} ↔ {mapping.crop.name}".'
#         )
#         return redirect("view-cropmapping", id=mapping.id)
# @method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
# class ViewCropMappingView(View):
#     this_feature = "view_cropmapping"
#     sub_menu_item = "cropmapping_details"

#     def get(self, request, id):
#         context = get_local_master_details_for_cropmapping_details()

#         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')

#         set_sub_menu_item(self.sub_menu_item, context)

#         # Read-only fetch; no transaction needed
#         mapping = get_object_or_404(
#             ProductCropMapping.objects.select_related('product', 'crop'),
#             pk=id
#         )
#         context["mapping"] = mapping
#         return render(request, 'view-cropmapping.html', context)
        
# @method_decorator(cache_control(no_cache=True, must_revalidate=True,no_store=True), name='dispatch')
# class CropMappingDetailsView(View):

#     this_feature = "cropmapping_details"
#     sub_menu_item = "cropmapping_details"

#     def get(self, request):

#         context = get_local_master_details_for_cropmapping_details()

#         user_profile = set_user_profile(request,context)

#         if user_profile==None:
#             return redirect('login')

#         get_privilleges(user_profile,context)

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


#         crop = request.GET.get('crop')
#         product = request.GET.get('product')
#         scale_order_number = request.GET.get('scale_order_number')


#         # is_active = request.GET.get('is_active')
        


#         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))

#         #filter logic applies here

#         filter_kwargs = {}

#         if scale_order_number:
#             context["default_scale_order_number"] = scale_order_number
#             filter_kwargs['scale_order_number__icontains']= scale_order_number
#         if product:
#     # filter by product code (case-insensitive exact)
#           filter_kwargs["product__code__iexact"] = product
#           context ["default_product"] = product


#         if crop:
#             context["default_crop"] = crop
#             filter_kwargs['crop__name']= crop
                
#         # if is_active:
#         #     if is_active.lower()!="all":
#         #         context["default_is_active"] = ActiveStatus(is_active).value
#         #         filter_kwargs['is_active']= ActiveStatus(is_active).value
                

        

       
#         mapping_list = ProductCropMapping.objects.filter(**filter_kwargs).order_by('-id')

#         if len(mapping_list)>0:
#             context["has_entries"] = True
#         else:
#             context["has_entries"] = False

#         paginator = Paginator(mapping_list,per_page_count)
#         page = Paginator.get_page(paginator,page_number)
#         page_list = list(paginator.get_elided_page_range(page_number, on_each_side=1))
        
#         context["page"] = page
#         context["page_list"] = page_list

#         return render(request, 'cropmapping-details.html', context)
    
# @method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
# class UpdateCropMappingView(View):

#     this_feature = "update_cropmapping"
#     sub_menu_item = "cropmapping_details"

#     def get(self, request, id):
#         context = get_local_master_details_for_add_update_cropmapping()
#         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')

#         set_sub_menu_item(self.sub_menu_item, context)

#         # Load mapping (NOT Crop)
#         mapping = get_object_or_404(ProductCropMapping, pk=id)
#         context["mapping"] = mapping

#         # Pre-fill form fields (useful if your template reads old_input_field_values)
#         context["old_input_field_values"] = {
#             "product": str(mapping.product_id),
#             "crop": str(mapping.crop_id),
#             "scale_order_number": mapping.scale_order_number or "",
#             "universal_moisture_threshold": (
#                 "" if mapping.universal_moisture_threshold is None
#                 else str(mapping.universal_moisture_threshold)
#             ),
#         }

#         return render(request, 'update-cropmapping.html', context)

#     def post(self, request, id):
#         context = get_local_master_details_for_add_update_cropmapping()
#         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')

#         set_sub_menu_item(self.sub_menu_item, context)

#         mapping = ProductCropMapping.objects.filter(pk=id).select_related("product", "crop").first()
#         if not mapping:
#             messages.error(request, "Product–Crop mapping not found.")
#             return render(request, 'update-cropmapping.html', context)

#         # Read & normalize inputs (expecting **IDs** from the form)
#         product_id = (request.POST.get("product") or "").strip()
#         crop_id = (request.POST.get("crop") or "").strip()
#         scale_order_number = (request.POST.get("scale_order_number") or "").strip()
#         universal_moisture_threshold = (request.POST.get("universal_moisture_threshold") or "").strip()

#         # Preserve user input on error
#         context["old_input_field_values"] = {
#             "product": product_id,
#             "crop": crop_id,
#             "scale_order_number": scale_order_number,
#             "universal_moisture_threshold": universal_moisture_threshold,
#         }

#         # Basic required fields for FKs
#         if not product_id or not crop_id:
#             messages.error(request, "Product and crop are required.")
#             return render(request, 'update-cropmapping.html', context)

#         # Resolve FKs by primary key (IDs). If your template posts codes/names instead,
#         # switch to filtering by unique fields (code__iexact/name__iexact) as shown earlier.
#         try:
#             product = Product.objects.get(pk=product_id)
#             crop =   Crop.objects.get(pk=crop_id)
#         except (Product.DoesNotExist, Crop.DoesNotExist):
#             messages.error(request, "Invalid product or crop selection.")
#             return render(request, 'update-cropmapping.html', context)

#         # Parse optional numeric field (Decimal to match DecimalField; no range enforcement here)
#         umt = None
#         if universal_moisture_threshold:
#             try:
#                 umt = Decimal(universal_moisture_threshold)
#             except (InvalidOperation, ValueError):
#                 messages.error(request, "Moisture threshold must be a numeric value.")
#                 return render(request, 'update-cropmapping.html', context)

#         # Update safely; handle unique (product, crop) collisions
#         try:
#             with transaction.atomic():
#                 mapping.product = product
#                 mapping.crop = crop
#                 mapping.scale_order_number = scale_order_number
#                 mapping.universal_moisture_threshold = umt  # can be None
#                 mapping.updated_user = user_profile
#                 mapping.updated_at = timezone.now()
#                 mapping.save()  # may raise IntegrityError on unique constraint

#         except IntegrityError:
#             # Likely violates UniqueConstraint('product', 'crop')
#             messages.error(request, "Another mapping already exists for that Product ↔ Crop pair.")
#             return render(request, 'update-cropmapping.html', context)
#         except Exception:
#             messages.error(request, "Unexpected error. Please contact the system administrator.")
#             return render(request, 'update-cropmapping.html', context)

#         messages.success(
#             request,
#             f'Updated mapping: "{mapping.product.name} ↔ {mapping.crop.name}".'
#         )
#         # Choose the redirect that matches your URLs:
#         # return redirect('cropmapping-details')  # if it’s an index/list page
#         return redirect('view-cropmapping', id=mapping.id)  # if you have a detail view

# @method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
# class DeleteCropMappingView(View):
#     this_feature = "delete_cropmapping"
#     sub_menu_item = "cropmapping_details"

#     def post(self, request):
#         context = get_local_master_details_for_cropmapping_details()

#         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')

#         set_sub_menu_item(self.sub_menu_item, context)

#         mapping_id = (request.POST.get('mapping_id') or '').strip()
#         if not mapping_id:
#             messages.error(request, "No mapping id provided.")
#             return redirect('cropmapping-details')

#         # Fetch the mapping first (no lock yet)
#         mapping = ProductCropMapping.objects.select_related("product", "crop").filter(pk=mapping_id).first()
#         if not mapping:
#             messages.error(request, "Product–Crop mapping not found.")
#             return redirect('cropmapping-details')

#         # 1) Pre-check for dependencies (reverse relations)
#         blockers = find_reverse_dependencies(mapping)
#         if blockers:
#             # Build a friendly message listing what blocks the deletion
#             lines = []
#             for label, ids in blockers.items():
#                 lines.append(f"{label} (example ids: {', '.join(map(str, ids))})")
#             msg = "Cannot delete: this mapping is referenced by other records → " + "; ".join(lines)
#             messages.error(request, msg)
#             return redirect('cropmapping-details')

#         # 2) No blockers found; lock + delete atomically to prevent race
#         try:
#             with transaction.atomic():
#                 # Lock the same row before deleting, to avoid a new reference being created mid-flight
#                 locked = ProductCropMapping.objects.select_for_update().get(pk=mapping.pk)

#                 # Double-check dependencies right before delete (TOCTOU safety)
#                 blockers_now = find_reverse_dependencies(locked)
#                 if blockers_now:
#                     lines = []
#                     for label, ids in blockers_now.items():
#                         lines.append(f"{label} (example ids: {', '.join(map(str, ids))})")
#                     msg = "Cannot delete (just referenced): " + "; ".join(lines)
#                     messages.error(request, msg)
#                     return redirect('cropmapping-details')

#                 product_name = locked.product.name
#                 crop_name = locked.crop.name
#                 locked.delete()
#                 messages.success(request, f'Mapping "{product_name} ↔ {crop_name}" deleted.')
#                 return redirect('cropmapping-details')

#         except ProductCropMapping.DoesNotExist:
#             messages.error(request, "Product–Crop mapping not found.")
#             return redirect('cropmapping-details')
#         except Exception as e:
#             # Fallback: if your DB-level FKs are RESTRICT/PROTECT, this will still be safe.
#             messages.error(request, f"Could not delete. {e}")
#             return redirect('cropmapping-details')     

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

#     def get(self, request):
#         product_id = request.query_params.get("product_id")

#         # Validate query param
#         if not product_id:
#             return JsonResponse(
#                 {
#                     "success": False,
#                     "message": get_api_message("MISSING_PRODUCT_ID", request),
#                     "code": "MISSING_PRODUCT_ID",
#                     "errors": {"product_id": ["This query parameter is required."]},
#                 },
#                 status=400,
#             )

#         # Validate product exists
#         if not Product.objects.filter(pk=product_id).exists():
#             return JsonResponse(
#                 {
#                     "success": False,
#                     "message": get_api_message("PRODUCT_NOT_FOUND", request),
#                     "code": "PRODUCT_NOT_FOUND",
#                     "errors": {"product_id": [f"No product with id {product_id}."]},
#                 },
#                 status=404,
#             )

#         # Fetch relevant mappings
#         qs = (
#             ProductCropMapping.objects
#             .filter(product_id=product_id)
#             .select_related("crop")
#             .order_by("scale_order_number")
#         )

#         data = ProductCropMappingLiteSerializer(qs, many=True).data

#         return JsonResponse(
#             {
#                 "success": True,
#                 "message": get_api_message("MAPPINGS_FETCHED", request),
#                 "code": "MAPPINGS_FETCHED",
#                 "data": data,  # list directly
#             },
#             status=200,
#         )
