"""Collaboration service for VoIdea. Voting, threaded comments, activity logging, and notifications. """ from typing import Any, Optional from uuid import UUID from sqlalchemy import select, func, delete from sqlalchemy.ext.asyncio import AsyncSession from app.models.collaboration import IdeaActivity, IdeaComment, IdeaVote, Notification from app.models.user import User class CollaborationService: def __init__(self, db: AsyncSession): self.db = db # ── Voting ── async def toggle_vote(self, idea_id: UUID, user_id: UUID, vote: str) -> dict[str, Any]: """Toggle vote. Returns current vote state.""" result = await self.db.execute( select(IdeaVote).where( IdeaVote.idea_id == idea_id, IdeaVote.user_id == user_id, ) ) existing = result.scalar_one_or_none() if existing: if existing.vote == vote: await self.db.delete(existing) await self.db.commit() return {"vote": None} existing.vote = vote await self.db.commit() return {"vote": vote} else: v = IdeaVote(idea_id=idea_id, user_id=user_id, vote=vote) self.db.add(v) await self.db.commit() return {"vote": vote} async def get_vote_counts(self, idea_id: UUID) -> dict[str, Any]: """Get vote counts and current user's vote.""" up = await self.db.execute( select(func.count(IdeaVote.id)).where( IdeaVote.idea_id == idea_id, IdeaVote.vote == "up" ) ) down = await self.db.execute( select(func.count(IdeaVote.id)).where( IdeaVote.idea_id == idea_id, IdeaVote.vote == "down" ) ) return { "up": up.scalar() or 0, "down": down.scalar() or 0, } async def get_user_vote(self, idea_id: UUID, user_id: UUID) -> Optional[str]: result = await self.db.execute( select(IdeaVote).where( IdeaVote.idea_id == idea_id, IdeaVote.user_id == user_id ) ) v = result.scalar_one_or_none() return v.vote if v else None # ── Comments ── async def create_comment( self, idea_id: UUID, user_id: UUID, content: str, parent_id: Optional[UUID] = None ) -> IdeaComment: c = IdeaComment( idea_id=idea_id, user_id=user_id, content=content, parent_id=parent_id ) self.db.add(c) await self.db.commit() await self.db.refresh(c) return c async def list_comments(self, idea_id: UUID) -> list[dict[str, Any]]: result = await self.db.execute( select(IdeaComment, User) .join(User, IdeaComment.user_id == User.id) .where(IdeaComment.idea_id == idea_id) .order_by(IdeaComment.created_at.asc()) ) rows = result.all() comments = [] for c, u in rows: comments.append({ "id": str(c.id), "idea_id": str(c.idea_id), "user_id": str(c.user_id), "content": c.content, "parent_id": str(c.parent_id) if c.parent_id else None, "display_name": u.display_name, "avatar_url": u.avatar_url, "created_at": c.created_at, "updated_at": c.updated_at, }) return comments async def delete_comment(self, comment_id: UUID, user_id: UUID) -> bool: result = await self.db.execute( select(IdeaComment).where(IdeaComment.id == comment_id) ) c = result.scalar_one_or_none() if not c or str(c.user_id) != str(user_id): return False await self.db.delete(c) await self.db.commit() return True # ── Activity ── async def log_activity( self, idea_id: UUID, user_id: Optional[UUID], action: str, details: Optional[dict] = None ) -> IdeaActivity: a = IdeaActivity( idea_id=idea_id, user_id=user_id, action=action, details=details ) self.db.add(a) await self.db.commit() await self.db.refresh(a) return a async def list_activities(self, idea_id: UUID, limit: int = 50) -> list[dict[str, Any]]: result = await self.db.execute( select(IdeaActivity, User) .join(User, IdeaActivity.user_id == User.id, isouter=True) .where(IdeaActivity.idea_id == idea_id) .order_by(IdeaActivity.created_at.desc()) .limit(limit) ) rows = result.all() return [ { "id": str(a.id), "idea_id": str(a.idea_id), "user_id": str(a.user_id) if a.user_id else None, "action": a.action, "details": a.details, "display_name": u.display_name if u else "System", "created_at": a.created_at, } for a, u in rows ] # ── Notifications ── async def create_notification( self, user_id: UUID, type: str, title: str, body: str, link: Optional[str] = None ) -> Notification: n = Notification( user_id=user_id, type=type, title=title, body=body, link=link, ) self.db.add(n) await self.db.commit() await self.db.refresh(n) return n async def list_notifications( self, user_id: UUID, unread_only: bool = False, limit: int = 50 ) -> list[Notification]: query = ( select(Notification) .where(Notification.user_id == user_id) .order_by(Notification.created_at.desc()) .limit(limit) ) if unread_only: query = query.where(Notification.is_read == False) result = await self.db.execute(query) return list(result.scalars().all()) async def mark_notification_read(self, notification_id: UUID, user_id: UUID) -> bool: result = await self.db.execute( select(Notification).where( Notification.id == notification_id, Notification.user_id == user_id, ) ) n = result.scalar_one_or_none() if not n: return False n.is_read = True await self.db.commit() return True async def mark_all_read(self, user_id: UUID) -> int: result = await self.db.execute( select(func.count(Notification.id)).where( Notification.user_id == user_id, Notification.is_read == False, ) ) count = result.scalar() or 0 await self.db.execute( delete(Notification).where( Notification.user_id == user_id, Notification.is_read == False, ) ) await self.db.commit() return count async def get_unread_count(self, user_id: UUID) -> int: result = await self.db.execute( select(func.count(Notification.id)).where( Notification.user_id == user_id, Notification.is_read == False, ) ) return result.scalar() or 0