"""Project management mixin for gRPC service."""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, cast
from uuid import UUID

from noteflow.config.constants import (
    ERROR_PROJECT_ID_REQUIRED,
    ERROR_WORKSPACE_ID_REQUIRED,
)
from noteflow.domain.errors import CannotArchiveDefaultProjectError
from noteflow.infrastructure.logging import get_logger

from ...proto import noteflow_pb2
from ..errors import (
    ENTITY_PROJECT,
    abort_failed_precondition,
    abort_invalid_argument,
    abort_not_found,
    parse_project_id,
    parse_workspace_id,
    require_feature_projects,
    require_feature_workspaces,
    require_project_service,
)
from ._converters import (
    project_to_proto,
    proto_to_project_settings,
)

if TYPE_CHECKING:
    from .._types import GrpcContext
    from ._types import ProjectServicer

logger = get_logger(__name__)


class _HasField(Protocol):
    def HasField(self, field_name: str) -> bool: ...


class ProjectMixin:
    """Mixin providing project management functionality.

    Require host to implement ProjectServicer protocol.
    Provide CRUD operations for projects and project memberships.
    """

    # -------------------------------------------------------------------------
    # Project CRUD
    # -------------------------------------------------------------------------

    async def CreateProject(
        self: ProjectServicer,
        request: noteflow_pb2.CreateProjectRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ProjectProto:
        """Create a new project in a workspace."""
        project_service = await require_project_service(self.project_service, context)

        if not request.workspace_id:
            await abort_invalid_argument(context, ERROR_WORKSPACE_ID_REQUIRED)

        if not request.name:
            await abort_invalid_argument(context, "name is required")

        workspace_id = await parse_workspace_id(request.workspace_id, context)

        slug = request.slug if cast(_HasField, request).HasField("slug") else None
        description = (
            request.description
            if cast(_HasField, request).HasField("description")
            else None
        )
        settings = (
            proto_to_project_settings(request.settings)
            if cast(_HasField, request).HasField("settings")
            else None
        )

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)

            project = await project_service.create_project(
                uow=uow,
                workspace_id=workspace_id,
                name=request.name,
                slug=slug,
                description=description,
                settings=settings,
            )
            return project_to_proto(project)

    async def GetProject(
        self: ProjectServicer,
        request: noteflow_pb2.GetProjectRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ProjectProto:
        """Get a project by ID."""
        project_service = await require_project_service(self.project_service, context)

        if not request.project_id:
            await abort_invalid_argument(context, ERROR_PROJECT_ID_REQUIRED)

        project_id = await parse_project_id(request.project_id, context)

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)

            project = await project_service.get_project(uow, project_id)
            if project is None:
                await abort_not_found(context, ENTITY_PROJECT, request.project_id)
                raise  # Unreachable but helps type checker

            return project_to_proto(project)

    async def GetProjectBySlug(
        self: ProjectServicer,
        request: noteflow_pb2.GetProjectBySlugRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ProjectProto:
        """Get a project by workspace and slug."""
        project_service = await require_project_service(self.project_service, context)

        if not request.workspace_id:
            await abort_invalid_argument(context, ERROR_WORKSPACE_ID_REQUIRED)
        if not request.slug:
            await abort_invalid_argument(context, "slug is required")

        workspace_id = await parse_workspace_id(request.workspace_id, context)

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)

            project = await project_service.get_project_by_slug(uow, workspace_id, request.slug)
            if project is None:
                await abort_not_found(context, ENTITY_PROJECT, request.slug)
                raise  # Unreachable but helps type checker

            return project_to_proto(project)

    async def ListProjects(
        self: ProjectServicer,
        request: noteflow_pb2.ListProjectsRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ListProjectsResponse:
        """List projects in a workspace."""
        project_service = await require_project_service(self.project_service, context)

        if not request.workspace_id:
            await abort_invalid_argument(context, ERROR_WORKSPACE_ID_REQUIRED)

        workspace_id = await parse_workspace_id(request.workspace_id, context)

        limit = request.limit if request.limit > 0 else 50
        offset = max(request.offset, 0)

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)

            projects = await project_service.list_projects(
                uow=uow,
                workspace_id=workspace_id,
                include_archived=request.include_archived,
                limit=limit,
                offset=offset,
            )

            total_count = await project_service.count_projects(
                uow=uow,
                workspace_id=workspace_id,
                include_archived=request.include_archived,
            )

            return noteflow_pb2.ListProjectsResponse(
                projects=[project_to_proto(p) for p in projects],
                total_count=total_count,
            )

    async def UpdateProject(
        self: ProjectServicer,
        request: noteflow_pb2.UpdateProjectRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ProjectProto:
        """Update a project."""
        project_service = await require_project_service(self.project_service, context)

        if not request.project_id:
            await abort_invalid_argument(context, ERROR_PROJECT_ID_REQUIRED)

        project_id = await parse_project_id(request.project_id, context)

        name = request.name if cast(_HasField, request).HasField("name") else None
        slug = request.slug if cast(_HasField, request).HasField("slug") else None
        description = (
            request.description
            if cast(_HasField, request).HasField("description")
            else None
        )
        settings = (
            proto_to_project_settings(request.settings)
            if cast(_HasField, request).HasField("settings")
            else None
        )

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)

            project = await project_service.update_project(
                uow=uow,
                project_id=project_id,
                name=name,
                slug=slug,
                description=description,
                settings=settings,
            )
            if project is None:
                await abort_not_found(context, ENTITY_PROJECT, request.project_id)
                raise  # Unreachable but helps type checker

            return project_to_proto(project)

    async def ArchiveProject(
        self: ProjectServicer,
        request: noteflow_pb2.ArchiveProjectRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ProjectProto:
        """Archive a project."""
        project_service = await require_project_service(self.project_service, context)

        if not request.project_id:
            await abort_invalid_argument(context, ERROR_PROJECT_ID_REQUIRED)

        project_id = await parse_project_id(request.project_id, context)

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)

            try:
                project = await project_service.archive_project(uow, project_id)
            except CannotArchiveDefaultProjectError:
                await abort_failed_precondition(context, "Cannot archive the default project")
                raise  # Unreachable but helps type checker

            if project is None:
                await abort_not_found(context, ENTITY_PROJECT, request.project_id)
                raise  # Unreachable but helps type checker

            return project_to_proto(project)

    async def RestoreProject(
        self: ProjectServicer,
        request: noteflow_pb2.RestoreProjectRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.ProjectProto:
        """Restore an archived project."""
        project_service = await require_project_service(self.project_service, context)

        if not request.project_id:
            await abort_invalid_argument(context, ERROR_PROJECT_ID_REQUIRED)

        project_id = await parse_project_id(request.project_id, context)

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)

            project = await project_service.restore_project(uow, project_id)
            if project is None:
                await abort_not_found(context, ENTITY_PROJECT, request.project_id)
                raise  # Unreachable but helps type checker

            return project_to_proto(project)

    async def DeleteProject(
        self: ProjectServicer,
        request: noteflow_pb2.DeleteProjectRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.DeleteProjectResponse:
        """Delete a project permanently."""
        project_service = await require_project_service(self.project_service, context)

        if not request.project_id:
            await abort_invalid_argument(context, ERROR_PROJECT_ID_REQUIRED)

        project_id = await parse_project_id(request.project_id, context)

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)

            deleted = await project_service.delete_project(uow, project_id)
            return noteflow_pb2.DeleteProjectResponse(success=deleted)

    # -------------------------------------------------------------------------
    # Active Project
    # -------------------------------------------------------------------------

    async def SetActiveProject(
        self: ProjectServicer,
        request: noteflow_pb2.SetActiveProjectRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.SetActiveProjectResponse:
        """Set the active project for a workspace."""
        project_service = await require_project_service(self.project_service, context)

        if not request.workspace_id:
            await abort_invalid_argument(context, ERROR_WORKSPACE_ID_REQUIRED)

        workspace_id = await parse_workspace_id(request.workspace_id, context)

        project_id: UUID | None = None
        if request.project_id:
            project_id = await parse_project_id(request.project_id, context)

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)
            await require_feature_workspaces(uow, context)

            try:
                await project_service.set_active_project(
                    uow=uow,
                    workspace_id=workspace_id,
                    project_id=project_id,
                )
            except ValueError as exc:
                await abort_invalid_argument(context, str(exc))

            await uow.commit()

            return noteflow_pb2.SetActiveProjectResponse()

    async def GetActiveProject(
        self: ProjectServicer,
        request: noteflow_pb2.GetActiveProjectRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.GetActiveProjectResponse:
        """Get the active project for a workspace."""
        project_service = await require_project_service(self.project_service, context)

        if not request.workspace_id:
            await abort_invalid_argument(context, ERROR_WORKSPACE_ID_REQUIRED)

        workspace_id = await parse_workspace_id(request.workspace_id, context)

        async with self.create_repository_provider() as uow:
            await require_feature_projects(uow, context)
            await require_feature_workspaces(uow, context)

            try:
                active_id, project = await project_service.get_active_project(
                    uow=uow,
                    workspace_id=workspace_id,
                )
            except ValueError as exc:
                await abort_invalid_argument(context, str(exc))
                raise  # Unreachable but helps type checker

            if project is None:
                await abort_not_found(context, ENTITY_PROJECT, "default")
                raise  # Unreachable but helps type checker

            response = noteflow_pb2.GetActiveProjectResponse(
                project=project_to_proto(project),
            )
            if active_id is not None:
                response.project_id = str(active_id)

            return response
