from __future__ import annotations from django.apps import apps from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView from config.openapi import ( build_envelope_serializer, build_response, build_task_status_data_serializer, ) from .growth_simulation import MAX_PAGE_SIZE, paginate_growth_stages from .serializers import ( CurrentFarmChartRequestSerializer, CurrentFarmChartResponseSerializer, GrowthSimulationQueuedSerializer, GrowthSimulationRequestSerializer, GrowthSimulationResultSerializer, HarvestPredictionRequestSerializer, HarvestPredictionResponseSerializer, YieldHarvestSummaryQuerySerializer, YieldHarvestSummaryResponseSerializer, YieldPredictionRequestSerializer, YieldPredictionResponseSerializer, ) from .tasks import run_growth_simulation_task from .yield_harvest_summary import YieldHarvestSummaryService GrowthSimulationQueuedResponseSerializer = build_envelope_serializer( "GrowthSimulationQueuedResponseSerializer", GrowthSimulationQueuedSerializer, ) GrowthSimulationStatusResponseSerializer = build_envelope_serializer( "GrowthSimulationStatusResponseSerializer", build_task_status_data_serializer( "GrowthSimulationTaskStatusDataSerializer", GrowthSimulationResultSerializer, ), ) GrowthSimulationErrorSerializer = build_envelope_serializer( "GrowthSimulationErrorSerializer", data_required=False, allow_null=True, ) def _get_async_result(task_id: str): from celery.result import AsyncResult return AsyncResult(task_id) def _coerce_positive_int(value, default: int) -> int: try: parsed = int(value) except (TypeError, ValueError): return default return max(parsed, 1) class PlantGrowthSimulationView(APIView): @extend_schema( tags=["Crop Simulation"], summary="شروع شبیه سازی رشد گیاه", description=( "نوع گیاه و پارامترهای متغیر رشد را می گیرد، " "شبیه سازی را داخل Celery اجرا می کند و فقط task_id برمی گرداند." ), request=GrowthSimulationRequestSerializer, responses={ 202: build_response( GrowthSimulationQueuedResponseSerializer, "تسک شبیه سازی رشد گیاه در صف قرار گرفت.", ), 400: build_response( GrowthSimulationErrorSerializer, "داده ورودی نامعتبر است.", ), }, examples=[ OpenApiExample( "نمونه درخواست با weather مستقیم", value={ "plant_name": "گوجه‌فرنگی", "dynamic_parameters": ["DVS", "LAI", "TAGP", "TWSO", "SM"], "weather": [ { "DAY": "2026-04-01", "LAT": 35.7, "LON": 51.4, "TMIN": 12, "TMAX": 24, "RAIN": 0.0, "ET0": 0.32, } ], "soil_parameters": {"SMFCF": 0.34, "SMW": 0.14, "RDMSOL": 120.0}, "site_parameters": {"WAV": 40.0}, "page_size": 2, }, request_only=True, ), OpenApiExample( "نمونه درخواست با farm", value={ "plant_name": "گوجه‌فرنگی", "dynamic_parameters": ["DVS", "LAI", "TAGP"], "farm_uuid": "11111111-1111-1111-1111-111111111111", }, request_only=True, ), ], ) def post(self, request): serializer = GrowthSimulationRequestSerializer(data=request.data) if not serializer.is_valid(): return Response( {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) task = run_growth_simulation_task.delay(serializer.validated_data) return Response( { "code": 202, "msg": "تسک شبیه سازی رشد در صف قرار گرفت.", "data": { "task_id": task.id, "status_url": f"/api/crop-simulation/growth/{task.id}/status/", "plant_name": serializer.validated_data["plant_name"], }, }, status=status.HTTP_202_ACCEPTED, ) class PlantGrowthSimulationStatusView(APIView): @extend_schema( tags=["Crop Simulation"], summary="وضعیت شبیه سازی رشد گیاه", description="وضعیت تسک Celery را برمی گرداند و در صورت موفقیت مراحل رشد را به صورت صفحه بندی شده بازمی گرداند.", responses={ 200: build_response( GrowthSimulationStatusResponseSerializer, "وضعیت فعلی تسک شبیه سازی رشد گیاه.", ) }, ) def get(self, request, task_id: str): result = _get_async_result(task_id) payload = {"task_id": task_id, "status": result.state} if result.state == "PENDING": payload["message"] = "تسک در صف یا یافت نشد." elif result.state == "PROGRESS": payload["progress"] = result.info elif result.state == "SUCCESS": task_result = dict(result.result or {}) page = _coerce_positive_int(request.query_params.get("page", 1), 1) page_size = min( _coerce_positive_int( request.query_params.get("page_size", task_result.get("default_page_size", 10)), 10, ), MAX_PAGE_SIZE, ) paginated = paginate_growth_stages( task_result.get("stage_timeline", []), page=page, page_size=page_size, ) task_result["stages_page"] = paginated["items"] task_result["pagination"] = paginated["pagination"] payload["result"] = task_result elif result.state == "FAILURE": payload["error"] = str(result.result) return Response( {"code": 200, "msg": "success", "data": payload}, status=status.HTTP_200_OK, ) CurrentFarmChartEnvelopeSerializer = build_envelope_serializer( "CurrentFarmChartEnvelopeSerializer", CurrentFarmChartResponseSerializer, ) HarvestPredictionEnvelopeSerializer = build_envelope_serializer( "HarvestPredictionEnvelopeSerializer", HarvestPredictionResponseSerializer, ) YieldPredictionEnvelopeSerializer = build_envelope_serializer( "YieldPredictionEnvelopeSerializer", YieldPredictionResponseSerializer, ) YieldHarvestSummaryEnvelopeSerializer = build_envelope_serializer( "YieldHarvestSummaryEnvelopeSerializer", YieldHarvestSummaryResponseSerializer, ) class CurrentFarmSimulationChartView(APIView): @extend_schema( tags=["Crop Simulation"], summary="chart شبیه سازی وضعیت فعلی مزرعه", description=( "با دریافت farm_uuid، یک شبیه سازی از وضعیت فعلی مزرعه اجرا می کند و داده chart شامل برگ، وزن، بیوماس، رطوبت و خروجی روزانه را برمی گرداند." ), request=CurrentFarmChartRequestSerializer, responses={ 200: build_response( CurrentFarmChartEnvelopeSerializer, "خروجی chart شبیه سازی وضعیت فعلی مزرعه.", ), 400: build_response( GrowthSimulationErrorSerializer, "داده ورودی نامعتبر است.", ), 500: build_response( GrowthSimulationErrorSerializer, "خطا در اجرای chart شبیه سازی مزرعه.", ), }, examples=[ OpenApiExample( "نمونه درخواست chart", value={ "farm_uuid": "11111111-1111-1111-1111-111111111111", "plant_name": "گوجه‌فرنگی", }, request_only=True, ), ], ) def post(self, request): serializer = CurrentFarmChartRequestSerializer(data=request.data) if not serializer.is_valid(): return Response( {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) simulator = apps.get_app_config("crop_simulation").get_current_farm_chart_simulator() try: result = simulator.simulate(**serializer.validated_data) except Exception as exc: return Response( {"code": 500, "msg": f"خطا در اجرای chart شبیه سازی مزرعه: {exc}", "data": None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) return Response( {"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK, ) class HarvestPredictionView(APIView): @extend_schema( tags=["Crop Simulation"], summary="پیش بینی زمان تقریبی برداشت", description=( "با دریافت farm_uuid، از شبیه ساز رشد برای برآورد زمان باقی مانده تا برداشت استفاده می کند " "و تاریخ تقریبی برداشت را برمی گرداند." ), request=HarvestPredictionRequestSerializer, responses={ 200: build_response( HarvestPredictionEnvelopeSerializer, "خروجی پیش بینی زمان برداشت مزرعه.", ), 400: build_response( GrowthSimulationErrorSerializer, "داده ورودی نامعتبر است.", ), 500: build_response( GrowthSimulationErrorSerializer, "خطا در پیش بینی زمان برداشت.", ), }, examples=[ OpenApiExample( "نمونه درخواست harvest prediction", value={ "farm_uuid": "11111111-1111-1111-1111-111111111111", "plant_name": "گوجه‌فرنگی", }, request_only=True, ), ], ) def post(self, request): serializer = HarvestPredictionRequestSerializer(data=request.data) if not serializer.is_valid(): return Response( {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) service = apps.get_app_config("crop_simulation").get_harvest_prediction_service() try: result = service.get_harvest_prediction(**serializer.validated_data) except Exception as exc: return Response( {"code": 500, "msg": f"خطا در پیش بینی زمان برداشت: {exc}", "data": None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) return Response( {"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK, ) class YieldPredictionView(APIView): @extend_schema( tags=["Crop Simulation"], summary="پیش بینی عملکرد مزرعه", description="با دریافت farm_uuid، خروجی شبیه ساز رشد را به برآورد عملکرد قابل استفاده در KPI تبدیل می کند.", request=YieldPredictionRequestSerializer, responses={ 200: build_response(YieldPredictionEnvelopeSerializer, "خروجی پیش بینی عملکرد مزرعه."), 400: build_response(GrowthSimulationErrorSerializer, "داده ورودی نامعتبر است."), 500: build_response(GrowthSimulationErrorSerializer, "خطا در پیش بینی عملکرد."), }, examples=[ OpenApiExample( "نمونه درخواست yield prediction", value={ "farm_uuid": "11111111-1111-1111-1111-111111111111", "plant_name": "گوجه‌فرنگی", }, request_only=True, ), ], ) def post(self, request): serializer = YieldPredictionRequestSerializer(data=request.data) if not serializer.is_valid(): return Response( {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) service = apps.get_app_config("crop_simulation").get_yield_prediction_service() try: result = service.get_yield_prediction(**serializer.validated_data) except Exception as exc: return Response( {"code": 500, "msg": f"خطا در پیش بینی عملکرد: {exc}", "data": None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) return Response({"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK) class YieldHarvestSummaryView(APIView): @extend_schema( tags=["Crop Simulation"], summary="خلاصه عملکرد و برداشت", description=( "خروجی داشبورد Yield & Harvest Summary را با اتکا به داده های قطعی شبیه سازی برمی گرداند. " "فعلا پاسخ به صورت mock با کارت های خالی بازگردانده می شود." ), parameters=[ OpenApiParameter( name="farm_uuid", type=str, location=OpenApiParameter.QUERY, required=True, description="شناسه یکتای مزرعه", ), OpenApiParameter( name="season_year", type=int, location=OpenApiParameter.QUERY, required=False, description="سال زراعی", ), OpenApiParameter( name="crop_name", type=str, location=OpenApiParameter.QUERY, required=False, description="نام محصول", ), OpenApiParameter( name="include_narrative", type=bool, location=OpenApiParameter.QUERY, required=False, description="در آینده روایت متنی را نیز اضافه می کند.", ), ], responses={ 200: build_response( YieldHarvestSummaryEnvelopeSerializer, "خروجی خلاصه عملکرد و برداشت مزرعه.", ), 400: build_response( GrowthSimulationErrorSerializer, "پارامترهای query نامعتبر است.", ), }, examples=[ OpenApiExample( "نمونه پاسخ yield harvest summary", value={ "code": 200, "msg": "success", "data": { "farm_uuid": "11111111-1111-1111-1111-111111111111", "season_highlights_card": {}, "yield_prediction": {}, "harvest_prediction_card": {}, "harvest_readiness_zones": {}, "yield_quality_bands": {}, "harvest_operations_card": {}, "yield_prediction_chart": {}, }, }, response_only=True, ), ], ) def get(self, request): serializer = YieldHarvestSummaryQuerySerializer(data=request.query_params) if not serializer.is_valid(): return Response( {"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST, ) validated = serializer.validated_data service = YieldHarvestSummaryService() payload = service.get_summary( farm_uuid=str(validated["farm_uuid"]), season_year=str(validated.get("season_year") or ""), crop_name=validated.get("crop_name") or "", include_narrative=validated.get("include_narrative", False), ) return Response({"code": 200, "msg": "success", "data": payload}, status=status.HTTP_200_OK)