import json from uuid import UUID import asyncpg from app.exceptions import NotFoundError from app.models.products import ProductCreate, ProductUpdate async def list_products( db: asyncpg.Connection, page: int, per_page: int, offset: int, include_hidden: bool = False, category: str | None = None, bestseller: bool = False, is_new: bool = False, search: str | None = None, exclude_id: str | None = None, ) -> tuple[list[dict], int]: conditions: list[str] = [] params: list = [] if not include_hidden: conditions.append("is_hidden = false") if category: params.append(category) conditions.append(f"category = ${len(params)}") if bestseller: conditions.append("is_bestseller = true") if is_new: conditions.append("is_new = true") if search: params.append(f"%{search}%") conditions.append(f"(name ILIKE ${len(params)} OR description ILIKE ${len(params)})") if exclude_id: params.append(exclude_id) conditions.append(f"id != ${len(params)}") where = f"WHERE {' AND '.join(conditions)}" if conditions else "" total = await db.fetchval(f"SELECT COUNT(*) FROM products {where}", *params) params.extend([per_page, offset]) rows = await db.fetch( f""" SELECT id, name, description, price, original_price, category, images, colors, lengths, features, stock_quantity, is_featured, is_hidden, is_new, is_bestseller, rating, review_count, created_at, updated_at FROM products {where} ORDER BY is_bestseller DESC, is_new DESC, created_at DESC LIMIT ${len(params) - 1} OFFSET ${len(params)} """, *params, ) return [_row(r) for r in rows], total async def get_product(db: asyncpg.Connection, product_id: str) -> dict: row = await db.fetchrow("SELECT * FROM products WHERE id = $1", product_id) if not row: raise NotFoundError("product") return _row(row) async def create_product(db: asyncpg.Connection, data: ProductCreate) -> dict: row = await db.fetchrow( """ INSERT INTO products ( name, description, price, original_price, category, colors, lengths, features, stock_quantity, is_featured, is_hidden, is_new, is_bestseller, images ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,'[]'::jsonb) RETURNING * """, data.name, data.description, data.price, data.original_price, data.category, data.colors, data.lengths, data.features, data.stock_quantity, data.is_featured, data.is_hidden, data.is_new, data.is_bestseller, ) return _row(row) async def update_product(db: asyncpg.Connection, product_id: str, data: ProductUpdate) -> dict: await get_product(db, product_id) updates = {k: v for k, v in data.model_dump().items() if v is not None} if not updates: return await get_product(db, product_id) json_fields = {"colors", "lengths", "features"} set_parts = [] values = [] for i, (k, v) in enumerate(updates.items()): set_parts.append(f"{k} = ${i + 2}") values.append(v) row = await db.fetchrow( f"UPDATE products SET {', '.join(set_parts)}, updated_at = now() WHERE id = $1 RETURNING *", product_id, *values, ) return _row(row) async def delete_product(db: asyncpg.Connection, product_id: str): result = await db.execute("DELETE FROM products WHERE id = $1", product_id) if result == "DELETE 0": raise NotFoundError("product") async def add_image(db: asyncpg.Connection, product_id: str, url: str) -> dict: await get_product(db, product_id) row = await db.fetchrow( """ UPDATE products SET images = images || $2, updated_at = now() WHERE id = $1 RETURNING * """, product_id, [url], ) return _row(row) async def remove_image(db: asyncpg.Connection, product_id: str, url: str) -> dict: product = await get_product(db, product_id) images = [img for img in product["_raw_images"] if img != url] row = await db.fetchrow( "UPDATE products SET images = $2, updated_at = now() WHERE id = $1 RETURNING *", product_id, images, ) return _row(row) async def bulk_stock_update(db: asyncpg.Connection, updates: list[dict]): async with db.transaction(): for u in updates: await db.execute( "UPDATE products SET stock_quantity = $2, updated_at = now() WHERE id = $1", str(u["id"]), u["stock_quantity"], ) def _row(r) -> dict: d = dict(r) # Decode JSONB lists for field in ("images", "colors", "lengths", "features"): val = d.get(field, []) if isinstance(val, str): val = json.loads(val) d[field] = val if isinstance(val, list) else [] # images stored as plain URL strings; keep a raw copy for internal use d["_raw_images"] = d["images"] # Derive convenience `image` field (first URL for product cards) d["image"] = d["images"][0] if d["images"] else "" return d