from __future__ import annotations

from copy import deepcopy
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from math import exp
from typing import Any
import logging

from django.apps import apps
from django.core.paginator import EmptyPage, Paginator

from farm_data.models import PlantCatalogSnapshot, SensorData
from farm_data.services import clone_snapshot_as_runtime_plant, get_canonical_farm_record, get_runtime_plant_for_farm
from location_data.satellite_snapshot import build_location_satellite_snapshot
from plant.gdd import calculate_daily_gdd, resolve_growth_profile
from weather.models import WeatherForecast

from .services import CropSimulationService, build_simulation_payload_from_farm


DEFAULT_DYNAMIC_PARAMETERS = ["DVS", "LAI", "TAGP", "TWSO", "SM"]
DEFAULT_PAGE_SIZE = 10
MAX_PAGE_SIZE = 50
logger = logging.getLogger(__name__)

DEFAULT_STAGE_LABELS = {
    "pre_emergence": "پیش از سبز شدن",
    "establishment": "استقرار",
    "vegetative": "رشد رویشی",
    "flowering": "گلدهی",
    "reproductive": "پرشدن محصول",
    "maturity": "رسیدگی",
}

ENGINE_LABELS = {
    "pcse": "موتور شبیه سازی PCSE",
    "growth_projection": "موتور برآورد رشد",
}

MODEL_LABELS = {
    "growth_projection_v1": "مدل برآورد رشد نسخه ۱",
    "wofost": "مدل ووفوست",
}


class GrowthSimulationError(Exception):
    pass


def _fa_engine_name(name: str | None) -> str | None:
    if not name:
        return name
    return ENGINE_LABELS.get(name, name)


def _fa_model_name(name: str | None) -> str | None:
    if not name:
        return name
    return MODEL_LABELS.get(name, name)


@dataclass
class GrowthSimulationContext:
    farm_uuid: str | None
    plant_name: str
    plant: Any
    dynamic_parameters: list[str]
    weather: list[dict[str, Any]]
    crop_parameters: dict[str, Any]
    soil_parameters: dict[str, Any]
    site_parameters: dict[str, Any]
    agromanagement: list[dict[str, Any]]
    page_size: int


def _safe_float(value: Any, default: float = 0.0) -> float:
    try:
        if value in (None, ""):
            return default
        return float(value)
    except (TypeError, ValueError):
        return default


def _pick_first_not_none(*values: Any) -> Any:
    for value in values:
        if value is not None:
            return value
    return None


def _clamp(value: float, minimum: float, maximum: float) -> float:
    if minimum > maximum:
        minimum, maximum = maximum, minimum
    return max(minimum, min(value, maximum))


def _mm_to_cm_day(value: Any, default: float) -> float:
    scaled = _safe_float(value, default * 10.0) / 10.0
    return round(max(scaled, 0.0), 4)


def _coerce_date(value: Any) -> date:
    if isinstance(value, date) and not isinstance(value, datetime):
        return value
    if isinstance(value, datetime):
        return value.date()
    if isinstance(value, str):
        return date.fromisoformat(value)
    raise GrowthSimulationError(f"Invalid date value: {value!r}")


def _json_ready(value: Any) -> Any:
    if isinstance(value, dict):
        return {str(key): _json_ready(item) for key, item in value.items()}
    if isinstance(value, list):
        return [_json_ready(item) for item in value]
    if isinstance(value, tuple):
        return [_json_ready(item) for item in value]
    if isinstance(value, (date, datetime)):
        return value.isoformat()
    return value


def _normalize_weather_records(weather: Any) -> list[dict[str, Any]]:
    if not weather:
        return []

    records = weather.get("records") if isinstance(weather, dict) and "records" in weather else weather
    if not isinstance(records, list):
        records = [records]

    normalized = []
    for item in records:
        if not isinstance(item, dict):
            raise GrowthSimulationError("Weather records must be JSON objects.")
        current_date = _coerce_date(item.get("DAY") or item.get("day"))
        normalized.append(
            {
                "DAY": current_date,
                "LAT": _safe_float(item.get("LAT", item.get("lat")), 35.7),
                "LON": _safe_float(item.get("LON", item.get("lon")), 51.4),
                "ELEV": _safe_float(item.get("ELEV", item.get("elev")), 1200.0),
                "IRRAD": _safe_float(item.get("IRRAD", item.get("irrad")), 16_000_000.0),
                "TMIN": _safe_float(item.get("TMIN", item.get("tmin")), 12.0),
                "TMAX": _safe_float(item.get("TMAX", item.get("tmax")), 24.0),
                "VAP": _safe_float(item.get("VAP", item.get("vap")), 12.0),
                "WIND": _safe_float(item.get("WIND", item.get("wind")), 2.0),
                "RAIN": _safe_float(item.get("RAIN", item.get("rain")), 0.0),
                "E0": _safe_float(item.get("E0", item.get("e0")), 0.35),
                "ES0": _safe_float(item.get("ES0", item.get("es0")), 0.3),
                "ET0": _safe_float(item.get("ET0", item.get("et0")), 0.32),
            }
        )
    if not normalized:
        raise GrowthSimulationError("At least one weather record is required.")
    return normalized


def _build_weather_from_farm(sensor: SensorData) -> list[dict[str, Any]]:
    forecasts = list(
        WeatherForecast.objects.filter(location=sensor.center_location)
        .order_by("forecast_date")[:14]
    )
    if not forecasts:
        raise GrowthSimulationError("No forecast data found for the selected farm.")

    records = []
    for forecast in forecasts:
        records.append(
            {
                "DAY": forecast.forecast_date,
                "LAT": float(sensor.center_location.latitude),
                "LON": float(sensor.center_location.longitude),
                "ELEV": 1200.0,
                "IRRAD": 16_000_000.0,
                "TMIN": _safe_float(forecast.temperature_min, 12.0),
                "TMAX": _safe_float(forecast.temperature_max, 24.0),
                "VAP": max(_safe_float(forecast.humidity_mean, 55.0) / 5.0, 6.0),
                "WIND": _safe_float(forecast.wind_speed_max, 7.2) / 3.6,
                # WeatherForecast stores precipitation/ET0 in mm/day, while PCSE expects cm/day.
                "RAIN": _mm_to_cm_day(forecast.precipitation, 0.0),
                "E0": _mm_to_cm_day(forecast.et0, 0.35),
                "ES0": max(round(_mm_to_cm_day(forecast.et0, 0.35) * 0.9, 4), 0.1),
                "ET0": _mm_to_cm_day(forecast.et0, 0.35),
            }
        )
    return records


def _build_soil_and_site_from_farm(sensor: SensorData) -> tuple[dict[str, Any], dict[str, Any]]:
    satellite_metrics = build_location_satellite_snapshot(sensor.center_location).get("resolved_metrics") or {}
    ndwi = _safe_float(satellite_metrics.get("ndwi"), 0.28)
    smfcf = _safe_float(ndwi, 0.34)
    smw = max(round(smfcf * 0.45, 3), 0.12)
    sm0 = min(max(smfcf + 0.08, smw + 0.12), 0.6)
    soil_moisture = None
    payload = sensor.sensor_payload or {}
    if isinstance(payload, dict):
        for block in payload.values():
            if isinstance(block, dict) and block.get("soil_moisture") is not None:
                soil_moisture = _safe_float(block.get("soil_moisture"))
                break
    site = {
        "WAV": soil_moisture if soil_moisture is not None else 40.0,
        "IFUNRN": 0,
        "NOTINF": 0.0,
        "SSI": 0.0,
        "SSMAX": 0.0,
        "SMLIM": round(_clamp(smfcf, smw, sm0), 3),
    }
    soil = {
        "SMFCF": smfcf,
        "SMW": smw,
        "SM0": sm0,
        "RDMSOL": 120.0,
        "CRAIRC": 0.06,
        "SOPE": 10.0,
        "KSUB": 10.0,
    }
    return soil, site


def _build_default_crop_parameters(plant: Any) -> dict[str, Any]:
    profile = resolve_growth_profile(plant)
    required_gdd = _safe_float(profile.get("required_gdd_for_maturity"), 1200.0)
    return {
        "crop_name": plant.name,
        "TSUM1": round(required_gdd * 0.45, 3),
        "TSUM2": round(required_gdd * 0.55, 3),
        "YIELD_SCALE": 1.0,
        "MAX_LAI": 5.0,
        "MAX_BIOMASS": 12000.0,
    }


def _build_default_agromanagement(plant_name: str, weather: list[dict[str, Any]]) -> list[dict[str, Any]]:
    first_day = weather[0]["DAY"]
    last_day = weather[-1]["DAY"]
    crop_start = first_day
    crop_end = max(last_day, crop_start + timedelta(days=1))
    return [
        {
            first_day: {
                "CropCalendar": {
                    "crop_name": plant_name,
                    "variety_name": "default",
                    "crop_start_date": crop_start,
                    "crop_start_type": "sowing",
                    "crop_end_date": crop_end,
                    "crop_end_type": "harvest",
                    "max_duration": max((crop_end - crop_start).days, 1),
                },
                "TimedEvents": [],
                "StateEvents": [],
            }
        },
        {},
    ]


def _resolve_plant_simulation_defaults(plant: Any) -> tuple[dict[str, Any] | None, list[dict[str, Any]] | None]:
    for attr in ("growth_profile", "irrigation_profile", "health_profile"):
        profile = getattr(plant, attr, None) or {}
        if not isinstance(profile, dict):
            continue
        simulation = profile.get("simulation")
        if not isinstance(simulation, dict):
            continue
        crop_parameters = simulation.get("crop_parameters")
        agromanagement = simulation.get("agromanagement")
        if isinstance(crop_parameters, dict) and agromanagement:
            return deepcopy(crop_parameters), deepcopy(agromanagement)
    return None, None


def build_growth_context(payload: dict[str, Any]) -> GrowthSimulationContext:
    plant_name = apps.get_app_config("plant").resolve_plant_name(payload["plant_name"]) or payload["plant_name"]
    snapshot = (
        PlantCatalogSnapshot.objects.filter(name=plant_name).first()
        or PlantCatalogSnapshot.objects.filter(name__iexact=plant_name).first()
    )
    plant = clone_snapshot_as_runtime_plant(snapshot)
    if plant is None:
        raise GrowthSimulationError("Plant not found.")

    dynamic_parameters = payload.get("dynamic_parameters") or DEFAULT_DYNAMIC_PARAMETERS
    page_size = min(max(int(payload.get("page_size") or DEFAULT_PAGE_SIZE), 1), MAX_PAGE_SIZE)

    sensor = None
    resolved_farm_uuid = str(payload["farm_uuid"]) if payload.get("farm_uuid") else None
    if payload.get("farm_uuid"):
        sensor = (
            SensorData.objects.select_related("center_location")
            .filter(farm_uuid=payload["farm_uuid"])
            .first()
        )
        if sensor is None:
            raise GrowthSimulationError("Farm not found.")

    if resolved_farm_uuid:
        farm_payload = build_simulation_payload_from_farm(
            farm_uuid=resolved_farm_uuid,
            plant_name=plant_name,
            weather=payload.get("weather"),
            soil=payload.get("soil_parameters"),
            crop_parameters=payload.get("crop_parameters"),
            agromanagement=payload.get("agromanagement"),
            site_parameters=payload.get("site_parameters"),
        )
        weather = farm_payload["weather"]
        crop_parameters = farm_payload["crop_parameters"]
        soil_parameters = farm_payload["soil"]
        site_parameters = farm_payload["site_parameters"]
        agromanagement = farm_payload["agromanagement"]
        plant = farm_payload["plant"] or plant
        return GrowthSimulationContext(
            farm_uuid=resolved_farm_uuid,
            plant_name=plant_name,
            plant=plant,
            dynamic_parameters=dynamic_parameters,
            weather=weather,
            crop_parameters=crop_parameters,
            soil_parameters=soil_parameters,
            site_parameters=site_parameters,
            agromanagement=agromanagement,
            page_size=page_size,
        )

    weather = (
        _normalize_weather_records(payload["weather"])
        if payload.get("weather")
        else _build_weather_from_farm(sensor)
        if sensor is not None
        else []
    )
    if not weather:
        raise GrowthSimulationError("Weather input is required.")

    default_crop_parameters, default_agromanagement = _resolve_plant_simulation_defaults(plant)
    crop_parameters = deepcopy(payload.get("crop_parameters") or default_crop_parameters or _build_default_crop_parameters(plant))
    crop_parameters.setdefault("crop_name", plant.name)

    soil_parameters = deepcopy(payload.get("soil_parameters") or {})
    site_parameters = deepcopy(payload.get("site_parameters") or {})
    if sensor is not None:
        farm_soil, farm_site = _build_soil_and_site_from_farm(sensor)
        soil_parameters = {**farm_soil, **soil_parameters}
        site_parameters = {**farm_site, **site_parameters}
    soil_parameters.setdefault("SMFCF", 0.34)
    soil_parameters.setdefault("SMW", 0.14)
    soil_parameters.setdefault("SM0", 0.42)
    soil_parameters.setdefault("RDMSOL", 120.0)
    soil_parameters.setdefault("CRAIRC", 0.06)
    soil_parameters.setdefault("SOPE", 10.0)
    soil_parameters.setdefault("KSUB", 10.0)
    site_parameters.setdefault("WAV", 40.0)
    site_parameters.setdefault("IFUNRN", 0)
    site_parameters.setdefault("NOTINF", 0.0)
    site_parameters.setdefault("SSI", 0.0)
    site_parameters.setdefault("SSMAX", 0.0)
    site_parameters.setdefault(
        "SMLIM",
        round(
            _clamp(
                _safe_float(site_parameters.get("SMLIM"), soil_parameters.get("SMFCF", 0.34)),
                _safe_float(soil_parameters.get("SMW"), 0.14),
                _safe_float(soil_parameters.get("SM0"), 0.42),
            ),
            3,
        ),
    )

    agromanagement = deepcopy(
        payload.get("agromanagement")
        or default_agromanagement
        or _build_default_agromanagement(plant.name, weather)
    )

    return GrowthSimulationContext(
        farm_uuid=resolved_farm_uuid,
        plant_name=plant_name,
        plant=plant,
        dynamic_parameters=dynamic_parameters,
        weather=weather,
        crop_parameters=crop_parameters,
        soil_parameters=soil_parameters,
        site_parameters=site_parameters,
        agromanagement=agromanagement,
        page_size=page_size,
    )


def _derive_stage(dvs: float) -> tuple[str, str]:
    if dvs < 0:
        return "pre_emergence", DEFAULT_STAGE_LABELS["pre_emergence"]
    if dvs < 0.2:
        return "establishment", DEFAULT_STAGE_LABELS["establishment"]
    if dvs < 1.0:
        return "vegetative", DEFAULT_STAGE_LABELS["vegetative"]
    if dvs < 1.3:
        return "flowering", DEFAULT_STAGE_LABELS["flowering"]
    if dvs < 2.0:
        return "reproductive", DEFAULT_STAGE_LABELS["reproductive"]
    return "maturity", DEFAULT_STAGE_LABELS["maturity"]


def _logistic(value: float, midpoint: float, steepness: float, upper: float) -> float:
    return upper / (1.0 + exp(-steepness * (value - midpoint)))


def _run_projection_engine(context: GrowthSimulationContext) -> dict[str, Any]:
    profile = resolve_growth_profile(context.plant)
    required_gdd = _safe_float(profile.get("required_gdd_for_maturity"), 1200.0)
    current_gdd = _safe_float(profile.get("current_cumulative_gdd"), 0.0)
    base_temperature = _safe_float(profile.get("base_temperature"), 10.0)
    max_lai = _safe_float(context.crop_parameters.get("MAX_LAI"), 5.0)
    max_biomass = _safe_float(context.crop_parameters.get("MAX_BIOMASS"), 12000.0)
    soil_moisture = _safe_float(context.site_parameters.get("WAV"), 40.0)

    daily_output = []
    for record in context.weather:
        tmax = _safe_float(record.get("TMAX"), 24.0)
        tmin = _safe_float(record.get("TMIN"), 12.0)
        rain = _safe_float(record.get("RAIN"), 0.0)
        et0 = _safe_float(record.get("ET0"), 0.32)
        daily_gdd = calculate_daily_gdd(tmax=tmax, tmin=tmin, tbase=base_temperature)
        current_gdd += daily_gdd
        dvs = min(max((current_gdd / max(required_gdd, 1.0)) * 2.0, 0.0), 2.0)

        if dvs <= 1.0:
            lai = _logistic(dvs, midpoint=0.55, steepness=7.5, upper=max_lai)
        else:
            decline_factor = max(0.25, 1.0 - ((dvs - 1.0) / 1.1))
            lai = max_lai * decline_factor

        biomass_factor = min(current_gdd / max(required_gdd, 1.0), 1.25)
        weather_modifier = max(0.65, min(1.15, 1.0 + (rain * 0.02) - (et0 * 0.08)))
        tagp = max_biomass * biomass_factor * weather_modifier
        twso = tagp * max(min((dvs - 0.95) / 0.85, 0.55), 0.0)
        soil_moisture = max(5.0, min(95.0, soil_moisture + (rain * 0.7) - (et0 * 8.5)))

        entry = {
            "DAY": record["DAY"],
            "DVS": round(dvs, 4),
            "LAI": round(lai, 4),
            "TAGP": round(tagp, 4),
            "TWSO": round(twso, 4),
            "SM": round(soil_moisture / 100.0, 4),
            "GDD": round(daily_gdd, 4),
            "TMIN": round(tmin, 4),
            "TMAX": round(tmax, 4),
            "RAIN": round(rain, 4),
            "ET0": round(et0, 4),
        }
        daily_output.append(entry)

    final_entry = daily_output[-1] if daily_output else {}
    return {
        "engine": "growth_projection",
        "model_name": "growth_projection_v1",
        "metrics": {
            "yield_estimate": round(_safe_float(final_entry.get("TWSO"), 0.0), 4),
            "biomass": round(_safe_float(final_entry.get("TAGP"), 0.0), 4),
            "max_lai": round(max((_safe_float(item.get("LAI"), 0.0) for item in daily_output), default=0.0), 4),
        },
        "daily_output": _json_ready(daily_output),
        "summary_output": [],
        "terminal_output": [_json_ready(final_entry)] if final_entry else [],
    }


def _run_simulation(
    context: GrowthSimulationContext,
    *,
    irrigation_recommendation: dict[str, Any] | None = None,
    fertilization_recommendation: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], int | None, str | None]:
    try:
        response = CropSimulationService().run_single_simulation(
            farm_uuid=context.farm_uuid,
            plant_name=context.plant_name,
            weather=context.weather,
            soil=context.soil_parameters,
            crop_parameters=context.crop_parameters,
            agromanagement=context.agromanagement,
            site_parameters=context.site_parameters,
            irrigation_recommendation=irrigation_recommendation,
            fertilization_recommendation=fertilization_recommendation,
            name=f"growth:{context.plant_name}",
        )
        return response["result"], response.get("scenario_id"), None
    except Exception as exc:
        logger.warning(
            "Falling back to projection engine for farm_uuid=%s plant_name=%s because PCSE failed: %s",
            context.farm_uuid,
            context.plant_name,
            exc,
        )
        fallback_result = _run_projection_engine(context)
        warning = f"موتور شبیه سازی با خطا مواجه شد و برآورد جایگزین استفاده شد: {exc}"
        return fallback_result, None, warning


def summarize_growth_stages(
    daily_output: list[dict[str, Any]],
    dynamic_parameters: list[str],
) -> list[dict[str, Any]]:
    if not daily_output:
        return []

    stage_items = []
    current = None

    for raw in daily_output:
        record = dict(raw)
        day = _coerce_date(record.get("DAY") or record.get("day"))
        dvs = _safe_float(record.get("DVS"), 0.0)
        stage_code, stage_name = _derive_stage(dvs)
        parameter_values = {}
        for param in dynamic_parameters:
            if record.get(param) is not None:
                parameter_values[param] = _safe_float(record.get(param))

        if current is None or current["stage_code"] != stage_code:
            if current is not None:
                stage_items.append(current)
            current = {
                "stage_code": stage_code,
                "stage_name": stage_name,
                "start_date": day,
                "end_date": day,
                "days_count": 1,
                "raw_days": [
                    {
                        "date": day,
                        "parameters": parameter_values,
                    }
                ],
            }
            continue

        current["end_date"] = day
        current["days_count"] += 1
        current["raw_days"].append({"date": day, "parameters": parameter_values})

    if current is not None:
        stage_items.append(current)

    summarized = []
    for index, item in enumerate(stage_items, start=1):
        metrics = {}
        for param in dynamic_parameters:
            values = [
                day_item["parameters"][param]
                for day_item in item["raw_days"]
                if param in day_item["parameters"]
            ]
            if not values:
                continue
            metrics[param] = {
                "start": round(values[0], 4),
                "end": round(values[-1], 4),
                "min": round(min(values), 4),
                "max": round(max(values), 4),
                "avg": round(sum(values) / len(values), 4),
            }

        summarized.append(
            {
                "order": index,
                "stage_code": item["stage_code"],
                "stage_name": item["stage_name"],
                "start_date": item["start_date"].isoformat(),
                "end_date": item["end_date"].isoformat(),
                "days_count": item["days_count"],
                "metrics": metrics,
            }
        )
    return summarized


def paginate_growth_stages(
    stage_timeline: list[dict[str, Any]],
    *,
    page: int,
    page_size: int,
) -> dict[str, Any]:
    page_size = min(max(page_size, 1), MAX_PAGE_SIZE)
    if not stage_timeline:
        return {
            "items": [],
            "pagination": {
                "page": 1,
                "page_size": page_size,
                "total_items": 0,
                "total_pages": 0,
                "has_next": False,
                "has_previous": False,
            },
        }
    paginator = Paginator(stage_timeline, page_size)
    try:
        page_obj = paginator.page(page)
    except EmptyPage:
        page_obj = paginator.page(paginator.num_pages or 1)

    return {
        "items": list(page_obj.object_list),
        "pagination": {
            "page": page_obj.number,
            "page_size": page_size,
            "total_items": paginator.count,
            "total_pages": paginator.num_pages,
            "has_next": page_obj.has_next(),
            "has_previous": page_obj.has_previous(),
        },
    }


def run_growth_simulation(payload: dict[str, Any], progress_callback=None) -> dict[str, Any]:
    context = build_growth_context(payload)
    if progress_callback is not None:
        progress_callback(
            state="PROGRESS",
            meta={"current": 1, "total": 3, "message": "simulation input resolved"},
        )

    simulation_result, scenario_id, simulation_error = _run_simulation(
        context,
        irrigation_recommendation=payload.get("irrigation_recommendation"),
        fertilization_recommendation=payload.get("fertilization_recommendation"),
    )
    if progress_callback is not None:
        progress_callback(
            state="PROGRESS",
            meta={"current": 2, "total": 3, "message": "simulation finished"},
        )

    stage_timeline = summarize_growth_stages(
        daily_output=simulation_result.get("daily_output", []),
        dynamic_parameters=context.dynamic_parameters,
    )
    if progress_callback is not None:
        progress_callback(
            state="PROGRESS",
            meta={"current": 3, "total": 3, "message": "growth stages prepared"},
        )

    paginated = paginate_growth_stages(
        stage_timeline,
        page=1,
        page_size=context.page_size,
    )
    return {
        "plant_name": context.plant_name,
        "dynamic_parameters": context.dynamic_parameters,
        "engine": _fa_engine_name(simulation_result.get("engine")),
        "model_name": _fa_model_name(simulation_result.get("model_name")),
        "scenario_id": scenario_id,
        "simulation_warning": simulation_error,
        "summary_metrics": simulation_result.get("metrics", {}),
        "stage_timeline": stage_timeline,
        "stages_page": paginated["items"],
        "pagination": paginated["pagination"],
        "daily_records_count": len(simulation_result.get("daily_output", [])),
        "default_page_size": context.page_size,
    }


def _estimate_leaf_count(lai: float) -> float:
    return max(lai, 0.0) * 12000.0


def _build_current_farm_chart_payload(
    context: GrowthSimulationContext,
    simulation_result: dict[str, Any],
    scenario_id: int | None,
    simulation_warning: str | None,
) -> dict[str, Any]:
    daily_output = simulation_result.get("daily_output") or []
    categories = [str(item.get("DAY")) for item in daily_output]

    leaf_count_series = [round(_estimate_leaf_count(_safe_float(item.get("LAI"))), 2) for item in daily_output]
    biomass_series = [round(_safe_float(item.get("TAGP")), 2) for item in daily_output]
    storage_weight_series = [round(_safe_float(item.get("TWSO")), 2) for item in daily_output]
    lai_series = [round(_safe_float(item.get("LAI")), 4) for item in daily_output]
    moisture_series = [round(_safe_float(item.get("SM")) * 100.0, 2) for item in daily_output]

    latest = daily_output[-1] if daily_output else {}
    latest_lai = _safe_float(latest.get("LAI"), 0.0)
    latest_biomass = _safe_float(latest.get("TAGP"), 0.0)
    latest_storage = _safe_float(latest.get("TWSO"), 0.0)
    latest_moisture = _safe_float(latest.get("SM"), 0.0) * 100.0

    summary = [
        {
            "title": "تعداد برگ تخمینی",
            "subtitle": "وضعیت فعلی",
            "amount": round(_estimate_leaf_count(latest_lai), 2),
            "unit": "برگ",
            "avatarColor": "success",
            "avatarIcon": "tabler-leaf",
        },
        {
            "title": "وزن بیوماس",
            "subtitle": "برآورد فعلی",
            "amount": round(latest_biomass, 2),
            "unit": "کیلوگرم در هکتار",
            "avatarColor": "primary",
            "avatarIcon": "tabler-chart-bar",
        },
        {
            "title": "وزن محصول",
            "subtitle": "برآورد فعلی",
            "amount": round(latest_storage, 2),
            "unit": "کیلوگرم در هکتار",
            "avatarColor": "warning",
            "avatarIcon": "tabler-scale",
        },
        {
            "title": "رطوبت خاک",
            "subtitle": "آخرین روز",
            "amount": round(latest_moisture, 2),
            "unit": "%",
            "avatarColor": "info",
            "avatarIcon": "tabler-droplet",
        },
    ]

    return {
        "farm_uuid": context.farm_uuid,
        "plant_name": context.plant_name,
        "engine": _fa_engine_name(simulation_result.get("engine")),
        "model_name": _fa_model_name(simulation_result.get("model_name")),
        "scenario_id": scenario_id,
        "simulation_warning": simulation_warning,
        "categories": categories,
        "series": [
            {"name": "تعداد برگ تخمینی", "key": "leaf_count_estimate", "data": leaf_count_series},
            {"name": "وزن بیوماس", "key": "biomass_weight", "data": biomass_series},
            {"name": "وزن محصول", "key": "storage_organ_weight", "data": storage_weight_series},
            {"name": "شاخص سطح برگ", "key": "lai", "data": lai_series},
            {"name": "رطوبت خاک", "key": "soil_moisture_percent", "data": moisture_series},
        ],
        "summary": summary,
        "current_state": {
            "date": latest.get("DAY"),
            "leaf_count_estimate": round(_estimate_leaf_count(latest_lai), 2),
            "leaf_area_index": round(latest_lai, 4),
            "biomass_weight": round(latest_biomass, 2),
            "storage_organ_weight": round(latest_storage, 2),
            "soil_moisture_percent": round(latest_moisture, 2),
            "development_stage": round(_safe_float(latest.get("DVS"), 0.0), 4),
            "gdd": round(_safe_float(latest.get("GDD"), 0.0), 2),
        },
        "metrics": simulation_result.get("metrics") or {},
        "daily_output": daily_output,
    }


class CurrentFarmChartSimulator:
    """سازنده chart وضعیت فعلی مزرعه برای خروجی مستقل از dashboard."""

    def simulate(
        self,
        *,
        farm_uuid: str,
        plant_name: str | None = None,
        irrigation_recommendation: dict[str, Any] | None = None,
        fertilization_recommendation: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        if not farm_uuid:
            raise GrowthSimulationError("ارسال farm_uuid الزامی است.")

        resolved_plant_name = plant_name
        if not resolved_plant_name:
            sensor = get_canonical_farm_record(farm_uuid)
            if sensor is None:
                raise GrowthSimulationError("مزرعه پیدا نشد.")
            plant = get_runtime_plant_for_farm(sensor)
            if plant is None:
                raise GrowthSimulationError("گیاهی برای مزرعه انتخاب شده پیدا نشد.")
            resolved_plant_name = plant.name

        context = build_growth_context(
            {
                "farm_uuid": farm_uuid,
                "plant_name": resolved_plant_name,
                "dynamic_parameters": DEFAULT_DYNAMIC_PARAMETERS,
                "page_size": DEFAULT_PAGE_SIZE,
            }
        )
        simulation_result, scenario_id, simulation_warning = _run_simulation(
            context,
            irrigation_recommendation=irrigation_recommendation,
            fertilization_recommendation=fertilization_recommendation,
        )
        return _build_current_farm_chart_payload(
            context,
            simulation_result,
            scenario_id,
            simulation_warning,
        )
