from __future__ import annotations from pathlib import Path from typing import Any, Optional import httpx from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import Response from fastapi.templating import Jinja2Templates from pydantic import BaseModel, Field from app.config import settings from app.services import packaging_nas, projects router = APIRouter(tags=["packaging"]) BASE_DIR = Path(__file__).resolve().parent.parent.parent templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) TOOLS = settings.TOOLS_API_URL.rstrip("/") class PackagingGenerateBody(BaseModel): type: str = Field(default="folding_box") width_mm: float = Field(default=120, gt=0) height_mm: float = Field(default=80, gt=0) depth_mm: float = Field(default=40, ge=0) bleed_mm: float = Field(default=3, ge=0) elements: dict[str, bool] = Field(default_factory=dict) brand: dict[str, str] = Field(default_factory=dict) text: dict[str, Any] = Field(default_factory=dict) barcode_value: str | None = None qr_value: str | None = None design_name: str | None = None project_id: int | None = None project_name: str | None = None client_id: int | None = None created_by: str = "ceo" class PackagingEmailBody(BaseModel): to: list[str] = Field(..., min_length=1) subject: str = Field(..., min_length=1) body: str = "" cc: list[str] = Field(default_factory=list) client_id: int | None = None class CopyToProjectBody(BaseModel): target_project_id: int async def _tools_request(method: str, path: str, **kwargs) -> httpx.Response: async with httpx.AsyncClient(timeout=120.0) as client: r = await client.request(method, f"{TOOLS}{path}", **kwargs) return r def _tools_error(exc: httpx.HTTPStatusError) -> HTTPException: detail = exc.response.text[:500] if exc.response else str(exc) return HTTPException(status_code=exc.response.status_code if exc.response else 502, detail=detail) async def _ensure_project(body: PackagingGenerateBody) -> PackagingGenerateBody: if body.project_id or not body.project_name: return body proj = projects.create_project( body.project_name.strip(), client_id=body.client_id, description="Packaging design project", project_type="packaging", ensure_nas=True, ) data = body.model_dump() data["project_id"] = proj.get("id") return PackagingGenerateBody(**data) async def _post_nas_export(result: dict[str, Any]) -> dict[str, Any]: pid = result.get("cockpit_project_id") packaging_id = result.get("id") svg = result.get("svg") or "" spec = result.get("spec") or {} if not pid or not packaging_id or not svg: return result png_bytes = pdf_bytes = None try: png_r = await _tools_request("GET", f"/packaging/download/{packaging_id}", params={"format": "png"}) if png_r.status_code < 400: png_bytes = png_r.content pdf_r = await _tools_request("GET", f"/packaging/download/{packaging_id}", params={"format": "pdf"}) if pdf_r.status_code < 400: pdf_bytes = pdf_r.content except Exception: pass try: nas_info = packaging_nas.export_packaging_files(packaging_id, int(pid), svg, spec, png_bytes, pdf_bytes) result["nas"] = nas_info except Exception as exc: result["nas"] = {"ok": False, "error": str(exc)} return result @router.get("/packaging") async def packaging_page(request: Request, project_id: Optional[int] = None): return templates.TemplateResponse( "packaging.html", {"request": request, "page_title": "Packaging Studio", "initial_project_id": project_id}, ) @router.get("/api/packaging/types") async def proxy_packaging_types(): try: r = await _tools_request("GET", "/packaging/types") r.raise_for_status() return r.json() except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.post("/api/packaging/preview") async def proxy_packaging_preview(body: PackagingGenerateBody): try: r = await _tools_request("POST", "/packaging/preview", json=body.model_dump()) r.raise_for_status() return r.json() except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.post("/api/packaging/generate") async def proxy_packaging_generate(body: PackagingGenerateBody): try: body = await _ensure_project(body) r = await _tools_request("POST", "/packaging/generate", json=body.model_dump()) r.raise_for_status() result = r.json() return await _post_nas_export(result) except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.get("/api/packaging/projects") async def proxy_packaging_projects(limit: int = 30): try: r = await _tools_request("GET", "/packaging/projects", params={"limit": limit}) r.raise_for_status() return r.json() except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.get("/api/packaging/download/{project_id}") async def proxy_packaging_download(project_id: str, format: str = "svg"): try: r = await _tools_request("GET", f"/packaging/download/{project_id}", params={"format": format}) r.raise_for_status() media = r.headers.get("content-type", "application/octet-stream") disposition = r.headers.get("content-disposition") headers = {"content-disposition": disposition} if disposition else {} return Response(content=r.content, media_type=media, headers=headers) except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.get("/api/packaging/{packaging_id}") async def proxy_packaging_get(packaging_id: str): try: r = await _tools_request("GET", f"/packaging/{packaging_id}") r.raise_for_status() return r.json() except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.patch("/api/packaging/{packaging_id}") async def proxy_packaging_update(packaging_id: str, body: PackagingGenerateBody): try: r = await _tools_request("PATCH", f"/packaging/{packaging_id}", json={"spec": body.model_dump()}) r.raise_for_status() result = r.json() return await _post_nas_export(result) except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.post("/api/packaging/{packaging_id}/duplicate") async def proxy_packaging_duplicate(packaging_id: str): try: r = await _tools_request("POST", f"/packaging/{packaging_id}/duplicate") r.raise_for_status() result = r.json() return await _post_nas_export(result) except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.post("/api/packaging/{packaging_id}/copy-to-project") async def proxy_packaging_copy(packaging_id: str, body: CopyToProjectBody): try: r = await _tools_request( "POST", f"/packaging/{packaging_id}/copy-to-project", json=body.model_dump(), ) r.raise_for_status() result = r.json() return await _post_nas_export(result) except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.delete("/api/packaging/{packaging_id}") async def proxy_packaging_delete(packaging_id: str): try: r = await _tools_request("DELETE", f"/packaging/{packaging_id}") r.raise_for_status() return r.json() except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc @router.post("/api/packaging/{packaging_id}/email") async def proxy_packaging_email(packaging_id: str, body: PackagingEmailBody): base = settings.COCKPIT_PUBLIC_URL if hasattr(settings, "COCKPIT_PUBLIC_URL") else "http://10.4.7.18:8600" links = "\n".join( f"- {fmt.upper()}: {base}/api/packaging/download/{packaging_id}?format={fmt}" for fmt in ("svg", "png", "pdf") ) email_body = (body.body or "").strip() if email_body: email_body += "\n\n" email_body += f"Packaging downloads:\n{links}" try: r = await _tools_request( "POST", "/emails/send", json={ "to": body.to, "cc": body.cc, "subject": body.subject, "body": email_body, "client_id": body.client_id, }, ) r.raise_for_status() return r.json() except httpx.HTTPStatusError as exc: raise _tools_error(exc) from exc