
from decimal import Decimal, InvalidOperation
from rest_framework.response import Response
from django.shortcuts import get_object_or_404, render,redirect
from django.views import View
from codesofy.custom_config import get_local_date, set_user_profile,convert_to_decimal,get_privilleges,get_standard_text_input,get_unique_text,is_authorized,set_menu_items,get_global_master_details
from codesofy.master_details import PerPageSelector, ProductType
from constants.general_const import ActiveStatus
from core.utils import get_api_message
from cropmanagement.models import Crop
from mydevicemanagement import apps
from products.models import Product
from scalemanagement.serializers import ScaleCropMappingLiteSerializer
from .models import Scale, ScaleCropMapping
from usermanagement.models import UserProfile
from utils.product_utils import dict_diff, product_to_dict
from rest_framework import serializers
from django.contrib import messages
from django.db import transaction, IntegrityError
from django.db.models import RestrictedError
from django.views.decorators.cache import cache_control
from django.utils.decorators import method_decorator
import math
from django.core.paginator import Paginator
from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
import json
from django.db.models import Max
from PIL import Image
from io import BytesIO
from django.utils.timezone import now
from django.contrib import messages
from django.db import transaction, IntegrityError, models
from rest_framework import viewsets, permissions, status
from django.db.models.deletion import RestrictedError
from django.db.models import ProtectedError
from rest_framework import viewsets, permissions

from django.db.models import Q
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.core.paginator import Paginator
from django.db.models import RestrictedError
from django.core.exceptions import ObjectDoesNotExist

menu_item = "scale_registry"

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_customer(request):
    cust = getattr(request.user, "customer", None)
    if not cust:
        # Should not happen because HasCustomerProfile is applied, but keep safe
        raise Http404("Not found.")
    return cust
class HasCustomerProfile(permissions.BasePermission):
    """Allow only authenticated users that have a related .customer object."""
    def has_permission(self, request, view):
        return bool(request.user and request.user.is_authenticated and getattr(request.user, "customer", None))

class IsCustomerOwner(permissions.BasePermission):
    """
    Object-level permission helper for APIViews.
    Use it by calling `self.check_object_permissions(request, obj)` in your view after loading `obj`.
    Expects obj.customer_device.customer_id to match the current user's customer.id.
    """
    def has_object_permission(self, request, view, obj):
        cust = getattr(request.user, "customer", None)
        if not cust:
            return False
        device = getattr(obj, "customer_device", None)
        if not device:
            return False
        return getattr(device, "customer_id", None) == cust.id



def get_local_master_details_for_add_update_scalecropmapping():
    
    context = get_local_master_details()
    scale_list = Scale.objects.all()
    crop_list = Crop.objects.all()
    mapping_status = ActiveStatus.to_list()
    context["mapping_status_list"] = mapping_status
    context['scale_list'] = scale_list
    context['crop_list'] = crop_list
    
    return context

def get_local_master_details_for_scalecropmapping_details():
    
    context = get_local_master_details()
    scale_list = Scale.objects.all()
    mapping_status = ActiveStatus.to_list_for_reports()
    context["mapping_status_list"] = mapping_status
    context['scale_list'] = scale_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 AddScaleView(View):
    this_feature = "add_scale"
    sub_menu_item = "scale_list"

    def get(self, request):
        # This page is just the form; but your form+list live together.
        # Redirect to the list so the template gets full context.
        return redirect('scale-list')

    def post(self, request):
        
        user_profile = set_user_profile(request, {})
        if user_profile is None:
            return redirect('login')

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

        description = (request.POST.get('description') or '').strip()
        scale_id = (request.POST.get('scale_id') or '').strip()

        # Basic server-side validation
        if not scale_id or not description :
            messages.error(request, "Scale ID $Description  are required.")
            return redirect('scale-list')

        try:
            with transaction.atomic():
                # Normalize case consistently for both check and save
                scale_id = scale_id

                if Scale.objects.filter(scale_id__iexact=scale_id).exists():
                    messages.error(request, "Unique Scale ID already exists.")
                    return redirect('scale-list')

                Scale.objects.create(
                    description=description,
                    scale_id=scale_id,
                    created_by=user_profile,
                    updated_by=user_profile,
                    created_At = now(),
                    updated_At = now(),
                )

                messages.success(request, "New scale was created successfully.")
                return redirect('scale-list')

        except Exception as exp:
            messages.error(request, f"Unexpected Error: {exp}")
            messages.error(request, "Error occurred in atomic operation. Contact the administrator.")
            return redirect('scale-list')


@method_decorator(cache_control(no_cache=True, must_revalidate=True,no_store=True), name='dispatch')
class ViewScaleView(View):

    this_feature = "view_scale"
    sub_menu_item = "scale_list"
    
    def get(self, request,id):

        context = get_local_master_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)

        try:
            with transaction.atomic():
                if Scale.objects.filter(pk=id).exists():
                    scale = Scale.objects.get(pk=id)
                    context["scale"] = scale
                    return render(request, 'scale.html', context)
                else:
                    return redirect ('scale')
        except Exception or IntegrityError as exp:
            print (exp)
            messages.error(
                request, "Error Occured in atomic operation. Contact the System Administrator")
            return redirect ('scale')
        
     


@method_decorator(cache_control(no_cache=True, must_revalidate=True,no_store=True), name='dispatch')
class ScaleListView(View):

    this_feature = "scale_list"
    sub_menu_item = "scale_list"

    def get(self, request):

        context = get_local_master_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)
       


        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 = 10

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

        context["default_per_page_count"] = per_page_count


        scale_list = Scale.objects.all().order_by('-id')

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

        paginator = Paginator(scale_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, 'scale.html', context)
    
@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class UpdateScaleView(View):
    this_feature  = "update_scale"
    sub_menu_item = "scale_list"

    # Handles: GET /update-scale/<int:id>
    def get(self, request, 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')

        set_sub_menu_item(self.sub_menu_item, context)

        lang = get_object_or_404(Scale, pk=id)
        context["scale"] = lang
        context["old_input_field_values"] = {}
        return render(request, "update-scale.html", context)

    # Handles: POST /update-scale (id is taken from form)
    def post(self, request):
        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')

        set_sub_menu_item(self.sub_menu_item, context)

        try:
            id_ = int(request.POST.get("id") or 0)
        except ValueError:
            messages.error(request, "Invalid scale id.")
            return redirect("scale-list")


        description = (request.POST.get("description") or "").strip()
        scale_id = (request.POST.get("scale_id") or"").strip()
        
        if not id_ or not description or not scale_id:
            messages.error(request, "Scale id, and name are required.")
            return redirect("scale-list")

        lang = get_object_or_404(Scale, pk=id_)

        # Check unique  (ignore current row)
        if Scale.objects.exclude(pk=id_).filter(scale_id__iexact=scale_id).exists():
            messages.error(request, "Scale id already exists.")
            return redirect("scale-list")

        # unique check stays the same...
        try:
            with transaction.atomic():
                changed = False
                if lang.description != description:
                    lang.description = description; changed = True
                if lang.scale_id != scale_id:
                    lang.scale_id = scale_id; changed =True
                if not changed:
                    messages.info(request, "No changes detected.")
                    return redirect("scale-list")


                lang.updated_by = user_profile
                lang.save(update_fields=["description","scale_id","updated_by", "updated_At"])

            messages.success(request, "Scale updated successfully.")
            return redirect("scale-list")

        except Exception as e:
            messages.error(request, f"Update failed: {e}")
            return redirect("scale-list")



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

    # Handles: POST /delete-scale (id is taken from form)
    def post(self, request):
        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')

        try:
            id_ = int(request.POST.get("id") or 0)
        except ValueError:
            messages.error(request, "Invalid scale id.")
            return redirect("scale-list")

        if not id_:
            messages.error(request, "Missing scale id.")
            return redirect("scale-list")

        scale_id = get_object_or_404(Scale, pk=id_)

        try:
            with transaction.atomic():
                scale_id.delete()
            messages.success(request, "Scale deleted.")
            return redirect("scale-list")

        except ProtectedError:
            messages.error(request, "Deletion blocked: related data exists.")
            return redirect("scale-list")
        except IntegrityError:
            messages.error(request, "Deletion failed due to related constraints.")
            return redirect("scale-list")
        except Exception as e:
            messages.error(request, f"Unexpected error during delete: {e}")
            return redirect("scale-list")
        
        
        

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

    def get(self, request):
        context = get_local_master_details_for_add_update_scalecropmapping()

        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-scalecropmapping.html", context)

    def post(self, request):
        context = get_local_master_details_for_add_update_scalecropmapping()

        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) ----
        scale_id = (request.POST.get("scale") 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"] = {
                "scale": scale_id,
                "crop": crop_id,
                "scale_order_number": scale_order_number,
                "universal_moisture_threshold": universal_moisture_threshold,
            }

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

        # ---- Resolve FKs ----
        try:
            scale = Scale.objects.get(pk=scale_id)
            crop = Crop.objects.get(pk=crop_id)
        except ObjectDoesNotExist:
            messages.error(request, "Invalid scale or crop selection.")
            keep_form_values()
            return render(request, "add-scalecropmapping.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-scalecropmapping.html", context)

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

        messages.success(
            request, f'Mapping created: "{mapping.scale.scale_id} ↔ {mapping.crop.name}".'
        )
        return redirect("view-scalecropmapping", id=mapping.id)
@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class ViewScaleCropMappingView(View):
    this_feature = "view_scalecropmapping"
    sub_menu_item = "scalecropmapping_details"

    def get(self, request, id):
        context = get_local_master_details_for_scalecropmapping_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(
            ScaleCropMapping.objects.select_related('scale', 'crop'),
            pk=id
        )
        context["mapping"] = mapping
        return render(request, 'view-scalecropmapping.html', context)
        
@method_decorator(cache_control(no_cache=True, must_revalidate=True,no_store=True), name='dispatch')
class ScaleCropMappingDetailsView(View):

    this_feature = "scalecropmapping_details"
    sub_menu_item = "scalecropmapping_details"

    def get(self, request):

        context = get_local_master_details_for_scalecropmapping_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')
        scale = request.GET.get('scale')
        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 scale:
            context ["default_scale"] = scale
            filter_kwargs["scale__scale_id__iexact"] = scale
          


        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 = ScaleCropMapping.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, 'scalecropmapping-details.html', context)
    
@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name="dispatch")
class UpdateScaleCropMappingView(View):
    this_feature = "update_scalecropmapping"
    sub_menu_item = "scalecropmapping_details"

    def get(self, request, id):
        context = get_local_master_details_for_add_update_scalecropmapping()
        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 = get_object_or_404(
            ScaleCropMapping.objects.select_related("scale", "crop"),
            pk=id
        )
        context["mapping"] = mapping

        context["old_input_field_values"] = {
            "scale": str(mapping.scale_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-scalecropmapping.html", context)

    def post(self, request, id):
        context = get_local_master_details_for_add_update_scalecropmapping()
        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 = get_object_or_404(
            ScaleCropMapping.objects.select_related("scale", "crop"),
            pk=id
        )
        context["mapping"] = mapping  

        scale_id = (request.POST.get("scale") 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()

        context["old_input_field_values"] = {
            "scale": scale_id,
            "crop": crop_id,
            "scale_order_number": scale_order_number,
            "universal_moisture_threshold": universal_moisture_threshold,
        }

        if not scale_id or not crop_id:
            messages.error(request, "Scale and crop are required.")
            return render(request, "update-scalecropmapping.html", context)

        try:
            scale = Scale.objects.get(pk=scale_id)
            crop = Crop.objects.get(pk=crop_id)
        except (Scale.DoesNotExist, Crop.DoesNotExist):
            messages.error(request, "Invalid scale or crop selection.")
            return render(request, "update-scalecropmapping.html", context)

        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-scalecropmapping.html", context)

        try:
            with transaction.atomic():
                mapping.scale = scale
                mapping.crop = crop
                mapping.scale_order_number = scale_order_number
                mapping.universal_moisture_threshold = umt
                mapping.updated_user = user_profile
                mapping.updated_at =now()
                mapping.save() 

        except IntegrityError:
            messages.error(request, "Another mapping already exists for that Scale ↔ Crop pair.")
            return render(request, "update-scalecropmapping.html", context)

        messages.success(request, f'Updated mapping: "{mapping.scale.scale_id} ↔ {mapping.crop.name}".')
        return redirect("view-scalecropmapping", id=mapping.id)
@method_decorator(cache_control(no_cache=True, must_revalidate=True, no_store=True), name='dispatch')
class DeleteScaleCropMappingView(View):
    this_feature = "delete_scalecropmapping"
    sub_menu_item = "scalecropmapping_details"

    def post(self, request):
        context = get_local_master_details_for_scalecropmapping_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('scalecropmapping-details')

        # Fetch the mapping first (no lock yet)
        mapping = ScaleCropMapping.objects.select_related("scale", "crop").filter(pk=mapping_id).first()
        if not mapping:
            messages.error(request, "Scale–Crop mapping not found.")
            return redirect('scalecropmapping-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('scalecropmapping-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 = ScaleCropMapping.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('scalecropmapping-details')

                scale_name = locked.scale.scale_id
                crop_name = locked.crop.name
                locked.delete()
                messages.success(request, f'Mapping "{scale_name} ↔ {crop_name}" deleted.')
                return redirect('scalecropmapping-details')

        except ScaleCropMapping.DoesNotExist:
            messages.error(request, "Scale–Crop mapping not found.")
            return redirect('scalecropmapping-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('scalecropmapping-details')     

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

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

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

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

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

        data = ScaleCropMappingLiteSerializer(qs, many=True).data

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