"""Import des données de la base Laravel `journal` vers le modèle Django.

Principes :
- la base Laravel est seulement LUE (aucune écriture) ;
- import idempotent : identifiants conservés, relances sans doublon (update_or_create) ;
- aucun enregistrement Laravel n'est ignoré silencieusement : chaque anomalie est signalée ;
- les fichiers de storage/app/public sont copiés tels quels (mêmes chemins relatifs).
"""

import re
import shutil
from datetime import timezone as dt_timezone
from dataclasses import dataclass, field
from pathlib import Path

from django.conf import settings
from django.db import transaction
from django.utils import timezone

from apps.accounts.models import Role, User
from apps.ads.models import Publicite
from apps.contact.models import Message, Subscriber
from apps.core.html import sanitize_html
from apps.core.models import SiteSettings
from apps.editorial.models import Article, ArticleImage, Status
from apps.engagement.models import Comment, CommentVote, Reaction
from apps.medialib.models import MediaAsset
from apps.medialib.services import register_existing_file
from apps.taxonomy.models import Category, Tag

ROLE_MAP = {"admin": Role.ADMIN, "editor": Role.EDITEUR, "redacteur": Role.EDITEUR, "user": Role.LECTEUR}
STATUS_MAP = {"brouillon": Status.BROUILLON, "publie": Status.PUBLIE, "archive": Status.ARCHIVE}
UPLOAD_DIRS = ("articles", "uploads", "promotions", "settings")
# URL absolues vers /storage/<dossier>/… (ex. http://localhost/Journal/journal/public/storage/articles/content/x.jpg)
STORAGE_URL_RE = re.compile(r"""(?:https?:)?//[^\s"'<>]*?/storage/(?=(?:%s)/)""" % "|".join(UPLOAD_DIRS))
SETTING_FIELDS = {
    "site_name", "site_description", "contact_email", "contact_phone", "contact_address",
    "facebook_url", "twitter_url", "linkedin_url", "youtube_url", "youtube_live_id",
}


@dataclass
class Report:
    counts: dict = field(default_factory=dict)
    warnings: list = field(default_factory=list)

    def add(self, key, n=1):
        self.counts[key] = self.counts.get(key, 0) + n

    def warn(self, msg):
        self.warnings.append(msg)


def aware(value):
    if value is None or value == "":
        return None
    if isinstance(value, str):
        from django.utils.dateparse import parse_datetime

        value = parse_datetime(value)
        if value is None:
            return None
    if timezone.is_naive(value):
        # Laravel stockait en UTC (config/app.php : timezone = UTC).
        value = timezone.make_aware(value, dt_timezone.utc)
    return value


def rewrite_storage_urls(html):
    return STORAGE_URL_RE.sub("/storage/", html or "")


def convert_password(hashed):
    """Hash bcrypt Laravel ($2y$…) → format Django (bcrypt$$2b$…)."""
    if hashed and hashed.startswith(("$2y$", "$2b$", "$2a$")):
        return "bcrypt$" + "$2b$" + hashed[4:]
    return "!"  # mot de passe inutilisable : réinitialisation nécessaire


class LaravelImporter:
    def __init__(self, cursor, storage_dir=None, report=None, log=print):
        self.cur = cursor
        self.storage_dir = Path(storage_dir) if storage_dir else None
        self.r = report or Report()
        self.log = log
        self._media_cache = {}

    # --- utilitaires ---------------------------------------------------------------
    def rows(self, table):
        if not self.table_exists(table):
            self.r.warn(f"Table absente dans la base Laravel : {table} (ignorée)")
            return []
        self.cur.execute(f"SELECT * FROM {table}")
        cols = [c[0] for c in self.cur.description]
        return [dict(zip(cols, row)) for row in self.cur.fetchall()]

    def table_exists(self, table):
        try:
            self.cur.execute(f"SELECT 1 FROM {table} WHERE 1=0")
            return True
        except Exception:
            return False

    def media(self, path, collection, user=None, **meta):
        if not path:
            return None
        path = str(path).lstrip("/")
        if path.startswith("storage/"):
            path = path[len("storage/"):]
        if path in self._media_cache:
            return self._media_cache[path]
        if not (Path(settings.MEDIA_ROOT) / path).exists():
            self.r.warn(f"Fichier introuvable dans le stockage : {path}")
        asset, created = register_existing_file(path, user=user, collection=collection, **meta)
        if created:
            self.r.add("medias")
        self._media_cache[path] = asset
        return asset

    # --- étapes ----------------------------------------------------------------------
    def copy_files(self):
        if not self.storage_dir:
            return
        dest = Path(settings.MEDIA_ROOT)
        n = 0
        for src in self.storage_dir.rglob("*"):
            if src.is_file() and not src.name.startswith("."):
                target = dest / src.relative_to(self.storage_dir)
                if not target.exists():
                    target.parent.mkdir(parents=True, exist_ok=True)
                    shutil.copy2(src, target)
                    n += 1
        self.r.add("fichiers copiés", n)

    def import_users(self):
        for u in self.rows("users"):
            User.objects.update_or_create(pk=u["id"], defaults={
                "name": u.get("name") or u["email"],
                "email": u["email"].lower(),
                "password": convert_password(u.get("password")),
                "role": ROLE_MAP.get((u.get("role") or "user").lower(), Role.LECTEUR),
                "email_verified_at": aware(u.get("email_verified_at")),
                "date_joined": aware(u.get("created_at")) or timezone.now(),
                "is_active": True,
            })
            if (u.get("role") or "user").lower() not in ROLE_MAP:
                self.r.warn(f"Rôle inconnu « {u.get('role')} » pour {u['email']} : importé comme lecteur")
            self.r.add("utilisateurs")

    def import_taxonomy(self):
        for c in self.rows("categories"):
            Category.objects.update_or_create(pk=c["id"], defaults={
                "nom": c["nom"], "slug": c["slug"], "description": c.get("description") or "",
                "est_dans_menu": bool(c.get("est_dans_menu")), "ordre": c.get("ordre"),
                "created_at": aware(c.get("created_at")) or timezone.now(),
            })
            self.r.add("rubriques")
        for t in self.rows("tags"):
            Tag.objects.update_or_create(pk=t["id"], defaults={"nom": t["nom"], "slug": t["slug"],
                                                              "created_at": aware(t.get("created_at")) or timezone.now()})
            self.r.add("mots-clés")

    def import_articles(self):
        users = {u.pk: u for u in User.objects.all()}
        for a in self.rows("articles"):
            statut = STATUS_MAP.get(a.get("statut"), Status.BROUILLON)
            deleted_at = aware(a.get("deleted_at"))
            author = users.get(a["auteur_id"])
            image = self.media(a.get("image_principale"), MediaAsset.Collection.ARTICLES, author,
                               legende=a.get("legende_image") or "")
            publie_le = aware(a.get("publie_le"))
            if statut == Status.PUBLIE and publie_le is None:
                publie_le = aware(a.get("created_at"))
                self.r.warn(f"Article {a['id']} publié sans date : date de création utilisée")
            art, _ = Article.objects.update_or_create(pk=a["id"], defaults={
                "titre": a["titre"], "slug": a["slug"], "resume": a.get("resume") or "",
                "contenu": sanitize_html(rewrite_storage_urls(a.get("contenu"))),
                "image_principale": image, "legende_image": a.get("legende_image") or "",
                "auteur_id": a["auteur_id"], "categorie_id": a["categorie_id"],
                "statut": statut, "vues": a.get("vues") or 0, "est_a_la_une": bool(a.get("est_a_la_une")),
                "publie_le": publie_le, "archive_le": aware(a.get("updated_at")) if statut == Status.ARCHIVE else None,
                "created_at": aware(a.get("created_at")) or timezone.now(), "deleted_at": deleted_at,
            })
            # updated_at (auto_now) : on rétablit la valeur d'origine
            if a.get("updated_at"):
                Article.objects.filter(pk=art.pk).update(updated_at=aware(a["updated_at"]))
            self.r.add("articles")
        for link in self.rows("article_tag"):
            Article.tags.through.objects.get_or_create(article_id=link["article_id"], tag_id=link["tag_id"])
            self.r.add("liens article/mot-clé")
        for i, im in enumerate(self.rows("article_images")):
            asset = self.media(im["image_path"], MediaAsset.Collection.GALERIE, legende=im.get("legende") or "")
            ArticleImage.objects.update_or_create(pk=im["id"], defaults={
                "article_id": im["article_id"], "media": asset, "legende": im.get("legende") or "", "ordre": i,
            })
            self.r.add("images de galerie")
        # Laravel affichait les 8 derniers articles publiés en flash info : on reprend cette sélection
        # tant que la rédaction n'a rien coché elle-même.
        if not Article.objects.filter(est_flash_info=True).exists():
            ids = list(Article.objects.published().order_by("-publie_le").values_list("pk", flat=True)[:8])
            self.r.add("flash info", Article.objects.filter(pk__in=ids).update(est_flash_info=True))

    def register_content_files(self):
        """Référence dans la médiathèque tous les autres fichiers du stockage (insertions TinyMCE, uploads…)."""
        root = Path(settings.MEDIA_ROOT)
        for d in UPLOAD_DIRS:
            base = root / d
            if not base.exists():
                continue
            for f in base.rglob("*"):
                if not f.is_file() or f.name.startswith(".") or re.search(r"__(sm|md|lg)\.\w+$", f.name):
                    continue
                rel = f.relative_to(root).as_posix()
                collection = {
                    "articles/content": MediaAsset.Collection.CONTENU,
                    "articles/gallery": MediaAsset.Collection.GALERIE,
                    "promotions": MediaAsset.Collection.PUBLICITES,
                }
                col = next((v for k, v in collection.items() if rel.startswith(k)), MediaAsset.Collection.ARTICLES if rel.startswith("articles/") else MediaAsset.Collection.DIVERS)
                self.media(rel, col)

    def import_engagement(self):
        rows = sorted(self.rows("comments"), key=lambda c: (c.get("parent_id") is not None, c["id"]))
        for c in rows:
            Comment.objects.update_or_create(pk=c["id"], defaults={
                "article_id": c["article_id"], "user_id": c.get("user_id"), "parent_id": c.get("parent_id"),
                "nom": c.get("nom") or "", "email": c.get("email") or "", "contenu": c["contenu"],
                "statut": Comment.Status.APPROUVE if c.get("approuve") else Comment.Status.EN_ATTENTE,
                "created_at": aware(c.get("created_at")) or timezone.now(),
            })
            self.r.add("commentaires")
        for v in self.rows("comment_votes"):
            CommentVote.objects.update_or_create(pk=v["id"], defaults={
                "comment_id": v["comment_id"], "user_id": v.get("user_id"), "ip_address": v.get("ip_address") or None,
                "vote": v["vote"], "created_at": aware(v.get("created_at")) or timezone.now(),
            })
            self.r.add("votes")
        for x in self.rows("reactions"):
            Reaction.objects.update_or_create(pk=x["id"], defaults={
                "article_id": x["article_id"], "user_id": x.get("user_id"), "ip_address": x.get("ip_address") or None,
                "reaction_type": x.get("reaction_type") or "like", "created_at": aware(x.get("created_at")) or timezone.now(),
            })
            self.r.add("réactions")

    def import_ads_contact(self):
        for p in self.rows("publicites"):
            asset = self.media(p["image_path"], MediaAsset.Collection.PUBLICITES)
            Publicite.objects.update_or_create(pk=p["id"], defaults={
                "titre": p["titre"], "image": asset, "lien": p.get("lien") or "",
                "position": p.get("position") or "sidebar", "actif": bool(p.get("actif")),
                "date_debut": aware(p.get("date_debut")), "date_fin": aware(p.get("date_fin")),
                "impressions": p.get("impressions") or 0, "clics": p.get("clics") or 0,
                "created_at": aware(p.get("created_at")) or timezone.now(),
            })
            self.r.add("publicités")
        for m in self.rows("messages"):
            Message.objects.update_or_create(pk=m["id"], defaults={
                "nom": m["nom"], "email": m["email"], "sujet": m["sujet"], "message": m["message"],
                "lu": bool(m.get("lu")), "created_at": aware(m.get("created_at")) or timezone.now(),
            })
            self.r.add("messages")
        for s in self.rows("subscribers"):
            Subscriber.objects.update_or_create(pk=s["id"], defaults={
                "email": s["email"].lower(), "is_active": bool(s.get("is_active")),
                "created_at": aware(s.get("created_at")) or timezone.now(),
            })
            self.r.add("abonnés")

    def import_settings(self):
        site = SiteSettings.objects.get_or_create(pk=1)[0]
        for s in self.rows("settings"):
            key, value = s["key"], s.get("value") or ""
            if key in SETTING_FIELDS:
                setattr(site, key, value)
            elif key in ("logo", "favicon") and value:
                getattr(site, key).name = value
            else:
                self.r.warn(f"Paramètre Laravel non repris automatiquement : {key} = {value[:60]!r}")
            self.r.add("paramètres")
        site.save()

    def reset_sequences(self):
        from django.core.management.color import no_style
        from django.db import connection

        models = [User, Category, Tag, Article, ArticleImage, Comment, CommentVote, Reaction, Publicite, Message, Subscriber]
        with connection.cursor() as c:
            for sql in connection.ops.sequence_reset_sql(no_style(), models):
                c.execute(sql)

    def run(self):
        self.copy_files()
        with transaction.atomic():
            self.import_users()
            self.import_taxonomy()
            self.import_articles()
            self.register_content_files()
            self.import_engagement()
            self.import_ads_contact()
            self.import_settings()
        self.reset_sequences()
        return self.r
