"""Milestone management service."""
import logging
from datetime import datetime, timezone

from sqlalchemy import func, select

from scribe.models import async_session
from scribe.models.milestone import Milestone
from scribe.models.note import Note

logger = logging.getLogger(__name__)


async def create_milestone(
    user_id: int,
    project_id: int,
    title: str,
    description: str | None = None,
    body: str | None = None,
    order_index: int = 0,
    status: str = "active",
) -> Milestone:
    async with async_session() as session:
        milestone = Milestone(
            user_id=user_id,
            project_id=project_id,
            title=title,
            description=description,
            body=body,
            status=status,
            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,
                Milestone.deleted_at.is_(None),
            )
        )
        return result.scalars().first()


async def get_milestone_in_project(project_id: int, milestone_id: int) -> Milestone | None:
    """Fetch a milestone by id within a project, without a user_id ownership check.
    Callers must verify project access separately before using this."""
    async with async_session() as session:
        result = await session.execute(
            select(Milestone).where(
                Milestone.id == milestone_id,
                Milestone.project_id == project_id,
                Milestone.deleted_at.is_(None),
            )
        )
        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,
            Milestone.deleted_at.is_(None),
        )
        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.deleted_at.is_(None),
            )
        )
        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),
                Note.deleted_at.is_(None),
            )
            .group_by(Note.status)
        )
        status_counts: dict[str, int] = {}
        for status, count in rows.fetchall():
            status_counts[status] = count

    # Same rule as the batch path, computed in one place so the two cannot
    # drift on the cancelled-exclusion.
    return _progress_from_counts(status_counts)


def _progress_from_counts(status_counts: dict[str, int]) -> dict:
    """The progress shape, computed from already-fetched counts.

    Split out of get_milestone_progress so the batch path can reuse the rule
    rather than restate it — the cancelled-exclusion below is easy to get
    subtly different in a second copy, and then two screens disagree about
    whether a milestone is finished.
    """
    total = sum(status_counts.values())
    cancelled = status_counts.get("cancelled", 0)
    completed = status_counts.get("done", 0)
    # Cancelled tasks are resolved work, not pending — excluded from the
    # denominator so a milestone whose only open task was cancelled reaches
    # 100% instead of stalling.
    active_total = total - cancelled
    return {
        "total": total,
        "completed": completed,
        "pct": round(completed / active_total * 100, 1) if active_total > 0 else 0.0,
        "status_counts": {
            "todo": status_counts.get("todo", 0),
            "in_progress": status_counts.get("in_progress", 0),
            "done": status_counts.get("done", 0),
            "cancelled": cancelled,
        },
    }


async def get_project_milestone_summaries(
    user_id: int, project_ids: list[int]
) -> dict[int, list[dict]]:
    """Milestone summaries for MANY projects in two queries total.

    The per-project version below is a nested fan-out: one query to list a
    project's milestones, then one more per milestone for its progress. Called
    for 25 projects concurrently it asked for ~250 pooled connections against a
    pool of 15, and every one of them waited out the 30-second checkout timeout
    (#2384). This does the same work in two queries and one session.
    """
    if not project_ids:
        return {}

    async with async_session() as session:
        milestones = list((await session.execute(
            select(Milestone).where(
                Milestone.user_id == user_id,
                Milestone.project_id.in_(project_ids),
                Milestone.deleted_at.is_(None),
            ).order_by(Milestone.order_index.asc(), Milestone.created_at.asc())
        )).scalars().all())

        counts: dict[int, dict[str, int]] = {}
        if milestones:
            rows = await session.execute(
                select(Note.milestone_id, Note.status, func.count(Note.id))
                .where(
                    Note.milestone_id.in_([m.id for m in milestones]),
                    Note.status.isnot(None),
                    Note.deleted_at.is_(None),
                )
                .group_by(Note.milestone_id, Note.status)
            )
            for milestone_id, status, count in rows.fetchall():
                counts.setdefault(milestone_id, {})[status] = count

    out: dict[int, list[dict]] = {pid: [] for pid in project_ids}
    for m in milestones:
        entry = m.to_dict()
        entry.update(_progress_from_counts(counts.get(m.id, {})))
        out.setdefault(m.project_id, []).append(entry)
    return out


async def get_project_milestone_summary(user_id: int, project_id: int) -> list[dict]:
    """Ordered milestones with progress — the one-project view of
    get_project_milestone_summaries (two queries, not N+1)."""
    return (await get_project_milestone_summaries(user_id, [project_id])).get(project_id, [])
