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

from sqlalchemy import func, select

from scribe.models import async_session
from scribe.models.note import Note
from scribe.models.project import Project, ProjectStatus

logger = logging.getLogger(__name__)


def _validate_status(status: str) -> str:
    """Coerce/validate a project status against ProjectStatus.

    Canonical gate so the MCP create/update_project path (which passes status
    straight through) can't persist an out-of-enum value — there's no DB CHECK.
    """
    try:
        return ProjectStatus(status).value
    except ValueError:
        raise ValueError(
            f"Invalid status: {status!r}. Must be one of: {[s.value for s in ProjectStatus]}"
        )


async def create_project(
    user_id: int,
    title: str,
    description: str = "",
    goal: str = "",
    color: str | None = None,
    status: str = "active",
) -> Project:
    status = _validate_status(status)
    async with async_session() as session:
        project = Project(
            user_id=user_id,
            title=title,
            description=description,
            goal=goal,
            color=color,
            status=status,
        )
        session.add(project)
        await session.commit()
        await session.refresh(project)
        return project


async def get_project(user_id: int, project_id: int) -> Project | None:
    async with async_session() as session:
        result = await session.execute(
            select(Project).where(
                Project.id == project_id, Project.user_id == user_id,
                Project.deleted_at.is_(None),
            )
        )
        return result.scalars().first()


async def get_project_by_title(user_id: int, title: str) -> Project | None:
    async with async_session() as session:
        result = await session.execute(
            select(Project).where(
                Project.user_id == user_id,
                func.lower(Project.title) == func.lower(title.strip()),
                Project.deleted_at.is_(None),
            ).limit(1)
        )
        return result.scalars().first()


async def get_or_create_project(user_id: int, title: str) -> Project:
    project = await get_project_by_title(user_id, title)
    if project:
        return project
    return await create_project(user_id, title=title)


async def list_projects(user_id: int, status: str | None = None) -> list[Project]:
    async with async_session() as session:
        query = select(Project).where(
            Project.user_id == user_id, Project.deleted_at.is_(None)
        )
        if status:
            query = query.where(Project.status == status)
        query = query.order_by(Project.updated_at.desc())
        result = await session.execute(query)
        return list(result.scalars().all())


async def update_project(user_id: int, project_id: int, **fields: object) -> Project | None:
    async with async_session() as session:
        result = await session.execute(
            select(Project).where(
                Project.id == project_id, Project.user_id == user_id,
                Project.deleted_at.is_(None),
            )
        )
        project = result.scalars().first()
        if project is None:
            return None
        if "status" in fields and fields["status"] is not None:
            fields["status"] = _validate_status(fields["status"])
        for key, value in fields.items():
            if hasattr(project, key):
                setattr(project, key, value)
        project.updated_at = datetime.now(timezone.utc)
        await session.commit()
        await session.refresh(project)
    return project


async def delete_project(user_id: int, project_id: int) -> bool:
    async with async_session() as session:
        result = await session.execute(
            select(Project).where(Project.id == project_id, Project.user_id == user_id)
        )
        project = result.scalars().first()
        if project is None:
            return False
        # Unlink notes (cascade handled by DB ON DELETE SET NULL, but we delete project here)
        await session.delete(project)
        await session.commit()
        return True


async def get_project_summaries(
    user_id: int, project_ids: list[int]
) -> dict[int, dict]:
    """Summaries for MANY projects — four queries and one session, total.

    Replaces an `asyncio.gather` over the per-project version below, which was
    a nested fan-out: each project opened its own session for three queries,
    then called the milestone summary, which opened one more per milestone. For
    25 projects that asked for roughly 250 pooled connections at once against a
    pool of 15 (SQLAlchemy's default 5 + 10 overflow), so most of them sat out
    the 30-second checkout timeout and everything else on the instance queued
    behind them — including unrelated routes, which is why /api/settings
    returned 500 while /api/projects took 30.9s (#2384).

    The comment it replaced said "one backend pass instead of N+1 frontend
    calls". It did remove the N+1 from the network — and recreated it against
    the connection pool, where it is worse: the browser had at least been
    serialising those calls.
    """
    if not project_ids:
        return {}

    async with async_session() as session:
        task_rows = await session.execute(
            select(Note.project_id, Note.status, func.count(Note.id))
            .where(
                Note.user_id == user_id,
                Note.project_id.in_(project_ids),
                Note.status.isnot(None),
                Note.deleted_at.is_(None),
            )
            .group_by(Note.project_id, Note.status)
        )
        task_counts: dict[int, dict[str, int]] = {}
        for project_id, status, count in task_rows.fetchall():
            task_counts.setdefault(project_id, {})[status] = count

        note_rows = await session.execute(
            select(Note.project_id, func.count(Note.id))
            .where(
                Note.user_id == user_id,
                Note.project_id.in_(project_ids),
                Note.status.is_(None),
                Note.deleted_at.is_(None),
            )
            .group_by(Note.project_id)
        )
        note_counts = {pid: count for pid, count in note_rows.fetchall()}

        # Deliberately NOT filtered by deleted_at, matching the per-project
        # version: "last activity" includes trashing something.
        activity_rows = await session.execute(
            select(Note.project_id, func.max(Note.updated_at))
            .where(Note.user_id == user_id, Note.project_id.in_(project_ids))
            .group_by(Note.project_id)
        )
        last_activity = {pid: ts for pid, ts in activity_rows.fetchall()}

    from scribe.services.milestones import get_project_milestone_summaries
    milestones = await get_project_milestone_summaries(user_id, project_ids)

    return {
        pid: {
            # All three lifecycle keys present so consumers can sum without
            # `?? 0` guards — the frontend declares them required, and
            # `undefined + N` renders as NaN.
            "task_counts": {
                "todo": 0, "in_progress": 0, "done": 0,
                **task_counts.get(pid, {}),
            },
            "note_count": note_counts.get(pid, 0),
            "last_activity": (
                last_activity[pid].isoformat() if last_activity.get(pid) else None
            ),
            "milestone_summary": milestones.get(pid, []),
        }
        for pid in project_ids
    }


async def get_project_summary(user_id: int, project_id: int) -> dict:
    """Return task counts by status, note count, and last activity — the
    one-project view of get_project_summaries (one rule, not two copies)."""
    return (await get_project_summaries(user_id, [project_id]))[project_id]


# ---------------------------------------------------------------------------
# Shared-access variants (honour ProjectShare in addition to ownership)
# ---------------------------------------------------------------------------

async def get_project_for_user(
    accessing_user_id: int, project_id: int
) -> tuple[Project, str] | None:
    """Returns (project, permission) if user has any access, else None."""
    from scribe.services.access import get_project_permission
    perm = await get_project_permission(accessing_user_id, project_id)
    if perm is None:
        return None
    async with async_session() as session:
        project = await session.get(Project, project_id)
    return (project, perm) if project else None


async def list_projects_for_user(user_id: int, status: str | None = None) -> list[dict]:
    """Owned projects + shared projects, each dict has 'permission' field."""
    from scribe.models.group import GroupMembership
    from scribe.models.share import ProjectShare
    owned = await list_projects(user_id, status)
    owned_ids = {p.id for p in owned}

    result = []
    for p in owned:
        d = p.to_dict() if hasattr(p, "to_dict") else {"id": p.id, "title": p.title}
        d["permission"] = "owner"
        result.append(d)

    async with async_session() as session:
        user_group_ids = (await session.execute(
            select(GroupMembership.group_id).where(GroupMembership.user_id == user_id)
        )).scalars().all()

        shared_direct = (await session.execute(
            select(ProjectShare).where(ProjectShare.shared_with_user_id == user_id)
        )).scalars().all()

        shared_group: list[ProjectShare] = []
        if user_group_ids:
            shared_group = (await session.execute(
                select(ProjectShare).where(
                    ProjectShare.shared_with_group_id.in_(user_group_ids)
                )
            )).scalars().all()

    from scribe.services.sharing import best_permission_by
    seen = {
        pid: perm
        for pid, perm in best_permission_by(
            list(shared_direct) + list(shared_group), "project_id"
        ).items()
        if pid not in owned_ids
    }

    for pid, perm in seen.items():
        if status:
            async with async_session() as session:
                proj = await session.get(Project, pid)
            if not proj or proj.status != status:
                continue
        else:
            async with async_session() as session:
                proj = await session.get(Project, pid)
        if proj:
            d = proj.to_dict() if hasattr(proj, "to_dict") else {"id": proj.id, "title": proj.title}
            d["permission"] = perm
            d["is_shared"] = True
            result.append(d)

    return result
