"""Router exposing crafting recipe endpoints. This is separated from `router.py` to keep modules focused. """ from __future__ import annotations from typing import List, Optional, Tuple, Any from fastapi import APIRouter, Depends, HTTPException, Path, Query from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel from .database import get_session # Map craft names -> table names in Postgres ALLOWED_CRAFTS = { "woodworking": "recipes_woodworking", "smithing": "recipes_smithing", "alchemy": "recipes_alchemy", "bonecraft": "recipes_bonecraft", "goldsmithing": "recipes_goldsmithing", "clothcraft": "recipes_clothcraft", "leathercraft": "recipes_leathercraft", "cooking": "recipes_cooking", } router = APIRouter(tags=["recipes"]) class RecipeUsageSummary(BaseModel): craft: str id: int name: str level: int class ItemRecipeUsage(BaseModel): crafted: list[RecipeUsageSummary] = [] ingredient: list[RecipeUsageSummary] = [] class RecipeDetail(BaseModel): id: int name: str level: int category: str crystal: str key_item: Optional[str] | None = None ingredients: List[Tuple[str, int]] hq_yields: List[Optional[Tuple[str, int]]] subcrafts: List[Tuple[str, int]] class Config: from_attributes = True def _craft_table(craft: str) -> str: craft_lower = craft.lower() if craft_lower not in ALLOWED_CRAFTS: raise HTTPException(status_code=404, detail="Unknown craft") return ALLOWED_CRAFTS[craft_lower] @router.get("/recipes/{craft}", response_model=List[RecipeDetail]) async def list_recipes( craft: str = Path(..., description="Craft name, e.g. woodworking"), session: AsyncSession = Depends(get_session), ): """Return full list of recipes for a craft.""" table = _craft_table(craft) q = text(f"SELECT * FROM {table} ORDER BY level, name") result = await session.execute(q) rows = result.fetchall() print("[DEBUG] list_recipes", table, len(rows)) details: List[RecipeDetail] = [] for r in rows: details.append( RecipeDetail( id=r.id, name=r.name, level=r.level, category=r.category, crystal=r.crystal, key_item=r.key_item, ingredients=_to_list_tuples(r.ingredients), hq_yields=[tuple(h) if h else None for h in r.hq_yields] if r.hq_yields else [], subcrafts=_to_list_tuples(r.subcrafts), ) ) return details def _to_list_tuples(value: Any) -> List[Tuple[str, int]]: """Convert JSON/array from Postgres to List[Tuple[str, int]].""" if not value: return [] # asyncpg already converts jsonb to Python lists/dicts formatted: List[Tuple[str, int]] = [] for item in value: # Accept [name, qty] or {"name": n, "qty": q} if isinstance(item, (list, tuple)) and len(item) == 2: name, qty = item elif isinstance(item, dict): name = item.get("0") or item.get("name") or item.get("item") qty = item.get("1") or item.get("qty") or item.get("quantity") else: # Fallback: treat as string name, qty=1 name, qty = str(item), 1 if name is None or qty is None: continue try: qty_int = int(qty) except Exception: qty_int = 1 formatted.append((str(name), qty_int)) return formatted @router.get("/recipes/usage/{item_name}", response_model=ItemRecipeUsage) async def item_recipe_usage(item_name: str, session: AsyncSession = Depends(get_session)): """Return lists of recipes that craft `item_name` or use it as an ingredient (excluding crystals).""" if item_name.lower().endswith(" crystal"): return ItemRecipeUsage() crafted: list[RecipeUsageSummary] = [] ingredient: list[RecipeUsageSummary] = [] for craft, table in ALLOWED_CRAFTS.items(): # Crafted results q1 = text(f"SELECT id, name, level FROM {table} WHERE name = :n LIMIT 50") res1 = await session.execute(q1, {"n": item_name}) crafted.extend( [RecipeUsageSummary(craft=craft, id=r.id, name=r.name, level=r.level) for r in res1.fetchall()] ) # As ingredient (simple text match in JSON/array column) q2 = text( f"SELECT id, name, level FROM {table} WHERE ingredients::text ILIKE :pat LIMIT 50" ) res2 = await session.execute(q2, {"pat": f"%{item_name}%"}) for r in res2.fetchall(): if not any(c.id == r.id and c.craft == craft for c in crafted) and not any( i.id == r.id and i.craft == craft for i in ingredient ): ingredient.append( RecipeUsageSummary(craft=craft, id=r.id, name=r.name, level=r.level) ) return ItemRecipeUsage(crafted=crafted, ingredient=ingredient) @router.get("/recipes/{craft}/{recipe_id}", response_model=RecipeDetail) async def recipe_detail( craft: str = Path(..., description="Craft name"), recipe_id: int = Path(..., description="Recipe ID"), session: AsyncSession = Depends(get_session), ): """Return full recipe record.""" table = _craft_table(craft) q = text(f"SELECT * FROM {table} WHERE id = :id LIMIT 1") result = await session.execute(q, {"id": recipe_id}) row = result.fetchone() if not row: raise HTTPException(status_code=404, detail="Recipe not found") print("[DEBUG] recipe_detail", craft, recipe_id) return RecipeDetail( id=row.id, name=row.name, level=row.level, category=row.category, crystal=row.crystal, key_item=row.key_item, ingredients=_to_list_tuples(row.ingredients), hq_yields=[tuple(h) if h else None for h in row.hq_yields] if row.hq_yields else [], subcrafts=_to_list_tuples(row.subcrafts), )