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>();

    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<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");
        });
    }
}
