using MaBibli.Shared.Entites;
using Microsoft.EntityFrameworkCore;

namespace MaBibli.Api.Data;

public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbContext(options)
{
    public DbSet<Livre> Livres => Set<Livre>();

    public DbSet<Auteur> Auteurs => Set<Auteur>();

    public DbSet<LivreAuteur> LivreAuteurs => Set<LivreAuteur>();

    public DbSet<StatutLecture> StatutsLecture => Set<StatutLecture>();

    public DbSet<RapprochementRefuse> RapprochementsRefuses => Set<RapprochementRefuse>();

    public DbSet<Pret> Prets => Set<Pret>();

    /// <summary>
    /// Liste d'envies, <b>personnelle</b>. Table séparée des <see cref="Livres"/> : un livre
    /// souhaité n'est pas un livre possédé, et ne doit apparaître ni au catalogue, ni dans les
    /// compteurs, ni dans les prêts.
    /// </summary>
    public DbSet<LivreSouhaite> LivresSouhaites => Set<LivreSouhaite>();

    public DbSet<BibliographieMasquee> BibliographiesMasquees => Set<BibliographieMasquee>();

    /// <summary>
    /// Sagas, cycles et séries. <b>Communs au foyer</b>, comme le catalogue : l'ordre de lecture
    /// est une propriété de l'œuvre, pas du lecteur.
    /// </summary>
    public DbSet<Serie> Series => Set<Serie>();

    public DbSet<ElementSerie> ElementsSerie => Set<ElementSerie>();

    /// <summary>
    /// Revues et magazines. <b>Table séparée des <see cref="Livres"/></b> : une revue n'est pas
    /// un livre, et n'a rien à faire dans le catalogue, ses compteurs ou ses doublons.
    /// </summary>
    public DbSet<Revue> Revues => Set<Revue>();

    public DbSet<NumeroRevue> NumerosRevue => Set<NumeroRevue>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Livre>(livre =>
        {
            livre.Property(l => l.Titre).IsRequired();
            livre.Property(l => l.TitreNormalise).IsRequired();
            livre.HasIndex(l => l.Isbn);

            // Le tri et la recherche passent par là : sans index, chaque frappe balaie la table.
            livre.HasIndex(l => l.TitreNormalise);

            livre.HasMany(l => l.Prets)
                 .WithOne(p => p.Livre)
                 .HasForeignKey(p => p.LivreId)
                 .OnDelete(DeleteBehavior.Cascade);
        });

        modelBuilder.Entity<Auteur>(auteur =>
        {
            auteur.Property(a => a.Nom).IsRequired();
            auteur.Property(a => a.NomNormalise).IsRequired();
            auteur.Property(a => a.CleRegroupement).IsRequired();

            // Unicité de la clé de regroupement : « Zola, Émile » et « Émile Zola » produisent
            // la même clé et ne peuvent donc pas coexister. C'est la garantie structurelle du
            // regroupement — le code applicatif peut avoir un trou, la base non.
            auteur.HasIndex(a => a.CleRegroupement).IsUnique();
            auteur.HasIndex(a => a.NomNormalise);
        });

        modelBuilder.Entity<LivreAuteur>(lien =>
        {
            lien.HasKey(la => new { la.LivreId, la.AuteurId });

            lien.HasOne(la => la.Livre)
                .WithMany(l => l.Auteurs)
                .HasForeignKey(la => la.LivreId)
                .OnDelete(DeleteBehavior.Cascade);

            // Restrict : supprimer un auteur encore rattaché à des livres les priverait de leur
            // auteur en silence. On passe par une fusion, ou par la suppression des livres.
            lien.HasOne(la => la.Auteur)
                .WithMany(a => a.Livres)
                .HasForeignKey(la => la.AuteurId)
                .OnDelete(DeleteBehavior.Restrict);

            lien.HasIndex(la => la.AuteurId);
        });

        modelBuilder.Entity<StatutLecture>(statut =>
        {
            statut.Property(s => s.Utilisateur).IsRequired();

            statut.HasOne(s => s.Livre)
                  .WithMany(l => l.Statuts)
                  .HasForeignKey(s => s.LivreId)
                  .OnDelete(DeleteBehavior.Cascade);

            // Une personne, un statut par livre. L'unicité est ce qui rend l'écriture idempotente :
            // on lit la ligne, on la met à jour, et il ne peut jamais y en avoir deux à départager.
            statut.HasIndex(s => new { s.LivreId, s.Utilisateur }).IsUnique();
            statut.HasIndex(s => s.Utilisateur);
        });

        modelBuilder.Entity<RapprochementRefuse>(refus =>
        {
            // Le couple est rangé (petit identifiant d'abord) à l'écriture : l'unicité vaut donc
            // dans les deux sens, et un refus ne peut pas être enregistré deux fois.
            refus.HasIndex(r => new { r.AuteurAId, r.AuteurBId }).IsUnique();
        });

        modelBuilder.Entity<LivreSouhaite>(souhait =>
        {
            souhait.Property(s => s.Utilisateur).IsRequired();
            souhait.Property(s => s.Titre).IsRequired();
            souhait.Property(s => s.TitreNormalise).IsRequired();

            // Jamais nullable : SQLite tient deux NULL pour distincts, et l'unicité ci-dessous
            // laisserait alors passer autant de doublons qu'on veut sur les envies sans auteur.
            souhait.Property(s => s.AuteurNormalise).IsRequired().HasDefaultValue(string.Empty);

            // Toute lecture part de là : la liste est personnelle, on la parcourt par personne.
            souhait.HasIndex(s => s.Utilisateur, "IX_LivresSouhaites_Utilisateur");

            // Une même œuvre ne s'ajoute qu'une fois par personne — l'écran de bibliographie
            // rend le double clic facile. L'auteur fait partie de la clé : deux auteurs peuvent
            // avoir publié deux « Œuvres » différentes, et l'un ne doit pas bloquer l'autre.
            // Deux personnes peuvent en revanche souhaiter le même livre, d'où l'utilisateur
            // en tête de l'index.
            souhait.HasIndex(
                    s => new { s.Utilisateur, s.TitreNormalise, s.AuteurNormalise },
                    "IX_LivresSouhaites_Utilisateur_Oeuvre")
                .IsUnique();
        });

        modelBuilder.Entity<BibliographieMasquee>(masquee =>
        {
            masquee.Property(m => m.Utilisateur).IsRequired();
            masquee.Property(m => m.TitreNormalise).IsRequired();
            masquee.HasIndex(
                    m => new { m.AuteurId, m.Utilisateur, m.TitreNormalise },
                    "IX_BibliographiesMasquees_Auteur_Utilisateur_Titre")
                .IsUnique();
            masquee.HasIndex(m => new { m.Utilisateur, m.AuteurId });
        });

        modelBuilder.Entity<Serie>(serie =>
        {
            serie.Property(s => s.Titre).IsRequired();
            serie.Property(s => s.TitreNormalise).IsRequired();

            // Une série, une fiche. Voir l'entité : l'unicité est GLOBALE et non par parent,
            // le parent étant nullable et deux NULL étant distincts pour SQLite.
            serie.HasIndex(s => s.TitreNormalise).IsUnique();

            // SetNull et non Cascade : supprimer un cycle ne doit pas emporter les trilogies
            // qu'il contient — ce sont de vraies séries de vrais livres, qui se tiennent seules.
            serie.HasOne(s => s.SerieParente)
                 .WithMany(s => s.SousSeries)
                 .HasForeignKey(s => s.SerieParenteId)
                 .OnDelete(DeleteBehavior.SetNull);

            serie.HasIndex(s => s.SerieParenteId);
        });

        modelBuilder.Entity<ElementSerie>(element =>
        {
            element.Property(e => e.Titre).IsRequired();

            element.HasOne(e => e.Serie)
                   .WithMany(s => s.Elements)
                   .HasForeignKey(e => e.SerieId)
                   .OnDelete(DeleteBehavior.Cascade);

            // ⚠️ SetNull, surtout pas Cascade : supprimer un livre du catalogue doit laisser sa
            // place dans la saga, devenue un trou nommé. Cascade effacerait le tome 3 de la
            // série parce qu'on a prêté puis perdu son exemplaire.
            element.HasOne(e => e.Livre)
                   .WithMany()
                   .HasForeignKey(e => e.LivreId)
                   .OnDelete(DeleteBehavior.SetNull);

            element.HasIndex(e => e.SerieId, "IX_ElementsSerie_SerieId");

            // Un même livre ne tient qu'une place dans une série donnée. Index PARTIEL, comme
            // pour les prêts : les positions sans livre (les trous) doivent pouvoir se répéter
            // autant que nécessaire, et deux NULL passeraient de toute façon.
            // Un livre peut en revanche appartenir à PLUSIEURS séries — une intégrale, une
            // série, un cycle — d'où l'absence d'unicité sur LivreId seul.
            element.HasIndex(e => new { e.SerieId, e.LivreId }, "IX_ElementsSerie_SerieId_LivreId")
                   .IsUnique()
                   .HasFilter("\"LivreId\" IS NOT NULL");
        });

        modelBuilder.Entity<Revue>(revue =>
        {
            revue.Property(r => r.Titre).IsRequired();
            revue.Property(r => r.TitreNormalise).IsRequired();

            revue.HasIndex(r => r.TitreNormalise).IsUnique();

            // Index PARTIEL : une revue peut n'avoir aucun ISSN (saisie à la main), et deux
            // NULL étant distincts pour SQLite, une unicité simple ne dirait rien. Filtré, il
            // garantit qu'un ISSN connu ne désigne qu'une fiche — ce qui permet au flux « 977 »
            // de retrouver la revue déjà créée au lieu d'en faire une seconde.
            revue.HasIndex(r => r.Issn, "IX_Revues_Issn")
                 .IsUnique()
                 .HasFilter("\"Issn\" IS NOT NULL");
        });

        modelBuilder.Entity<NumeroRevue>(numero =>
        {
            numero.Property(n => n.Numero).IsRequired();
            numero.Property(n => n.NumeroNormalise).IsRequired();

            numero.HasOne(n => n.Revue)
                  .WithMany(r => r.Numeros)
                  .HasForeignKey(n => n.RevueId)
                  .OnDelete(DeleteBehavior.Cascade);

            // Un numéro ne se possède qu'une fois par revue : rescanner le même exemplaire ne
            // doit pas allonger la liste. Contrairement aux livres, un second exemplaire d'un
            // numéro de magazine n'a pas d'usage identifié — et l'index se retire si un jour si.
            numero.HasIndex(n => new { n.RevueId, n.NumeroNormalise }).IsUnique();
        });

        modelBuilder.Entity<Pret>(pret =>
        {
            pret.Property(p => p.Emprunteur).IsRequired();

            // Deux index sur la même colonne, et ce n'est pas une redondance. Ils doivent porter
            // un nom explicite : EF identifie un index par ses colonnes, et sans nom distinct le
            // second remplacerait purement et simplement le premier.

            // Celui-ci sert l'historique d'un livre — toutes ses lignes, closes comprises.
            pret.HasIndex(p => p.LivreId, "IX_Prets_LivreId");

            // Celui-là est une contrainte : il ne peut y avoir qu'UN prêt ouvert par livre, on
            // ne prête pas deux fois un exemplaire qui n'est pas revenu. Le service le vérifie,
            // mais entre sa vérification et son insertion il y a une fenêtre ; l'index la ferme.
            // Le filtre est ce qui rend la chose possible : seules les lignes ouvertes sont
            // indexées, les prêts clos restent libres de se répéter autant que nécessaire.
            // Il sert accessoirement la vue « prêts en cours », dont c'est exactement le critère.
            pret.HasIndex(p => p.LivreId, "IX_Prets_LivreId_EnCours")
                .IsUnique()
                .HasFilter("\"DateRetour\" IS NULL");
        });
    }
}
