from copy import deepcopy from uuid import UUID from django.core.exceptions import ImproperlyConfigured from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework import serializers, status from rest_framework.response import Response from rest_framework.views import APIView from config.swagger import code_response from external_api_adapter.adapter import request as external_api_request from external_api_adapter.exceptions import ExternalAPIRequestError from farm_hub.models import FarmHub from .serializers import ( FarmUUIDRequestSerializer, KOptionActivateSerializer, LocationDataUpsertSerializer, ) from .services import ( AI_CLUSTER_RECOMMENDATIONS_PATH, AI_LOCATION_DATA_PATH, AI_REMOTE_SENSING_PATH, ) AI_PROXY_ERROR_MESSAGE = "ارتباط با سرویس AI ناموفق بود." FARM_NOT_FOUND_MESSAGE = "مزرعه پیدا نشد." QUERY_FARM_NOT_FOUND_MESSAGE = "location پیدا نشد." SUCCESS_RESPONSE = code_response("LocationDataGenericSuccess", data=serializers.JSONField()) ERROR_RESPONSE = code_response("LocationDataGenericError", data=serializers.JSONField()) LOCATION_DATA_QUERY_PARAMETERS = [ OpenApiParameter("lat", OpenApiTypes.NUMBER, OpenApiParameter.QUERY, required=False), OpenApiParameter("lon", OpenApiTypes.NUMBER, OpenApiParameter.QUERY, required=False), OpenApiParameter("farm_uuid", OpenApiTypes.UUID, OpenApiParameter.QUERY, required=False), ] REMOTE_SENSING_QUERY_PARAMETERS = [ OpenApiParameter("farm_uuid", OpenApiTypes.UUID, OpenApiParameter.QUERY, required=True), OpenApiParameter("page", OpenApiTypes.INT, OpenApiParameter.QUERY, required=False), OpenApiParameter("page_size", OpenApiTypes.INT, OpenApiParameter.QUERY, required=False), OpenApiParameter("start_date", OpenApiTypes.DATE, OpenApiParameter.QUERY, required=False), OpenApiParameter("end_date", OpenApiTypes.DATE, OpenApiParameter.QUERY, required=False), ] CLUSTER_BLOCK_LIVE_QUERY_PARAMETERS = [ OpenApiParameter("start_date", OpenApiTypes.DATE, OpenApiParameter.QUERY, required=False), OpenApiParameter("end_date", OpenApiTypes.DATE, OpenApiParameter.QUERY, required=False), OpenApiParameter("farm_uuid", OpenApiTypes.UUID, OpenApiParameter.QUERY, required=False), ] OPTIONAL_FARM_UUID_QUERY_PARAMETER = [ OpenApiParameter("farm_uuid", OpenApiTypes.UUID, OpenApiParameter.QUERY, required=False), ] class AILocationDataProxyView(APIView): ai_path = AI_LOCATION_DATA_PATH farm_uuid_locations = () farm_not_found_message = FARM_NOT_FOUND_MESSAGE def _build_path(self, **kwargs): return self.ai_path.format(**kwargs) def _get_payload(self, request): if not request.data: return None if isinstance(request.data, dict): return deepcopy(request.data) return request.data def _get_query(self, request): if not request.query_params: return None query = {} for key, values in request.query_params.lists(): query[key] = values if len(values) > 1 else values[0] return query def _parse_uuid(self, value): if not value: return None try: return UUID(str(value)) except (TypeError, ValueError, AttributeError): return None def _extract_farm_uuid(self, request, payload, query): for location in self.farm_uuid_locations: if location == "body" and isinstance(payload, dict) and payload.get("farm_uuid"): parsed = self._parse_uuid(payload.get("farm_uuid")) if parsed is not None: return parsed if location == "query" and isinstance(query, dict) and query.get("farm_uuid"): parsed = self._parse_uuid(query.get("farm_uuid")) if parsed is not None: return parsed return None def _ensure_farm_access(self, request, farm_uuid): if farm_uuid is None: return None if FarmHub.objects.filter(farm_uuid=farm_uuid, owner=request.user).exists(): return None return Response( {"code": 404, "msg": self.farm_not_found_message, "data": None}, status=status.HTTP_404_NOT_FOUND, ) def _build_proxy_error(self, exc): return Response( {"code": 502, "msg": AI_PROXY_ERROR_MESSAGE, "data": {"detail": str(exc)}}, status=status.HTTP_502_BAD_GATEWAY, ) def _proxy(self, request, *, method, **path_kwargs): payload = self._get_payload(request) query = self._get_query(request) farm_uuid = self._extract_farm_uuid(request, payload, query) farm_error = self._ensure_farm_access(request, farm_uuid) if farm_error is not None: return farm_error try: adapter_response = external_api_request( "ai", self._build_path(**path_kwargs), method=method, payload=payload, query=query, ) except (ExternalAPIRequestError, ImproperlyConfigured) as exc: return self._build_proxy_error(exc) response_payload = adapter_response.data if not isinstance(response_payload, dict): response_payload = { "code": adapter_response.status_code, "msg": "success" if adapter_response.status_code < 400 else "error", "data": response_payload, } return Response(response_payload, status=adapter_response.status_code) class LocationDataView(AILocationDataProxyView): farm_uuid_locations = ("query", "body") farm_not_found_message = QUERY_FARM_NOT_FOUND_MESSAGE @extend_schema( tags=["Location Data"], parameters=LOCATION_DATA_QUERY_PARAMETERS, responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE}, ) def get(self, request): return self._proxy(request, method="GET") @extend_schema( tags=["Location Data"], request=LocationDataUpsertSerializer, responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE}, ) def post(self, request): return self._proxy(request, method="POST") class LocationDataNdviHealthView(AILocationDataProxyView): ai_path = f"{AI_LOCATION_DATA_PATH}ndvi-health/" farm_uuid_locations = ("body",) @extend_schema( tags=["Location Data"], request=FarmUUIDRequestSerializer, responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE}, ) def post(self, request): return self._proxy(request, method="POST") class LocationDataRemoteSensingView(AILocationDataProxyView): ai_path = AI_REMOTE_SENSING_PATH farm_uuid_locations = ("query", "body") @extend_schema( tags=["Location Data"], parameters=REMOTE_SENSING_QUERY_PARAMETERS, responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE}, ) def get(self, request): return self._proxy(request, method="GET") @extend_schema( tags=["Location Data"], request=FarmUUIDRequestSerializer, responses={202: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE}, ) def post(self, request): return self._proxy(request, method="POST") class ClusterBlockLiveView(AILocationDataProxyView): ai_path = f"{AI_REMOTE_SENSING_PATH}cluster-blocks/{{cluster_uuid}}/live/" farm_uuid_locations = ("query",) @extend_schema( tags=["Location Data"], parameters=CLUSTER_BLOCK_LIVE_QUERY_PARAMETERS, responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE, 502: ERROR_RESPONSE}, ) def get(self, request, cluster_uuid): return self._proxy(request, method="GET", cluster_uuid=cluster_uuid) class ClusterRecommendationsView(AILocationDataProxyView): ai_path = AI_CLUSTER_RECOMMENDATIONS_PATH farm_uuid_locations = ("query",) @extend_schema( tags=["Location Data"], parameters=REMOTE_SENSING_QUERY_PARAMETERS[:1], responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE}, ) def get(self, request): return self._proxy(request, method="GET") class KOptionsView(AILocationDataProxyView): ai_path = f"{AI_REMOTE_SENSING_PATH}results/{{result_id}}/k-options/" farm_uuid_locations = ("query",) @extend_schema( tags=["Location Data"], parameters=OPTIONAL_FARM_UUID_QUERY_PARAMETER, responses={200: SUCCESS_RESPONSE, 404: ERROR_RESPONSE}, ) def get(self, request, result_id): return self._proxy(request, method="GET", result_id=result_id) class KOptionsActivateView(AILocationDataProxyView): ai_path = f"{AI_REMOTE_SENSING_PATH}results/{{result_id}}/k-options/activate/" farm_uuid_locations = ("query", "body") @extend_schema( tags=["Location Data"], parameters=OPTIONAL_FARM_UUID_QUERY_PARAMETER, request=KOptionActivateSerializer, responses={200: SUCCESS_RESPONSE, 400: ERROR_RESPONSE, 404: ERROR_RESPONSE}, ) def post(self, request, result_id): return self._proxy(request, method="POST", result_id=result_id) class RunStatusView(AILocationDataProxyView): ai_path = f"{AI_REMOTE_SENSING_PATH}runs/{{run_id}}/status/" farm_uuid_locations = ("query",) @extend_schema( tags=["Location Data"], parameters=OPTIONAL_FARM_UUID_QUERY_PARAMETER, responses={200: SUCCESS_RESPONSE, 404: ERROR_RESPONSE}, ) def get(self, request, run_id): return self._proxy(request, method="GET", run_id=run_id)