from __future__ import annotations import json from datetime import UTC, datetime from typing import Any from sqlalchemy import DateTime, select from app.core.database import session_scope from app.modules.zootech.catalog_models import ( ZootechDailyComponentNormAdjustment, ZootechDailyIngredientReplacement, ZootechDailyIngredientSkip, ZootechDailyTripSkip, ZootechDailyUnloadingGroupSkip, ZootechFeedDispenser, ZootechFeedingLocation, ZootechFeedingPeriod, ZootechFeedingPoint, ZootechFeedMixer, ZootechPeriodRecipe, ZootechTrip, ZootechUnloadingGroup, ) from app.modules.zootech.models import ZootechComponent, ZootechIngredient, ZootechRecipe TABLE_MODEL_MAP = { "component": ZootechComponent, "recipe": ZootechRecipe, "ingredient": ZootechIngredient, "unloading_group": ZootechUnloadingGroup, "feed_mixer": ZootechFeedMixer, "feed_dispenser": ZootechFeedDispenser, "feeding_location": ZootechFeedingLocation, "feeding_period": ZootechFeedingPeriod, "feeding_point": ZootechFeedingPoint, "period_recipes": ZootechPeriodRecipe, "daily_trip_skip": ZootechDailyTripSkip, "daily_ingredient_skip": ZootechDailyIngredientSkip, "daily_unloading_group_skip": ZootechDailyUnloadingGroupSkip, "daily_ingredient_replacement": ZootechDailyIngredientReplacement, "daily_component_norm_adjustment": ZootechDailyComponentNormAdjustment, "trip": ZootechTrip, } def _coerce_for_model(model, key: str, value: Any) -> Any: if value is None: return value column = model.__table__.columns.get(key) if column is None: return value if isinstance(column.type, DateTime) and isinstance(value, str): try: return datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError: return datetime.now(UTC) return value def _parse_period_recipe_id(record_id: str) -> tuple[str, str]: if ":" in record_id: period_id, recipe_id = record_id.split(":", 1) return period_id, recipe_id return record_id, record_id def load_catalog_row(enterprise_id: str, table_name: str, record_id: str) -> dict[str, Any] | None: model = TABLE_MODEL_MAP.get(table_name) if not model: return None with session_scope() as db: if model is ZootechPeriodRecipe: period_id, recipe_id = _parse_period_recipe_id(record_id) row = db.scalar( select(model).where( model.enterprise_id == enterprise_id, model.period_id == period_id, model.recipe_id == recipe_id, ) ) else: row = db.scalar( select(model).where(model.enterprise_id == enterprise_id, model.id == record_id) ) if not row: return None return _row_to_dict(row, table_name) def apply_catalog_change( enterprise_id: str, table_name: str, record_id: str, action: str, payload: dict[str, Any], version: int, content_hash: str, ) -> None: model = TABLE_MODEL_MAP.get(table_name) if not model: return with session_scope() as db: if model is ZootechPeriodRecipe: period_id, recipe_id = _parse_period_recipe_id(record_id) row = db.scalar( select(model).where( model.enterprise_id == enterprise_id, model.period_id == period_id, model.recipe_id == recipe_id, ) ) else: row = db.scalar( select(model).where(model.enterprise_id == enterprise_id, model.id == record_id) ) if action == "delete": if row: row.is_deleted = True row.version = version row.content_hash = content_hash row.updated_at = datetime.now(UTC) return data = dict(payload) data["enterprise_id"] = enterprise_id data["version"] = version data["content_hash"] = content_hash data["is_deleted"] = False if model is ZootechPeriodRecipe: data["period_id"] = data.get("period_id") or _parse_period_recipe_id(record_id)[0] data["recipe_id"] = data.get("recipe_id") or _parse_period_recipe_id(record_id)[1] else: data["id"] = record_id if row: _apply_fields(row, data, model) row.updated_at = datetime.now(UTC) else: allowed = {c.key for c in model.__table__.columns} filtered = { k: _coerce_for_model(model, k, v) for k, v in data.items() if k in allowed } extra = {k: v for k, v in data.items() if k not in allowed} if extra and "payload_json" in allowed: filtered["payload_json"] = json.dumps(extra, ensure_ascii=False) db.add(model(**filtered)) def _apply_fields(row, data: dict[str, Any], model) -> None: allowed = {c.key for c in model.__table__.columns} extra: dict[str, Any] = {} for key, value in data.items(): if key in allowed and key not in ("enterprise_id", "created_at"): setattr(row, key, _coerce_for_model(model, key, value)) elif key not in ("enterprise_id", "created_at", "id"): extra[key] = value if model is ZootechRecipe and "ingredients" in data: row.payload_json = json.dumps(data.get("ingredients"), ensure_ascii=False) elif extra and "payload_json" in allowed: row.payload_json = json.dumps(extra, ensure_ascii=False) def _row_to_dict(row, table_name: str) -> dict[str, Any]: result = {} for col in row.__table__.columns: val = getattr(row, col.key) if isinstance(val, datetime): val = val.isoformat() result[col.key] = val if isinstance(row, ZootechRecipe) and row.payload_json: try: parsed = json.loads(row.payload_json) if isinstance(parsed, list): result["ingredients"] = parsed except json.JSONDecodeError: pass if table_name == "period_recipes": result["id"] = f"{row.period_id}:{row.recipe_id}" elif hasattr(row, "payload_json") and row.payload_json and table_name != "recipe": try: result.update(json.loads(row.payload_json)) except json.JSONDecodeError: pass return result