from copy import deepcopy from django.db import transaction from drf_spectacular.utils import ( OpenApiExample, OpenApiResponse, extend_schema, inline_serializer, ) from rest_framework import serializers as drf_serializers from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView from config.integration_contract import build_integration_meta from config.openapi import build_envelope_serializer, build_response from .models import ParameterUpdateLog, SensorData, SensorParameter from .serializers import ( FarmDetailSerializer, SensorDataResponseSerializer, SensorDataUpdateSerializer, SensorParameterSerializer, ) from .services import ( BackendSyncError, assign_farm_plants_from_backend_ids, ExternalDataSyncError, ensure_location_and_weather_data, get_farm_details, resolve_center_location_from_boundary, sync_sensor_parameters_from_payload, sync_plant_catalog_from_backend, ) SensorDataEnvelopeSerializer = build_envelope_serializer( "SensorDataEnvelopeSerializer", SensorDataResponseSerializer, ) SensorDataValidationErrorSerializer = build_envelope_serializer( "SensorDataValidationErrorSerializer", data_required=False, allow_null=True, ) SensorDataNotFoundSerializer = build_envelope_serializer( "SensorDataNotFoundSerializer", data_required=False, allow_null=True, ) FarmDetailEnvelopeSerializer = build_envelope_serializer( "FarmDetailEnvelopeSerializer", FarmDetailSerializer, ) SensorParameterResponseSerializer = build_envelope_serializer( "SensorParameterEnvelopeSerializer", inline_serializer( name="SensorParameterPayloadSerializer", fields={ "id": drf_serializers.IntegerField(), "sensor_key": drf_serializers.CharField(), "code": drf_serializers.CharField(), "name_fa": drf_serializers.CharField(), "unit": drf_serializers.CharField(), "data_type": drf_serializers.CharField(), "metadata": drf_serializers.JSONField(), "created_at": drf_serializers.DateTimeField(), "action": drf_serializers.CharField(), }, ), ) class FarmDataUpsertView(APIView): """ ایجاد یا آپدیت داده farm. """ @extend_schema( tags=["Farm Data"], summary="ایجاد یا آپدیت داده farm", description=( "داده farm را با `POST /api/farm-data/` ایجاد یا آپدیت می‌کند. " "`farm_uuid` باید از API ارسال شود و هرگز خودکار ساخته نمی‌شود. " "مرز مزرعه را می‌گیرد، مرکز زمین را خودش محاسبه و در location_data ذخیره می‌کند. " "رکورد آب‌وهوا هم از همان مرکز زمین به‌صورت خودکار پیدا می‌شود. " 'خوانش‌ها داخل `sensor_payload` مثل `{"sensor-7-1": {...}}` نگه‌داری می‌شوند.' ), request=SensorDataUpdateSerializer, responses={ 200: build_response( SensorDataEnvelopeSerializer, "داده farm با موفقیت به‌روزرسانی شد.", ), 201: build_response( SensorDataEnvelopeSerializer, "داده farm با موفقیت ایجاد شد.", ), 400: build_response( SensorDataValidationErrorSerializer, "داده ورودی نامعتبر است.", ), 502: build_response( SensorDataNotFoundSerializer, "واکشی داده خاک یا آب‌وهوا از سرویس بیرونی ناموفق بود.", ), }, examples=[ OpenApiExample( "نمونه درخواست", value={ "farm_uuid": "550e8400-e29b-41d4-a716-446655440000", "farm_boundary": { "type": "Polygon", "coordinates": [ [ [51.3900, 35.7000], [51.4100, 35.7000], [51.4100, 35.7200], [51.3900, 35.7200], [51.3900, 35.7000], ] ], }, "sensor_payload": { "sensor-7-1": { "soil_moisture": 45.2, "soil_temperature": 22.5, "soil_ph": 6.8, "electrical_conductivity": 1.2, "nitrogen": 30.0, "phosphorus": 15.0, "potassium": 20.0, } }, }, request_only=True, ), OpenApiExample( "نمونه چند سنسور", value={ "farm_uuid": "550e8400-e29b-41d4-a716-446655440000", "farm_boundary": { "corners": [ {"lat": 35.7000, "lon": 51.3900}, {"lat": 35.7000, "lon": 51.4100}, {"lat": 35.7200, "lon": 51.4100}, {"lat": 35.7200, "lon": 51.3900}, ] }, "sensor_payload": { "sensor-7-1": { "soil_moisture": 45.2, "soil_temperature": 22.5, }, "leaf-sensor": { "leaf_wetness": 11.0, "leaf_temperature": 19.3, }, }, }, request_only=True, ), ], ) def post(self, request): serializer = SensorDataUpdateSerializer(data=request.data) if not serializer.is_valid(): return Response( {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) farm_uuid = serializer.validated_data["farm_uuid"] farm_boundary = serializer.validated_data["farm_boundary"] plant_ids = serializer.validated_data.get("plant_ids") irrigation_method_id = serializer.validated_data.get("irrigation_method_id") sensor_payload = serializer.validated_data.get("sensor_payload", {}) try: center_location = resolve_center_location_from_boundary(farm_boundary) except ValueError as exc: return Response( {"code": 400, "msg": "داده نامعتبر.", "data": {"farm_boundary": [str(exc)]}}, status=status.HTTP_400_BAD_REQUEST, ) try: center_location, weather_forecast = ensure_location_and_weather_data( center_location ) except ExternalDataSyncError as exc: return Response( {"code": 502, "msg": str(exc), "data": None}, status=status.HTTP_502_BAD_GATEWAY, ) with transaction.atomic(): sync_sensor_parameters_from_payload(sensor_payload) farm_data, created = SensorData.objects.get_or_create( farm_uuid=farm_uuid, defaults={ "center_location": center_location, "weather_forecast": weather_forecast, "sensor_payload": sensor_payload, }, ) if not created and sensor_payload: merged_payload = deepcopy(farm_data.sensor_payload or {}) for sensor_key, sensor_values in sensor_payload.items(): current_values = merged_payload.get(sensor_key, {}) if not isinstance(current_values, dict): current_values = {} current_values.update(sensor_values) merged_payload[sensor_key] = current_values farm_data.sensor_payload = merged_payload elif created: farm_data.sensor_payload = sensor_payload farm_data.center_location = center_location farm_data.weather_forecast = weather_forecast if "irrigation_method_id" in serializer.validated_data: farm_data.irrigation_method_id = irrigation_method_id if not created: farm_data.save( update_fields=[ "center_location", "weather_forecast", "sensor_payload", "irrigation_method", "updated_at", ] ) else: farm_data.save() if plant_ids is not None: try: assign_farm_plants_from_backend_ids(farm_data, plant_ids) except BackendSyncError as exc: return Response( {"code": 400, "msg": str(exc), "data": None}, status=status.HTTP_400_BAD_REQUEST, ) response_status = ( status.HTTP_201_CREATED if created else status.HTTP_200_OK ) return Response( { "code": 201 if created else 200, "msg": "success", "data": SensorDataResponseSerializer(farm_data).data, "meta": build_integration_meta( flow_type="ai_owned_derived_output", source_type="provider", source_service="ai_farm_data", ownership="ai", live=True, cached=False, generated_at=farm_data.updated_at, notes=["AI farm_data stores a derived read-model enriched with location and weather data."], ), }, status=response_status, ) class FarmDetailView(APIView): @extend_schema( tags=["Farm Data"], summary="دریافت همه اطلاعات farm", description=( "اطلاعات تجمیعی farm را برمی‌گرداند. " "برای resolved_metrics، داده‌های sensor روی داده‌های خاک اولویت دارند " "و در حالت چند سنسوره، مقادیر متعارض به‌صورت deterministic تجمیع می‌شوند." ), responses={ 200: build_response( FarmDetailEnvelopeSerializer, "اطلاعات farm با موفقیت بازگردانده شد.", ), 404: build_response( SensorDataNotFoundSerializer, "farm موردنظر یافت نشد.", ), }, ) def get(self, request, farm_uuid): data = get_farm_details(str(farm_uuid)) if data is None: return Response( {"code": 404, "msg": "farm یافت نشد.", "data": None}, status=status.HTTP_404_NOT_FOUND, ) return Response( { "code": 200, "msg": "success", "data": data, "meta": build_integration_meta( flow_type="ai_owned_derived_output", source_type="db", source_service="ai_farm_data", ownership="ai", live=False, cached=True, snapshot_at=getattr(data, "get", lambda *_: None)("updated_at") if isinstance(data, dict) else None, ), }, status=status.HTTP_200_OK, ) class PlantCatalogSyncView(APIView): @extend_schema( tags=["Farm Data"], summary="همگام‌سازی کاتالوگ گیاه از Backend", description="payload گیاه‌های canonical را از Backend دریافت و در `farm_data` snapshot می‌کند.", request=drf_serializers.ListSerializer( child=inline_serializer( name="PlantCatalogSyncItem", fields={ "id": drf_serializers.IntegerField(), "name": drf_serializers.CharField(), }, ) ), responses={ 200: OpenApiResponse(description="کاتالوگ گیاه با موفقیت sync شد."), 400: OpenApiResponse(description="payload نامعتبر است."), }, ) def post(self, request): if not isinstance(request.data, list): return Response( {"code": 400, "msg": "payload باید آرایه‌ای از گیاه‌ها باشد.", "data": None}, status=status.HTTP_400_BAD_REQUEST, ) try: snapshots = sync_plant_catalog_from_backend(request.data) except BackendSyncError as exc: return Response( {"code": 400, "msg": str(exc), "data": None}, status=status.HTTP_400_BAD_REQUEST, ) return Response( { "code": 200, "msg": "success", "data": { "count": len(snapshots), "plant_ids": [snapshot.backend_plant_id for snapshot in snapshots], }, "meta": build_integration_meta( flow_type="backend_owned_data_with_ai_enrichment", source_type="db", source_service="ai_farm_data_plant_catalog", ownership="backend", live=False, cached=False, generated_at=snapshots[-1].updated_at if snapshots else None, notes=["Backend is canonical for plant catalog; AI stores snapshots for derived services."], ), }, status=status.HTTP_200_OK, ) class SensorParameterCreateView(APIView): """ اضافه کردن پارامتر جدید و ثبت در ParameterUpdateLog. """ @extend_schema( tags=["Farm Parameters"], summary="افزودن/ویرایش پارامتر سنسور", description="پارامتر جدید اضافه یا پارامتر موجود را ویرایش می‌کند و در لاگ ثبت می‌شود.", request=SensorParameterSerializer, responses={ 201: build_response( SensorParameterResponseSerializer, "پارامتر سنسور با موفقیت ایجاد یا ویرایش شد.", ), 400: build_response( SensorDataValidationErrorSerializer, "داده ورودی نامعتبر است.", ), }, examples=[ OpenApiExample( "نمونه درخواست", value={ "sensor_key": "sensor-7-1", "code": "soil_moisture", "name_fa": "رطوبت خاک", "unit": "%", "data_type": "float", "metadata": {"min": 0, "max": 100}, }, request_only=True, ), ], ) def post(self, request): serializer = SensorParameterSerializer(data=request.data) if not serializer.is_valid(): return Response( {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) sensor_key = serializer.validated_data.get("sensor_key") code = serializer.validated_data["code"] name_fa = serializer.validated_data["name_fa"] unit = serializer.validated_data.get("unit", "") data_type = serializer.validated_data.get("data_type", "") metadata = serializer.validated_data.get("metadata", {}) with transaction.atomic(): parameter, created = SensorParameter.objects.update_or_create( sensor_key=sensor_key, code=code, defaults={ "name_fa": name_fa, "unit": unit, "data_type": data_type, "metadata": metadata, }, ) action = ( ParameterUpdateLog.ACTION_ADDED if created else ParameterUpdateLog.ACTION_MODIFIED ) ParameterUpdateLog.objects.create( parameter=parameter, action=action, payload={ "sensor_key": parameter.sensor_key, "code": parameter.code, "name_fa": parameter.name_fa, "unit": parameter.unit, "data_type": parameter.data_type, "metadata": parameter.metadata, }, ) return Response( { "code": 201, "msg": "success", "data": { "id": parameter.id, "sensor_key": parameter.sensor_key, "code": parameter.code, "name_fa": parameter.name_fa, "unit": parameter.unit, "data_type": parameter.data_type, "metadata": parameter.metadata, "created_at": parameter.created_at, "action": action, }, "meta": build_integration_meta( flow_type="ai_owned_derived_output", source_type="db", source_service="ai_farm_parameters", ownership="ai", live=False, cached=False, generated_at=parameter.created_at, ), }, status=status.HTTP_201_CREATED, )