"""Back-office éditorial. Les vues sont génériques et paramétrées pour Article et Annonce."""

import json

from django import forms
from django.contrib import messages
from django.core import signing
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator
from django.db import transaction
from django.db.models import Count, Q
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views import View

from apps.accounts import permissions as perms
from apps.core.mixins import BackofficeMixin, safe_next
from apps.taxonomy.models import Category

from .. import workflow
from ..signals import flush_public_cache
from ..forms import AnnonceForm, ArticleForm, ContentFilterForm, GalleryFormSet
from ..models import Annonce, Article, ArticleImage, Revision, Status

SORTS = {
    "-date": ["-created_at"],
    "date": ["created_at"],
    "-publie": ["-publie_le", "-created_at"],
    "publie": ["publie_le", "created_at"],
    "titre": ["titre"],
    "-titre": ["-titre"],
    "-vues": ["-vues", "-created_at"],
    "vues": ["vues"],
}


class ContentKind:
    """Configuration d'un type de contenu géré dans le back-office."""

    def __init__(self, key, model, form_class, singular, plural, new_label, icon, feminine=False):
        self.key, self.model, self.form_class = key, model, form_class
        self.singular, self.plural, self.new_label, self.icon = singular, plural, new_label, icon
        self.feminine = feminine

    def url(self, name, *args):
        return reverse(f"editorial:{self.key}-{name}", args=args)

    def extra_filters(self):
        if self.model is Article:
            return {
                "categorie": forms.ModelChoiceField(
                    queryset=Category.objects.order_by("nom"), required=False, empty_label="Toutes les rubriques"
                )
            }
        return {
            "type": forms.ChoiceField(choices=[("", "Tous les types")] + list(Annonce.Type.choices), required=False)
        }

    def base_queryset(self, user):
        qs = self.model.objects.visible_to(user).select_related("auteur", "image_principale")
        if self.model is Article:
            qs = qs.select_related("categorie")
        return qs


ARTICLE = ContentKind("article", Article, ArticleForm, "article", "articles", "Nouvel article", "newspaper")
ANNONCE = ContentKind("annonce", Annonce, AnnonceForm, "annonce", "annonces", "Nouvelle annonce", "megaphone", True)


def _media_json(assets):
    return json.dumps(
        [{"id": a.pk, "url": a.variant_url("sm"), "name": a.titre or a.filename, "type": a.type} for a in assets if a]
    )


class KindMixin(BackofficeMixin):
    kind: ContentKind = None

    def get_object(self, pk):
        obj = get_object_or_404(self.kind.model.objects.select_related("auteur"), pk=pk)
        if not perms.can_view_content(self.request.user, obj):
            raise PermissionDenied
        return obj


# --- Liste --------------------------------------------------------------------

class ContentListView(KindMixin, View):
    template_name = "backoffice/editorial/list.html"

    def get(self, request):
        kind, user = self.kind, request.user
        form = ContentFilterForm(request.GET or None, extra_filters=kind.extra_filters(), user=user)
        form.is_valid()
        f = getattr(form, "cleaned_data", {}) or {}
        base = kind.base_queryset(user)

        statut = f.get("statut") or ""
        if statut == "corbeille":
            qs = base.trashed()
        elif statut == "programme":
            qs = base.scheduled()
        elif statut:
            qs = base.alive().filter(statut=statut)
        else:
            qs = base.alive()

        if q := f.get("q"):
            qs = qs.filter(Q(titre__icontains=q) | Q(resume__icontains=q) | Q(contenu__icontains=q))
        if f.get("categorie"):
            qs = qs.filter(categorie=f["categorie"])
        if f.get("type"):
            qs = qs.filter(type=f["type"])
        if f.get("auteur"):
            qs = qs.filter(auteur=f["auteur"])
        if f.get("du"):
            qs = qs.filter(created_at__date__gte=f["du"])
        if f.get("au"):
            qs = qs.filter(created_at__date__lte=f["au"])
        if f.get("une"):
            qs = qs.filter(est_a_la_une=True)
        if f.get("flash") and kind.model is Article:
            qs = qs.filter(est_flash_info=True)
        tri = f.get("tri") or "-date"
        if "vues" in tri and kind.model is not Article:
            tri = "-date"
        qs = qs.order_by(*SORTS.get(tri, SORTS["-date"]))

        page = Paginator(qs, f.get("par_page") or 20).get_page(request.GET.get("page"))
        rows = [(obj, perms.content_actions(user, obj)) for obj in page.object_list]

        alive = base.alive()
        counts = alive.aggregate(
            tous=Count("pk"),
            brouillon=Count("pk", filter=Q(statut=Status.BROUILLON)),
            relecture=Count("pk", filter=Q(statut=Status.RELECTURE)),
            publie=Count("pk", filter=Q(statut=Status.PUBLIE, publie_le__lte=timezone.now())),
            programme=Count("pk", filter=Q(statut=Status.PUBLIE, publie_le__gt=timezone.now())),
            archive=Count("pk", filter=Q(statut=Status.ARCHIVE)),
        )
        counts["corbeille"] = base.trashed().count()
        tabs = [
            ("", "Tous", counts["tous"]),
            ("brouillon", "Brouillons", counts["brouillon"]),
            ("relecture", "En relecture", counts["relecture"]),
            ("programme", "Programmés", counts["programme"]),
            ("publie", "Publiés", counts["publie"]),
            ("archive", "Archivés", counts["archive"]),
            ("corbeille", "Corbeille", counts["corbeille"]),
        ]
        return render(request, self.template_name, {
            "kind": kind,
            "form": form,
            "page_obj": page,
            "rows": rows,
            "tabs": tabs,
            "current_status": statut,
            "current_sort": tri,
            "is_trash": statut == "corbeille",
            "can_publish": perms.can_publish(user),
            "can_destroy": perms.can_destroy(user),
            "advanced_open": any(f.get(k) for k in ("auteur", "du", "au", "une", "flash", "categorie", "type")),
        })


# --- Création / modification --------------------------------------------------

class ContentFormView(KindMixin, View):
    template_name = "backoffice/editorial/form.html"

    def get_instance(self, pk):
        if pk is None:
            if not perms.can_create_content(self.request.user):
                raise PermissionDenied
            return self.kind.model(auteur=self.request.user)
        obj = self.get_object(pk)
        if obj.is_trashed:
            messages.warning(self.request, "Ce contenu est dans la corbeille : restaurez-le pour le modifier.")
        return obj

    def build(self, obj, data=None):
        form = self.kind.form_class(data, instance=obj, user=self.request.user)
        gallery = None
        if self.kind.model is Article and obj.pk:
            gallery = GalleryFormSet(data, instance=obj, prefix="gallery")
        return form, gallery

    def get(self, request, pk=None):
        obj = self.get_instance(pk)
        form, gallery = self.build(obj)
        return self.render(obj, form, gallery)

    def post(self, request, pk=None):
        obj = self.get_instance(pk)
        user = request.user
        if obj.pk and not perms.can_edit_content(user, obj):
            raise PermissionDenied("Vous ne pouvez plus modifier ce contenu.")
        form, gallery = self.build(obj, request.POST)
        if not (form.is_valid() and (gallery is None or gallery.is_valid())):
            messages.error(request, "Le formulaire contient des erreurs. Vérifiez les champs signalés.")
            return self.render(obj, form, gallery, status=400)

        creating = obj.pk is None
        action = request.POST.get("submit_action", "save")
        with transaction.atomic():
            obj = form.save(commit=False)
            if creating:
                obj.auteur = user
            obj.modifie_par = user
            obj.save()
            form.save_m2m()
            if gallery is not None:
                gallery.save()
            if self.kind.model is Article:
                start = obj.images.count()
                for i, asset in enumerate(form.cleaned_data.get("gallery_media") or []):
                    ArticleImage.objects.get_or_create(
                        article=obj, media=asset, defaults={"legende": asset.legende, "ordre": start + i}
                    )
            workflow.record_revision(obj, user, "create" if creating else "save")

        msg = f"{self.kind.singular.capitalize()} enregistré{'e' if self.kind.feminine else ''}."
        if action in {"publish", "submit"}:
            try:
                msg = workflow.transition(obj, action, user)
            except PermissionDenied as exc:
                messages.error(request, str(exc))
                return redirect(self.kind.url("edit", obj.pk))
        messages.success(request, msg)
        if action == "preview":
            return redirect(self.kind.url("preview", obj.pk))
        return redirect(self.kind.url("edit", obj.pk))

    def render(self, obj, form, gallery, status=200):
        user = self.request.user
        revisions = obj.revisions.select_related("auteur")[:15] if obj.pk else []
        picked = []
        if self.request.method == "POST" and form.data.get("image_principale"):
            from apps.medialib.models import MediaAsset

            picked = list(MediaAsset.objects.filter(pk=form.data.get("image_principale")))
        elif obj.image_principale_id:
            picked = [obj.image_principale]
        ctx = {
            "kind": self.kind,
            "obj": obj,
            "form": form,
            "gallery": gallery,
            "revisions": revisions,
            "actions": perms.content_actions(user, obj) if obj.pk else {},
            "edit_allowed": obj.pk is None or perms.can_edit_content(user, obj),
            "can_publish": perms.can_publish(user),
            "image_initial": _media_json(picked),
            "annex_initial": _media_json([getattr(obj, "piece_jointe", None)]) if self.kind.model is Annonce else "[]",
            "action_labels": workflow.ACTION_LABELS,
        }
        return render(self.request, self.template_name, ctx, status=status)


# --- Actions éditoriales ------------------------------------------------------

class ContentActionView(KindMixin, View):
    def post(self, request, pk):
        obj = self.get_object(pk)
        action = request.POST.get("action")
        list_url = self.kind.url("list")
        try:
            if action == "destroy":
                workflow.destroy(obj, request.user)
                messages.success(request, "Contenu supprimé définitivement.")
                return redirect(safe_next(request, list_url + "?statut=corbeille"))
            msg = workflow.transition(obj, action, request.user, note=request.POST.get("note", ""))
            messages.success(request, msg)
        except PermissionDenied as exc:
            messages.error(request, str(exc) or "Action non autorisée.")
        except workflow.WorkflowError as exc:
            messages.error(request, str(exc))
        return redirect(safe_next(request, self.kind.url("edit", obj.pk)))


class ContentBulkView(KindMixin, View):
    ALLOWED = {"publish", "unpublish", "archive", "trash", "restore", "destroy"}
    FLASH = {"flash_on": True, "flash_off": False}

    def post(self, request):
        action = request.POST.get("action")
        ids = request.POST.getlist("ids")
        back = safe_next(request, self.kind.url("list"))
        if action in self.FLASH and ids and self.kind.model is Article:
            if not perms.is_editeur(request.user):
                raise PermissionDenied
            n = Article.objects.filter(pk__in=ids).update(est_flash_info=self.FLASH[action], modifie_par=request.user)
            flush_public_cache()
            messages.success(request, f"{n} article(s) {'ajouté(s) au' if self.FLASH[action] else 'retiré(s) du'} flash info.")
            return redirect(back)
        if action not in self.ALLOWED or not ids:
            messages.warning(request, "Sélectionnez au moins un élément et une action.")
            return redirect(back)
        done = refused = 0
        for obj in self.kind.model.objects.filter(pk__in=ids):
            try:
                if not perms.can_view_content(request.user, obj):
                    raise PermissionDenied
                if action == "destroy":
                    workflow.destroy(obj, request.user)
                else:
                    workflow.transition(obj, action, request.user)
                done += 1
            except (PermissionDenied, workflow.WorkflowError):
                refused += 1
        if done:
            messages.success(request, f"{done} élément(s) traité(s).")
        if refused:
            messages.warning(request, f"{refused} élément(s) ignoré(s) (statut incompatible ou droits insuffisants).")
        return redirect(back)


class RevisionRevertView(KindMixin, View):
    def post(self, request, pk, rid):
        obj = self.get_object(pk)
        revision = get_object_or_404(Revision, pk=rid, object_id=obj.pk)
        if revision.content_type.model_class() is not self.kind.model:
            raise Http404
        workflow.revert(obj, revision, request.user)
        messages.success(request, f"Version du {timezone.localtime(revision.created_at):%d/%m/%Y à %H:%M} restaurée.")
        return redirect(self.kind.url("edit", obj.pk))


# --- Aperçu -------------------------------------------------------------------

PREVIEW_SALT = "editorial.preview"


def preview_token(obj):
    return signing.dumps({"m": obj._meta.model_name, "pk": obj.pk}, salt=PREVIEW_SALT)


class ContentPreviewView(KindMixin, View):
    """Aperçu interne + lien partageable signé (valable 48 h)."""

    def get(self, request, pk):
        from .public import render_annonce, render_article

        obj = self.get_object(pk)
        share_url = request.build_absolute_uri(reverse("public:preview", args=[preview_token(obj)]))
        render_fn = render_article if self.kind.model is Article else render_annonce
        return render_fn(request, obj, preview={"share_url": share_url, "edit_url": self.kind.url("edit", obj.pk)})
