"""Contact folder service — CRUD, tree building, move contacts.""" from __future__ import annotations import uuid from typing import Any from sqlalchemy import func, select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.models.contact import Contact from app.models.contact_folder import ContactFolder def _serialize_folder(f: ContactFolder, contact_count: int = 0) -> dict: return { "id": str(f.id), "name": f.name, "parent_id": str(f.parent_id) if f.parent_id else None, "user_id": str(f.user_id), "sort_order": f.sort_order, "contact_count": contact_count, } async def list_folders( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID ) -> list[dict]: """List all folders for a user (flat list, frontend builds tree).""" result = await db.execute( select(ContactFolder) .where(ContactFolder.tenant_id == tenant_id) .where(ContactFolder.user_id == user_id) .where(ContactFolder.deleted_at.is_(None)) .order_by(ContactFolder.parent_id, ContactFolder.sort_order, ContactFolder.name) ) folders = result.scalars().all() # Count contacts per folder folder_ids = [f.id for f in folders] counts: dict[uuid.UUID, int] = {} if folder_ids: count_result = await db.execute( select(Contact.folder_id, func.count(Contact.id)) .where(Contact.folder_id.in_(folder_ids)) .where(Contact.deleted_at.is_(None)) .group_by(Contact.folder_id) ) for fid, cnt in count_result: counts[fid] = cnt return [ {**_serialize_folder(f), "contact_count": counts.get(f.id, 0)} for f in folders ] async def create_folder( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, name: str, parent_id: str | None = None, ) -> dict: """Create a new contact folder.""" parent_uuid = uuid.UUID(parent_id) if parent_id else None # Determine sort_order: max + 1 among siblings max_order_result = await db.execute( select(func.max(ContactFolder.sort_order)) .where(ContactFolder.tenant_id == tenant_id) .where(ContactFolder.user_id == user_id) .where(ContactFolder.parent_id == parent_uuid if parent_uuid else ContactFolder.parent_id.is_(None)) ) max_order = max_order_result.scalar() or 0 folder = ContactFolder( tenant_id=tenant_id, user_id=user_id, name=name, parent_id=parent_uuid, sort_order=max_order + 1, ) db.add(folder) await db.commit() await db.refresh(folder) return _serialize_folder(folder) async def update_folder( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, folder_id: str, data: dict, ) -> dict: """Update a contact folder (name, parent_id, sort_order).""" result = await db.execute( select(ContactFolder) .where(ContactFolder.id == uuid.UUID(folder_id)) .where(ContactFolder.tenant_id == tenant_id) .where(ContactFolder.user_id == user_id) .where(ContactFolder.deleted_at.is_(None)) ) folder = result.scalar_one_or_none() if not folder: raise ValueError("Folder not found") if "name" in data and data["name"]: folder.name = data["name"] if "parent_id" in data: new_parent = data["parent_id"] folder.parent_id = uuid.UUID(new_parent) if new_parent else None if "sort_order" in data and data["sort_order"] is not None: folder.sort_order = data["sort_order"] await db.commit() await db.refresh(folder) return _serialize_folder(folder) async def delete_folder( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, folder_id: str, ) -> None: """Delete a contact folder. Contacts are unassigned (folder_id SET NULL).""" result = await db.execute( select(ContactFolder) .where(ContactFolder.id == uuid.UUID(folder_id)) .where(ContactFolder.tenant_id == tenant_id) .where(ContactFolder.user_id == user_id) .where(ContactFolder.deleted_at.is_(None)) ) folder = result.scalar_one_or_none() if not folder: raise ValueError("Folder not found") # Unassign contacts in this folder (and subfolders via CASCADE on folder delete) await db.execute( update(Contact) .where(Contact.folder_id == folder.id) .values(folder_id=None) ) await db.delete(folder) await db.commit() async def move_contact( db: AsyncSession, tenant_id: uuid.UUID, contact_id: str, folder_id: str | None, ) -> dict: """Move a contact to a folder (or unassign with folder_id=None).""" result = await db.execute( select(Contact) .where(Contact.id == uuid.UUID(contact_id)) .where(Contact.tenant_id == tenant_id) .where(Contact.deleted_at.is_(None)) ) contact = result.scalar_one_or_none() if not contact: raise ValueError("Contact not found") contact.folder_id = uuid.UUID(folder_id) if folder_id else None await db.commit() await db.refresh(contact) return {"id": str(contact.id), "folder_id": str(contact.folder_id) if contact.folder_id else None} async def reorder_folders( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, folder_orders: list[dict], ) -> None: """Batch update sort_order for folders. Each item: {id, sort_order, parent_id?}.""" for item in folder_orders: fid = uuid.UUID(item["id"]) await db.execute( update(ContactFolder) .where(ContactFolder.id == fid) .where(ContactFolder.tenant_id == tenant_id) .where(ContactFolder.user_id == user_id) .values( sort_order=item.get("sort_order", 0), parent_id=uuid.UUID(item["parent_id"]) if item.get("parent_id") else None, ) ) await db.commit()