# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

# Python imports
import json

# Django imports
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
from django.db.models import Q, Value, UUIDField
from django.db.models.functions import Coalesce
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField

# Third party imports
from rest_framework import status
from rest_framework.response import Response
from drf_spectacular.utils import OpenApiResponse, OpenApiRequest

# Module imports
from plane.api.serializers import (
    IntakeIssueSerializer,
    IssueSerializer,
    IntakeIssueCreateSerializer,
    IntakeIssueUpdateSerializer,
)
from plane.app.permissions import ProjectLitePermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State, StateGroup
from plane.utils.host import base_host
from .base import BaseAPIView
from plane.db.models.intake import SourceType
from plane.utils.openapi import (
    intake_docs,
    WORKSPACE_SLUG_PARAMETER,
    PROJECT_ID_PARAMETER,
    ISSUE_ID_PARAMETER,
    CURSOR_PARAMETER,
    PER_PAGE_PARAMETER,
    FIELDS_PARAMETER,
    EXPAND_PARAMETER,
    create_paginated_response,
    # Request Examples
    INTAKE_ISSUE_CREATE_EXAMPLE,
    INTAKE_ISSUE_UPDATE_EXAMPLE,
    # Response Examples
    INTAKE_ISSUE_EXAMPLE,
    INVALID_REQUEST_RESPONSE,
    DELETED_RESPONSE,
)


class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
    """Intake Work Item List and Create Endpoint"""

    serializer_class = IntakeIssueSerializer

    model = Intake
    permission_classes = [ProjectLitePermission]
    use_read_replica = True

    def get_queryset(self):
        intake = Intake.objects.filter(
            workspace__slug=self.kwargs.get("slug"),
            project_id=self.kwargs.get("project_id"),
        ).first()

        project = Project.objects.get(workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id"))

        if intake is None or not project.intake_view:
            return IntakeIssue.objects.none()

        return (
            IntakeIssue.objects.filter(
                Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
                workspace__slug=self.kwargs.get("slug"),
                project_id=self.kwargs.get("project_id"),
                intake_id=intake.id,
            )
            .select_related("issue", "workspace", "project")
            .order_by(self.kwargs.get("order_by", "-created_at"))
        )

    @intake_docs(
        operation_id="get_intake_work_items_list",
        summary="List intake work items",
        description="Retrieve all work items in the project's intake queue. Returns paginated results when listing all intake work items.",  # noqa: E501
        parameters=[
            WORKSPACE_SLUG_PARAMETER,
            PROJECT_ID_PARAMETER,
            CURSOR_PARAMETER,
            PER_PAGE_PARAMETER,
            FIELDS_PARAMETER,
            EXPAND_PARAMETER,
        ],
        responses={
            200: create_paginated_response(
                IntakeIssueSerializer,
                "PaginatedIntakeIssueResponse",
                "Paginated list of intake work items",
                "Paginated Intake Work Items",
            ),
        },
    )
    def get(self, request, slug, project_id):
        """List intake work items

        Retrieve all work items in the project's intake queue.
        Returns paginated results when listing all intake work items.
        """
        issue_queryset = self.get_queryset()
        return self.paginate(
            request=request,
            queryset=(issue_queryset),
            on_results=lambda intake_issues: IntakeIssueSerializer(
                intake_issues, many=True, fields=self.fields, expand=self.expand
            ).data,
        )

    @intake_docs(
        operation_id="create_intake_work_item",
        summary="Create intake work item",
        description="Submit a new work item to the project's intake queue for review and triage. Automatically creates the work item with default triage state and tracks activity.",  # noqa: E501
        parameters=[
            WORKSPACE_SLUG_PARAMETER,
            PROJECT_ID_PARAMETER,
        ],
        request=OpenApiRequest(
            request=IntakeIssueCreateSerializer,
            examples=[INTAKE_ISSUE_CREATE_EXAMPLE],
        ),
        responses={
            201: OpenApiResponse(
                description="Intake work item created",
                response=IntakeIssueSerializer,
                examples=[INTAKE_ISSUE_EXAMPLE],
            ),
            400: INVALID_REQUEST_RESPONSE,
        },
    )
    def post(self, request, slug, project_id):
        """Create intake work item

        Submit a new work item to the project's intake queue for review and triage.
        Automatically creates the work item with default triage state and tracks activity.
        """
        if not request.data.get("issue", {}).get("name", False):
            return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)

        intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()

        project = Project.objects.get(workspace__slug=slug, pk=project_id)

        # Intake view
        if intake is None and not project.intake_view:
            return Response(
                {"error": "Intake is not enabled for this project enable it through the project's api"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Check for valid priority
        if request.data.get("issue", {}).get("priority", "none") not in [
            "low",
            "medium",
            "high",
            "urgent",
            "none",
        ]:
            return Response({"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST)

        # get the triage state
        triage_state = State.triage_objects.filter(project_id=project_id, workspace__slug=slug).first()

        if not triage_state:
            triage_state = State.objects.create(
                name="Triage",
                group=StateGroup.TRIAGE.value,
                project_id=project_id,
                workspace_id=project.workspace_id,
                color="#4E5355",
                sequence=65000,
                default=False,
            )

        # create an issue
        issue_data = request.data.get("issue", {})
        # Accept both "description" and "description_json" keys for the description_json field
        description_json = issue_data.get("description") or issue_data.get("description_json") or {}
        issue = Issue.objects.create(
            name=issue_data.get("name"),
            description_json=description_json,
            description_html=issue_data.get("description_html", "<p></p>"),
            priority=issue_data.get("priority", "none"),
            project_id=project_id,
            state_id=triage_state.id,
        )

        # create an intake issue
        intake_issue = IntakeIssue.objects.create(
            intake_id=intake.id,
            project_id=project_id,
            issue=issue,
            source=SourceType.IN_APP,
        )
        # Create an Issue Activity
        issue_activity.delay(
            type="issue.activity.created",
            requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
            actor_id=str(request.user.id),
            issue_id=str(issue.id),
            project_id=str(project_id),
            current_instance=None,
            epoch=int(timezone.now().timestamp()),
            intake=str(intake_issue.id),
        )

        serializer = IntakeIssueSerializer(intake_issue)
        return Response(serializer.data, status=status.HTTP_201_CREATED)


class IntakeIssueDetailAPIEndpoint(BaseAPIView):
    """Intake Issue API Endpoint"""

    permission_classes = [ProjectLitePermission]

    serializer_class = IntakeIssueSerializer
    model = IntakeIssue
    use_read_replica = True

    filterset_fields = ["status"]

    def get_queryset(self):
        intake = Intake.objects.filter(
            workspace__slug=self.kwargs.get("slug"),
            project_id=self.kwargs.get("project_id"),
        ).first()

        project = Project.objects.get(workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id"))

        if intake is None or not project.intake_view:
            return IntakeIssue.objects.none()

        return (
            IntakeIssue.objects.filter(
                Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
                workspace__slug=self.kwargs.get("slug"),
                project_id=self.kwargs.get("project_id"),
                intake_id=intake.id,
            )
            .select_related("issue", "workspace", "project")
            .order_by(self.kwargs.get("order_by", "-created_at"))
        )

    @intake_docs(
        operation_id="retrieve_intake_work_item",
        summary="Retrieve intake work item",
        description="Retrieve details of a specific intake work item.",
        parameters=[
            WORKSPACE_SLUG_PARAMETER,
            PROJECT_ID_PARAMETER,
            ISSUE_ID_PARAMETER,
        ],
        responses={
            200: OpenApiResponse(
                description="Intake work item",
                response=IntakeIssueSerializer,
                examples=[INTAKE_ISSUE_EXAMPLE],
            ),
        },
    )
    def get(self, request, slug, project_id, issue_id):
        """Retrieve intake work item

        Retrieve details of a specific intake work item.
        """
        intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
        intake_issue_data = IntakeIssueSerializer(intake_issue_queryset, fields=self.fields, expand=self.expand).data
        return Response(intake_issue_data, status=status.HTTP_200_OK)

    @intake_docs(
        operation_id="update_intake_work_item",
        summary="Update intake work item",
        description="Modify an existing intake work item's properties or status for triage processing. Supports status changes like accept, reject, or mark as duplicate.",  # noqa: E501
        parameters=[
            WORKSPACE_SLUG_PARAMETER,
            PROJECT_ID_PARAMETER,
            ISSUE_ID_PARAMETER,
        ],
        request=OpenApiRequest(
            request=IntakeIssueUpdateSerializer,
            examples=[INTAKE_ISSUE_UPDATE_EXAMPLE],
        ),
        responses={
            200: OpenApiResponse(
                description="Intake work item updated",
                response=IntakeIssueSerializer,
                examples=[INTAKE_ISSUE_EXAMPLE],
            ),
            400: INVALID_REQUEST_RESPONSE,
        },
    )
    def patch(self, request, slug, project_id, issue_id):
        """Update intake work item

        Modify an existing intake work item's properties or status for triage processing.
        Supports status changes like accept, reject, or mark as duplicate.
        """
        intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()

        project = Project.objects.get(workspace__slug=slug, pk=project_id)

        # Intake view
        if intake is None and not project.intake_view:
            return Response(
                {"error": "Intake is not enabled for this project enable it through the project's api"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Get the intake issue
        intake_issue = IntakeIssue.objects.get(
            issue_id=issue_id,
            workspace__slug=slug,
            project_id=project_id,
            intake_id=intake.id,
        )

        # Get the project member
        project_member = ProjectMember.objects.get(
            workspace__slug=slug,
            project_id=project_id,
            member=request.user,
            is_active=True,
        )

        # Only project members admins and created_by users can access this endpoint
        if project_member.role <= 5 and str(intake_issue.created_by_id) != str(request.user.id):
            return Response(
                {"error": "You cannot edit intake work items"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Get issue data
        issue_data = request.data.pop("issue", False)
        issue_serializer = None
        intake_serializer = None

        # Validate issue data if provided
        if bool(issue_data):
            issue = Issue.objects.annotate(
                label_ids=Coalesce(
                    ArrayAgg(
                        "labels__id",
                        distinct=True,
                        filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
                    ),
                    Value([], output_field=ArrayField(UUIDField())),
                ),
                assignee_ids=Coalesce(
                    ArrayAgg(
                        "assignees__id",
                        distinct=True,
                        filter=Q(
                            ~Q(assignees__id__isnull=True)
                            & Q(assignees__member_project__is_active=True)
                            & Q(issue_assignee__deleted_at__isnull=True)
                        ),
                    ),
                    Value([], output_field=ArrayField(UUIDField())),
                ),
            ).get(pk=issue_id, workspace__slug=slug, project_id=project_id)

            # Only allow guests to edit name and description
            if project_member.role <= 5:
                description_json = issue_data.get("description") or issue_data.get("description_json") or {}
                issue_data = {
                    "name": issue_data.get("name", issue.name),
                    "description_html": issue_data.get("description_html", issue.description_html),
                    "description_json": description_json,
                }

            issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)

            if not issue_serializer.is_valid():
                return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        # Only project admins and members can edit intake issue attributes
        if project_member.role > 15:
            intake_serializer = IntakeIssueUpdateSerializer(intake_issue, data=request.data, partial=True)

            if not intake_serializer.is_valid():
                return Response(intake_serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        # Both serializers are valid, now save them
        if issue_serializer:
            current_instance = issue
            # Log all the updates
            requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
            issue_activity.delay(
                type="issue.activity.updated",
                requested_data=requested_data,
                actor_id=str(request.user.id),
                issue_id=str(issue_id),
                project_id=str(project_id),
                current_instance=json.dumps(
                    IssueSerializer(current_instance).data,
                    cls=DjangoJSONEncoder,
                ),
                epoch=int(timezone.now().timestamp()),
                intake=str(intake_issue.id),
            )
            issue_serializer.save()

        # Save intake issue (state transition happens in serializer's update method)
        if intake_serializer:
            current_instance = json.dumps(IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder)
            intake_serializer.save()

            # create a activity for status change
            issue_activity.delay(
                type="intake.activity.created",
                requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
                actor_id=str(request.user.id),
                issue_id=str(issue_id),
                project_id=str(project_id),
                current_instance=current_instance,
                epoch=int(timezone.now().timestamp()),
                notification=False,
                origin=base_host(request=request, is_app=True),
                intake=str(intake_issue.id),
            )
            return Response(IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK)
        else:
            return Response(IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK)

    @intake_docs(
        operation_id="delete_intake_work_item",
        summary="Delete intake work item",
        description="Permanently remove an intake work item from the triage queue. Also deletes the underlying work item if it hasn't been accepted yet.",  # noqa: E501
        parameters=[
            WORKSPACE_SLUG_PARAMETER,
            PROJECT_ID_PARAMETER,
            ISSUE_ID_PARAMETER,
        ],
        responses={
            204: DELETED_RESPONSE,
        },
    )
    def delete(self, request, slug, project_id, issue_id):
        """Delete intake work item

        Permanently remove an intake work item from the triage queue.
        Also deletes the underlying work item if it hasn't been accepted yet.
        """
        intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()

        project = Project.objects.get(workspace__slug=slug, pk=project_id)

        # Intake view
        if intake is None and not project.intake_view:
            return Response(
                {"error": "Intake is not enabled for this project enable it through the project's api"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # Get the intake issue
        intake_issue = IntakeIssue.objects.get(
            issue_id=issue_id,
            workspace__slug=slug,
            project_id=project_id,
            intake_id=intake.id,
        )

        # Check the issue status
        if intake_issue.status in [-2, -1, 0, 2]:
            # Delete the issue also
            issue = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=issue_id).first()
            if issue.created_by_id != request.user.id and (
                not ProjectMember.objects.filter(
                    workspace__slug=slug,
                    member=request.user,
                    role=20,
                    project_id=project_id,
                    is_active=True,
                ).exists()
            ):
                return Response(
                    {"error": "Only admin or creator can delete the work item"},
                    status=status.HTTP_403_FORBIDDEN,
                )
            issue.delete()

        intake_issue.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
