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

from collections import defaultdict
from typing import Any, Dict, List, Optional

from django.db.models import F, QuerySet

from plane.db.models import CycleIssue, FileAsset

from .base import (
    DateField,
    DateTimeField,
    ExportSchema,
    JSONField,
    ListField,
    NumberField,
    StringField,
)


def get_issue_attachments_dict(issues_queryset: QuerySet) -> Dict[str, List[str]]:
    """Get attachments dictionary for the given issues queryset.

    Args:
        issues_queryset: Queryset of Issue objects

    Returns:
        Dictionary mapping issue IDs to lists of attachment IDs
    """
    file_assets = FileAsset.objects.filter(
        issue_id__in=issues_queryset.values_list("id", flat=True),
        entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
    ).annotate(work_item_id=F("issue_id"), asset_id=F("id"))

    attachment_dict = defaultdict(list)
    for asset in file_assets:
        attachment_dict[asset.work_item_id].append(asset.asset_id)

    return attachment_dict


def get_issue_last_cycles_dict(issues_queryset: QuerySet) -> Dict[str, Optional[CycleIssue]]:
    """Get the last cycle for each issue in the given queryset.

    Args:
        issues_queryset: Queryset of Issue objects

    Returns:
        Dictionary mapping issue IDs to their last CycleIssue object
    """
    # Fetch all cycle issues for the given issues, ordered by created_at descending
    # select_related is used to fetch cycle data in the same query
    cycle_issues = (
        CycleIssue.objects.filter(issue_id__in=issues_queryset.values_list("id", flat=True))
        .select_related("cycle")
        .order_by("issue_id", "-created_at")
    )

    # Keep only the last (most recent) cycle for each issue
    last_cycles_dict = {}
    for cycle_issue in cycle_issues:
        if cycle_issue.issue_id not in last_cycles_dict:
            last_cycles_dict[cycle_issue.issue_id] = cycle_issue

    return last_cycles_dict


class IssueExportSchema(ExportSchema):
    """Schema for exporting issue data in various formats."""

    @staticmethod
    def _get_created_by(obj) -> str:
        """Get the created by user for the given object."""
        try:
            if getattr(obj, "created_by", None):
                return f"{obj.created_by.first_name} {obj.created_by.last_name}"
        except Exception:
            pass
        return ""

    @staticmethod
    def _format_date(date_obj) -> str:
        """Format date object to string."""
        if date_obj and hasattr(date_obj, "strftime"):
            return date_obj.strftime("%a, %d %b %Y")
        return ""

    # Field definitions with display labels
    id = StringField(label="ID")
    project_identifier = StringField(source="project.identifier", label="Project Identifier")
    project_name = StringField(source="project.name", label="Project")
    project_id = StringField(source="project.id", label="Project ID")
    sequence_id = NumberField(source="sequence_id", label="Sequence ID")
    name = StringField(source="name", label="Name")
    description = StringField(source="description_stripped", label="Description")
    priority = StringField(source="priority", label="Priority")
    start_date = DateField(source="start_date", label="Start Date")
    target_date = DateField(source="target_date", label="Target Date")
    state_name = StringField(label="State")
    created_at = DateTimeField(source="created_at", label="Created At")
    updated_at = DateTimeField(source="updated_at", label="Updated At")
    completed_at = DateTimeField(source="completed_at", label="Completed At")
    archived_at = DateTimeField(source="archived_at", label="Archived At")
    module_name = ListField(label="Module Name")
    created_by = StringField(label="Created By")
    labels = ListField(label="Labels")
    comments = JSONField(label="Comments")
    estimate = StringField(label="Estimate")
    link = ListField(label="Link")
    assignees = ListField(label="Assignees")
    subscribers_count = NumberField(label="Subscribers Count")
    attachment_count = NumberField(label="Attachment Count")
    attachment_links = ListField(label="Attachment Links")
    cycle_name = StringField(label="Cycle Name")
    cycle_start_date = DateField(label="Cycle Start Date")
    cycle_end_date = DateField(label="Cycle End Date")
    parent = StringField(label="Parent")
    relations = JSONField(label="Relations")

    def prepare_id(self, i):
        return f"{i.project.identifier}-{i.sequence_id}"

    def prepare_state_name(self, i):
        return i.state.name if i.state else None

    def prepare_module_name(self, i):
        return [m.module.name for m in i.issue_module.all()]

    def prepare_created_by(self, i):
        return self._get_created_by(i)

    def prepare_labels(self, i):
        return [label.name for label in i.labels.all()]

    def prepare_comments(self, i):
        return [
            {
                "comment": comment.comment_stripped,
                "created_at": self._format_date(comment.created_at),
                "created_by": self._get_created_by(comment),
            }
            for comment in i.issue_comments.all()
        ]

    def prepare_estimate(self, i):
        return i.estimate_point.value if i.estimate_point and i.estimate_point.value else ""

    def prepare_link(self, i):
        return [link.url for link in i.issue_link.all()]

    def prepare_assignees(self, i):
        return [f"{u.first_name} {u.last_name}" for u in i.assignees.all()]

    def prepare_subscribers_count(self, i):
        return i.issue_subscribers.count()

    def prepare_attachment_count(self, i):
        return len((self.context.get("attachments_dict") or {}).get(i.id, []))

    def prepare_attachment_links(self, i):
        return [
            f"/api/assets/v2/workspaces/{i.workspace.slug}/projects/{i.project_id}/issues/{i.id}/attachments/{asset}/"
            for asset in (self.context.get("attachments_dict") or {}).get(i.id, [])
        ]

    def prepare_cycle_name(self, i):
        cycles_dict = self.context.get("cycles_dict") or {}
        last_cycle = cycles_dict.get(i.id)
        return last_cycle.cycle.name if last_cycle else ""

    def prepare_cycle_start_date(self, i):
        cycles_dict = self.context.get("cycles_dict") or {}
        last_cycle = cycles_dict.get(i.id)
        if last_cycle and last_cycle.cycle.start_date:
            return self._format_date(last_cycle.cycle.start_date)
        return ""

    def prepare_cycle_end_date(self, i):
        cycles_dict = self.context.get("cycles_dict") or {}
        last_cycle = cycles_dict.get(i.id)
        if last_cycle and last_cycle.cycle.end_date:
            return self._format_date(last_cycle.cycle.end_date)
        return ""

    def prepare_parent(self, i):
        if not i.parent:
            return ""
        return f"{i.parent.project.identifier}-{i.parent.sequence_id}"

    def prepare_relations(self, i):
        # Should show reverse relation as well
        from plane.db.models.issue import IssueRelationChoices

        relations = {
            r.relation_type: f"{r.related_issue.project.identifier}-{r.related_issue.sequence_id}"
            for r in i.issue_relation.all()
        }
        reverse_relations = {}
        for relation in i.issue_related.all():
            reverse_relations[IssueRelationChoices._REVERSE_MAPPING[relation.relation_type]] = (
                f"{relation.issue.project.identifier}-{relation.issue.sequence_id}"
            )
        relations.update(reverse_relations)
        return relations

    @classmethod
    def get_context_data(cls, queryset: QuerySet) -> Dict[str, Any]:
        """Get context data for issue serialization."""
        return {
            "attachments_dict": get_issue_attachments_dict(queryset),
            "cycles_dict": get_issue_last_cycles_dict(queryset),
        }
