from collections import defaultdict

from django.conf import settings
from django.db import models
from django.utils import timezone
from django.db.models import Avg, Max, Count, Q, Sum
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, OpenApiParameter

from .models import Publication, PublicationCategory, Author, Submission, ExternalPublication, CSISPublication
from .serializers import (
    PublicationSerializer, PublicationCategorySerializer, AuthorSerializer,
    SubmissionSerializer, ExternalPublicationSerializer, CSISPublicationSerializer,
)
from apps.core.tenancy import TenantScopedMixin, get_active_organization


class CSISPublicationPagination(PageNumberPagination):
    page_size = 50
    page_size_query_param = "page_size"
    max_page_size = 500


class PublicationViewSet(TenantScopedMixin, viewsets.ModelViewSet):
    """ViewSet for Publication management."""
    rbac_domain = "publications"
    queryset = (
        Publication.objects
        .select_related("category", "author", "byline_contact")
        .prefetch_related("co_author_contacts")
    )
    serializer_class = PublicationSerializer
    pagination_class = CSISPublicationPagination

    @extend_schema(
        parameters=[
            OpenApiParameter(name="status", type=str, description="Filter by status"),
            OpenApiParameter(name="category", type=str, description="Filter by category ID"),
            OpenApiParameter(name="author", type=str, description="Filter by author ID"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

    def perform_create(self, serializer):
        extra = {"organization": get_active_organization(self.request), "author": self.request.user}
        if serializer.validated_data.get("status") == "published":
            extra["published_at"] = timezone.now()
        serializer.save(**extra)

    def perform_update(self, serializer):
        # Stamp published_at the first time a publication transitions to
        # "published" — the client doesn't (and shouldn't) set this itself.
        if serializer.validated_data.get("status") == "published" and serializer.instance.published_at is None:
            serializer.save(published_at=timezone.now())
        else:
            serializer.save()

    @action(detail=False, methods=["get"])
    def stats(self, request):
        # Published only — drafts/in-review haven't reached readers yet, so
        # they shouldn't count toward reach/engagement stats.
        qs = self.get_queryset().filter(status="published")

        overall = qs.aggregate(
            total=Count("id"),
            total_views=Sum("views"),
            avg_views=Avg("views"),
            max_views=Max("views"),
        )

        by_category = list(
            qs.values("category__id", "category__name")
              .annotate(count=Count("id"), total_views=Sum("views"), avg_views=Avg("views"), max_views=Max("views"))
              .order_by("-count")
        )

        top_publications = list(
            qs.order_by("-views")
              .values("id", "title", "views")
              .annotate(category_name=models.F("category__name"))[:10]
        )

        # Aggregate by byline author — the submitting User (author FK), which
        # is always set; byline_contact/co_author_contacts are optional.
        author_rows = defaultdict(lambda: {"publications": 0, "views": 0})
        author_meta: dict[str, dict] = {}
        for user_id, first_name, last_name, username, avatar, views in qs.values_list(
            "author__id", "author__first_name", "author__last_name", "author__username",
            "author__avatar", "views",
        ):
            name = f"{first_name} {last_name}".strip() or username
            key = str(user_id)
            author_rows[key]["publications"] += 1
            author_rows[key]["views"] += views or 0
            if key not in author_meta:
                avatar_url = request.build_absolute_uri(settings.MEDIA_URL + avatar) if avatar else None
                author_meta[key] = {"id": key, "name": name, "avatar_url": avatar_url}
        top_authors = sorted(
            (
                {**author_meta[k], **v}
                for k, v in author_rows.items()
            ),
            key=lambda r: (r["views"], r["publications"]),
            reverse=True,
        )[:10]

        # Monthly counts/views from published_at.
        monthly_buckets: dict[str, dict] = {}
        for published_at, views in qs.exclude(published_at__isnull=True).values_list("published_at", "views"):
            key = published_at.strftime("%Y-%m")
            bucket = monthly_buckets.setdefault(key, {"month": key, "count": 0, "views": 0})
            bucket["count"] += 1
            bucket["views"] += views or 0
        monthly = sorted(monthly_buckets.values(), key=lambda r: r["month"])[-12:]

        return Response({
            "overall": {
                "total": overall["total"] or 0,
                "total_views": overall["total_views"] or 0,
                "avg_views": float(overall["avg_views"] or 0),
                "max_views": overall["max_views"] or 0,
            },
            "by_category": [
                {
                    "category_id": str(row["category__id"]) if row["category__id"] else None,
                    "category_name": row["category__name"] or "Uncategorized",
                    "count": row["count"],
                    "total_views": row["total_views"] or 0,
                    "avg_views": float(row["avg_views"] or 0),
                    "max_views": row["max_views"] or 0,
                }
                for row in by_category
            ],
            "top_publications": [
                {
                    "id": str(row["id"]),
                    "title": row["title"],
                    "category_name": row["category_name"] or "Uncategorized",
                    "views": row["views"],
                }
                for row in top_publications
            ],
            "top_authors": top_authors,
            "monthly": monthly,
        })

    @action(detail=True, methods=["get"], url_path="stats", url_name="detail-stats")
    def detail_stats(self, request, pk=None):
        pub = self.get_object()
        published = self.get_queryset().filter(status="published")
        overall = published.aggregate(avg_views=Avg("views"))
        category_stats = (
            published.filter(category=pub.category).aggregate(avg_views=Avg("views"), count=Count("id"))
            if pub.category else {"avg_views": None, "count": None}
        )
        related = list(
            published.filter(category=pub.category).exclude(id=pub.id).select_related("author")
            .order_by("-views")[:5]
            .values("id", "title", "status", "views", "created_at", "author__first_name", "author__last_name", "author__username")
        )
        return Response({
            "views": pub.views,
            "overall_avg_views": float(overall["avg_views"] or 0),
            "category_avg_views": float(category_stats["avg_views"]) if category_stats["avg_views"] is not None else None,
            "category_count": category_stats["count"],
            "related": [
                {
                    "id": str(r["id"]),
                    "title": r["title"],
                    "status": r["status"],
                    "author_name": f"{r['author__first_name']} {r['author__last_name']}".strip() or r["author__username"],
                    "views": r["views"],
                    "created_at": r["created_at"].isoformat(),
                }
                for r in related
            ],
        })


class PublicationCategoryViewSet(TenantScopedMixin, viewsets.ModelViewSet):
    """ViewSet for PublicationCategory management."""
    rbac_domain = "publications"
    queryset = PublicationCategory.objects.all()
    serializer_class = PublicationCategorySerializer


class AuthorViewSet(TenantScopedMixin, viewsets.ModelViewSet):
    """ViewSet for Author management."""
    rbac_domain = "publications"
    queryset = Author.objects.select_related("user").prefetch_related("publications")
    serializer_class = AuthorSerializer


class SubmissionViewSet(TenantScopedMixin, viewsets.ModelViewSet):
    """ViewSet for Submission management."""
    rbac_domain = "publications"
    queryset = Submission.objects.select_related("submitted_by", "reviewed_by")
    serializer_class = SubmissionSerializer
    pagination_class = CSISPublicationPagination

    @extend_schema(
        parameters=[
            OpenApiParameter(name="status", type=str, description="Filter by status"),
            OpenApiParameter(name="publication", type=str, description="Filter by publication ID"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)


class ExternalPublicationViewSet(TenantScopedMixin, viewsets.ModelViewSet):
    """ViewSet for ExternalPublication management."""
    rbac_domain = "publications"
    queryset = ExternalPublication.objects.prefetch_related("byline_contacts")
    serializer_class = ExternalPublicationSerializer


class CSISPublicationViewSet(viewsets.ReadOnlyModelViewSet):
    """Read-only access to publications synced from api.csis.or.id."""
    rbac_domain = "publications"
    queryset = CSISPublication.objects.all()
    serializer_class = CSISPublicationSerializer
    pagination_class = CSISPublicationPagination

    def get_queryset(self):
        qs = super().get_queryset()
        params = self.request.query_params
        search = params.get("search")
        category = params.get("category")
        if search:
            qs = qs.filter(Q(title__icontains=search) | Q(description__icontains=search))
        if category:
            qs = qs.filter(Q(category_slug=category) | Q(category_name__iexact=category))
        return qs

    @extend_schema(
        parameters=[
            OpenApiParameter(name="search", type=str, description="Filter by title/description"),
            OpenApiParameter(name="category", type=str, description="Filter by category slug or name"),
            OpenApiParameter(name="page_size", type=int, description="Items per page (max 500)"),
        ]
    )
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

    @action(detail=False, methods=["get"])
    def stats(self, request):
        from collections import defaultdict
        qs = CSISPublication.objects.all()
        overall = qs.aggregate(
            total=Count("id"),
            total_views=Sum("viewed"),
            total_downloads=Sum("download_count"),
            avg_views=Avg("viewed"),
            avg_downloads=Avg("download_count"),
            max_views=Max("viewed"),
            max_downloads=Max("download_count"),
        )
        by_category = list(
            qs.values("category_name")
              .annotate(
                  count=Count("id"),
                  total_views=Sum("viewed"),
                  total_downloads=Sum("download_count"),
                  avg_views=Avg("viewed"),
                  avg_downloads=Avg("download_count"),
                  max_views=Max("viewed"),
              )
              .order_by("-count")
        )
        top_published = list(
            qs.order_by("-viewed")
              .values("id", "title", "slug", "category_name", "viewed", "download_count")[:10]
        )

        # Aggregate authors (stored in JSON `authors` field per pub).
        author_pub_count: dict[str, int] = defaultdict(int)
        author_views: dict[str, int] = defaultdict(int)
        author_meta: dict[str, dict] = {}
        for row in qs.values_list("authors", "viewed"):
            authors, views = row
            if not authors:
                continue
            for a in authors:
                name = (a or {}).get("name")
                if not name:
                    continue
                author_pub_count[name] += 1
                author_views[name] += views or 0
                if name not in author_meta:
                    author_meta[name] = {
                        "name": name,
                        "slug": (a or {}).get("slug", ""),
                        "profile_img": (a or {}).get("profile_img", ""),
                    }
        top_authors = sorted(
            (
                {
                    **author_meta[n],
                    "publications": author_pub_count[n],
                    "views": author_views[n],
                }
                for n in author_pub_count
            ),
            key=lambda r: (r["views"], r["publications"]),
            reverse=True,
        )[:10]

        # Monthly publication counts from date_publish.
        monthly_buckets: dict[str, dict] = {}
        for date_publish, viewed, downloads in qs.exclude(date_publish__isnull=True).values_list("date_publish", "viewed", "download_count"):
            key = date_publish.strftime("%Y-%m")
            bucket = monthly_buckets.setdefault(key, {"month": key, "count": 0, "views": 0, "downloads": 0})
            bucket["count"] += 1
            bucket["views"] += viewed or 0
            bucket["downloads"] += downloads or 0
        monthly = sorted(monthly_buckets.values(), key=lambda r: r["month"])[-12:]
        return Response({
            "overall": {
                "total": overall["total"] or 0,
                "total_views": overall["total_views"] or 0,
                "total_downloads": overall["total_downloads"] or 0,
                "avg_views": float(overall["avg_views"] or 0),
                "avg_downloads": float(overall["avg_downloads"] or 0),
                "max_views": overall["max_views"] or 0,
                "max_downloads": overall["max_downloads"] or 0,
            },
            "by_category": [
                {
                    "category_name": row["category_name"],
                    "count": row["count"],
                    "total_views": row["total_views"] or 0,
                    "total_downloads": row["total_downloads"] or 0,
                    "avg_views": float(row["avg_views"] or 0),
                    "avg_downloads": float(row["avg_downloads"] or 0),
                    "max_views": row["max_views"] or 0,
                }
                for row in by_category
            ],
            "top_publications": top_published,
            "top_authors": top_authors,
            "monthly": monthly,
        })
