"""Milestone management service.""" import logging from datetime import datetime, timezone from sqlalchemy import func, select from fabledassistant.models import async_session from fabledassistant.models.milestone import Milestone from fabledassistant.models.note import Note logger = logging.getLogger(__name__) async def create_milestone( user_id: int, project_id: int, title: str, description: str | None = None, order_index: int = 0, ) -> Milestone: async with async_session() as session: milestone = Milestone( user_id=user_id, project_id=project_id, title=title, description=description, status="active", order_index=order_index, ) session.add(milestone) await session.commit() await session.refresh(milestone) return milestone async def get_milestone(user_id: int, milestone_id: int) -> Milestone | None: async with async_session() as session: result = await session.execute( select(Milestone).where(Milestone.id == milestone_id, Milestone.user_id == user_id) ) return result.scalars().first() async def get_milestone_by_title(user_id: int, project_id: int, title: str) -> Milestone | None: async with async_session() as session: result = await session.execute( select(Milestone).where( Milestone.user_id == user_id, Milestone.project_id == project_id, func.lower(Milestone.title) == func.lower(title.strip()), ).limit(1) ) return result.scalars().first() async def find_milestone_by_title(user_id: int, title: str) -> Milestone | None: """Find a milestone by title across ALL projects for this user (case-insensitive).""" async with async_session() as session: result = await session.execute( select(Milestone).where( Milestone.user_id == user_id, func.lower(Milestone.title) == func.lower(title.strip()), ).order_by(Milestone.id).limit(1) ) return result.scalars().first() async def get_or_create_milestone(user_id: int, project_id: int, title: str) -> Milestone: milestone = await get_milestone_by_title(user_id, project_id, title) if milestone: return milestone return await create_milestone(user_id, project_id, title=title) async def list_milestones( user_id: int, project_id: int, status: str | None = None ) -> list[Milestone]: async with async_session() as session: query = select(Milestone).where( Milestone.user_id == user_id, Milestone.project_id == project_id, ) if status: query = query.where(Milestone.status == status) query = query.order_by(Milestone.order_index.asc(), Milestone.created_at.asc()) result = await session.execute(query) return list(result.scalars().all()) async def update_milestone(user_id: int, milestone_id: int, **fields: object) -> Milestone | None: async with async_session() as session: result = await session.execute( select(Milestone).where(Milestone.id == milestone_id, Milestone.user_id == user_id) ) milestone = result.scalars().first() if milestone is None: return None for key, value in fields.items(): if hasattr(milestone, key): setattr(milestone, key, value) milestone.updated_at = datetime.now(timezone.utc) await session.commit() await session.refresh(milestone) return milestone async def delete_milestone(user_id: int, milestone_id: int) -> bool: async with async_session() as session: result = await session.execute( select(Milestone).where(Milestone.id == milestone_id, Milestone.user_id == user_id) ) milestone = result.scalars().first() if milestone is None: return False await session.delete(milestone) await session.commit() return True async def get_milestone_progress(milestone_id: int) -> dict: """Return task completion stats for a milestone.""" async with async_session() as session: rows = await session.execute( select(Note.status, func.count(Note.id)) .where(Note.milestone_id == milestone_id, Note.status.isnot(None)) .group_by(Note.status) ) status_counts: dict[str, int] = {} for status, count in rows.fetchall(): status_counts[status] = count total = sum(status_counts.values()) completed = status_counts.get("done", 0) pct = round(completed / total * 100, 1) if total > 0 else 0.0 return { "total": total, "completed": completed, "pct": pct, "status_counts": { "todo": status_counts.get("todo", 0), "in_progress": status_counts.get("in_progress", 0), "done": status_counts.get("done", 0), }, } async def get_project_milestone_summary(user_id: int, project_id: int) -> list[dict]: """Return ordered list of milestones with their progress stats.""" milestones = await list_milestones(user_id, project_id) result = [] for m in milestones: progress = await get_milestone_progress(m.id) entry = m.to_dict() entry.update(progress) result.append(entry) return result