# app/crud/syndarix/sprint.py
"""Async CRUD operations for Sprint model using SQLAlchemy 2.0 patterns."""

import logging
from datetime import date
from typing import Any
from uuid import UUID

from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload

from app.crud.base import CRUDBase
from app.models.syndarix import Issue, Sprint
from app.models.syndarix.enums import IssueStatus, SprintStatus
from app.schemas.syndarix import SprintCreate, SprintUpdate

logger = logging.getLogger(__name__)


class CRUDSprint(CRUDBase[Sprint, SprintCreate, SprintUpdate]):
    """Async CRUD operations for Sprint model."""

    async def create(self, db: AsyncSession, *, obj_in: SprintCreate) -> Sprint:
        """Create a new sprint with error handling."""
        try:
            db_obj = Sprint(
                project_id=obj_in.project_id,
                name=obj_in.name,
                number=obj_in.number,
                goal=obj_in.goal,
                start_date=obj_in.start_date,
                end_date=obj_in.end_date,
                status=obj_in.status,
                planned_points=obj_in.planned_points,
                velocity=obj_in.velocity,
            )
            db.add(db_obj)
            await db.commit()
            await db.refresh(db_obj)
            return db_obj
        except IntegrityError as e:
            await db.rollback()
            error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
            logger.error(f"Integrity error creating sprint: {error_msg}")
            raise ValueError(f"Database integrity error: {error_msg}")
        except Exception as e:
            await db.rollback()
            logger.error(f"Unexpected error creating sprint: {e!s}", exc_info=True)
            raise

    async def get_with_details(
        self,
        db: AsyncSession,
        *,
        sprint_id: UUID,
    ) -> dict[str, Any] | None:
        """
        Get a sprint with full details including issue counts.

        Returns:
            Dictionary with sprint and related details
        """
        try:
            # Get sprint with joined project
            result = await db.execute(
                select(Sprint)
                .options(joinedload(Sprint.project))
                .where(Sprint.id == sprint_id)
            )
            sprint = result.scalar_one_or_none()

            if not sprint:
                return None

            # Get issue counts
            issue_counts = await db.execute(
                select(
                    func.count(Issue.id).label("total"),
                    func.count(Issue.id)
                    .filter(Issue.status == IssueStatus.OPEN)
                    .label("open"),
                    func.count(Issue.id)
                    .filter(Issue.status == IssueStatus.CLOSED)
                    .label("completed"),
                ).where(Issue.sprint_id == sprint_id)
            )
            counts = issue_counts.one()

            return {
                "sprint": sprint,
                "project_name": sprint.project.name if sprint.project else None,
                "project_slug": sprint.project.slug if sprint.project else None,
                "issue_count": counts.total,
                "open_issues": counts.open,
                "completed_issues": counts.completed,
            }
        except Exception as e:
            logger.error(
                f"Error getting sprint with details {sprint_id}: {e!s}", exc_info=True
            )
            raise

    async def get_by_project(
        self,
        db: AsyncSession,
        *,
        project_id: UUID,
        status: SprintStatus | None = None,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[Sprint], int]:
        """Get sprints for a specific project."""
        try:
            query = select(Sprint).where(Sprint.project_id == project_id)

            if status is not None:
                query = query.where(Sprint.status == status)

            # Get total count
            count_query = select(func.count()).select_from(query.alias())
            count_result = await db.execute(count_query)
            total = count_result.scalar_one()

            # Apply sorting (by number descending - newest first)
            query = query.order_by(Sprint.number.desc())
            query = query.offset(skip).limit(limit)
            result = await db.execute(query)
            sprints = list(result.scalars().all())

            return sprints, total
        except Exception as e:
            logger.error(
                f"Error getting sprints by project {project_id}: {e!s}", exc_info=True
            )
            raise

    async def get_active_sprint(
        self,
        db: AsyncSession,
        *,
        project_id: UUID,
    ) -> Sprint | None:
        """Get the currently active sprint for a project."""
        try:
            result = await db.execute(
                select(Sprint).where(
                    Sprint.project_id == project_id,
                    Sprint.status == SprintStatus.ACTIVE,
                )
            )
            return result.scalar_one_or_none()
        except Exception as e:
            logger.error(
                f"Error getting active sprint for project {project_id}: {e!s}",
                exc_info=True,
            )
            raise

    async def get_next_sprint_number(
        self,
        db: AsyncSession,
        *,
        project_id: UUID,
    ) -> int:
        """Get the next sprint number for a project."""
        try:
            result = await db.execute(
                select(func.max(Sprint.number)).where(Sprint.project_id == project_id)
            )
            max_number = result.scalar_one_or_none()
            return (max_number or 0) + 1
        except Exception as e:
            logger.error(
                f"Error getting next sprint number for project {project_id}: {e!s}",
                exc_info=True,
            )
            raise

    async def start_sprint(
        self,
        db: AsyncSession,
        *,
        sprint_id: UUID,
        start_date: date | None = None,
    ) -> Sprint | None:
        """Start a planned sprint.

        Uses row-level locking (SELECT FOR UPDATE) to prevent race conditions
        when multiple requests try to start sprints concurrently.
        """
        try:
            # Lock the sprint row to prevent concurrent modifications
            result = await db.execute(
                select(Sprint)
                .where(Sprint.id == sprint_id)
                .with_for_update()
            )
            sprint = result.scalar_one_or_none()

            if not sprint:
                return None

            if sprint.status != SprintStatus.PLANNED:
                raise ValueError(
                    f"Cannot start sprint with status {sprint.status.value}"
                )

            # Check for existing active sprint with lock to prevent race condition
            # Lock all sprints for this project to ensure atomic check-and-update
            active_check = await db.execute(
                select(Sprint)
                .where(
                    Sprint.project_id == sprint.project_id,
                    Sprint.status == SprintStatus.ACTIVE,
                )
                .with_for_update()
            )
            active_sprint = active_check.scalar_one_or_none()
            if active_sprint:
                raise ValueError(
                    f"Project already has an active sprint: {active_sprint.name}"
                )

            sprint.status = SprintStatus.ACTIVE
            if start_date:
                sprint.start_date = start_date

            # Calculate planned points from issues
            points_result = await db.execute(
                select(func.sum(Issue.story_points)).where(Issue.sprint_id == sprint_id)
            )
            sprint.planned_points = points_result.scalar_one_or_none() or 0

            await db.commit()
            await db.refresh(sprint)
            return sprint
        except ValueError:
            raise
        except Exception as e:
            await db.rollback()
            logger.error(f"Error starting sprint {sprint_id}: {e!s}", exc_info=True)
            raise

    async def complete_sprint(
        self,
        db: AsyncSession,
        *,
        sprint_id: UUID,
    ) -> Sprint | None:
        """Complete an active sprint and calculate completed points.

        Uses row-level locking (SELECT FOR UPDATE) to prevent race conditions
        when velocity is being calculated and other operations might modify issues.
        """
        try:
            # Lock the sprint row to prevent concurrent modifications
            result = await db.execute(
                select(Sprint)
                .where(Sprint.id == sprint_id)
                .with_for_update()
            )
            sprint = result.scalar_one_or_none()

            if not sprint:
                return None

            if sprint.status != SprintStatus.ACTIVE:
                raise ValueError(
                    f"Cannot complete sprint with status {sprint.status.value}"
                )

            sprint.status = SprintStatus.COMPLETED

            # Calculate velocity (completed points) from closed issues
            # Note: Issues are not locked, but sprint lock ensures this sprint's
            # completion is atomic and prevents concurrent completion attempts
            points_result = await db.execute(
                select(func.sum(Issue.story_points)).where(
                    Issue.sprint_id == sprint_id,
                    Issue.status == IssueStatus.CLOSED,
                )
            )
            sprint.velocity = points_result.scalar_one_or_none() or 0

            await db.commit()
            await db.refresh(sprint)
            return sprint
        except ValueError:
            raise
        except Exception as e:
            await db.rollback()
            logger.error(f"Error completing sprint {sprint_id}: {e!s}", exc_info=True)
            raise

    async def cancel_sprint(
        self,
        db: AsyncSession,
        *,
        sprint_id: UUID,
    ) -> Sprint | None:
        """Cancel a sprint (only PLANNED or ACTIVE sprints can be cancelled).

        Uses row-level locking to prevent race conditions with concurrent
        sprint status modifications.
        """
        try:
            # Lock the sprint row to prevent concurrent modifications
            result = await db.execute(
                select(Sprint)
                .where(Sprint.id == sprint_id)
                .with_for_update()
            )
            sprint = result.scalar_one_or_none()

            if not sprint:
                return None

            if sprint.status not in [SprintStatus.PLANNED, SprintStatus.ACTIVE]:
                raise ValueError(
                    f"Cannot cancel sprint with status {sprint.status.value}"
                )

            sprint.status = SprintStatus.CANCELLED
            await db.commit()
            await db.refresh(sprint)
            return sprint
        except ValueError:
            raise
        except Exception as e:
            await db.rollback()
            logger.error(f"Error cancelling sprint {sprint_id}: {e!s}", exc_info=True)
            raise

    async def get_velocity(
        self,
        db: AsyncSession,
        *,
        project_id: UUID,
        limit: int = 5,
    ) -> list[dict[str, Any]]:
        """Get velocity data for completed sprints."""
        try:
            result = await db.execute(
                select(Sprint)
                .where(
                    Sprint.project_id == project_id,
                    Sprint.status == SprintStatus.COMPLETED,
                )
                .order_by(Sprint.number.desc())
                .limit(limit)
            )
            sprints = list(result.scalars().all())

            velocity_data = []
            for sprint in reversed(sprints):  # Return in chronological order
                velocity_ratio = None
                if sprint.planned_points and sprint.planned_points > 0:
                    velocity_ratio = (sprint.velocity or 0) / sprint.planned_points
                velocity_data.append(
                    {
                        "sprint_number": sprint.number,
                        "sprint_name": sprint.name,
                        "planned_points": sprint.planned_points,
                        "velocity": sprint.velocity,
                        "velocity_ratio": velocity_ratio,
                    }
                )

            return velocity_data
        except Exception as e:
            logger.error(
                f"Error getting velocity for project {project_id}: {e!s}",
                exc_info=True,
            )
            raise

    async def get_sprints_with_issue_counts(
        self,
        db: AsyncSession,
        *,
        project_id: UUID,
        skip: int = 0,
        limit: int = 100,
    ) -> tuple[list[dict[str, Any]], int]:
        """Get sprints with issue counts in optimized queries."""
        try:
            # Get sprints
            sprints, total = await self.get_by_project(
                db, project_id=project_id, skip=skip, limit=limit
            )

            if not sprints:
                return [], 0

            sprint_ids = [s.id for s in sprints]

            # Get issue counts in bulk
            issue_counts = await db.execute(
                select(
                    Issue.sprint_id,
                    func.count(Issue.id).label("total"),
                    func.count(Issue.id)
                    .filter(Issue.status == IssueStatus.OPEN)
                    .label("open"),
                    func.count(Issue.id)
                    .filter(Issue.status == IssueStatus.CLOSED)
                    .label("completed"),
                )
                .where(Issue.sprint_id.in_(sprint_ids))
                .group_by(Issue.sprint_id)
            )
            counts_map = {
                row.sprint_id: {
                    "issue_count": row.total,
                    "open_issues": row.open,
                    "completed_issues": row.completed,
                }
                for row in issue_counts
            }

            # Combine results
            results = [
                {
                    "sprint": sprint,
                    **counts_map.get(
                        sprint.id, {"issue_count": 0, "open_issues": 0, "completed_issues": 0}
                    ),
                }
                for sprint in sprints
            ]

            return results, total
        except Exception as e:
            logger.error(
                f"Error getting sprints with counts for project {project_id}: {e!s}",
                exc_info=True,
            )
            raise


# Create a singleton instance for use across the application
sprint = CRUDSprint(Sprint)
