// ============================================================================
// [LIVRE] BLAZOR - GUIDE ULTRA-DÉTAILLÉ POUR DÉBUTANTS EN GÉNIE LOGICIEL
// ============================================================================
//
// [OBJECTIF] PARTIE 1 : FONDAMENTAUX .NET & BLAZOR
//
// CHAPITRE 1 : Rappels C# essentiels pour Blazor
// CHAPITRE 2 : Comprendre l'écosystème .NET
// CHAPITRE 3 : Introduction à Blazor
//
// [TEMPS] TEMPS ESTIMÉ : ~6-8 heures
// [DOCS] PRÉREQUIS : Connaissances de base en programmation (variables, fonctions)
// ============================================================================

/*
[OBJECTIF] PHILOSOPHIE DE CE GUIDE

COMMENT ? -> Explications pas à pas avec code concret
POURQUOI ? -> Raisons de chaque concept
QUAND ?   -> Cas d'usage réels
PRATIQUE  -> Exercices avec corrigés complets

Ce guide est votre SEULE référence Blazor !
*/

// ============================================================================
// [GUIDE] CHAPITRE 1 : RAPPELS C# ESSENTIELS POUR BLAZOR
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser les classes et interfaces en C#
[OK] Maîtriser async/await pour les appels réseau
[OK] Comprendre les events et delegates
[OK] Utiliser l'injection de dépendances (DI)
[OK] Travailler avec les types nullable
[OK] Écrire des requêtes LINQ
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] CLASSES, INTERFACES ET RECORDS
// ----------------------------------------------------------------------------

/*
POURQUOI C'EST IMPORTANT POUR BLAZOR ?

Dans Blazor, TOUT est basé sur des classes C# :
- Les composants sont des classes
- Les services sont des classes
- Les modèles de données sont des classes
- Les formulaires utilisent des classes


1. CLASSES — La base de tout
-----------------------------

UNE CLASSE = Un moule pour créer des objets
*/

// Exemple : Modèle utilisateur
public class Utilisateur
{
    // Propriétés (données)
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;    // <- = string.Empty évite null
    public string Email { get; set; } = string.Empty;
    public DateTime DateInscription { get; set; }
    public bool EstActif { get; set; } = true;

    // Constructeur (pour créer un objet)
    public Utilisateur()
    {
        DateInscription = DateTime.Now;
    }

    // Constructeur avec paramètres
    public Utilisateur(string nom, string email)
    {
        Nom = nom;
        Email = email;
        DateInscription = DateTime.Now;
    }

    // Méthode (comportement)
    public string ObtenirAffichage()
    {
        return $"{Nom} ({Email})";
    }
}

// UTILISATION :
// var user = new Utilisateur("Alice", "alice@example.com");
// Console.WriteLine(user.ObtenirAffichage()); // -> "Alice (alice@example.com)"


/*
2. INTERFACES — Contrats entre classes
----------------------------------------

UNE INTERFACE = Liste de méthodes qu'une classe DOIT implémenter
POURQUOI ? Pour l'injection de dépendances (DI) dans Blazor !

[IDEE] ANALOGIE :
Interface = Fiche de poste
Classe = Employé qui remplit ce poste
*/

// Définir une interface (contrat)
public interface IUtilisateurService
{
    Task<List<Utilisateur>> ObtenirTousAsync();
    Task<Utilisateur?> ObtenirParIdAsync(int id);
    Task<Utilisateur> CreerAsync(Utilisateur utilisateur);
    Task SupprimerAsync(int id);
}

// Implémenter l'interface
public class UtilisateurService : IUtilisateurService
{
    // Implémentation OBLIGATOIRE de toutes les méthodes
    public async Task<List<Utilisateur>> ObtenirTousAsync()
    {
        // En vrai : appel à une BD ou API
        await Task.Delay(100); // Simuler délai réseau
        return new List<Utilisateur>
        {
            new Utilisateur("Alice", "alice@example.com"),
            new Utilisateur("Bob", "bob@example.com")
        };
    }

    public async Task<Utilisateur?> ObtenirParIdAsync(int id)
    {
        var utilisateurs = await ObtenirTousAsync();
        return utilisateurs.FirstOrDefault(u => u.Id == id);
    }

    public async Task<Utilisateur> CreerAsync(Utilisateur utilisateur)
    {
        // En vrai : sauvegarder en BD
        await Task.Delay(50);
        utilisateur.Id = new Random().Next(1, 1000);
        return utilisateur;
    }

    public async Task SupprimerAsync(int id)
    {
        await Task.Delay(50);
        // En vrai : supprimer de la BD
    }
}

/*
3. RECORDS — Classes immuables (idéales pour les données)
----------------------------------------------------------

POURQUOI LES RECORDS ?
- Parfaits pour les DTOs (Data Transfer Objects)
- Comparaison par valeur (pas par référence)
- Syntaxe concise
- Immuables par défaut

QUAND UTILISER ?
-> Pour transférer des données entre couches
-> Pour les réponses d'API
-> Pour les paramètres de requêtes
*/

// Record simple (propriétés immuables)
public record UtilisateurDto(int Id, string Nom, string Email);

// Record avec méthodes
public record ProduitDto(int Id, string Nom, decimal Prix)
{
    public string PrixAffichage => $"{Prix:C}"; // Propriété calculée
}

// UTILISATION :
// var dto = new UtilisateurDto(1, "Alice", "alice@example.com");
// var copieModifiee = dto with { Nom = "Alice Dupont" }; // Copier avec modification

/*
[IDEE] RECORD vs CLASS

CLASS :
- Mutable (modifiable)
- Comparaison par référence
- Pour objets avec comportement complexe

RECORD :
- Immuable par défaut
- Comparaison par valeur
- Pour données simples, DTOs
*/


// ----------------------------------------------------------------------------
// [RAPIDE] ASYNC / AWAIT — Crucial pour Blazor !
// ----------------------------------------------------------------------------

/*
POURQUOI C'EST ABSOLUMENT ESSENTIEL POUR BLAZOR ?

Dans Blazor, vous faites constamment :
- Appels à des APIs web (HTTP)
- Lectures de bases de données
- Chargement de fichiers
- Opérations longues

SANS async/await -> L'interface se bloque, aucune réactivité !
AVEC async/await -> L'interface reste réactive pendant les opérations

COMMENT ÇA MARCHE ?

1. La méthode commence
2. Rencontre "await"
3. Rend le contrôle à l'UI
4. L'UI reste réactive
5. Quand l'opération finit, reprend là où elle s'est arrêtée

ANALOGIE :
Commander au restaurant (async) vs faire la cuisine vous-même (sync)
Vous commandez -> continuez à faire autre chose -> plat arrive plus tard
*/

// [X] VERSION SYNCHRONE (BLOQUE L'INTERFACE)
public List<Utilisateur> ChargerUtilisateursSync()
{
    // Thread.Sleep(2000); // <- Bloque TOUT pendant 2 secondes !
    // L'utilisateur ne peut RIEN faire pendant ce temps
    return new List<Utilisateur>();
}

// [OK] VERSION ASYNCHRONE (N'BLOQUE PAS L'INTERFACE)
public async Task<List<Utilisateur>> ChargerUtilisateursAsync()
{
    // await = "attend SANS bloquer"
    await Task.Delay(2000); // L'UI reste réactive !
    return new List<Utilisateur>();
}

/*
RÈGLES D'ASYNC/AWAIT :

1. async -> Toujours avec await dedans (sinon inutile)
2. Task -> Méthode async sans retour (équivalent void)
3. Task<T> -> Méthode async qui retourne quelque chose
4. await -> Attend la fin de l'opération async
5. Propagation -> Si tu awaittes, tu dois être async
*/

// Exemples pratiques pour Blazor :

public class ExemplesAsync
{
    private readonly HttpClient _httpClient;

    public ExemplesAsync(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    // Appel API
    public async Task<List<Produit>> ChargerProduitsAsync()
    {
        // GetFromJsonAsync -> deserialize JSON automatiquement
        var produits = await _httpClient.GetFromJsonAsync<List<Produit>>("api/produits");
        return produits ?? new List<Produit>();
    }

    // Appel API avec gestion d'erreur
    public async Task<List<Produit>> ChargerProduitsSecurisesAsync()
    {
        try
        {
            var produits = await _httpClient.GetFromJsonAsync<List<Produit>>("api/produits");
            return produits ?? new List<Produit>();
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"Erreur réseau: {ex.Message}");
            return new List<Produit>();
        }
    }

    // Plusieurs appels en parallèle
    public async Task ChargerDonneesEnParalleleAsync()
    {
        // Lancer les 2 tâches EN MÊME TEMPS (plus rapide !)
        var tacheUtilisateurs = _httpClient.GetFromJsonAsync<List<Utilisateur>>("api/users");
        var tacheProduits = _httpClient.GetFromJsonAsync<List<Produit>>("api/produits");

        // Attendre que LES DEUX soient finies
        await Task.WhenAll(tacheUtilisateurs, tacheProduits);

        var utilisateurs = await tacheUtilisateurs;
        var produits = await tacheProduits;
    }

    // Annulation (pour éviter les fuites mémoire)
    public async Task ChargerAvecAnnulationAsync(CancellationToken cancellationToken)
    {
        try
        {
            var produits = await _httpClient.GetFromJsonAsync<List<Produit>>(
                "api/produits",
                cancellationToken);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Opération annulée");
        }
    }
}

// Classe Produit pour les exemples
public class Produit
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public decimal Prix { get; set; }
    public string Description { get; set; } = string.Empty;
    public string Categorie { get; set; } = string.Empty;
    public int Stock { get; set; }
}


// ----------------------------------------------------------------------------
// [RESEAU] EVENTS ET DELEGATES — Communication entre composants
// ----------------------------------------------------------------------------

/*
POURQUOI C'EST IMPORTANT POUR BLAZOR ?

Dans Blazor, les composants communiquent via des events !
- Un bouton "cliqué" envoie un event au composant parent
- Un composant enfant notifie son parent via EventCallback
- Les changements de données déclenchent des re-rendus

DELEGATE = Type qui représente une MÉTHODE
EVENT = Notification envoyée quand quelque chose se passe

ANALOGIE :
Delegate = Numéro de téléphone d'une personne
Event = L'appel téléphonique quand quelque chose se passe
*/

// 1. DELEGATE SIMPLE
public delegate void NotificationDelegate(string message);

// 2. ACTION et FUNC (delegates prédéfinis, plus pratiques)
// Action -> Méthode sans retour
Action<string> afficherMessage = (msg) => Console.WriteLine(msg);
afficherMessage("Hello Blazor!"); // -> "Hello Blazor!"

// Func -> Méthode avec retour
Func<int, int, int> addition = (a, b) => a + b;
int resultat = addition(3, 4); // -> 7

// 3. EVENTS — Notification pattern
public class PanierAchats
{
    private List<Produit> _produits = new();

    // Définir un event (notification)
    public event Action<int>? ProduitAjoute;     // Notifie quand produit ajouté
    public event Action<int>? ProduitSupprime;   // Notifie quand produit supprimé
    public event Action? PanierVide;             // Notifie quand panier vidé

    public void AjouterProduit(Produit produit)
    {
        _produits.Add(produit);

        // Déclencher l'event (notifier tous les abonnés)
        ProduitAjoute?.Invoke(_produits.Count);
        // Le ?. évite NullReferenceException si personne n'écoute
    }

    public void ViderPanier()
    {
        _produits.Clear();
        PanierVide?.Invoke();
    }
}

// UTILISATION DES EVENTS :
// var panier = new PanierAchats();

// S'abonner à l'event (écouter)
// panier.ProduitAjoute += (count) => Console.WriteLine($"Panier: {count} produits");

// Déclencher l'event
// panier.AjouterProduit(new Produit { Nom = "Laptop" });
// -> Affiche : "Panier: 1 produits"

/*
4. EVENTCALLBACK — Version Blazor des events
---------------------------------------------

Dans Blazor, on utilise EventCallback plutôt que event classique
C'est plus optimisé pour les composants Blazor
*/

// Dans un composant Blazor (on verra en détail au chapitre 4)
// [Parameter] public EventCallback<string> OnValeurChangee { get; set; }

// Invoquer dans le composant enfant :
// await OnValeurChangee.InvokeAsync("nouvelle valeur");


// ----------------------------------------------------------------------------
// [SYRINGE] INJECTION DE DÉPENDANCES (DI)
// ----------------------------------------------------------------------------

/*
POURQUOI C'EST FONDAMENTAL POUR BLAZOR ?

L'injection de dépendances est LE mécanisme central de .NET/Blazor !
- Blazor l'utilise pour TOUT
- Les services sont injectés dans les composants
- Les composants n'ont pas besoin de créer leurs dépendances

PROBLÈME SANS DI :
*/

// [X] MAUVAISE APPROCHE (Sans DI)
public class ComposantSansDI
{
    // Crée directement sa dépendance -> Couplage fort !
    private readonly UtilisateurService _service = new UtilisateurService();
    // Impossible de changer l'implémentation, de tester, etc.
}

// [OK] BONNE APPROCHE (Avec DI)
public class ComposantAvecDI
{
    // REÇOIT sa dépendance de l'extérieur -> Couplage faible !
    private readonly IUtilisateurService _service;

    public ComposantAvecDI(IUtilisateurService service)
    {
        _service = service;
    }
}

/*
LES 3 DURÉES DE VIE (Lifetimes) :

1. SINGLETON -> Une seule instance pour TOUTE l'application
   QUAND ? Services partagés, caches, configuration
   EXEMPLE : Logger, Cache, Configuration

2. SCOPED -> Une instance par REQUÊTE (ou connexion SignalR en Blazor Server)
   QUAND ? Services liés à la session utilisateur
   EXEMPLE : DbContext, AuthService, UserState

3. TRANSIENT -> Une nouvelle instance à CHAQUE injection
   QUAND ? Services légers sans état
   EXEMPLE : Validateurs, Mappers
*/

// ENREGISTREMENT DANS Program.cs :
/*
builder.Services.AddSingleton<IMonCache, MonCache>();        // Singleton
builder.Services.AddScoped<IUserService, UserService>();     // Scoped
builder.Services.AddTransient<IEmailValidator, EmailValidator>(); // Transient
*/

// INJECTION DANS UN COMPOSANT BLAZOR :
// @inject IUtilisateurService UtilisateurService

// INJECTION DANS UNE CLASSE :
public class MonService
{
    private readonly IAutreService _autreService;

    // Injection par constructeur
    public MonService(IAutreService autreService)
    {
        _autreService = autreService;
    }
}


// ----------------------------------------------------------------------------
// [?] TYPES NULLABLE — Éviter les NullReferenceException
// ----------------------------------------------------------------------------

/*
POURQUOI C'EST CRUCIAL POUR BLAZOR ?

NullReferenceException = Erreur N°1 en C# !
Dans Blazor, les données peuvent ne pas être chargées encore
Les retours d'API peuvent être null

AVEC LE NULLABLE CONTEXT (activé par défaut en .NET 6+) :
- Le compilateur AVERTIT des risques de null
- Vous devez explicitement indiquer ce qui peut être null (avec ?)
*/

public class ExemplesNullable
{
    // [X] ANCIEN CODE (ambigu, peut être null ?)
    string ancienNom;          // On ne sait pas si null est autorisé

    // [OK] NOUVEAU CODE (explicite)
    string nomNonNull = string.Empty;  // Ne peut PAS être null
    string? nomNullable = null;        // PEUT être null (le ? l'indique)

    public void ExempleNullable()
    {
        string? nom = null;

        // [OK] Opérateur ? (accès conditionnel)
        int? longueur = nom?.Length;        // null si nom est null

        // [OK] Opérateur ?? (valeur par défaut si null)
        string affichage = nom ?? "Inconnu"; // "Inconnu" si nom est null

        // [OK] Opérateur ??= (assigner si null)
        nom ??= "Valeur par défaut";         // Assigne seulement si null

        // [OK] Null check avec pattern matching
        if (nom is not null)
        {
            Console.WriteLine(nom.Length);  // Sûr ici !
        }

        // [OK] Guard clause
        if (nom is null) return;            // Sortir si null
        Console.WriteLine(nom.Length);      // Sûr ici aussi !
    }

    // [OK] DANS BLAZOR - Chargement de données nullable
    public async Task ExempleBlazorNullable()
    {
        Utilisateur? utilisateur = null; // Pas encore chargé

        // ... Chargement async ...
        utilisateur = await ChargerUtilisateurAsync(1);

        // Vérifier avant d'utiliser
        if (utilisateur is not null)
        {
            Console.WriteLine(utilisateur.Nom);
        }

        // Ou avec l'opérateur ?. (null-conditional)
        Console.WriteLine(utilisateur?.Nom ?? "Non trouvé");
    }

    private Task<Utilisateur?> ChargerUtilisateurAsync(int id)
        => Task.FromResult<Utilisateur?>(null); // Simulation
}


// ----------------------------------------------------------------------------
// [RECHERCHE] LINQ — Requêtes sur les collections
// ----------------------------------------------------------------------------

/*
POURQUOI LINQ EST IMPORTANT POUR BLAZOR ?

Dans Blazor, vous manipulez constamment des listes :
- Filtrer des produits dans un catalogue
- Trier une liste d'utilisateurs
- Transformer des données pour l'affichage
- Chercher des éléments

LINQ = Language Integrated Query
Permet d'écrire des requêtes directement en C#
*/

public class ExemplesLinq
{
    private List<Produit> _produits = new List<Produit>
    {
        new() { Id = 1, Nom = "Laptop", Prix = 999.99m, Categorie = "Électronique", Stock = 5 },
        new() { Id = 2, Nom = "Souris", Prix = 29.99m, Categorie = "Électronique", Stock = 50 },
        new() { Id = 3, Nom = "Bureau", Prix = 299.99m, Categorie = "Mobilier", Stock = 10 },
        new() { Id = 4, Nom = "Chaise", Prix = 199.99m, Categorie = "Mobilier", Stock = 15 },
        new() { Id = 5, Nom = "Écran", Prix = 399.99m, Categorie = "Électronique", Stock = 8 },
    };

    public void DemosLinq()
    {
        // WHERE -> Filtrer
        var produitsElectro = _produits.Where(p => p.Categorie == "Électronique");
        // -> Laptop, Souris, Écran

        // ORDER BY -> Trier
        var parPrix = _produits.OrderBy(p => p.Prix);
        var parPrixDesc = _produits.OrderByDescending(p => p.Prix);

        // SELECT -> Transformer (projection)
        var noms = _produits.Select(p => p.Nom);
        // -> ["Laptop", "Souris", "Bureau", ...]

        // SELECT -> Objet différent
        var dtos = _produits.Select(p => new ProduitDto(p.Id, p.Nom, p.Prix));

        // FIRST / FIRSTORDEFAULT -> Premier élément
        var premierProduit = _produits.First();
        var produitCher = _produits.FirstOrDefault(p => p.Prix > 1000); // null si absent

        // SINGLE -> Exactement un élément (erreur si 0 ou 2+)
        // var produit = _produits.Single(p => p.Id == 1);

        // COUNT -> Compter
        int nbElectronique = _produits.Count(p => p.Categorie == "Électronique");

        // SUM, MIN, MAX, AVERAGE -> Agrégation
        decimal totalStock = _produits.Sum(p => p.Prix * p.Stock);
        decimal prixMin = _produits.Min(p => p.Prix);
        decimal prixMax = _produits.Max(p => p.Prix);
        double prixMoyen = (double)_produits.Average(p => p.Prix);

        // ANY -> Au moins un répond à la condition
        bool aDesStocksVides = _produits.Any(p => p.Stock == 0);

        // ALL -> Tous répondent à la condition
        bool tousEnStock = _produits.All(p => p.Stock > 0);

        // GROUP BY -> Grouper
        var parCategorie = _produits.GroupBy(p => p.Categorie);
        foreach (var groupe in parCategorie)
        {
            Console.WriteLine($"{groupe.Key}: {groupe.Count()} produits");
        }

        // CHAINER LES OPÉRATIONS (très courant en Blazor !)
        var resultat = _produits
            .Where(p => p.Categorie == "Électronique") // Filtrer
            .Where(p => p.Prix < 500)                   // Encore filtrer
            .OrderBy(p => p.Prix)                        // Trier
            .Select(p => new { p.Nom, p.Prix })          // Projeter
            .Take(3);                                     // Prendre 3

        // DISTINCT -> Valeurs uniques
        var categories = _produits.Select(p => p.Categorie).Distinct();

        // SKIP + TAKE -> Pagination
        int page = 1;
        int parPage = 2;
        var pagine = _produits
            .Skip((page - 1) * parPage)
            .Take(parPage);
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 2 : COMPRENDRE L'ÉCOSYSTÈME .NET
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre .NET SDK et runtime
[OK] Utiliser la CLI dotnet
[OK] Comprendre la structure d'un projet
[OK] Gérer les packages NuGet
[OK] Configurer l'application
*/


// ----------------------------------------------------------------------------
// [OUTIL] .NET SDK — La boîte à outils
// ----------------------------------------------------------------------------

/*
QU'EST-CE QUE .NET SDK ?

SDK = Software Development Kit
C'est l'ensemble d'outils pour CRÉER des applications .NET :
- Compilateur C#
- CLI (ligne de commande)
- Templates de projets
- Runtime inclus

INSTALLER .NET SDK :
-> https://dot.net -> Télécharger .NET 8.0 (LTS)

VÉRIFIER L'INSTALLATION :
dotnet --version          -> Affiche la version (ex: 8.0.100)
dotnet --info             -> Infos détaillées
dotnet --list-sdks        -> Toutes les versions installées
*/


// ----------------------------------------------------------------------------
// [CODE] CLI DOTNET — Commandes essentielles
// ----------------------------------------------------------------------------

/*
LA CLI DOTNET = Votre meilleur ami !
Toutes les opérations se font en ligne de commande.

CRÉER DES PROJETS :

dotnet new blazorwasm -n MonAppBlazor
    ^ Template    ^ Nom du projet
    Templates disponibles :
    - blazorwasm     -> Blazor WebAssembly standalone
    - blazorserver   -> Blazor Server
    - blazorwasm-empty -> WebAssembly vide (sans exemples)

dotnet new blazorwasm -n MonApp --hosted
    --hosted -> Crée aussi un projet ASP.NET Core serveur

LISTE DE TOUS LES TEMPLATES :
dotnet new list | grep blazor


GÉRER LES PACKAGES :

dotnet add package NomDuPackage                 -> Ajouter
dotnet add package NomDuPackage --version 8.0.0 -> Version spécifique
dotnet remove package NomDuPackage              -> Retirer
dotnet list package                             -> Lister les packages


CONSTRUIRE ET LANCER :

dotnet build            -> Compiler le projet
dotnet run              -> Compiler ET lancer
dotnet run --watch      -> Lancer avec Hot Reload (rechargement auto)
dotnet publish          -> Préparer pour production
dotnet test             -> Lancer les tests


COMMANDES UTILES :

dotnet restore          -> Restaurer les packages NuGet
dotnet clean            -> Nettoyer les fichiers de build
dotnet format           -> Formater le code
dotnet --help           -> Aide
*/


// ----------------------------------------------------------------------------
// [DOSSIER] STRUCTURE D'UN PROJET BLAZOR WEBASSEMBLY
// ----------------------------------------------------------------------------

/*
APRÈS dotnet new blazorwasm -n MonApp :

MonApp/
├── MonApp.csproj              <- Configuration du projet (XML)
├── Program.cs                 <- Point d'entrée de l'application
├── App.razor                  <- Composant racine (routing)
├── MainLayout.razor           <- Layout principal (dans Shared/)
├── _Imports.razor             <- Imports globaux (using)
├── wwwroot/                   <- Fichiers statiques (servis directement)
│   ├── index.html             <- Page HTML unique (SPA)
│   ├── favicon.ico
│   ├── css/
│   │   └── app.css            <- CSS principal
│   └── appsettings.json       <- Config côté client
├── Pages/                     <- Pages Blazor (composants avec @page)
│   ├── Home.razor             <- Page d'accueil
│   ├── Counter.razor          <- Exemple compteur
│   └── FetchData.razor        <- Exemple fetch données
└── Shared/                    <- Composants réutilisables
    ├── MainLayout.razor       <- Layout principal
    ├── NavMenu.razor          <- Navigation
    └── SurveyPrompt.razor     <- Exemple composant partagé


FICHIERS CLÉS EXPLIQUÉS :


[FICHIER] MonApp.csproj — Configuration du projet
*/

// MonApp.csproj (contenu)
/*
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>   <- Version .NET
    <Nullable>enable</Nullable>                 <- Nullable references activé
    <ImplicitUsings>enable</ImplicitUsings>     <- Using automatiques
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly"
                      Version="8.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer"
                      Version="8.0.0" PrivateAssets="all" />
  </ItemGroup>

</Project>
*/


// ----------------------------------------------------------------------------
// [FICHIER] PROGRAM.CS — Point d'entrée de l'application
// ----------------------------------------------------------------------------

/*
Program.cs est LE fichier de démarrage de votre application.
C'est ici que vous :
1. Créez l'application
2. Enregistrez les services (DI)
3. Configurez les middlewares
4. Lancez l'application
*/

// Program.cs pour Blazor WebAssembly
/*
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MonApp;

// 1. Créer le builder (configurateur)
var builder = WebAssemblyHostBuilder.CreateDefault(args);

// 2. Définir le composant racine et l'élément HTML cible
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// 3. Configurer HttpClient (pour les appels API)
builder.Services.AddScoped(sp => new HttpClient
{
    BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});

// 4. Enregistrer VOS services (Dependency Injection)
builder.Services.AddScoped<IUtilisateurService, UtilisateurService>();
builder.Services.AddSingleton<IMonCache, MonCache>();

// 5. Lancer l'application
await builder.Build().RunAsync();
*/


// ----------------------------------------------------------------------------
// [CONFIG] CONFIGURATION — appsettings.json
// ----------------------------------------------------------------------------

/*
LA CONFIGURATION dans Blazor WebAssembly est côté CLIENT.
Le fichier est dans wwwroot/appsettings.json

[ATTENTION] ATTENTION : En WebAssembly, les configs sont VISIBLES par l'utilisateur !
Ne jamais mettre de secrets (mots de passe, clés API privées) là dedans.
*/

// wwwroot/appsettings.json
/*
{
  "ApiSettings": {
    "BaseUrl": "https://api.monapp.com",
    "Version": "v1",
    "Timeout": 30
  },
  "FeatureFlags": {
    "NouvelleFeature": true,
    "ModeDebug": false
  }
}
*/

// Classe de configuration
public class ApiSettings
{
    public string BaseUrl { get; set; } = string.Empty;
    public string Version { get; set; } = "v1";
    public int Timeout { get; set; } = 30;
}

// Enregistrement dans Program.cs
// builder.Services.Configure<ApiSettings>(
//     builder.Configuration.GetSection("ApiSettings"));

// Utilisation dans un service
public class MonServiceAvecConfig
{
    private readonly ApiSettings _settings;

    public MonServiceAvecConfig(IOptions<ApiSettings> options)
    {
        _settings = options.Value;
    }

    public string ObtenirUrlComplete(string endpoint)
    {
        return $"{_settings.BaseUrl}/{_settings.Version}/{endpoint}";
    }
}


// ----------------------------------------------------------------------------
// [PACKAGE] PACKAGES NUGET ESSENTIELS POUR BLAZOR
// ----------------------------------------------------------------------------

/*
PACKAGES COURANTS EN BLAZOR :

UI COMPONENTS :
dotnet add package MudBlazor                     -> UI Framework complet (Material Design)
dotnet add package Blazorise                     -> Autre framework UI
dotnet add package AntDesign                     -> Ant Design pour Blazor

HTTP ET SERIALISATION :
dotnet add package System.Net.Http.Json          -> GetFromJsonAsync (déjà dans .NET 8)

FORMULAIRES ET VALIDATION :
dotnet add package FluentValidation              -> Validation avancée
dotnet add package FluentValidation.Blazor       -> Intégration Blazor

GESTION D'ÉTAT :
dotnet add package Fluxor                        -> Pattern Redux/Flux
dotnet add package Blazored.LocalStorage         -> LocalStorage

AUTHENTIFICATION :
dotnet add package Microsoft.Authentication.WebAssembly.Msal -> Azure AD
dotnet add package Blazored.Toast                -> Notifications toast

DIAGRAMMES ET CHARTS :
dotnet add package Blazor.ApexCharts             -> Graphiques
dotnet add package ChartJs.Blazor               -> Chart.js pour Blazor

ROUTING :
(Déjà inclus dans Blazor)
*/


// ============================================================================
// [GUIDE] CHAPITRE 3 : INTRODUCTION À BLAZOR
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre l'architecture globale de Blazor
[OK] Différencier Blazor Server et WebAssembly
[OK] Comprendre le cycle de vie d'un composant
[OK] Écrire votre première syntaxe Razor
[OK] Créer votre premier composant
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] ARCHITECTURE GLOBALE DE BLAZOR
// ----------------------------------------------------------------------------

/*
QU'EST-CE QUE BLAZOR ?

Blazor = Browser + Razor
C'est un framework Microsoft pour créer des applications web interactives
avec C# au lieu de JavaScript !

AVANT BLAZOR :
Front-end -> JavaScript (React, Angular, Vue)
Back-end  -> C#, Java, Python...
-> Deux langages, deux équipes, deux contextes

AVEC BLAZOR :
Front-end -> C# (via WebAssembly ou SignalR)
Back-end  -> C#
-> Un seul langage, partage de code, une seule équipe !

COMMENT ÇA MARCHE ?

Option 1 : BLAZOR WEBASSEMBLY
- Le code C# EST COMPILÉ pour tourner dans le navigateur
- Utilise WebAssembly (standard navigateur)
- Fonctionne ENTIÈREMENT côté client (comme React)

Option 2 : BLAZOR SERVER
- Le code C# tourne sur le SERVEUR
- Le DOM est mis à jour via SignalR (WebSocket)
- Le navigateur n'exécute que du JS minimal

COMPOSANT = Brique de base de Blazor
Un composant = Un fichier .razor = HTML + C# + CSS (optionnel)


SCHÉMA CONCEPTUEL BLAZOR WEBASSEMBLY :

Navigateur
┌─────────────────────────────────────────┐
│  HTML/CSS                               │
│  ┌─────────────────────────────────┐    │
│  │  WebAssembly Runtime (.NET)     │    │
│  │  ┌───────────────────────────┐  │    │
│  │  │  Votre code C#/Blazor     │  │    │
│  │  │  - Composants             │  │    │
│  │  │  - Services               │  │    │
│  │  │  - Logique métier         │  │    │
│  │  └───────────────────────────┘  │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘
         ^v HTTP (API calls)
Serveur (ASP.NET Core API)
*/


// ----------------------------------------------------------------------------
// [SCALES] BLAZOR SERVER vs BLAZOR WEBASSEMBLY
// ----------------------------------------------------------------------------

/*
╔══════════════════════════╦══════════════════════╦════════════════════════╗
║ Critère                  ║ Blazor Server         ║ Blazor WebAssembly     ║
╠══════════════════════════╬══════════════════════╬════════════════════════╣
║ Exécution du code        ║ Serveur               ║ Navigateur (WASM)      ║
║ Communication            ║ SignalR (WebSocket)   ║ HTTP (API calls)       ║
║ Chargement initial       ║ Rapide (petit bundle) ║ Lent (télécharge .NET) ║
║ Latence                  ║ Oui (aller-retour)    ║ Non (exécution locale) ║
║ Accès base de données    ║ Direct                ║ Via API seulement      ║
║ Connexion requise        ║ Permanente (SignalR)  ║ Non (offline possible) ║
║ Secrets côté client      ║ Sécurisés             ║ Exposés                ║
║ SEO                      ║ Excellent             ║ Limité (amélioré)      ║
║ Scalabilité              ║ Mémoire par user      ║ Excellente             ║
║ Idéal pour               ║ Intranets, backoffice ║ Applications publiques ║
╚══════════════════════════╩══════════════════════╩════════════════════════╝


QUAND CHOISIR QUOI ?

BLAZOR SERVER -> Choisir si :
[OK] Application interne (intranet)
[OK] Données sensibles / sécurité forte
[OK] Chargement rapide important
[OK] Accès direct à la base de données
[OK] Connexion stable garantie

BLAZOR WEBASSEMBLY -> Choisir si :
[OK] Application publique
[OK] Offline/PWA nécessaire
[OK] Pas de serveur (GitHub Pages, Netlify)
[OK] Réactivité maximale (pas de latence)
[OK] Coût serveur minimal
*/


// ----------------------------------------------------------------------------
// [SYNC] CYCLE DE VIE D'UN COMPOSANT
// ----------------------------------------------------------------------------

/*
COMPRENDRE LE CYCLE DE VIE = COMPRENDRE BLAZOR !

Un composant passe par plusieurs étapes :
1. INITIALISATION -> Composant créé, paramètres reçus
2. RENDU -> HTML généré et affiché
3. MISE À JOUR -> Paramètres changent, re-rendu
4. DESTRUCTION -> Composant retiré du DOM


MÉTHODES DU CYCLE DE VIE :
*/

// Exemple de composant avec TOUTES les méthodes de cycle de vie
// (Fichier MonComposant.razor)

/*
@implements IDisposable

@code {
    [Parameter]
    public int Id { get; set; }

    private string _donnees = string.Empty;
    private bool _estCharge = false;

    // 1. SETPARAMETERSASYNC — Premier appelé
    // Reçoit les paramètres du composant parent
    // Rarement overridé, mais possible pour contrôle total
    public override async Task SetParametersAsync(ParameterView parameters)
    {
        await base.SetParametersAsync(parameters);
        // Appelé AVANT OnInitialized
    }

    // 2. ONINITIALIZED — Exécuté UNE FOIS à la création
    // Parfait pour initialiser l'état
    protected override void OnInitialized()
    {
        // Synchrone : utilisé pour init rapide
        _donnees = "Valeur initiale";
        Console.WriteLine("OnInitialized: composant créé");
    }

    // 3. ONINITIALIZEDASYNC — Version async de OnInitialized
    // Chargement de données depuis une API
    protected override async Task OnInitializedAsync()
    {
        // Asynchrone : utilisé pour appels API, BD, etc.
        _donnees = await ChargerDonneesAsync(Id);
        _estCharge = true;
        Console.WriteLine("OnInitializedAsync: données chargées");
    }

    // 4. ONPARAMETERSSET — À CHAQUE changement de paramètres
    // Appelé après OnInitialized, puis à chaque mise à jour params
    protected override void OnParametersSet()
    {
        Console.WriteLine($"OnParametersSet: Id = {Id}");
    }

    // 5. ONPARAMETERSSETASYNC — Version async
    protected override async Task OnParametersSetAsync()
    {
        // Si les données dépendent des paramètres, recharger ici
        if (Id > 0)
        {
            _donnees = await ChargerDonneesAsync(Id);
        }
    }

    // 6. ONAFTERRENDER — APRÈS le rendu HTML
    // firstRender = true seulement la première fois
    // Parfait pour les interopérations JavaScript
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("Premier rendu : initialiser JS ici");
            // await JSRuntime.InvokeVoidAsync("initializeChart", ...);
        }
    }

    // 7. ONAFTERRENDERASYNC — Version async
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await InitialiserJavaScriptAsync();
        }
    }

    // 8. SHOULDRENDER — Contrôle le re-rendu
    // Retourner false pour éviter un re-rendu inutile
    // Optimisation de performance
    protected override bool ShouldRender()
    {
        // Par défaut retourne true (toujours re-rendre)
        return _estCharge; // Ne rendre que si données chargées
    }

    // 9. DISPOSE — Nettoyage (IDisposable)
    // IMPORTANT : Éviter les fuites mémoire !
    // Se désabonner des events, annuler les timers, etc.
    public void Dispose()
    {
        Console.WriteLine("Dispose: nettoyage des ressources");
        // _timer?.Dispose();
        // _subscription?.Dispose();
    }

    private Task<string> ChargerDonneesAsync(int id)
        => Task.FromResult($"Données pour Id={id}");

    private Task InitialiserJavaScriptAsync()
        => Task.CompletedTask;
}
*/

/*
RÉSUMÉ DU CYCLE DE VIE :

Création du composant
    v
SetParametersAsync -> OnInitialized(Async)
    v
Rendu initial
    v
OnAfterRender(Async) [firstRender=true]
    v
Paramètres changent -> OnParametersSet(Async) -> Re-rendu
    v
État change -> StateHasChanged() -> Re-rendu
    v
OnAfterRender(Async) [firstRender=false]
    v
Composant retiré -> Dispose()
*/


// ----------------------------------------------------------------------------
// [EDIT] SYNTAXE RAZOR — HTML + C# dans un seul fichier
// ----------------------------------------------------------------------------

/*
RAZOR = Moteur de templates qui mélange HTML et C#
@ = Symbole pour passer du HTML au C#


LES RÈGLES DE BASE :
*/

/*
@page "/exemple"
@using MonApp.Models
@inject IUtilisateurService UtilisateurService

<!-- Contenu HTML normal -->
<div class="container">

    <!-- 1. AFFICHER UNE VARIABLE : @variable -->
    <h1>Bonjour, @_prenom !</h1>

    <!-- 2. EXPRESSION C# INLINE : @(expression) -->
    <p>Age : @(_anneeNaissance.Year) ans</p>

    <!-- 3. CODE C# INLINE : @ { ... } -->
    @{
        var message = _estConnecte ? "Connecté" : "Déconnecté";
        var cssClass = _estConnecte ? "success" : "danger";
    }

    <!-- 4. UTILISER VARIABLE CRÉÉE AU-DESSUS -->
    <span class="badge @cssClass">@message</span>

    <!-- 5. CONDITIONS : @if ... -->
    @if (_chargement)
    {
        <p>Chargement en cours...</p>
    }
    else if (_utilisateurs.Count == 0)
    {
        <p>Aucun utilisateur trouvé.</p>
    }
    else
    {
        <p>@_utilisateurs.Count utilisateur(s) trouvé(s)</p>
    }

    <!-- 6. BOUCLES : @foreach ... -->
    <ul>
        @foreach (var user in _utilisateurs)
        {
            <li>@user.Nom - @user.Email</li>
        }
    </ul>

    <!-- 7. SWITCH -->
    @switch (_statut)
    {
        case "actif":
            <span class="text-success">[OK] Actif</span>
            break;
        case "inactif":
            <span class="text-danger">[X] Inactif</span>
            break;
        default:
            <span class="text-warning">[HOURGLASS_WITH_FLOWING_SAND] En attente</span>
            break;
    }

    <!-- 8. ÉVÉNEMENTS : @onclick, @oninput, etc. -->
    <button @onclick="ChargerUtilisateurs">Charger</button>
    <input @oninput="MettreAJourRecherche" placeholder="Rechercher..." />

    <!-- 9. BINDING BIDIRECTIONNEL : @bind -->
    <input @bind="_prenom" placeholder="Votre prénom" />
    <!-- @bind = Liaison dans les DEUX sens (input <-> variable) -->

</div>

<!-- SECTION CODE C# DU COMPOSANT -->
@code {
    private string _prenom = "Visiteur";
    private DateTime _anneeNaissance = new(2000, 1, 1);
    private bool _estConnecte = false;
    private bool _chargement = false;
    private string _statut = "actif";
    private List<Utilisateur> _utilisateurs = new();

    // Méthode appelée par le bouton
    private async Task ChargerUtilisateurs()
    {
        _chargement = true;
        _utilisateurs = await UtilisateurService.ObtenirTousAsync();
        _chargement = false;
        // StateHasChanged() est appelé automatiquement après await
    }

    private void MettreAJourRecherche(ChangeEventArgs e)
    {
        var recherche = e.Value?.ToString() ?? string.Empty;
        // Filtrer la liste...
    }
}
*/


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE — CHAPITRE 1, 2 ET 3
// ----------------------------------------------------------------------------

/*
═══════════════════════════════════════════════════════════════
EXERCICE : MINI-CATALOGUE DE PRODUITS
═══════════════════════════════════════════════════════════════

OBJECTIF :
Créer une application Blazor WebAssembly simple qui affiche
un catalogue de produits avec filtre, tri et compteur.

CAHIER DES CHARGES :

1. Créer un modèle Produit avec :
   - Id, Nom, Prix, Catégorie, StockDisponible, EstNouveau

2. Créer une interface IProduitService avec :
   - ObtenirTousAsync()
   - RechercherAsync(string terme)
   - FiltrerParCategorieAsync(string categorie)

3. Créer ProduitService (implémentation avec données fictives)

4. Créer une page Blazor /produits qui :
   - Affiche "Chargement..." pendant le chargement
   - Liste les produits dans des cartes
   - Permet de filtrer par catégorie (dropdown)
   - Affiche le nombre de produits trouvés
   - Affiche un badge "NOUVEAU" si EstNouveau = true
   - Gère le cas "aucun produit trouvé"

ÉTAPES :
1. dotnet new blazorwasm -n CatalogueProduits
2. Créer Models/Produit.cs
3. Créer Services/IProduitService.cs
4. Créer Services/ProduitService.cs
5. Créer Pages/Produits.razor
6. Enregistrer le service dans Program.cs

COMPÉTENCES UTILISÉES :
[OK] Records/Classes C#
[OK] Interfaces et DI
[OK] Async/await
[OK] LINQ (filtrage, tri)
[OK] Nullable types
[OK] Cycle de vie (OnInitializedAsync)
[OK] Syntaxe Razor (@if, @foreach, @bind)
═══════════════════════════════════════════════════════════════
*/


// ----------------------------------------------------------------------------
// [OK] CORRIGÉ DE L'EXERCICE
// ----------------------------------------------------------------------------

// ─── Models/Produit.cs ───────────────────────────────────────────────────────

public class Produit
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public decimal Prix { get; set; }
    public string Categorie { get; set; } = string.Empty;
    public int StockDisponible { get; set; }
    public bool EstNouveau { get; set; }

    // Propriété calculée
    public bool EnStock => StockDisponible > 0;
}

// ─── Services/IProduitService.cs ─────────────────────────────────────────────

public interface IProduitService
{
    Task<List<Produit>> ObtenirTousAsync();
    Task<List<Produit>> RechercherAsync(string terme);
    Task<List<Produit>> FiltrerParCategorieAsync(string categorie);
    Task<List<string>> ObtenirCategoriesAsync();
}

// ─── Services/ProduitService.cs ──────────────────────────────────────────────

public class ProduitService : IProduitService
{
    // Données fictives (en production : appel API)
    private readonly List<Produit> _catalogue = new()
    {
        new() { Id=1, Nom="Laptop Pro", Prix=1299.99m, Categorie="Électronique",
                StockDisponible=5, EstNouveau=true },
        new() { Id=2, Nom="Souris Ergonomique", Prix=49.99m, Categorie="Électronique",
                StockDisponible=30, EstNouveau=false },
        new() { Id=3, Nom="Clavier Mécanique", Prix=149.99m, Categorie="Électronique",
                StockDisponible=15, EstNouveau=true },
        new() { Id=4, Nom="Bureau Debout", Prix=599.99m, Categorie="Mobilier",
                StockDisponible=8, EstNouveau=false },
        new() { Id=5, Nom="Chaise Ergonomique", Prix=399.99m, Categorie="Mobilier",
                StockDisponible=12, EstNouveau=false },
        new() { Id=6, Nom="Lampe LED", Prix=89.99m, Categorie="Accessoires",
                StockDisponible=0, EstNouveau=false },
        new() { Id=7, Nom="Webcam 4K", Prix=199.99m, Categorie="Électronique",
                StockDisponible=7, EstNouveau=true },
    };

    public async Task<List<Produit>> ObtenirTousAsync()
    {
        await Task.Delay(300); // Simuler délai réseau
        return _catalogue.ToList();
    }

    public async Task<List<Produit>> RechercherAsync(string terme)
    {
        await Task.Delay(200);
        if (string.IsNullOrWhiteSpace(terme))
            return await ObtenirTousAsync();

        return _catalogue
            .Where(p => p.Nom.Contains(terme, StringComparison.OrdinalIgnoreCase)
                     || p.Categorie.Contains(terme, StringComparison.OrdinalIgnoreCase))
            .ToList();
    }

    public async Task<List<Produit>> FiltrerParCategorieAsync(string categorie)
    {
        await Task.Delay(150);
        if (string.IsNullOrEmpty(categorie) || categorie == "Toutes")
            return await ObtenirTousAsync();

        return _catalogue
            .Where(p => p.Categorie == categorie)
            .OrderBy(p => p.Nom)
            .ToList();
    }

    public async Task<List<string>> ObtenirCategoriesAsync()
    {
        await Task.Delay(50);
        return _catalogue
            .Select(p => p.Categorie)
            .Distinct()
            .OrderBy(c => c)
            .ToList();
    }
}


// ─── Pages/Produits.razor ────────────────────────────────────────────────────

/*
FICHIER : Pages/Produits.razor

@page "/produits"
@inject IProduitService ProduitService

<PageTitle>Catalogue Produits</PageTitle>

<div class="container mt-4">
    <h1>[SHOPPING_BAGS] Catalogue Produits</h1>

    <!-- Barre de filtres -->
    <div class="row mb-4">
        <div class="col-md-4">
            <label class="form-label">Filtrer par catégorie :</label>
            <select class="form-select" @bind="_categorieSelectionnee" @bind:after="FiltrerProduits">
                <option value="Toutes">Toutes les catégories</option>
                @foreach (var cat in _categories)
                {
                    <option value="@cat">@cat</option>
                }
            </select>
        </div>
        <div class="col-md-8 d-flex align-items-end">
            @if (!_chargement)
            {
                <span class="text-muted">
                    @_produitsFiltres.Count produit(s) trouvé(s)
                </span>
            }
        </div>
    </div>

    <!-- État de chargement -->
    @if (_chargement)
    {
        <div class="text-center">
            <div class="spinner-border" role="status"></div>
            <p class="mt-2">Chargement du catalogue...</p>
        </div>
    }
    else if (_produitsFiltres.Count == 0)
    {
        <div class="alert alert-info">
            <i class="bi bi-info-circle"></i>
            Aucun produit trouvé pour cette catégorie.
        </div>
    }
    else
    {
        <!-- Grille de produits -->
        <div class="row row-cols-1 row-cols-md-3 g-4">
            @foreach (var produit in _produitsFiltres)
            {
                <div class="col">
                    <div class="card h-100 @(produit.EnStock ? "" : "opacity-50")">
                        <div class="card-body">
                            <div class="d-flex justify-content-between">
                                <h5 class="card-title">@produit.Nom</h5>
                                @if (produit.EstNouveau)
                                {
                                    <span class="badge bg-success">NOUVEAU</span>
                                }
                            </div>
                            <p class="card-text text-muted">@produit.Categorie</p>
                            <p class="card-text fw-bold fs-5">@produit.Prix.ToString("C")</p>
                        </div>
                        <div class="card-footer">
                            @if (produit.EnStock)
                            {
                                <small class="text-success">
                                    [OK] En stock (@produit.StockDisponible restants)
                                </small>
                            }
                            else
                            {
                                <small class="text-danger">[X] Rupture de stock</small>
                            }
                        </div>
                    </div>
                </div>
            }
        </div>
    }
</div>

@code {
    private List<Produit> _tousLesProduits = new();
    private List<Produit> _produitsFiltres = new();
    private List<string> _categories = new();
    private string _categorieSelectionnee = "Toutes";
    private bool _chargement = true;

    // Cycle de vie : Chargement initial
    protected override async Task OnInitializedAsync()
    {
        _chargement = true;

        // Charger catégories et produits en parallèle
        var tacheCategories = ProduitService.ObtenirCategoriesAsync();
        var tacheProduits = ProduitService.ObtenirTousAsync();

        await Task.WhenAll(tacheCategories, tacheProduits);

        _categories = await tacheCategories;
        _tousLesProduits = await tacheProduits;
        _produitsFiltres = _tousLesProduits;

        _chargement = false;
    }

    // Filtrer quand la catégorie change
    private async Task FiltrerProduits()
    {
        _chargement = true;
        _produitsFiltres = await ProduitService
            .FiltrerParCategorieAsync(_categorieSelectionnee);
        _chargement = false;
    }
}
*/


// ─── Program.cs (ajout du service) ───────────────────────────────────────────

/*
// Dans Program.cs, ajouter AVANT builder.Build() :
builder.Services.AddScoped<IProduitService, ProduitService>();
*/


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 1

[OK] CHAPITRE 1 - C# ESSENTIEL :
- Classes, interfaces, records
- Async/await pour les opérations réseau
- Events et delegates pour la communication
- Dependency Injection (DI) : Singleton, Scoped, Transient
- Types nullable (?, ??, ??=)
- LINQ pour manipuler les collections

[OK] CHAPITRE 2 - ÉCOSYSTÈME .NET :
- SDK et CLI dotnet
- Structure d'un projet Blazor
- Program.cs : configuration et DI
- appsettings.json : configuration
- Packages NuGet essentiels

[OK] CHAPITRE 3 - INTRODUCTION BLAZOR :
- Architecture et différences Server/WebAssembly
- Cycle de vie complet d'un composant
- Syntaxe Razor : @if, @foreach, @bind, @onclick
- Premier composant fonctionnel

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 2
- Composants avancés avec [Parameter]
- EventCallback entre composants
- Data binding bidirectionnel
- Formulaires et validation
- Routing avancé
═══════════════════════════════════════════════════════════════
*/

// ============================================================================
// [LIVRE] BLAZOR - PARTIE 2 : BLAZOR FONDAMENTAL (CORE SKILLS)
// ============================================================================
//
// CHAPITRE 4 : Composants Blazor
// CHAPITRE 5 : Data Binding
// CHAPITRE 6 : Routing
// CHAPITRE 7 : Cycle de vie (approfondi)
//
// [TEMPS] TEMPS ESTIMÉ : ~8-10 heures
// [DOCS] PRÉREQUIS : Partie 1 complétée
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 4 : COMPOSANTS BLAZOR
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer des composants réutilisables
[OK] Utiliser les paramètres [Parameter]
[OK] Communiquer avec EventCallback
[OK] Utiliser CascadingParameter
[OK] Créer des templates avec RenderFragment
[OK] Organiser les composants en fichiers séparés
*/


// ----------------------------------------------------------------------------
// [MODULE] CRÉER UN COMPOSANT — La base de Blazor
// ----------------------------------------------------------------------------

/*
UN COMPOSANT BLAZOR = Fichier .razor
Il contient 3 parties (toutes optionnelles) :

1. @directives         -> Configuration (@page, @inject, @using...)
2. HTML/Markup         -> Le template visuel
3. @code { }           -> La logique C#


FORMES D'UN COMPOSANT :

Forme 1 : Tout dans un fichier .razor (simple, pratique)
Forme 2 : Séparé en .razor + .razor.cs (code-behind, propre pour grands projets)
Forme 3 : Séparé en .razor + .razor.cs + .razor.css (avec styles isolés)
*/

/*
─────────────────────────────────────────────────────────────────
FORME 1 : Composant simple (tout dans .razor)
─────────────────────────────────────────────────────────────────
Fichier : Components/AlertMessage.razor
*/

/*
<!-- Pas de @page -> C'est un composant, pas une page -->

<div class="alert alert-@_typeCss alert-dismissible" role="alert">
    <strong>@Titre</strong>
    <p>@Message</p>
    @if (Fermable)
    {
        <button type="button" class="btn-close" @onclick="Fermer"></button>
    }
</div>

@code {
    // PARAMÈTRES : Données reçues du composant parent
    [Parameter] public string Titre { get; set; } = "Information";
    [Parameter] public string Message { get; set; } = string.Empty;
    [Parameter] public string Type { get; set; } = "info"; // "info", "success", "danger", "warning"
    [Parameter] public bool Fermable { get; set; } = true;
    [Parameter] public EventCallback OnFerme { get; set; } // Event vers le parent

    private bool _visible = true;

    // Propriété calculée : déduire la classe CSS selon le type
    private string _typeCss => Type switch
    {
        "success" => "success",
        "danger"  => "danger",
        "warning" => "warning",
        _         => "info"
    };

    private async Task Fermer()
    {
        _visible = false;
        await OnFerme.InvokeAsync(); // Notifier le parent
    }
}
*/


// ----------------------------------------------------------------------------
// [ENTREE] PARAMÈTRES [Parameter] — Données du parent -> enfant
// ----------------------------------------------------------------------------

/*
RÈGLES DES PARAMÈTRES :

1. Doit être une propriété publique (public)
2. Doit avoir [Parameter] ou [CascadingParameter]
3. Généralement avec { get; set; }
4. Peut avoir une valeur par défaut

TYPES VALIDES :
- Types primitifs : string, int, bool, decimal, DateTime
- Objets/classes : Utilisateur, Produit, etc.
- Collections : List<T>, IEnumerable<T>
- EventCallback, EventCallback<T>
- RenderFragment
- Dictionaries pour attributs supplémentaires
*/

/*
─────────────────────────────────────────────────────────────────
Fichier : Components/CarteUtilisateur.razor
─────────────────────────────────────────────────────────────────
*/

/*
<div class="card @(Selectionne ? "border-primary" : "")">
    <div class="card-body">
        <h5 class="card-title">@Utilisateur.Nom</h5>
        <p class="card-text">@Utilisateur.Email</p>

        <!-- Affichage conditionnel selon paramètre -->
        @if (AfficherBoutons)
        {
            <button class="btn btn-primary btn-sm me-2"
                    @onclick="() => OnEditer.InvokeAsync(Utilisateur.Id)">
                Éditer
            </button>
            <button class="btn btn-danger btn-sm"
                    @onclick="() => OnSupprimer.InvokeAsync(Utilisateur.Id)">
                Supprimer
            </button>
        }
    </div>
</div>

@code {
    // ─── Paramètres obligatoires ───
    [Parameter, EditorRequired]  // EditorRequired -> Erreur si non fourni !
    public Utilisateur Utilisateur { get; set; } = default!;

    // ─── Paramètres optionnels avec valeurs par défaut ───
    [Parameter] public bool AfficherBoutons { get; set; } = true;
    [Parameter] public bool Selectionne { get; set; } = false;

    // ─── EventCallbacks : Notifications vers le parent ───
    [Parameter] public EventCallback<int> OnEditer { get; set; }
    [Parameter] public EventCallback<int> OnSupprimer { get; set; }
    [Parameter] public EventCallback<Utilisateur> OnSelectionner { get; set; }
}
*/

/*
─────────────────────────────────────────────────────────────────
UTILISATION DU COMPOSANT dans une page parente :
─────────────────────────────────────────────────────────────────
*/

/*
@page "/utilisateurs"
@inject IUtilisateurService UtilisateurService

<h1>Liste des Utilisateurs</h1>

@foreach (var user in _utilisateurs)
{
    <!-- Utiliser le composant CarteUtilisateur -->
    <CarteUtilisateur
        Utilisateur="@user"
        AfficherBoutons="true"
        Selectionne="@(_utilisateurSelectionne?.Id == user.Id)"
        OnEditer="OuvrirEdition"
        OnSupprimer="ConfirmerSuppression"
        OnSelectionner="Selectionner" />
}

@code {
    private List<Utilisateur> _utilisateurs = new();
    private Utilisateur? _utilisateurSelectionne;

    protected override async Task OnInitializedAsync()
    {
        _utilisateurs = await UtilisateurService.ObtenirTousAsync();
    }

    private void OuvrirEdition(int id)
    {
        Console.WriteLine($"Ouvrir édition pour {id}");
    }

    private async Task ConfirmerSuppression(int id)
    {
        await UtilisateurService.SupprimerAsync(id);
        _utilisateurs = await UtilisateurService.ObtenirTousAsync();
    }

    private void Selectionner(Utilisateur user)
    {
        _utilisateurSelectionne = user;
    }
}
*/


// ----------------------------------------------------------------------------
// [ANNONCE] EVENTCALLBACK — Communication enfant -> parent
// ----------------------------------------------------------------------------

/*
EVENTCALLBACK vs EVENT CLASSIQUE :

Event classique (C#) :
-> Pas de StateHasChanged() automatique
-> Gestion des exceptions manuelle

EventCallback (Blazor) :
-> StateHasChanged() automatique après invocation
-> Gestion exceptions intégrée
-> Supporte async

TYPES :
EventCallback       -> Sans paramètre
EventCallback<T>    -> Avec paramètre de type T
*/

/*
─────────────────────────────────────────────────────────────────
Exemple : Composant de recherche avec callbacks
─────────────────────────────────────────────────────────────────
Fichier : Components/BarreRecherche.razor
*/

/*
<div class="input-group mb-3">
    <input type="text"
           class="form-control"
           placeholder="@Placeholder"
           @bind="_terme"
           @bind:event="oninput"
           @onkeyup="GererToucheEntree" />
    <button class="btn btn-primary" @onclick="LancerRecherche">
        [RECHERCHE] Rechercher
    </button>
    @if (!string.IsNullOrEmpty(_terme))
    {
        <button class="btn btn-outline-secondary" @onclick="Effacer">
            [X]
        </button>
    }
</div>

@code {
    [Parameter] public string Placeholder { get; set; } = "Rechercher...";
    [Parameter] public EventCallback<string> OnRecherche { get; set; }
    [Parameter] public EventCallback OnEfface { get; set; }

    private string _terme = string.Empty;

    private async Task LancerRecherche()
    {
        await OnRecherche.InvokeAsync(_terme);
        // InvokeAsync -> Appelle la méthode du parent avec le terme
    }

    private async Task GererToucheEntree(KeyboardEventArgs e)
    {
        if (e.Key == "Enter")
            await LancerRecherche();
    }

    private async Task Effacer()
    {
        _terme = string.Empty;
        await OnRecherche.InvokeAsync(string.Empty);
        await OnEfface.InvokeAsync();
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
UTILISATION dans le parent :
─────────────────────────────────────────────────────────────────
*/

/*
<BarreRecherche
    Placeholder="Chercher un produit..."
    OnRecherche="RechercherProduits"
    OnEfface="ReinitialiserListe" />

@foreach (var produit in _produitsFiltres)
{
    <CarteProduit Produit="produit" />
}

@code {
    private List<Produit> _tousProduits = new();
    private List<Produit> _produitsFiltres = new();

    private async Task RechercherProduits(string terme)
    {
        // 'terme' vient du composant enfant BarreRecherche
        _produitsFiltres = _tousProduits
            .Where(p => p.Nom.Contains(terme, StringComparison.OrdinalIgnoreCase))
            .ToList();
        // StateHasChanged() appelé automatiquement par EventCallback !
    }

    private void ReinitialiserListe()
    {
        _produitsFiltres = _tousProduits.ToList();
    }
}
*/


// ----------------------------------------------------------------------------
// [WATER_WAVE] CASCADINGPARAMETER — Données propagées en profondeur
// ----------------------------------------------------------------------------

/*
PROBLÈME QUE RÉSOUT CASCADINGPARAMETER :

Parent -> Enfant -> Petit-enfant -> ... -> Descendant lointain

SANS CascadingParameter :
Parent passe param -> Enfant reçoit et retransmet -> Petit-enfant retransmet -> ...
-> "Prop drilling" : répétitif et fastidieux !

AVEC CascadingParameter :
Parent définit une valeur en cascade
-> N'importe quel descendant peut l'utiliser directement !

EXEMPLES COURANTS :
- Thème (dark/light)
- Utilisateur connecté
- Langue
- Permissions
*/

/*
─────────────────────────────────────────────────────────────────
Exemple 1 : Thème de l'application
─────────────────────────────────────────────────────────────────
Fichier : Shared/MainLayout.razor (ou App.razor)
*/

/*
<CascadingValue Name="Theme" Value="@_theme">
    <CascadingValue Name="UtilisateurConnecte" Value="@_utilisateurConnecte">

        <!-- Tout le contenu de l'app -->
        <Router AppAssembly="@typeof(App).Assembly">
            <Found Context="routeData">
                <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
            </Found>
        </Router>

    </CascadingValue>
</CascadingValue>

@code {
    private string _theme = "light";
    private Utilisateur? _utilisateurConnecte;

    public void BasculerTheme()
    {
        _theme = _theme == "light" ? "dark" : "light";
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Composant qui UTILISE la valeur en cascade :
─────────────────────────────────────────────────────────────────
*/

/*
// Dans n'importe quel composant descendant :
@code {
    // Recevoir la valeur en cascade
    [CascadingParameter(Name = "Theme")]
    private string Theme { get; set; } = "light";

    [CascadingParameter(Name = "UtilisateurConnecte")]
    private Utilisateur? UtilisateurConnecte { get; set; }

    // Utiliser dans le template
    private string ClasseTheme => Theme == "dark" ? "bg-dark text-light" : "";
}
*/

/*
<div class="@ClasseTheme container">
    @if (UtilisateurConnecte is not null)
    {
        <p>Bonjour, @UtilisateurConnecte.Nom !</p>
    }
</div>
*/


// ----------------------------------------------------------------------------
// [FICHIER] RENDERFRAGMENT — Templates de contenu flexibles
// ----------------------------------------------------------------------------

/*
RENDERFRAGMENT = Permet de passer du HTML/Razor en tant que paramètre

POURQUOI ?
-> Créer des composants "conteneurs" génériques
-> Le parent définit QUOI afficher, l'enfant définit COMMENT

ANALOGIE :
RenderFragment = Slot dans Vue.js, children dans React

EXEMPLES COURANTS :
- Modal avec contenu variable
- Carte avec header/body/footer personnalisables
- Table avec colonnes personnalisables
- Layout avec zones définissables
*/

/*
─────────────────────────────────────────────────────────────────
Composant Modal générique
─────────────────────────────────────────────────────────────────
Fichier : Components/Modal.razor
*/

/*
@if (_visible)
{
    <div class="modal d-block" tabindex="-1">
        <div class="modal-dialog modal-@Taille">
            <div class="modal-content">

                <!-- HEADER -->
                <div class="modal-header">
                    @if (HeaderTemplate is not null)
                    {
                        @HeaderTemplate  <!-- Contenu personnalisé -->
                    }
                    else
                    {
                        <h5 class="modal-title">@Titre</h5> <!-- Par défaut -->
                    }
                    <button type="button" class="btn-close" @onclick="Fermer"></button>
                </div>

                <!-- BODY : contenu principal personnalisable -->
                <div class="modal-body">
                    @BodyTemplate
                </div>

                <!-- FOOTER -->
                <div class="modal-footer">
                    @if (FooterTemplate is not null)
                    {
                        @FooterTemplate
                    }
                    else
                    {
                        <!-- Footer par défaut -->
                        <button class="btn btn-secondary" @onclick="Fermer">Annuler</button>
                        <button class="btn btn-primary" @onclick="Confirmer">Confirmer</button>
                    }
                </div>

            </div>
        </div>
    </div>
    <div class="modal-backdrop fade show"></div>
}

@code {
    [Parameter] public string Titre { get; set; } = string.Empty;
    [Parameter] public string Taille { get; set; } = "md"; // sm, md, lg, xl

    // RenderFragment = Contenu HTML/Razor passé par le parent
    [Parameter] public RenderFragment? HeaderTemplate { get; set; }
    [Parameter] public RenderFragment? BodyTemplate { get; set; }
    [Parameter] public RenderFragment? FooterTemplate { get; set; }

    [Parameter] public EventCallback OnConfirme { get; set; }
    [Parameter] public EventCallback OnFerme { get; set; }

    private bool _visible = false;

    public void Ouvrir() { _visible = true; StateHasChanged(); }
    public void Fermer() { _visible = false; StateHasChanged(); }

    private async Task Confirmer()
    {
        await OnConfirme.InvokeAsync();
        _visible = false;
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
UTILISATION du Modal générique :
─────────────────────────────────────────────────────────────────
*/

/*
<button @onclick="() => _modal.Ouvrir()">Ouvrir Modal</button>

<Modal @ref="_modal" Titre="Confirmer la suppression">
    <BodyTemplate>
        <p>Êtes-vous sûr de vouloir supprimer <strong>@_nomASupprimer</strong> ?</p>
        <p class="text-danger">Cette action est irréversible !</p>
    </BodyTemplate>
    <FooterTemplate>
        <button class="btn btn-secondary" @onclick="() => _modal.Fermer()">Annuler</button>
        <button class="btn btn-danger" @onclick="ConfirmerSuppression">Supprimer</button>
    </FooterTemplate>
</Modal>

@code {
    private Modal _modal = default!;
    private string _nomASupprimer = "Alice";

    private async Task ConfirmerSuppression()
    {
        // Logique de suppression
        _modal.Fermer();
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
RenderFragment<T> : Template avec contexte (données)
─────────────────────────────────────────────────────────────────
Utile pour les listes personnalisables
*/

/*
Fichier : Components/Liste.razor

@typeparam T  // Composant générique !

<div class="liste">
    @foreach (var item in Items)
    {
        @ItemTemplate(item)  // Appeler le template avec l'item
    }
</div>

@code {
    [Parameter, EditorRequired]
    public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();

    // RenderFragment<T> : Template qui reçoit un item de type T
    [Parameter, EditorRequired]
    public RenderFragment<T> ItemTemplate { get; set; } = default!;
}
*/

/*
─── Utilisation avec Context ────────────────────────────────────

<Liste Items="@_produits">
    <ItemTemplate Context="produit">
        <!-- 'produit' est le Produit courant de la boucle -->
        <div class="produit-item">
            <strong>@produit.Nom</strong> — @produit.Prix.ToString("C")
        </div>
    </ItemTemplate>
</Liste>
*/


// ============================================================================
// [GUIDE] CHAPITRE 5 : DATA BINDING
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le one-way et two-way binding
[OK] Utiliser @bind sur différents éléments
[OK] Créer des formulaires avec EditForm
[OK] Valider les formulaires avec DataAnnotations
[OK] Gérer les événements de formulaire
*/


// ----------------------------------------------------------------------------
// [SYNC] BINDING — Liaison entre données et interface
// ----------------------------------------------------------------------------

/*
TYPES DE BINDING :

1. ONE-WAY (sens unique : données -> UI)
   @variable    ou    Value="@variable"
   -> La variable s'affiche dans l'UI
   -> Modifier l'UI ne change PAS la variable

2. TWO-WAY (bidirectionnel : données <-> UI)
   @bind="variable"
   -> La variable s'affiche dans l'UI
   -> Modifier l'UI CHANGE la variable automatiquement

QUAND UTILISER QUOI ?
-> Afficher des données : One-way (@variable)
-> Saisir des données : Two-way (@bind)
*/

/*
─────────────────────────────────────────────────────────────────
Démo complète du binding
─────────────────────────────────────────────────────────────────
*/

/*
<div class="container">
    <h2>Démonstration Data Binding</h2>

    <!-- ─── ONE-WAY : Afficher ─────────────────── -->
    <h4>1. One-Way (lecture seule)</h4>
    <p>Nom : @_nom</p>
    <p>Score : @_score</p>
    <!-- Changer _nom ici n'affecte pas _nom dans @code -->

    <!-- ─── TWO-WAY : @bind ──────────────────── -->
    <h4>2. Two-Way avec @bind</h4>
    <input @bind="_nom" class="form-control" />
    <p>Résultat : @_nom</p>
    <!-- Taper dans l'input -> _nom change automatiquement -->

    <!-- ─── BINDING EN TEMPS RÉEL : @bind:event ── -->
    <h4>3. Binding en temps réel (oninput)</h4>
    <input @bind="_recherche" @bind:event="oninput" class="form-control" />
    <!-- oninput = mise à jour à CHAQUE frappe -->
    <!-- onchange (défaut) = mise à jour quand focus perdu -->
    <p>Recherche live : <strong>@_recherche</strong></p>

    <!-- ─── BINDING AVEC FORMAT ─────────────────── -->
    <h4>4. Binding avec format</h4>
    <input @bind="_date" @bind:format="dd/MM/yyyy" type="date" class="form-control" />
    <p>Date choisie : @_date.ToLongDateString()</p>

    <!-- ─── BINDING AVEC NUMBER ─────────────────── -->
    <h4>5. Binding nombre</h4>
    <input @bind="_score" type="number" class="form-control" />
    <p>Score : @_score (type: int)</p>

    <!-- ─── BINDING CHECKBOX ─────────────────── -->
    <h4>6. Binding checkbox</h4>
    <div class="form-check">
        <input class="form-check-input" type="checkbox" @bind="_accepte" id="cb" />
        <label class="form-check-label" for="cb">J'accepte les conditions</label>
    </div>
    <p>Accepté : @_accepte</p>

    <!-- ─── BINDING SELECT ──────────────────── -->
    <h4>7. Binding select</h4>
    <select @bind="_categorie" class="form-select">
        <option value="">Choisir...</option>
        <option value="tech">Technologie</option>
        <option value="sport">Sport</option>
        <option value="art">Art</option>
    </select>
    <p>Catégorie : @_categorie</p>

    <!-- ─── BINDING DE PROPRIÉTÉ PERSONNALISÉE ── -->
    <h4>8. @bind sur composant enfant</h4>
    <!-- Dans un composant enfant avec [Parameter] Value + OnValueChanged -->
    <MonComposantInput @bind-Valeur="_valeurPersonnalisee" />
    <p>Valeur : @_valeurPersonnalisee</p>
</div>

@code {
    private string _nom = "Alice";
    private string _recherche = "";
    private int _score = 0;
    private DateTime _date = DateTime.Today;
    private bool _accepte = false;
    private string _categorie = "";
    private string _valeurPersonnalisee = "";
}
*/

/*
─────────────────────────────────────────────────────────────────
IMPLÉMENTER @bind sur un composant personnalisé
─────────────────────────────────────────────────────────────────
Pour que @bind-Valeur fonctionne sur votre composant :
*/

/*
Fichier : Components/MonComposantInput.razor

<input class="form-control"
       value="@Valeur"
       @oninput="MettreAJour" />

@code {
    // 1. Paramètre Value
    [Parameter] public string Valeur { get; set; } = string.Empty;

    // 2. EventCallback nommé "ValeurChanged" (convention Blazor !)
    // @bind-Valeur -> cherche automatiquement ValeurChanged
    [Parameter] public EventCallback<string> ValeurChanged { get; set; }

    private async Task MettreAJour(ChangeEventArgs e)
    {
        var nouvelleValeur = e.Value?.ToString() ?? string.Empty;
        await ValeurChanged.InvokeAsync(nouvelleValeur);
        // Blazor met à jour automatiquement grâce à la convention
    }
}
*/


// ----------------------------------------------------------------------------
// [LISTE] FORMULAIRES AVEC EDITFORM
// ----------------------------------------------------------------------------

/*
EDITFORM = Composant Blazor pour les formulaires
Gère automatiquement :
- La validation
- Les états de validation (valid/invalid/pristine)
- La soumission

COMPOSANTS ASSOCIÉS :
- DataAnnotationsValidator -> Validation via attributs [Required], etc.
- ValidationSummary -> Afficher toutes les erreurs
- ValidationMessage<T> -> Erreur d'un champ spécifique
- InputText, InputNumber, InputDate, etc. -> Inputs Blazor
*/

/*
─────────────────────────────────────────────────────────────────
Modèle avec annotations de validation
─────────────────────────────────────────────────────────────────
*/

// Models/FormulaireInscription.cs
public class FormulaireInscription
{
    [Required(ErrorMessage = "Le nom est obligatoire")]
    [StringLength(50, MinimumLength = 2,
        ErrorMessage = "Le nom doit contenir entre 2 et 50 caractères")]
    public string Nom { get; set; } = string.Empty;

    [Required(ErrorMessage = "Le prénom est obligatoire")]
    [StringLength(50, MinimumLength = 2,
        ErrorMessage = "Le prénom doit contenir entre 2 et 50 caractères")]
    public string Prenom { get; set; } = string.Empty;

    [Required(ErrorMessage = "L'email est obligatoire")]
    [EmailAddress(ErrorMessage = "Format d'email invalide")]
    public string Email { get; set; } = string.Empty;

    [Required(ErrorMessage = "Le mot de passe est obligatoire")]
    [StringLength(100, MinimumLength = 8,
        ErrorMessage = "Le mot de passe doit contenir au moins 8 caractères")]
    [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$",
        ErrorMessage = "Doit contenir majuscule, minuscule et chiffre")]
    public string MotDePasse { get; set; } = string.Empty;

    [Required(ErrorMessage = "La confirmation est obligatoire")]
    [Compare("MotDePasse", ErrorMessage = "Les mots de passe ne correspondent pas")]
    public string ConfirmationMotDePasse { get; set; } = string.Empty;

    [Required(ErrorMessage = "L'âge est obligatoire")]
    [Range(18, 120, ErrorMessage = "L'âge doit être entre 18 et 120")]
    public int Age { get; set; }

    [Required(ErrorMessage = "Vous devez accepter les conditions")]
    [Range(typeof(bool), "true", "true",
        ErrorMessage = "Vous devez accepter les conditions d'utilisation")]
    public bool AccepterConditions { get; set; }

    public string? Telephone { get; set; } // Optionnel

    [Phone(ErrorMessage = "Format de téléphone invalide")]
    public string? TelephoneFormatte => Telephone;
}

/*
─────────────────────────────────────────────────────────────────
Formulaire Blazor complet avec EditForm
─────────────────────────────────────────────────────────────────
Fichier : Pages/Inscription.razor
*/

/*
@page "/inscription"

<h1>Inscription</h1>

<!-- EditForm : Composant de formulaire Blazor -->
<!-- Model -> L'objet lié au formulaire -->
<!-- OnValidSubmit -> Appelé si la validation est OK -->
<!-- OnInvalidSubmit -> Appelé si la validation échoue -->
<EditForm Model="@_formulaire"
          OnValidSubmit="SoumettreFomlulaire"
          OnInvalidSubmit="AfficherErreurs">

    <!-- Active la validation via DataAnnotations -->
    <DataAnnotationsValidator />

    <!-- Résumé de TOUTES les erreurs -->
    <ValidationSummary class="alert alert-danger" />

    <!-- ─── Nom ─────────────────────────────── -->
    <div class="mb-3">
        <label class="form-label">Nom *</label>
        <InputText @bind-Value="_formulaire.Nom"
                   class="form-control"
                   placeholder="Votre nom" />
        <!-- Erreur spécifique au champ Nom -->
        <ValidationMessage For="@(() => _formulaire.Nom)"
                          class="text-danger" />
    </div>

    <!-- ─── Prénom ───────────────────────────── -->
    <div class="mb-3">
        <label class="form-label">Prénom *</label>
        <InputText @bind-Value="_formulaire.Prenom"
                   class="form-control" />
        <ValidationMessage For="@(() => _formulaire.Prenom)" class="text-danger" />
    </div>

    <!-- ─── Email ───────────────────────────── -->
    <div class="mb-3">
        <label class="form-label">Email *</label>
        <InputText @bind-Value="_formulaire.Email"
                   type="email"
                   class="form-control" />
        <ValidationMessage For="@(() => _formulaire.Email)" class="text-danger" />
    </div>

    <!-- ─── Mot de passe ─────────────────────── -->
    <div class="mb-3">
        <label class="form-label">Mot de passe *</label>
        <InputText @bind-Value="_formulaire.MotDePasse"
                   type="password"
                   class="form-control" />
        <ValidationMessage For="@(() => _formulaire.MotDePasse)" class="text-danger" />
    </div>

    <!-- ─── Confirmation mot de passe ─────────── -->
    <div class="mb-3">
        <label class="form-label">Confirmer mot de passe *</label>
        <InputText @bind-Value="_formulaire.ConfirmationMotDePasse"
                   type="password"
                   class="form-control" />
        <ValidationMessage For="@(() => _formulaire.ConfirmationMotDePasse)"
                          class="text-danger" />
    </div>

    <!-- ─── Âge ─────────────────────────────── -->
    <div class="mb-3">
        <label class="form-label">Âge *</label>
        <InputNumber @bind-Value="_formulaire.Age"
                     class="form-control"
                     min="18" max="120" />
        <ValidationMessage For="@(() => _formulaire.Age)" class="text-danger" />
    </div>

    <!-- ─── Conditions ─────────────────────── -->
    <div class="mb-3 form-check">
        <InputCheckbox @bind-Value="_formulaire.AccepterConditions"
                      class="form-check-input"
                      id="conditions" />
        <label class="form-check-label" for="conditions">
            J'accepte les conditions d'utilisation *
        </label>
        <ValidationMessage For="@(() => _formulaire.AccepterConditions)"
                          class="text-danger d-block" />
    </div>

    <!-- ─── Submit ───────────────────────────── -->
    <button type="submit" class="btn btn-primary" disabled="@_envoi">
        @if (_envoi)
        {
            <span class="spinner-border spinner-border-sm me-2"></span>
        }
        @(_envoi ? "Envoi en cours..." : "S'inscrire")
    </button>

</EditForm>

<!-- Message de succès -->
@if (_succes)
{
    <div class="alert alert-success mt-3">
        [OK] Inscription réussie ! Bienvenue @_formulaire.Prenom !
    </div>
}

@code {
    private FormulaireInscription _formulaire = new();
    private bool _envoi = false;
    private bool _succes = false;

    private async Task SoumettreFomlulaire()
    {
        _envoi = true;

        try
        {
            // Simulation d'un appel API
            await Task.Delay(1500);
            _succes = true;
            _formulaire = new(); // Réinitialiser le formulaire
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Erreur: {ex.Message}");
        }
        finally
        {
            _envoi = false;
        }
    }

    private void AfficherErreurs()
    {
        Console.WriteLine("Formulaire invalide ! Corriger les erreurs.");
    }
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 6 : ROUTING
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Définir des routes avec @page
[OK] Passer des paramètres d'URL
[OK] Utiliser les layouts
[OK] Naviguer avec NavigationManager
[OK] Utiliser NavLink pour la navigation
*/


// ----------------------------------------------------------------------------
// [WORLD_MAP] ROUTING — Comment Blazor associe URLs aux composants
// ----------------------------------------------------------------------------

/*
LE SYSTÈME DE ROUTING BLAZOR :

1. App.razor configure le routeur
2. @page "/route" sur un composant -> Le rend accessible à cette URL
3. NavigationManager -> Pour naviguer par code
4. NavLink -> Pour créer des liens de navigation


RÈGLES DES ROUTES :
-> @page doit être en PREMIÈRE directive du fichier
-> Un composant peut avoir PLUSIEURS @page
-> Les routes sont case-insensitive par défaut
-> Les paramètres sont entre accolades : @page "/produit/{Id}"
*/

/*
─────────────────────────────────────────────────────────────────
App.razor — Configuration du routeur
─────────────────────────────────────────────────────────────────
*/

/*
<Router AppAssembly="@typeof(App).Assembly"
        AdditionalAssemblies="new[] { typeof(MonBibliothèque.Composant).Assembly }">

    <Found Context="routeData">
        <!-- Route trouvée -> Afficher le composant avec son layout -->
        <RouteView RouteData="@routeData"
                   DefaultLayout="@typeof(MainLayout)" />
        <!-- FocusOnNavigate : Accessibilité - focus sur le nouveau contenu -->
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>

    <NotFound>
        <!-- Route non trouvée -> Page 404 personnalisée -->
        <PageTitle>Page introuvable</PageTitle>
        <div class="container text-center mt-5">
            <h1>404 - Page introuvable</h1>
            <p>La page que vous cherchez n'existe pas.</p>
            <a href="/" class="btn btn-primary">Retour à l'accueil</a>
        </div>
    </NotFound>

</Router>
*/


// ----------------------------------------------------------------------------
// [LIEN] @PAGE ET PARAMÈTRES D'URL
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
Routes simples
─────────────────────────────────────────────────────────────────
*/

/*
@page "/accueil"           -> /accueil
@page "/"                  -> / (racine)
@page "/admin/dashboard"   -> /admin/dashboard
*/

/*
─────────────────────────────────────────────────────────────────
Routes avec paramètres
─────────────────────────────────────────────────────────────────
*/

/*
@page "/produit/{Id:int}"

// Dans @code :
[Parameter] public int Id { get; set; }
// URL /produit/42 -> Id = 42
// URL /produit/abc -> 404 (pas un int)
*/

/*
CONTRAINTES DE TYPES SUR LES PARAMÈTRES :

{Param}         -> string (aucune contrainte)
{Param:int}     -> Entier positif ou négatif
{Param:long}    -> Long
{Param:float}   -> Float
{Param:double}  -> Double
{Param:decimal} -> Decimal
{Param:bool}    -> true ou false
{Param:datetime} -> DateTime
{Param:guid}    -> GUID
{Param:min(x)}  -> int minimum x
{Param:max(x)}  -> int maximum x
{Param:range(x,y)} -> int entre x et y
{Param:alpha}   -> Lettres uniquement
{Param:regex(expr)} -> Expression régulière
*/

/*
─────────────────────────────────────────────────────────────────
Exemple : Page produit avec plusieurs paramètres
─────────────────────────────────────────────────────────────────
Fichier : Pages/DetailProduit.razor
*/

/*
@page "/produits/{Categorie}/{Id:int}"
@page "/produits/{Id:int}"  // Route alternative (Categorie optionnelle)
@inject NavigationManager NavManager
@inject IProduitService ProduitService

<PageTitle>@(_produit?.Nom ?? "Chargement...")</PageTitle>

@if (_chargement)
{
    <div class="spinner-border"></div>
}
else if (_produit is null)
{
    <div class="alert alert-warning">Produit introuvable (Id: @Id)</div>
    <button @onclick='() => NavManager.NavigateTo("/produits")'>
        <- Retour au catalogue
    </button>
}
else
{
    <div class="container">
        <nav aria-label="breadcrumb">
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/">Accueil</a></li>
                <li class="breadcrumb-item"><a href="/produits">Produits</a></li>
                @if (Categorie is not null)
                {
                    <li class="breadcrumb-item">
                        <a href="/produits?cat=@Categorie">@Categorie</a>
                    </li>
                }
                <li class="breadcrumb-item active">@_produit.Nom</li>
            </ol>
        </nav>

        <h1>@_produit.Nom</h1>
        <p class="text-muted">@_produit.Categorie</p>
        <h3 class="text-primary">@_produit.Prix.ToString("C")</h3>
    </div>
}

@code {
    [Parameter] public int Id { get; set; }
    [Parameter] public string? Categorie { get; set; } // Optionnel

    private Produit? _produit;
    private bool _chargement = true;

    protected override async Task OnParametersSetAsync()
    {
        // OnParametersSetAsync car les paramètres peuvent changer
        // (ex: naviguer de /produits/1 à /produits/2)
        _chargement = true;
        _produit = await ProduitService.ObtenirParIdAsync(Id);
        _chargement = false;
    }
}
*/


// ----------------------------------------------------------------------------
// [COMPASS] NAVIGATIONMANAGER — Naviguer par code
// ----------------------------------------------------------------------------

/*
NavigationManager = Service pour naviguer programmatiquement

MÉTHODES PRINCIPALES :
-> NavigateTo(url)           -> Naviguer vers une URL
-> NavigateTo(url, true)     -> Forcer rechargement complet
-> GetUriWithQueryParameter  -> Ajouter query string
-> Uri                       -> URL actuelle
-> ToAbsoluteUri(path)       -> Convertir en URL absolue
-> RegisterLocationChangingHandler -> Écouter les changements de route
*/

/*
─────────────────────────────────────────────────────────────────
Exemples d'utilisation de NavigationManager
─────────────────────────────────────────────────────────────────
*/

/*
@inject NavigationManager NavManager

@code {
    // ─── Navigation simple ────────────────────────────────────
    private void AllerAccueil()
    {
        NavManager.NavigateTo("/");
    }

    private void AllerProduit(int id)
    {
        NavManager.NavigateTo($"/produits/{id}");
    }

    // ─── Navigation avec force reload ─────────────────────────
    private void NaviguerAvecReload()
    {
        NavManager.NavigateTo("/auth/login", forceLoad: true);
        // forceLoad: true -> Rechargement complet de la page
    }

    // ─── Remplacer l'entrée historique ────────────────────────
    private void RedirigerSansPouvoirRevenir()
    {
        NavManager.NavigateTo("/dashboard", replace: true);
        // replace: true -> Remplace l'entrée actuelle dans l'historique
        // Le bouton "précédent" ne ramènera PAS ici
    }

    // ─── URL actuelle ─────────────────────────────────────────
    private void AfficherUrlActuelle()
    {
        var url = NavManager.Uri;
        Console.WriteLine($"URL actuelle: {url}");

        var baseUrl = NavManager.BaseUri;
        Console.WriteLine($"Base URL: {baseUrl}");
    }

    // ─── Navigation avec query string ─────────────────────────
    private void RechercherProduits(string terme)
    {
        // /produits?recherche=laptop&page=1
        var url = NavManager.GetUriWithQueryParameter("recherche", terme);
        NavManager.NavigateTo(url);
    }

    // ─── Lire les query parameters ────────────────────────────
    private void LireQueryParams()
    {
        var uri = new Uri(NavManager.Uri);
        var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers
            .ParseQuery(uri.Query);

        if (query.TryGetValue("recherche", out var terme))
        {
            Console.WriteLine($"Terme: {terme}");
        }
    }
}
*/


// ----------------------------------------------------------------------------
// [LIEN] NAVLINK — Liens de navigation intelligents
// ----------------------------------------------------------------------------

/*
NavLink = Composant Blazor pour les liens de navigation

AVANTAGES vs <a href="..."> :
-> Ajoute automatiquement la classe "active" si la route correspond
-> Compare l'URL actuelle avec href
-> Supporte Match pour contrôle fin

MATCH Options :
NavLinkMatch.All       -> Correspondance EXACTE
NavLinkMatch.Prefix    -> Si l'URL COMMENCE par href (défaut)
*/

/*
─────────────────────────────────────────────────────────────────
Fichier : Shared/NavMenu.razor
─────────────────────────────────────────────────────────────────
*/

/*
<nav class="sidebar">
    <ul class="nav flex-column">

        <!-- Correspondance exacte (seulement sur "/") -->
        <li class="nav-item">
            <NavLink class="nav-link" href="/" Match="NavLinkMatch.All">
                [ACCUEIL] Accueil
            </NavLink>
        </li>

        <!-- Correspondance préfixe (actif pour /produits, /produits/1, etc.) -->
        <li class="nav-item">
            <NavLink class="nav-link" href="/produits">
                [SHOPPING_BAGS] Produits
            </NavLink>
        </li>

        <!-- Section accordéon -->
        <li class="nav-item">
            <button class="nav-link" @onclick="BasculerAdmin">
                [CONFIG] Administration
            </button>
            @if (_adminOuvert)
            {
                <ul class="nav flex-column ms-3">
                    <li><NavLink class="nav-link small" href="/admin/utilisateurs">
                        Utilisateurs
                    </NavLink></li>
                    <li><NavLink class="nav-link small" href="/admin/produits">
                        Produits
                    </NavLink></li>
                </ul>
            }
        </li>

    </ul>
</nav>

@code {
    private bool _adminOuvert = false;
    private void BasculerAdmin() => _adminOuvert = !_adminOuvert;
}
*/


// ----------------------------------------------------------------------------
// [DESIGN] LAYOUTS — Structures de page réutilisables
// ----------------------------------------------------------------------------

/*
LAYOUT = Composant qui définit la structure commune des pages
(navbar, sidebar, footer...)

Il utilise @Body pour indiquer où afficher le contenu de la page.
*/

/*
─────────────────────────────────────────────────────────────────
Fichier : Shared/MainLayout.razor
─────────────────────────────────────────────────────────────────
*/

/*
@inherits LayoutComponentBase  // <- OBLIGATOIRE pour un Layout

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8" />
    <title>Mon Application</title>
</head>
<body>
    <div class="d-flex">

        <!-- Sidebar -->
        <aside class="sidebar">
            <NavMenu />
        </aside>

        <!-- Contenu principal -->
        <main class="flex-grow-1">
            <TopBar />

            <div class="container-fluid p-4">
                @Body  <!-- <- ICI s'affiche la page actuelle -->
            </div>

            <Footer />
        </main>

    </div>
</body>
</html>
*/

/*
─────────────────────────────────────────────────────────────────
Layouts différents selon les pages
─────────────────────────────────────────────────────────────────
*/

/*
Fichier : Shared/AuthLayout.razor (pour les pages de connexion)

@inherits LayoutComponentBase

<div class="auth-container d-flex vh-100 align-items-center justify-content-center">
    <div class="card p-4" style="width: 400px">
        @Body  <!-- Page de login/inscription ici -->
    </div>
</div>
*/

/*
Utiliser un layout différent dans une page :

@page "/login"
@layout AuthLayout  <- Utiliser le layout d'authentification

<h2>Connexion</h2>
<!-- Formulaire de login... -->
*/

/*
Configurer le layout par défaut pour un dossier :
Fichier : Pages/_Imports.razor

@layout AdminLayout  <- Toutes les pages du dossier Pages/ utilisent AdminLayout
*/


// ============================================================================
// [GUIDE] CHAPITRE 7 : CYCLE DE VIE (APPROFONDI)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS AVANCÉS

À la fin de ce chapitre, vous saurez :
[OK] Maîtriser l'ordre exact du cycle de vie
[OK] Éviter les fuites mémoire avec IDisposable
[OK] Gérer les re-rendus avec ShouldRender
[OK] Utiliser StateHasChanged correctement
[OK] Gérer les opérations async dans le cycle
*/

/*
─────────────────────────────────────────────────────────────────
Composant complet montrant le cycle de vie avec logs
─────────────────────────────────────────────────────────────────
Fichier : Components/DemoLifecycle.razor
*/

/*
@implements IDisposable
@implements IAsyncDisposable

<div class="lifecycle-demo">
    <p>Rendercount: @_renderCount</p>
    <p>Données: @_donnees</p>
    <button @onclick="ForceRerender">Forcer re-rendu</button>
    <button @onclick="MettreAJourDonnees">Mettre à jour données</button>
</div>

@code {
    [Parameter] public int Id { get; set; }

    private string _donnees = string.Empty;
    private int _renderCount = 0;
    private Timer? _timer;
    private CancellationTokenSource? _cts;

    // ─── 1. Première étape ───────────────────────────────────────────
    public override async Task SetParametersAsync(ParameterView parameters)
    {
        Console.WriteLine("1. SetParametersAsync");
        await base.SetParametersAsync(parameters);
    }

    // ─── 2. Initialisation (UNE SEULE FOIS) ─────────────────────────
    protected override void OnInitialized()
    {
        Console.WriteLine("2. OnInitialized");
        _cts = new CancellationTokenSource();
    }

    protected override async Task OnInitializedAsync()
    {
        Console.WriteLine("3. OnInitializedAsync - Début");
        _donnees = await ChargerAsync(Id, _cts!.Token);
        Console.WriteLine("3. OnInitializedAsync - Fin");
    }

    // ─── 3. Paramètres mis à jour ────────────────────────────────────
    protected override void OnParametersSet()
    {
        Console.WriteLine($"4. OnParametersSet - Id={Id}");
    }

    protected override async Task OnParametersSetAsync()
    {
        Console.WriteLine("5. OnParametersSetAsync - Début");
        // Si Id change, recharger
        _donnees = await ChargerAsync(Id, _cts!.Token);
        Console.WriteLine("5. OnParametersSetAsync - Fin");
    }

    // ─── 4. Contrôle du re-rendu ─────────────────────────────────────
    protected override bool ShouldRender()
    {
        _renderCount++;
        Console.WriteLine($"6. ShouldRender -> {_renderCount}");
        return true; // Toujours re-rendre (défaut)
        // Retourner false pour éviter un re-rendu inutile
    }

    // ─── 5. Après le rendu ───────────────────────────────────────────
    protected override void OnAfterRender(bool firstRender)
    {
        Console.WriteLine($"7. OnAfterRender - Premier: {firstRender}");

        if (firstRender)
        {
            // Initialiser timer seulement au premier rendu
            _timer = new Timer(_ =>
            {
                InvokeAsync(MettreAJourDonnees); // Thread-safe !
            }, null, 5000, 5000);
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("8. OnAfterRenderAsync - Premier rendu terminé");
            // Initialiser des interops JS ici
        }
    }

    // ─── Méthodes publiques ──────────────────────────────────────────
    private void ForceRerender()
    {
        // Forcer un re-rendu manuellement
        StateHasChanged();
        // Utile quand les changements viennent de l'extérieur
        // (event d'un service, callback JS...)
    }

    private async Task MettreAJourDonnees()
    {
        _donnees = await ChargerAsync(Id, CancellationToken.None);
        StateHasChanged(); // Notifier Blazor du changement
    }

    // ─── Nettoyage (IMPORTANT !) ─────────────────────────────────────
    public void Dispose()
    {
        Console.WriteLine("Dispose - Nettoyage synchrone");
        _timer?.Dispose();    // Stopper le timer
        _cts?.Cancel();       // Annuler les opérations en cours
        _cts?.Dispose();
    }

    public async ValueTask DisposeAsync()
    {
        Console.WriteLine("DisposeAsync - Nettoyage asynchrone");
        if (_cts is not null)
        {
            await _cts.CancelAsync();
            _cts.Dispose();
        }
    }

    private Task<string> ChargerAsync(int id, CancellationToken ct)
        => Task.FromResult($"Données #{id} chargées à {DateTime.Now:HH:mm:ss}");
}
*/

/*
ORDRE DU CYCLE DE VIE :

1. SetParametersAsync
2. OnInitialized
3. OnInitializedAsync     <- Premier chargement données
4. OnParametersSet
5. OnParametersSetAsync   <- Si paramètres changent
6. ShouldRender
7. [Rendu du HTML]
8. OnAfterRender(firstRender=true)
9. OnAfterRenderAsync(firstRender=true)

Quand état change (StateHasChanged) :
6. ShouldRender
7. [Rendu du HTML]
8. OnAfterRender(firstRender=false)
9. OnAfterRenderAsync(firstRender=false)

Quand composant retiré :
10. Dispose ou DisposeAsync
*/

/*
[ATTENTION] PIÈGES COURANTS ET SOLUTIONS :

PIÈGE 1 : Fuite mémoire avec events/timers
-> TOUJOURS implémenter IDisposable
-> Se désabonner des events dans Dispose()
-> Stopper les timers dans Dispose()

PIÈGE 2 : StateHasChanged dans un thread non-UI
-> Utiliser InvokeAsync(() => StateHasChanged())
-> Pour callbacks depuis des services

PIÈGE 3 : await dans OnInitializedAsync
-> Blazor fait 2 rendus : avant et après l'await
-> Ajouter une condition _chargement pour éviter les erreurs null

PIÈGE 4 : Modification de paramètres dans le composant
-> Ne JAMAIS modifier un [Parameter] directement
-> Utiliser EventCallback pour notifier le parent
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE — PARTIE 2
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
EXERCICE : GESTIONNAIRE DE TÂCHES (TODO APP)
═══════════════════════════════════════════════════════════════

OBJECTIF : Créer une application de gestion de tâches complète

FONCTIONNALITÉS REQUISES :

1. Composant TodoItem :
   - Afficher une tâche (titre, description, priorité)
   - Checkbox pour marquer comme terminée
   - Boutons Éditer et Supprimer
   - Badge de priorité (haute/moyenne/basse)
   - EventCallback OnComplete, OnEdit, OnDelete

2. Composant FormulaireAjout :
   - Champ titre (obligatoire, 3-50 chars)
   - Champ description (optionnel)
   - Select priorité (haute/moyenne/basse)
   - Validation avec EditForm + DataAnnotations
   - EventCallback<Todo> OnAjoutee

3. Page principale /todos :
   - Utilise les composants ci-dessus
   - Filtrer par statut (toutes/active/terminées)
   - Compteur : "@terminées / @total terminées"
   - NavLink dans la navbar

MODÈLES :
public class Todo {
    int Id, string Titre, string? Description,
    bool EstTerminee, string Priorite, DateTime CreeLe
}

COMPÉTENCES UTILISÉES :
[OK] Composants avec [Parameter]
[OK] EventCallback
[OK] EditForm + DataAnnotations
[OK] @bind et binding bidirectionnel
[OK] @page + routing
[OK] Layouts
[OK] Cycle de vie (OnInitializedAsync)
[OK] LINQ pour filtrage
[OK] CascadingParameter (optionnel, pour le thème)
═══════════════════════════════════════════════════════════════
*/


// ============================================================================
// [OK] CORRIGÉ DE L'EXERCICE
// ============================================================================

// ─── Models/Todo.cs ──────────────────────────────────────────────────────────

public class Todo
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Le titre est obligatoire")]
    [StringLength(50, MinimumLength = 3,
        ErrorMessage = "Le titre doit contenir entre 3 et 50 caractères")]
    public string Titre { get; set; } = string.Empty;

    [StringLength(200, ErrorMessage = "La description ne peut dépasser 200 caractères")]
    public string? Description { get; set; }

    public bool EstTerminee { get; set; } = false;

    [Required(ErrorMessage = "La priorité est obligatoire")]
    public string Priorite { get; set; } = "moyenne";

    public DateTime CreeLe { get; set; } = DateTime.Now;

    // Propriétés calculées
    public string CssBadge => Priorite switch
    {
        "haute"   => "bg-danger",
        "basse"   => "bg-success",
        _         => "bg-warning text-dark"
    };

    public string TexteBadge => Priorite switch
    {
        "haute"   => "[ROUGE] HAUTE",
        "basse"   => "[VERT] BASSE",
        _         => "[JAUNE] MOYENNE"
    };
}

/*
─────────────────────────────────────────────────────────────────
Fichier : Components/TodoItem.razor
─────────────────────────────────────────────────────────────────
*/

/*
<div class="card mb-2 @(Todo.EstTerminee ? "opacity-50" : "")">
    <div class="card-body d-flex align-items-center gap-3">

        <!-- Checkbox -->
        <input type="checkbox"
               class="form-check-input"
               checked="@Todo.EstTerminee"
               @onchange="BasculerTerminee" />

        <!-- Contenu -->
        <div class="flex-grow-1">
            <div class="d-flex align-items-center gap-2">
                <span class="@(Todo.EstTerminee ? "text-decoration-line-through text-muted" : "fw-bold")">
                    @Todo.Titre
                </span>
                <span class="badge @Todo.CssBadge">@Todo.TexteBadge</span>
            </div>
            @if (!string.IsNullOrEmpty(Todo.Description))
            {
                <small class="text-muted">@Todo.Description</small>
            }
            <br/>
            <small class="text-muted">Créée le @Todo.CreeLe.ToString("dd/MM/yyyy à HH:mm")</small>
        </div>

        <!-- Boutons action -->
        <div class="d-flex gap-2">
            <button class="btn btn-sm btn-outline-primary"
                    @onclick="() => OnEdit.InvokeAsync(Todo)">
                [EDIT] Éditer
            </button>
            <button class="btn btn-sm btn-outline-danger"
                    @onclick="() => OnDelete.InvokeAsync(Todo.Id)">
                [SUPPRIMER] Supprimer
            </button>
        </div>

    </div>
</div>

@code {
    [Parameter, EditorRequired] public Todo Todo { get; set; } = default!;
    [Parameter] public EventCallback<Todo> OnComplete { get; set; }
    [Parameter] public EventCallback<Todo> OnEdit { get; set; }
    [Parameter] public EventCallback<int> OnDelete { get; set; }

    private async Task BasculerTerminee()
    {
        Todo.EstTerminee = !Todo.EstTerminee;
        await OnComplete.InvokeAsync(Todo);
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Fichier : Components/FormulaireAjout.razor
─────────────────────────────────────────────────────────────────
*/

/*
<div class="card mb-4">
    <div class="card-header">+ Ajouter une tâche</div>
    <div class="card-body">
        <EditForm Model="@_nouvelleTodo" OnValidSubmit="Soumettre">
            <DataAnnotationsValidator />

            <div class="row g-3">
                <div class="col-md-5">
                    <InputText @bind-Value="_nouvelleTodo.Titre"
                               class="form-control"
                               placeholder="Titre de la tâche *" />
                    <ValidationMessage For="@(() => _nouvelleTodo.Titre)" class="text-danger" />
                </div>

                <div class="col-md-4">
                    <InputText @bind-Value="_nouvelleTodo.Description"
                               class="form-control"
                               placeholder="Description (optionnel)" />
                </div>

                <div class="col-md-2">
                    <InputSelect @bind-Value="_nouvelleTodo.Priorite" class="form-select">
                        <option value="haute">[ROUGE] Haute</option>
                        <option value="moyenne">[JAUNE] Moyenne</option>
                        <option value="basse">[VERT] Basse</option>
                    </InputSelect>
                </div>

                <div class="col-md-1">
                    <button type="submit" class="btn btn-primary w-100">Ajouter</button>
                </div>
            </div>
        </EditForm>
    </div>
</div>

@code {
    [Parameter] public EventCallback<Todo> OnAjoutee { get; set; }

    private Todo _nouvelleTodo = new() { Priorite = "moyenne" };
    private int _prochainId = 1;

    private async Task Soumettre()
    {
        _nouvelleTodo.Id = _prochainId++;
        await OnAjoutee.InvokeAsync(_nouvelleTodo);
        _nouvelleTodo = new() { Priorite = "moyenne" }; // Réinitialiser
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Fichier : Pages/Todos.razor
─────────────────────────────────────────────────────────────────
*/

/*
@page "/todos"

<PageTitle>Mes Tâches</PageTitle>

<div class="container mt-4">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h1>[LISTE] Mes Tâches</h1>
        <span class="badge bg-secondary fs-6">
            @_todos.Count(t => t.EstTerminee) / @_todos.Count terminées
        </span>
    </div>

    <!-- Formulaire d'ajout -->
    <FormulaireAjout OnAjoutee="AjouterTodo" />

    <!-- Filtres -->
    <div class="btn-group mb-3">
        <button class="btn @(_filtre == "toutes" ? "btn-primary" : "btn-outline-primary")"
                @onclick='() => _filtre = "toutes"'>Toutes (@_todos.Count)</button>
        <button class="btn @(_filtre == "actives" ? "btn-primary" : "btn-outline-primary")"
                @onclick='() => _filtre = "actives"'>Actives (@_todos.Count(t => !t.EstTerminee))</button>
        <button class="btn @(_filtre == "terminees" ? "btn-primary" : "btn-outline-primary")"
                @onclick='() => _filtre = "terminees"'>Terminées (@_todos.Count(t => t.EstTerminee))</button>
    </div>

    <!-- Liste des tâches -->
    @if (!TodosFiltrees.Any())
    {
        <div class="alert alert-info">Aucune tâche dans cette catégorie.</div>
    }
    else
    {
        @foreach (var todo in TodosFiltrees)
        {
            <TodoItem
                Todo="@todo"
                OnComplete="MettreAJourTodo"
                OnEdit="OuvrirEdition"
                OnDelete="SupprimerTodo" />
        }
    }
</div>

@code {
    private List<Todo> _todos = new()
    {
        new() { Id=1, Titre="Apprendre Blazor", Priorite="haute",
                Description="Finir la partie 2 du guide", EstTerminee=false },
        new() { Id=2, Titre="Créer une app Todo", Priorite="moyenne",
                EstTerminee=true },
    };

    private string _filtre = "toutes";

    // LINQ pour filtrer
    private IEnumerable<Todo> TodosFiltrees => _filtre switch
    {
        "actives"   => _todos.Where(t => !t.EstTerminee).OrderBy(t => t.CreeLe),
        "terminees" => _todos.Where(t => t.EstTerminee).OrderByDescending(t => t.CreeLe),
        _           => _todos.OrderBy(t => t.CreeLe)
    };

    private void AjouterTodo(Todo todo)
    {
        _todos.Add(todo);
    }

    private void MettreAJourTodo(Todo todo)
    {
        var existant = _todos.FirstOrDefault(t => t.Id == todo.Id);
        if (existant is not null)
        {
            existant.EstTerminee = todo.EstTerminee;
        }
    }

    private void OuvrirEdition(Todo todo)
    {
        // Fonctionnalité avancée : à implémenter avec Modal
        Console.WriteLine($"Éditer: {todo.Titre}");
    }

    private void SupprimerTodo(int id)
    {
        var todo = _todos.FirstOrDefault(t => t.Id == id);
        if (todo is not null) _todos.Remove(todo);
    }
}
*/


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 2

[OK] CHAPITRE 4 - COMPOSANTS :
- Création de composants (.razor)
- [Parameter] et [Parameter, EditorRequired]
- EventCallback et EventCallback<T>
- CascadingParameter pour données en cascade
- RenderFragment pour templates flexibles
- RenderFragment<T> pour templates avec contexte

[OK] CHAPITRE 5 - DATA BINDING :
- One-way binding (@variable)
- Two-way binding (@bind)
- @bind:event="oninput" pour temps réel
- @bind:format pour les dates
- EditForm + DataAnnotationsValidator
- InputText, InputNumber, InputSelect, InputCheckbox
- ValidationMessage et ValidationSummary
- Implémenter @bind sur composant personnalisé

[OK] CHAPITRE 6 - ROUTING :
- @page avec routes multiples
- Contraintes de types ({Id:int}, etc.)
- NavigationManager pour navigation par code
- NavLink avec classe active automatique
- Layouts avec @inherits LayoutComponentBase
- @layout pour changer de layout par page

[OK] CHAPITRE 7 - CYCLE DE VIE APPROFONDI :
- Ordre exact des méthodes de cycle de vie
- ShouldRender pour optimiser les re-rendus
- StateHasChanged et InvokeAsync
- IDisposable pour éviter les fuites mémoire
- Gestion des opérations async et annulation

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 3
- Communication avancée entre composants
- Gestion d'état (State Management)
- JavaScript Interop (IJSRuntime)
- Authentification et autorisation
═══════════════════════════════════════════════════════════════
*/

// ============================================================================
// [LIVRE] BLAZOR - PARTIE 3 : FONCTIONNALITÉS AVANCÉES
// ============================================================================
//
// CHAPITRE 8  : Communication entre composants
// CHAPITRE 9  : Gestion d'état (State Management)
// CHAPITRE 10 : JavaScript Interop
// CHAPITRE 11 : Authentification & Autorisation
//
// [TEMPS] TEMPS ESTIMÉ : ~10-12 heures
// [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 8 : COMMUNICATION ENTRE COMPOSANTS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Communiquer Parent -> Enfant (params)
[OK] Communiquer Enfant -> Parent (EventCallback)
[OK] Communiquer entre composants non liés (Event Bus)
[OK] Utiliser un State Container partagé
*/


// ----------------------------------------------------------------------------
// [GRAPHIQUE] RÉSUMÉ DES PATTERNS DE COMMUNICATION
// ----------------------------------------------------------------------------

/*
PATTERN 1 : Parent -> Enfant
─────────────────────────────
Via [Parameter]
Utilisé quand : Parent a les données, enfant les affiche
Exemple : Passer un Produit à une carte produit

PATTERN 2 : Enfant -> Parent
─────────────────────────────
Via EventCallback<T>
Utilisé quand : Enfant a une action, parent doit réagir
Exemple : Clic sur bouton "Supprimer" dans un item de liste

PATTERN 3 : Ancêtre -> Descendant lointain
──────────────────────────────────────────
Via [CascadingParameter]
Utilisé quand : Plusieurs niveaux de profondeur
Exemple : Thème, Utilisateur connecté, Permissions

PATTERN 4 : Composants non liés
────────────────────────────────
Via Service partagé (Scoped) avec events
Utilisé quand : Composants dans des branches différentes
Exemple : Panier <-> Header (nombre d'articles)

PATTERN 5 : N'importe où dans l'app
─────────────────────────────────────
Via State Management (Flux/Redux pattern)
Utilisé quand : État global complexe
Exemple : Utilisateur connecté accessible partout
*/


// ----------------------------------------------------------------------------
// [TRANSPORT] EVENT BUS — Communication entre composants non liés
// ----------------------------------------------------------------------------

/*
PROBLÈME :

Navigation (Header)    ─── (Aucun lien) ───    Panier (Sidebar)
         ^                                              ^
         └──────── Ne peuvent pas se parler ! ──────────┘

SOLUTION : Service intermédiaire (Event Bus / Message Bus)
*/

// Services/PanierEventBus.cs
public class PanierEventBus
{
    // Events que d'autres composants peuvent écouter
    public event Action<int>? ArticleAjoute;   // Notifie avec le nombre total
    public event Action<int>? ArticleRetire;
    public event Action? PanierVide;

    private int _nombreArticles = 0;

    public void NotifierAjout()
    {
        _nombreArticles++;
        ArticleAjoute?.Invoke(_nombreArticles);
    }

    public void NotifierRetrait()
    {
        if (_nombreArticles > 0)
        {
            _nombreArticles--;
            ArticleRetire?.Invoke(_nombreArticles);
        }
    }

    public void NotifierVidePanier()
    {
        _nombreArticles = 0;
        PanierVide?.Invoke();
    }

    public int ObtenirNombre() => _nombreArticles;
}

// Enregistrement dans Program.cs :
// builder.Services.AddScoped<PanierEventBus>();

/*
─────────────────────────────────────────────────────────────────
Composant qui ÉMET des events (page catalogue)
─────────────────────────────────────────────────────────────────
*/

/*
Fichier : Pages/Catalogue.razor

@page "/catalogue"
@inject PanierEventBus PanierBus

<h1>Catalogue</h1>
@foreach (var produit in _produits)
{
    <div class="card">
        <h5>@produit.Nom</h5>
        <button @onclick="() => AjouterAuPanier(produit)">
            Ajouter au panier
        </button>
    </div>
}

@code {
    private List<Produit> _produits = new(); // ... charger les produits

    private void AjouterAuPanier(Produit produit)
    {
        // Logique d'ajout...
        PanierBus.NotifierAjout(); // Notifier tous les abonnés
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Composant qui ÉCOUTE les events (header/navbar)
─────────────────────────────────────────────────────────────────
*/

/*
Fichier : Shared/BoutonPanier.razor

@implements IDisposable
@inject PanierEventBus PanierBus

<a href="/panier" class="btn btn-outline-primary position-relative">
    [SHOPPING_TROLLEY] Panier
    @if (_nombreArticles > 0)
    {
        <span class="position-absolute top-0 start-100 translate-middle
                     badge rounded-pill bg-danger">
            @_nombreArticles
        </span>
    }
</a>

@code {
    private int _nombreArticles = 0;

    protected override void OnInitialized()
    {
        // S'abonner aux events
        PanierBus.ArticleAjoute += MettreAJourCompteur;
        PanierBus.ArticleRetire += MettreAJourCompteur;
        PanierBus.PanierVide   += SurVidePanier;

        _nombreArticles = PanierBus.ObtenirNombre();
    }

    private void MettreAJourCompteur(int nombre)
    {
        _nombreArticles = nombre;
        InvokeAsync(StateHasChanged); // [ATTENTION] Thread-safe !
        // InvokeAsync est OBLIGATOIRE car l'event peut venir
        // d'un thread différent
    }

    private void SurVidePanier()
    {
        _nombreArticles = 0;
        InvokeAsync(StateHasChanged);
    }

    // [ATTENTION] TOUJOURS se désabonner dans Dispose !
    public void Dispose()
    {
        PanierBus.ArticleAjoute -= MettreAJourCompteur;
        PanierBus.ArticleRetire -= MettreAJourCompteur;
        PanierBus.PanierVide   -= SurVidePanier;
    }
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 9 : GESTION D'ÉTAT
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Implémenter un State Container simple
[OK] Comprendre la différence Scoped vs Singleton
[OK] Gérer l'état de l'application correctement
[OK] Persister l'état avec LocalStorage
*/


// ----------------------------------------------------------------------------
// [ARCHIVE] STATE CONTAINER — Pattern simple et efficace
// ----------------------------------------------------------------------------

/*
ÉTAT DE L'APPLICATION = Données partagées entre composants

EXEMPLES D'ÉTAT GLOBAL :
- Utilisateur actuellement connecté
- Thème (dark/light)
- Langue
- Panier d'achat
- Notifications non lues

SOLUTION : Service Scoped avec StateHasChanged
*/

// Services/AppState.cs
public class AppState
{
    // ─── État ────────────────────────────────────────────────────
    private string _theme = "light";
    private int _nombreNotifications = 0;
    private UtilisateurDto? _utilisateurConnecte;

    // ─── Events pour notifier les composants ─────────────────────
    public event Action? OnChange;

    // ─── Propriétés publiques ─────────────────────────────────────
    public string Theme
    {
        get => _theme;
        private set { _theme = value; NotifierChangement(); }
    }

    public int NombreNotifications
    {
        get => _nombreNotifications;
        private set { _nombreNotifications = value; NotifierChangement(); }
    }

    public UtilisateurDto? UtilisateurConnecte
    {
        get => _utilisateurConnecte;
        private set { _utilisateurConnecte = value; NotifierChangement(); }
    }

    public bool EstConnecte => _utilisateurConnecte is not null;

    // ─── Méthodes d'action ────────────────────────────────────────
    public void BasculerTheme()
    {
        Theme = _theme == "light" ? "dark" : "light";
    }

    public void ConnecterUtilisateur(UtilisateurDto utilisateur)
    {
        UtilisateurConnecte = utilisateur;
    }

    public void DeconnecterUtilisateur()
    {
        UtilisateurConnecte = null;
    }

    public void AjouterNotification()
    {
        NombreNotifications++;
    }

    public void MarquerNotificationsLues()
    {
        NombreNotifications = 0;
    }

    // ─── Méthode interne ─────────────────────────────────────────
    private void NotifierChangement()
    {
        OnChange?.Invoke();
    }
}

public record UtilisateurDto(int Id, string Nom, string Email, string Role);

/*
─────────────────────────────────────────────────────────────────
Utiliser AppState dans un composant
─────────────────────────────────────────────────────────────────
*/

/*
Fichier : Shared/Header.razor

@implements IDisposable
@inject AppState AppState

<header class="navbar navbar-@(AppState.Theme == "dark" ? "dark bg-dark" : "light bg-light")">

    <!-- Utilisateur connecté -->
    @if (AppState.EstConnecte)
    {
        <span>[UTILISATEUR] @AppState.UtilisateurConnecte!.Nom</span>

        <!-- Notifications -->
        <button @onclick="AppState.MarquerNotificationsLues"
                class="btn btn-sm btn-outline-secondary position-relative">
            [NOTIF]
            @if (AppState.NombreNotifications > 0)
            {
                <span class="badge bg-danger">@AppState.NombreNotifications</span>
            }
        </button>

        <button @onclick="SeDeconnecter" class="btn btn-sm btn-outline-danger">
            Déconnexion
        </button>
    }
    else
    {
        <a href="/login" class="btn btn-primary btn-sm">Connexion</a>
    }

    <!-- Bouton thème -->
    <button @onclick="AppState.BasculerTheme" class="btn btn-sm btn-outline-secondary">
        @(AppState.Theme == "dark" ? "[BLACK_SUN_WITH_RAYS]" : "[CRESCENT_MOON]")
    </button>

</header>

@code {
    protected override void OnInitialized()
    {
        // S'abonner aux changements d'état
        AppState.OnChange += ReRendreComposant;
    }

    private void ReRendreComposant()
    {
        InvokeAsync(StateHasChanged);
    }

    private void SeDeconnecter()
    {
        AppState.DeconnecterUtilisateur();
    }

    public void Dispose()
    {
        AppState.OnChange -= ReRendreComposant;
    }
}
*/

// Enregistrement (Scoped = une instance par connexion SignalR/utilisateur)
// builder.Services.AddScoped<AppState>();


// ----------------------------------------------------------------------------
// [SAUVEGARDE] PERSISTANCE AVEC LOCALSTORAGE
// ----------------------------------------------------------------------------

/*
LOCALSTORAGE = Stockage côté navigateur
- Persisté après fermeture du navigateur
- Max ~5-10 MB
- Données en string (JSON)

BLAZORED.LOCALSTORAGE = Package populaire pour Blazor
dotnet add package Blazored.LocalStorage
*/

// Program.cs
// builder.Services.AddBlazoredLocalStorage();

// Services/ThemeService.cs (avec persistance)
public class ThemeService
{
    private readonly ILocalStorageService _localStorage;
    private const string CLE_THEME = "app_theme";

    public ThemeService(ILocalStorageService localStorage)
    {
        _localStorage = localStorage;
    }

    public async Task<string> ChargerThemeAsync()
    {
        return await _localStorage.GetItemAsync<string>(CLE_THEME) ?? "light";
    }

    public async Task SauvegarderThemeAsync(string theme)
    {
        await _localStorage.SetItemAsync(CLE_THEME, theme);
    }
}

// Interface pour les interactions avec le stockage local
public interface ILocalStorageService
{
    Task<T?> GetItemAsync<T>(string key);
    Task SetItemAsync<T>(string key, T value);
    Task RemoveItemAsync(string key);
    Task ClearAsync();
}


// ============================================================================
// [GUIDE] CHAPITRE 10 : JAVASCRIPT INTEROP
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Appeler JavaScript depuis C# (IJSRuntime)
[OK] Appeler C# depuis JavaScript
[OK] Utiliser des bibliothèques JavaScript
[OK] Manipuler le DOM
[OK] Gérer les ressources JS correctement
*/


// ----------------------------------------------------------------------------
// [LIEN] IJSRUNTIME — Passerelle entre C# et JavaScript
// ----------------------------------------------------------------------------

/*
POURQUOI L'INTEROP JS ?

Blazor ne peut pas (encore) tout faire nativement :
- Accès clipboard (copier/coller)
- Notifications du navigateur
- Géolocalisation
- Bibliothèques JS existantes (Chart.js, etc.)
- Manipulation fine du DOM
- Download de fichiers
- Scroll vers un élément

IJSRuntime = Service injecté pour appeler JS

MÉTHODES PRINCIPALES :
-> InvokeVoidAsync(function, args)  : Appeler JS sans retour
-> InvokeAsync<T>(function, args)   : Appeler JS avec retour T
*/

/*
─────────────────────────────────────────────────────────────────
Fichier JavaScript : wwwroot/js/interop.js
─────────────────────────────────────────────────────────────────
*/

/*
// Toutes les fonctions JS appelables depuis C#

// Afficher une notification navigateur
window.afficherNotification = function(titre, message) {
    if ("Notification" in window && Notification.permission === "granted") {
        new Notification(titre, { body: message });
    }
};

// Copier dans le clipboard
window.copierTexte = async function(texte) {
    try {
        await navigator.clipboard.writeText(texte);
        return true;
    } catch (e) {
        console.error("Erreur clipboard:", e);
        return false;
    }
};

// Obtenir la géolocalisation
window.obtenirPosition = function() {
    return new Promise((resolve, reject) => {
        navigator.geolocation.getCurrentPosition(
            pos => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }),
            err => reject(err.message)
        );
    });
};

// Scroller vers un élément
window.scrollerVers = function(elementId) {
    const element = document.getElementById(elementId);
    if (element) {
        element.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
};

// Focus sur un élément
window.focusSur = function(elementId) {
    const element = document.getElementById(elementId);
    if (element) element.focus();
};

// Télécharger un fichier
window.telechargerFichier = function(contenu, nomFichier, typeMime) {
    const blob = new Blob([contenu], { type: typeMime });
    const url = URL.createObjectURL(blob);
    const lien = document.createElement('a');
    lien.href = url;
    lien.download = nomFichier;
    lien.click();
    URL.revokeObjectURL(url);
};

// Obtenir la taille de l'écran
window.obtenirTailleEcran = function() {
    return {
        largeur: window.innerWidth,
        hauteur: window.innerHeight,
        mobile: window.innerWidth < 768
    };
};

// Initialiser un graphique Chart.js
window.initialiserGraphique = function(canvasId, donnees) {
    const ctx = document.getElementById(canvasId).getContext('2d');
    return new Chart(ctx, {
        type: 'bar',
        data: donnees,
        options: { responsive: true }
    });
};
*/

/*
─────────────────────────────────────────────────────────────────
Référencer le fichier JS dans index.html (WebAssembly)
─────────────────────────────────────────────────────────────────
*/

/*
wwwroot/index.html :
<body>
    ...
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="js/interop.js"></script>  <- Ajouter ici
</body>
*/

/*
─────────────────────────────────────────────────────────────────
Utiliser IJSRuntime dans un composant
─────────────────────────────────────────────────────────────────
*/

/*
@inject IJSRuntime JS

@code {
    // ─── Sans retour ─────────────────────────────────────────────
    private async Task ScrollerEnHaut()
    {
        await JS.InvokeVoidAsync("scrollerVers", "top");
    }

    // ─── Avec retour ─────────────────────────────────────────────
    private async Task<bool> CopierTexteDansClipboard(string texte)
    {
        return await JS.InvokeAsync<bool>("copierTexte", texte);
    }

    // ─── Retour d'objet complexe ──────────────────────────────────
    private record TailleEcran(int Largeur, int Hauteur, bool Mobile);

    private async Task<TailleEcran> ObtenirTailleEcran()
    {
        return await JS.InvokeAsync<TailleEcran>("obtenirTailleEcran");
    }

    // ─── Objet complexe en paramètre ─────────────────────────────
    private async Task InitGraphique()
    {
        var donnees = new
        {
            labels = new[] { "Jan", "Fév", "Mar", "Avr" },
            datasets = new[]
            {
                new { label = "Ventes", data = new[] { 100, 150, 200, 175 } }
            }
        };
        await JS.InvokeVoidAsync("initialiserGraphique", "monCanvas", donnees);
    }

    // ─── Télécharger un fichier ───────────────────────────────────
    private async Task TelechargerCsv(string contenu)
    {
        await JS.InvokeVoidAsync("telechargerFichier",
            contenu,
            "export.csv",
            "text/csv");
    }
}
*/


// ----------------------------------------------------------------------------
// [IMPORTANT] ELEMENTREFERENCE — Accéder à un élément DOM
// ----------------------------------------------------------------------------

/*
ElementReference = Référence vers un élément HTML du DOM
Permet d'appeler JS sur un élément SPÉCIFIQUE sans ID

QUAND UTILISER ?
-> Focus automatique sur un input
-> Initialiser une lib JS sur cet élément précis
-> Mesurer dimensions
*/

/*
Fichier : Components/InputAutoFocus.razor

<input @ref="_inputRef"
       @bind="_valeur"
       class="form-control"
       type="@Type" />

@code {
    [Parameter] public string Type { get; set; } = "text";
    [Parameter] public bool AutoFocus { get; set; } = false;

    private ElementReference _inputRef;
    private string _valeur = string.Empty;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && AutoFocus)
        {
            // Utiliser ElementReference pour focus sur CET élément
            await _inputRef.FocusAsync();
            // Équivalent à : await JS.InvokeVoidAsync("focusSur", _inputRef);
        }
    }
}
*/


// ----------------------------------------------------------------------------
// <- C# DEPUIS JAVASCRIPT — DotNetObjectReference
// ----------------------------------------------------------------------------

/*
PARFOIS JavaScript doit appeler C# :
- Callbacks de bibliothèques JS
- Événements du navigateur (resize, etc.)
- Timers gérés côté JS

DotNetObjectReference = Expose un objet C# à JavaScript
*/

/*
─────────────────────────────────────────────────────────────────
Fichier : Components/TrackeurRedimension.razor
─────────────────────────────────────────────────────────────────
*/

/*
@implements IDisposable
@inject IJSRuntime JS

<p>Largeur fenêtre : @_largeur px</p>
<p>Mobile : @(_mobile ? "Oui" : "Non")</p>

@code {
    private int _largeur = 0;
    private bool _mobile = false;
    private DotNetObjectReference<TrackeurRedimension>? _dotNetRef;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Créer référence vers cet objet C#
            _dotNetRef = DotNetObjectReference.Create(this);

            // Passer la référence à JavaScript
            await JS.InvokeVoidAsync("abonnerRedimension", _dotNetRef);
        }
    }

    // Méthode APPELABLE depuis JavaScript
    [JSInvokable]
    public void MettreAJourTaille(int largeur, bool mobile)
    {
        _largeur = largeur;
        _mobile = mobile;
        StateHasChanged();
    }

    public void Dispose()
    {
        _dotNetRef?.Dispose(); // [ATTENTION] TOUJOURS Dispose !
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Côté JavaScript (wwwroot/js/interop.js)
─────────────────────────────────────────────────────────────────
*/

/*
window.abonnerRedimension = function(dotNetRef) {
    function notifier() {
        const largeur = window.innerWidth;
        const mobile = largeur < 768;
        // Appeler la méthode C# [JSInvokable]
        dotNetRef.invokeMethodAsync('MettreAJourTaille', largeur, mobile);
    }

    window.addEventListener('resize', notifier);
    notifier(); // Appel initial
};
*/


// ----------------------------------------------------------------------------
// [PACKAGE] JSRUNTIME ISOLÉ — IJSObjectReference
// ----------------------------------------------------------------------------

/*
PROBLÈME : Les fonctions JS globales polluent window

SOLUTION : Modules JavaScript (ES6 modules)
-> Encapsuler les fonctions dans des modules
-> Importer dynamiquement
-> Isoler le scope

AVANTAGES :
- Pas de pollution de window
- Imports paresseux (lazy loading)
- Meilleure organisation
*/

/*
─────────────────────────────────────────────────────────────────
Fichier : wwwroot/js/clipboard.js (module ES6)
─────────────────────────────────────────────────────────────────
*/

/*
// Export des fonctions (PAS de window.xxx)
export async function copier(texte) {
    try {
        await navigator.clipboard.writeText(texte);
        return true;
    } catch {
        return false;
    }
}

export async function coller() {
    return await navigator.clipboard.readText();
}
*/

/*
─────────────────────────────────────────────────────────────────
Service C# utilisant le module
─────────────────────────────────────────────────────────────────
*/

public class ClipboardService : IAsyncDisposable
{
    private readonly Lazy<Task<IJSObjectReference>> _moduleTask;

    public ClipboardService(IJSRuntime jsRuntime)
    {
        // Import paresseux du module JS
        _moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
            "import", "./js/clipboard.js").AsTask());
    }

    public async Task<bool> CopierAsync(string texte)
    {
        var module = await _moduleTask.Value;
        return await module.InvokeAsync<bool>("copier", texte);
    }

    public async Task<string> CollerAsync()
    {
        var module = await _moduleTask.Value;
        return await module.InvokeAsync<string>("coller");
    }

    // Libérer le module JS
    public async ValueTask DisposeAsync()
    {
        if (_moduleTask.IsValueCreated)
        {
            var module = await _moduleTask.Value;
            await module.DisposeAsync();
        }
    }
}

// Interface JS pour l'injection de dépendances
public interface IJSObjectReference : IAsyncDisposable
{
    ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args);
    ValueTask InvokeVoidAsync(string identifier, params object?[]? args);
}


// ============================================================================
// [GUIDE] CHAPITRE 11 : AUTHENTIFICATION & AUTORISATION
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre AuthenticationStateProvider
[OK] Implémenter JWT Authentication
[OK] Utiliser AuthorizeView dans les templates
[OK] Protéger les routes avec [Authorize]
[OK] Vérifier les rôles et policies
*/


// ----------------------------------------------------------------------------
// [SECURISE] AUTHENTICATION STATE — Le cœur de l'auth Blazor
// ----------------------------------------------------------------------------

/*
COMMENT BLAZOR GÈRE L'AUTHENTIFICATION :

AuthenticationStateProvider = Service qui fournit l'état d'auth
-> Blazor demande -> "Qui est connecté ?"
-> AuthenticationStateProvider répond avec un ClaimsPrincipal

ClaimsPrincipal = L'identité de l'utilisateur
Claims = Infos sur l'utilisateur (nom, email, rôle...)


FLUX COMPLET :

1. Utilisateur entre email/password
2. Appel API -> Serveur vérifie
3. Serveur retourne JWT Token
4. On stocke le token (LocalStorage / Cookie)
5. On configure AuthenticationStateProvider avec ce token
6. Tous les composants reçoivent l'état mis à jour
7. Les zones [Authorize] s'affichent/cachent automatiquement


PACKAGES NÉCESSAIRES :
dotnet add package Microsoft.AspNetCore.Components.Authorization
*/

/*
Dans Program.cs :
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, JwtAuthStateProvider>();
builder.Services.AddScoped<AuthService>();
*/

/*
Dans App.razor (ENTOURER de CascadingAuthenticationState) :

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                               DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
                <Authorizing>
                    <p>Vérification de l'authentification...</p>
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
    </Router>
</CascadingAuthenticationState>
*/


// ----------------------------------------------------------------------------
// [CLE] JWT AUTHENTICATION PROVIDER
// ----------------------------------------------------------------------------

// Services/JwtAuthStateProvider.cs
public class JwtAuthStateProvider : AuthenticationStateProvider
{
    private readonly ILocalStorageService _localStorage;
    private readonly HttpClient _httpClient;

    // Clé de stockage du token
    private const string CLE_TOKEN = "jwt_token";

    // Utilisateur non authentifié (par défaut)
    private static readonly AuthenticationState EtatNonAuth =
        new(new ClaimsPrincipal(new ClaimsIdentity()));

    public JwtAuthStateProvider(
        ILocalStorageService localStorage,
        HttpClient httpClient)
    {
        _localStorage = localStorage;
        _httpClient = httpClient;
    }

    // Blazor appelle cette méthode pour connaître l'état
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await _localStorage.GetItemAsync<string>(CLE_TOKEN);

        if (string.IsNullOrEmpty(token))
            return EtatNonAuth;

        // Vérifier si le token est expiré
        if (EstTokenExpire(token))
        {
            await DeconnecterAsync();
            return EtatNonAuth;
        }

        // Configurer le header Authorization
        _httpClient.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

        // Extraire les claims du token JWT
        var claims = ExtraireClaims(token);
        var identite = new ClaimsIdentity(claims, "jwt");
        var utilisateur = new ClaimsPrincipal(identite);

        return new AuthenticationState(utilisateur);
    }

    // Appeler après connexion réussie
    public async Task ConnecterAsync(string token)
    {
        await _localStorage.SetItemAsync(CLE_TOKEN, token);

        _httpClient.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

        var claims = ExtraireClaims(token);
        var identite = new ClaimsIdentity(claims, "jwt");
        var utilisateur = new ClaimsPrincipal(identite);

        // Notifier TOUS les composants du changement d'état
        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(utilisateur)));
    }

    // Appeler lors de la déconnexion
    public async Task DeconnecterAsync()
    {
        await _localStorage.RemoveItemAsync(CLE_TOKEN);

        _httpClient.DefaultRequestHeaders.Authorization = null;

        // Notifier TOUS les composants
        NotifyAuthenticationStateChanged(Task.FromResult(EtatNonAuth));
    }

    // Extraire les claims du token JWT
    private IEnumerable<Claim> ExtraireClaims(string token)
    {
        var payload = token.Split('.')[1];
        var jsonBytes = ParseBase64WithoutPadding(payload);
        var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes)!;

        return keyValuePairs.Select(kvp =>
            new Claim(kvp.Key, kvp.Value.ToString() ?? string.Empty));
    }

    private bool EstTokenExpire(string token)
    {
        var claims = ExtraireClaims(token);
        var expClaim = claims.FirstOrDefault(c => c.Type == "exp");

        if (expClaim is null) return true;

        var exp = long.Parse(expClaim.Value);
        var expDate = DateTimeOffset.FromUnixTimeSeconds(exp);
        return expDate < DateTimeOffset.UtcNow;
    }

    private byte[] ParseBase64WithoutPadding(string base64)
    {
        switch (base64.Length % 4)
        {
            case 2: base64 += "=="; break;
            case 3: base64 += "="; break;
        }
        return Convert.FromBase64String(base64);
    }
}

// Imports nécessaires (simulés)
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Authorization;


// ----------------------------------------------------------------------------
// [CLE] SERVICE D'AUTHENTIFICATION
// ----------------------------------------------------------------------------

// Models/LoginRequest.cs
public record LoginRequest(string Email, string MotDePasse);
public record LoginResponse(string Token, string RefreshToken, DateTime Expiration);

// Services/AuthService.cs
public class AuthService
{
    private readonly HttpClient _httpClient;
    private readonly JwtAuthStateProvider _authProvider;

    public AuthService(HttpClient httpClient, JwtAuthStateProvider authProvider)
    {
        _httpClient = httpClient;
        _authProvider = authProvider;
    }

    public async Task<(bool Succes, string? Erreur)> ConnecterAsync(LoginRequest requete)
    {
        try
        {
            var reponse = await _httpClient.PostAsJsonAsync("api/auth/login", requete);

            if (!reponse.IsSuccessStatusCode)
                return (false, "Email ou mot de passe incorrect");

            var loginReponse = await reponse.Content.ReadFromJsonAsync<LoginResponse>();

            if (loginReponse is null)
                return (false, "Réponse invalide du serveur");

            // Mettre à jour l'état d'authentification
            await _authProvider.ConnecterAsync(loginReponse.Token);

            return (true, null);
        }
        catch (HttpRequestException)
        {
            return (false, "Impossible de contacter le serveur");
        }
    }

    public async Task DeconnecterAsync()
    {
        await _authProvider.DeconnecterAsync();
    }
}


// ----------------------------------------------------------------------------
// [SECURITE] AUTHORIZE VIEW ET DIRECTIVES — Contrôle d'accès dans les templates
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
AuthorizeView — Afficher selon l'état d'auth
─────────────────────────────────────────────────────────────────
*/

/*
<!-- Affichage basé sur l'authentification -->
<AuthorizeView>

    <!-- Affiché si CONNECTÉ -->
    <Authorized>
        <p>Bonjour, @context.User.Identity?.Name !</p>
        <a href="/profil">Mon Profil</a>
        <a href="/logout">Déconnexion</a>
    </Authorized>

    <!-- Affiché si NON CONNECTÉ -->
    <NotAuthorized>
        <a href="/login">Connexion</a>
        <a href="/register">Inscription</a>
    </NotAuthorized>

    <!-- Optionnel : Pendant la vérification -->
    <Authorizing>
        <span>Vérification...</span>
    </Authorizing>

</AuthorizeView>


<!-- Affichage basé sur le RÔLE -->
<AuthorizeView Roles="Admin">
    <p>Contenu réservé aux admins</p>
    <a href="/admin">Panneau Admin</a>
</AuthorizeView>


<!-- Affichage basé sur une POLICY -->
<AuthorizeView Policy="PeutGererProduits">
    <button @onclick="AjouterProduit">+ Ajouter Produit</button>
</AuthorizeView>


<!-- Combiné : Rôle ET Policy -->
<AuthorizeView Roles="Admin,Gestionnaire">
    <!-- Visible si Admin OU Gestionnaire -->
    <a href="/rapports">Rapports</a>
</AuthorizeView>
*/


// ----------------------------------------------------------------------------
// [VERROUILLE] [AUTHORIZE] SUR LES PAGES — Protéger des routes entières
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
Protéger une page complète
─────────────────────────────────────────────────────────────────
*/

/*
@page "/dashboard"
@attribute [Authorize]   <- Toute la page est protégée
                           Si non connecté -> Redirigé vers /login

<h1>Tableau de bord</h1>
<p>Contenu réservé aux utilisateurs connectés.</p>
*/

/*
@page "/admin"
@attribute [Authorize(Roles = "Admin")]  <- Rôle requis

<h1>Administration</h1>
*/

/*
@page "/produits/edit"
@attribute [Authorize(Policy = "PeutGererProduits")]  <- Policy requise

<h1>Éditer les produits</h1>
*/


// ----------------------------------------------------------------------------
// [DOC] CLAIMS ET POLICIES
// ----------------------------------------------------------------------------

/*
CLAIMS = Informations sur l'utilisateur dans le JWT

Exemples de claims courants :
- "sub" -> ID utilisateur
- "name" -> Nom
- "email" -> Email
- "role" -> Rôle (Admin, User, etc.)
- "permission" -> Permission spécifique
- "exp" -> Date d'expiration

POLICIES = Règles d'autorisation personnalisées
Plus flexibles que les rôles simples
*/

// Configurer les policies dans Program.cs :
/*
builder.Services.AddAuthorizationCore(options =>
{
    // Policy basée sur un claim
    options.AddPolicy("PeutGererProduits", policy =>
        policy.RequireClaim("permission", "produits:write"));

    // Policy combinée
    options.AddPolicy("AdminOuGestionnaire", policy =>
        policy.RequireRole("Admin", "Gestionnaire"));

    // Policy personnalisée
    options.AddPolicy("CompteVérifié", policy =>
        policy.RequireClaim("email_verified", "true"));
});
*/

// Services/AuthorizationService.cs (pour vérifier par code)
public class AuthorizationHelper
{
    private readonly AuthenticationStateProvider _authProvider;

    public AuthorizationHelper(AuthenticationStateProvider authProvider)
    {
        _authProvider = authProvider;
    }

    public async Task<bool> EstConnecteAsync()
    {
        var state = await _authProvider.GetAuthenticationStateAsync();
        return state.User.Identity?.IsAuthenticated ?? false;
    }

    public async Task<bool> EstAdminAsync()
    {
        var state = await _authProvider.GetAuthenticationStateAsync();
        return state.User.IsInRole("Admin");
    }

    public async Task<string?> ObtenirIdUtilisateurAsync()
    {
        var state = await _authProvider.GetAuthenticationStateAsync();
        return state.User.FindFirst("sub")?.Value;
    }

    public async Task<string?> ObtenirNomAsync()
    {
        var state = await _authProvider.GetAuthenticationStateAsync();
        return state.User.Identity?.Name;
    }
}

/*
─────────────────────────────────────────────────────────────────
Page de Login complète
─────────────────────────────────────────────────────────────────
*/

/*
@page "/login"
@layout AuthLayout
@inject AuthService AuthService
@inject NavigationManager NavManager
@inject AuthenticationStateProvider AuthProvider

@{
    // Si déjà connecté -> Rediriger
    var state = await AuthProvider.GetAuthenticationStateAsync();
    if (state.User.Identity?.IsAuthenticated ?? false)
    {
        NavManager.NavigateTo("/dashboard");
    }
}

<div class="login-container">
    <h2>Connexion</h2>

    <EditForm Model="@_loginModel" OnValidSubmit="SeConnecter">
        <DataAnnotationsValidator />

        @if (!string.IsNullOrEmpty(_erreur))
        {
            <div class="alert alert-danger">@_erreur</div>
        }

        <div class="mb-3">
            <label>Email</label>
            <InputText @bind-Value="_loginModel.Email" type="email" class="form-control" />
            <ValidationMessage For="@(() => _loginModel.Email)" />
        </div>

        <div class="mb-3">
            <label>Mot de passe</label>
            <InputText @bind-Value="_loginModel.MotDePasse" type="password" class="form-control" />
            <ValidationMessage For="@(() => _loginModel.MotDePasse)" />
        </div>

        <button type="submit" class="btn btn-primary w-100" disabled="@_connexionEnCours">
            @(_connexionEnCours ? "Connexion..." : "Se connecter")
        </button>
    </EditForm>

    <p class="mt-3 text-center">
        Pas encore de compte ? <a href="/register">S'inscrire</a>
    </p>
</div>

@code {
    private LoginFormModel _loginModel = new();
    private string? _erreur;
    private bool _connexionEnCours = false;

    private async Task SeConnecter()
    {
        _connexionEnCours = true;
        _erreur = null;

        var requete = new LoginRequest(_loginModel.Email, _loginModel.MotDePasse);
        var (succes, erreur) = await AuthService.ConnecterAsync(requete);

        if (succes)
        {
            NavManager.NavigateTo("/dashboard");
        }
        else
        {
            _erreur = erreur;
        }

        _connexionEnCours = false;
    }

    public class LoginFormModel
    {
        [Required(ErrorMessage = "L'email est requis")]
        [EmailAddress(ErrorMessage = "Email invalide")]
        public string Email { get; set; } = string.Empty;

        [Required(ErrorMessage = "Le mot de passe est requis")]
        public string MotDePasse { get; set; } = string.Empty;
    }
}
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE — PARTIE 3
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
EXERCICE : TABLEAU DE BORD AVEC AUTHENTIFICATION ET ÉTAT GLOBAL
═══════════════════════════════════════════════════════════════

OBJECTIF : Application complète avec auth + état + JS Interop

FONCTIONNALITÉS :

1. AppState Service :
   - Thème (dark/light) persisté en LocalStorage
   - Utilisateur connecté
   - Compteur de notifications
   - Event OnChange

2. Page de Login (/login) :
   - Formulaire avec validation
   - Simulation de connexion (hardcoded credentials)
   - Redirection vers /dashboard après succès

3. Page Dashboard (/dashboard) :
   - Protégée avec [Authorize] simulé
   - Affiche le nom de l'utilisateur
   - Bouton de déconnexion
   - Bouton "Copier lien" (via JS Interop)
   - Bouton basculer thème (via AppState)

4. Header commun :
   - Affiche état connexion (AuthorizeView)
   - Badge notification (EventBus)
   - Bouton thème
   - Réactif aux changements AppState

POUR SIMPLIFIER : Utiliser un FakeAuthProvider
(sans vrai JWT, juste simuler l'état connecté/déconnecté)

COMPÉTENCES :
[OK] State Container (AppState)
[OK] Event Bus
[OK] JS Interop (clipboard)
[OK] AuthorizeView
[OK] IDisposable
[OK] NavigationManager
═══════════════════════════════════════════════════════════════
*/


// ============================================================================
// [OK] CORRIGÉ SIMPLIFIÉ DE L'EXERCICE
// ============================================================================

// ─── Services/AppState.cs ────────────────────────────────────────────────────

public class AppStateSimple
{
    // ─── ÉTAT ───────────────────────────────────────────────────
    private string _theme = "light";
    private string? _nomUtilisateur;
    private int _notifications = 0;
    private bool _estConnecte = false;

    // ─── EVENT ──────────────────────────────────────────────────
    public event Action? OnChange;

    // ─── PROPRIÉTÉS ─────────────────────────────────────────────
    public string Theme => _theme;
    public string? NomUtilisateur => _nomUtilisateur;
    public int Notifications => _notifications;
    public bool EstConnecte => _estConnecte;

    // ─── ACTIONS ────────────────────────────────────────────────
    public void BasculerTheme()
    {
        _theme = _theme == "light" ? "dark" : "light";
        NotifierChangement();
    }

    public void Connecter(string nomUtilisateur)
    {
        _nomUtilisateur = nomUtilisateur;
        _estConnecte = true;
        _notifications = 3; // Simulation: nouvelles notifs à la connexion
        NotifierChangement();
    }

    public void Deconnecter()
    {
        _nomUtilisateur = null;
        _estConnecte = false;
        _notifications = 0;
        NotifierChangement();
    }

    public void MarquerNotificationsLues()
    {
        _notifications = 0;
        NotifierChangement();
    }

    private void NotifierChangement() => OnChange?.Invoke();
}

// ─── Shared/HeaderAvecState.razor ────────────────────────────────────────────

/*
@implements IDisposable
@inject AppStateSimple AppState
@inject NavigationManager NavManager

<header class="navbar @(AppState.Theme == "dark" ? "navbar-dark bg-dark" : "navbar-light bg-light") px-4">

    <a class="navbar-brand" href="/">[RAPIDE] MonApp</a>

    <div class="ms-auto d-flex align-items-center gap-3">

        @if (AppState.EstConnecte)
        {
            <span class="text-muted">[UTILISATEUR] @AppState.NomUtilisateur</span>

            <button @onclick="AppState.MarquerNotificationsLues"
                    class="btn btn-sm btn-outline-secondary position-relative">
                [NOTIF]
                @if (AppState.Notifications > 0)
                {
                    <span class="position-absolute top-0 start-100
                                 translate-middle badge bg-danger rounded-pill">
                        @AppState.Notifications
                    </span>
                }
            </button>

            <button @onclick="SeDeconnecter" class="btn btn-sm btn-danger">
                Déconnexion
            </button>
        }
        else
        {
            <a href="/login" class="btn btn-sm btn-primary">Connexion</a>
        }

        <button @onclick="AppState.BasculerTheme"
                class="btn btn-sm btn-outline-secondary" title="Changer le thème">
            @(AppState.Theme == "dark" ? "[BLACK_SUN_WITH_RAYS]" : "[CRESCENT_MOON]")
        </button>
    </div>
</header>

@code {
    protected override void OnInitialized()
    {
        AppState.OnChange += ReRendre;
    }

    private void ReRendre() => InvokeAsync(StateHasChanged);

    private void SeDeconnecter()
    {
        AppState.Deconnecter();
        NavManager.NavigateTo("/login");
    }

    public void Dispose()
    {
        AppState.OnChange -= ReRendre;
    }
}
*/

// ─── Pages/Login.razor ───────────────────────────────────────────────────────

/*
@page "/login"
@inject AppStateSimple AppState
@inject NavigationManager NavManager

<div class="container d-flex vh-100 align-items-center justify-content-center">
    <div class="card p-4" style="width: 380px">
        <h3 class="mb-4 text-center">[SECURISE] Connexion</h3>

        @if (_erreur is not null)
        {
            <div class="alert alert-danger">@_erreur</div>
        }

        <EditForm Model="@_form" OnValidSubmit="SeConnecter">
            <DataAnnotationsValidator />

            <div class="mb-3">
                <label class="form-label">Email</label>
                <InputText @bind-Value="_form.Email" type="email" class="form-control" />
                <ValidationMessage For="@(() => _form.Email)" class="text-danger" />
            </div>

            <div class="mb-3">
                <label class="form-label">Mot de passe</label>
                <InputText @bind-Value="_form.MotDePasse" type="password" class="form-control" />
                <ValidationMessage For="@(() => _form.MotDePasse)" class="text-danger" />
            </div>

            <button type="submit" class="btn btn-primary w-100">
                Se connecter
            </button>
        </EditForm>

        <small class="text-muted text-center d-block mt-3">
            Essayez : admin@test.com / admin123
        </small>
    </div>
</div>

@code {
    private LoginFormModel _form = new();
    private string? _erreur;

    // Credentials hardcodés pour la démo
    private readonly Dictionary<string, string> _usersDemo = new()
    {
        ["admin@test.com"] = "admin123",
        ["user@test.com"] = "user123"
    };

    private void SeConnecter()
    {
        if (_usersDemo.TryGetValue(_form.Email, out var mdp) && mdp == _form.MotDePasse)
        {
            var nom = _form.Email.Split('@')[0];
            AppState.Connecter(nom);
            NavManager.NavigateTo("/dashboard");
        }
        else
        {
            _erreur = "Email ou mot de passe incorrect.";
        }
    }

    public class LoginFormModel
    {
        [Required(ErrorMessage = "Email requis")]
        [EmailAddress(ErrorMessage = "Email invalide")]
        public string Email { get; set; } = string.Empty;

        [Required(ErrorMessage = "Mot de passe requis")]
        public string MotDePasse { get; set; } = string.Empty;
    }
}
*/

// ─── Pages/Dashboard.razor ───────────────────────────────────────────────────

/*
@page "/dashboard"
@inject AppStateSimple AppState
@inject NavigationManager NavManager
@inject IJSRuntime JS

@if (!AppState.EstConnecte)
{
    NavManager.NavigateTo("/login", replace: true);
    return;
}

<div class="container mt-4">
    <h1>[GRAPHIQUE] Tableau de bord</h1>
    <p class="lead">Bienvenue, <strong>@AppState.NomUtilisateur</strong> !</p>

    <div class="row g-4 mt-2">
        <div class="col-md-4">
            <div class="card text-center p-4">
                <h5>[DESIGN] Thème actuel</h5>
                <p class="badge bg-secondary">@AppState.Theme</p>
                <button @onclick="AppState.BasculerTheme" class="btn btn-sm btn-outline-primary">
                    Changer le thème
                </button>
            </div>
        </div>

        <div class="col-md-4">
            <div class="card text-center p-4">
                <h5>[LISTE] Copier le lien</h5>
                <p class="text-muted small">@_messageClipboard</p>
                <button @onclick="CopierLien" class="btn btn-sm btn-outline-secondary">
                    [LISTE] Copier l'URL
                </button>
            </div>
        </div>

        <div class="col-md-4">
            <div class="card text-center p-4">
                <h5>[NOTIF] Notifications</h5>
                <p class="badge bg-danger">@AppState.Notifications en attente</p>
                <button @onclick="AppState.MarquerNotificationsLues"
                        class="btn btn-sm btn-outline-danger">
                    Marquer comme lues
                </button>
            </div>
        </div>
    </div>
</div>

@code {
    private string _messageClipboard = "Cliquer pour copier";

    private async Task CopierLien()
    {
        try
        {
            var url = "https://monapp.blazor.com/dashboard";
            await JS.InvokeVoidAsync("navigator.clipboard.writeText", url);
            _messageClipboard = "[OK] Lien copié !";
            await Task.Delay(2000);
            _messageClipboard = "Cliquer pour copier";
        }
        catch
        {
            _messageClipboard = "[X] Erreur de copie";
        }
    }
}
*/


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 3

[OK] CHAPITRE 8 - COMMUNICATION :
- Patterns : Parent->Enfant, Enfant->Parent, Cascade, Event Bus
- Service partagé Scoped avec events pour communication globale
- InvokeAsync(StateHasChanged) pour mises à jour thread-safe
- IDisposable : TOUJOURS se désabonner des events

[OK] CHAPITRE 9 - GESTION D'ÉTAT :
- State Container pattern : Service + Event OnChange
- Scoped = une instance par utilisateur/connexion
- Singleton = partagé entre tous les utilisateurs
- Persistance avec Blazored.LocalStorage

[OK] CHAPITRE 10 - JAVASCRIPT INTEROP :
- IJSRuntime.InvokeVoidAsync / InvokeAsync<T>
- ElementReference pour cibler des éléments DOM
- DotNetObjectReference + [JSInvokable] pour JS->C#
- Modules ES6 avec IJSObjectReference (plus propre)
- Toujours Dispose les DotNetObjectReference et modules

[OK] CHAPITRE 11 - AUTHENTIFICATION :
- AuthenticationStateProvider override
- JWT parsing et stockage
- AuthorizeView pour affichage conditionnel
- [Authorize] pour protéger les pages
- Roles et Policies
- NotifyAuthenticationStateChanged

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 4
- Intégration API REST (HttpClient)
- ASP.NET Core + Blazor (Full Stack)
- Entity Framework Core
- UI Professionnelle (MudBlazor)
═══════════════════════════════════════════════════════════════
*/

// ============================================================================
// [LIVRE] BLAZOR - PARTIE 4 : BACKEND & API INTEGRATION
// ============================================================================
//
// CHAPITRE 12 : Intégration API REST (HttpClient)
// CHAPITRE 13 : ASP.NET Core + Blazor (Full Stack)
// CHAPITRE 14 : Base de données (Entity Framework Core)
//
// [TEMPS] TEMPS ESTIMÉ : ~10-12 heures
// [DOCS] PRÉREQUIS : Parties 1-3 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 12 : INTÉGRATION API REST
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Configurer HttpClient dans Blazor
[OK] Faire des appels GET, POST, PUT, DELETE
[OK] Gérer les erreurs HTTP
[OK] Utiliser des Typed HttpClients
[OK] Implémenter la politique de retry
[OK] Gérer l'authentification JWT dans les requêtes
*/


// ----------------------------------------------------------------------------
// [WEB] HTTPCLIENT — Le client HTTP de .NET
// ----------------------------------------------------------------------------

/*
HTTPCLIENT = Classe .NET pour faire des requêtes HTTP vers des APIs

DANS BLAZOR WEBASSEMBLY :
- Utilise fetch() du navigateur sous le hood
- Même domaine = Pas de CORS nécessaire
- Autre domaine = CORS doit être configuré côté serveur


CONFIGURATION DANS Program.cs :
*/

/*
// Option 1 : Client simple (pour API même domaine)
builder.Services.AddScoped(sp => new HttpClient
{
    BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});

// Option 2 : Client avec URL spécifique
builder.Services.AddScoped(sp => new HttpClient
{
    BaseAddress = new Uri("https://api.monapp.com/"),
    Timeout = TimeSpan.FromSeconds(30)
});

// Option 3 : Typed Client (recommandé pour grandes apps)
builder.Services.AddHttpClient<IProduitApi, ProduitApi>(client =>
{
    client.BaseAddress = new Uri("https://api.monapp.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
    client.Timeout = TimeSpan.FromSeconds(30);
});
*/


// ----------------------------------------------------------------------------
// [RESEAU] REQUÊTES HTTPCLIENT — GET, POST, PUT, DELETE
// ----------------------------------------------------------------------------

public class ProduitApiService
{
    private readonly HttpClient _httpClient;

    public ProduitApiService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    // ─── GET — Récupérer des données ─────────────────────────────────────────

    // GET : Tous les produits
    public async Task<List<Produit>> ObtenirTousAsync()
    {
        // GetFromJsonAsync : GET + désérialisation JSON automatique
        var produits = await _httpClient
            .GetFromJsonAsync<List<Produit>>("api/produits");

        return produits ?? new List<Produit>();
    }

    // GET : Un produit par ID
    public async Task<Produit?> ObtenirParIdAsync(int id)
    {
        try
        {
            return await _httpClient.GetFromJsonAsync<Produit>($"api/produits/{id}");
        }
        catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return null; // Retourner null si 404
        }
    }

    // GET : Avec query string
    public async Task<PagedResult<Produit>> RechercherAsync(
        string? terme = null,
        string? categorie = null,
        int page = 1,
        int parPage = 20)
    {
        // Construire l'URL avec query string
        var queryParams = new Dictionary<string, string?>();

        if (!string.IsNullOrEmpty(terme))
            queryParams["terme"] = terme;
        if (!string.IsNullOrEmpty(categorie))
            queryParams["categorie"] = categorie;

        queryParams["page"] = page.ToString();
        queryParams["parPage"] = parPage.ToString();

        var url = Microsoft.AspNetCore.WebUtilities.QueryHelpers
            .AddQueryString("api/produits/recherche", queryParams);

        return await _httpClient.GetFromJsonAsync<PagedResult<Produit>>(url)
            ?? new PagedResult<Produit>();
    }

    // ─── POST — Créer une ressource ──────────────────────────────────────────

    public async Task<Produit?> CreerAsync(Produit produit)
    {
        // PostAsJsonAsync : POST + sérialisation JSON automatique
        var reponse = await _httpClient
            .PostAsJsonAsync("api/produits", produit);

        // Vérifier le succès
        reponse.EnsureSuccessStatusCode(); // Lance exception si erreur

        // Lire la réponse
        return await reponse.Content.ReadFromJsonAsync<Produit>();
    }

    // POST avec gestion d'erreur détaillée
    public async Task<(Produit? Produit, string? Erreur)> CreerAvecGestionErreurAsync(
        Produit produit)
    {
        try
        {
            var reponse = await _httpClient.PostAsJsonAsync("api/produits", produit);

            if (reponse.IsSuccessStatusCode)
            {
                var resultat = await reponse.Content.ReadFromJsonAsync<Produit>();
                return (resultat, null);
            }

            // Lire le message d'erreur de l'API
            var erreurTexte = await reponse.Content.ReadAsStringAsync();
            return (null, $"Erreur {(int)reponse.StatusCode}: {erreurTexte}");
        }
        catch (HttpRequestException ex)
        {
            return (null, $"Erreur réseau: {ex.Message}");
        }
        catch (Exception ex)
        {
            return (null, $"Erreur inattendue: {ex.Message}");
        }
    }

    // ─── PUT — Modifier une ressource ────────────────────────────────────────

    public async Task<bool> ModifierAsync(int id, Produit produit)
    {
        // PutAsJsonAsync : PUT + sérialisation JSON
        var reponse = await _httpClient
            .PutAsJsonAsync($"api/produits/{id}", produit);

        return reponse.IsSuccessStatusCode;
    }

    // PATCH — Modification partielle
    public async Task<bool> ModifierPrixAsync(int id, decimal nouveauPrix)
    {
        var patchData = new { Prix = nouveauPrix };
        var json = JsonSerializer.Serialize(patchData);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        var reponse = await _httpClient.PatchAsync($"api/produits/{id}", content);
        return reponse.IsSuccessStatusCode;
    }

    // ─── DELETE — Supprimer une ressource ────────────────────────────────────

    public async Task<bool> SupprimerAsync(int id)
    {
        var reponse = await _httpClient.DeleteAsync($"api/produits/{id}");
        return reponse.IsSuccessStatusCode;
    }
}

// Modèles
public class PagedResult<T>
{
    public List<T> Items { get; set; } = new();
    public int TotalItems { get; set; }
    public int Page { get; set; }
    public int ParPage { get; set; }
    public int TotalPages => (int)Math.Ceiling((double)TotalItems / ParPage);
}

// Imports nécessaires
using System.Text;
using System.Text.Json;


// ----------------------------------------------------------------------------
// [SYNC] TYPED HTTPCLIENT — Organisation professionnelle
// ----------------------------------------------------------------------------

/*
Typed HttpClient = Une classe dédiée par ressource/domaine

AVANTAGES :
- Encapsulation des appels API
- Configuration centralisée
- Testabilité (interfaces)
- Gestion des headers spécifiques
*/

// Interfaces
public interface IProduitApi
{
    Task<List<Produit>> ObtenirTousAsync();
    Task<Produit?> ObtenirParIdAsync(int id);
    Task<Produit?> CreerAsync(Produit produit);
    Task<bool> ModifierAsync(int id, Produit produit);
    Task<bool> SupprimerAsync(int id);
}

public interface IUtilisateurApi
{
    Task<List<Utilisateur>> ObtenirTousAsync();
    Task<Utilisateur?> ObtenirParIdAsync(int id);
    Task<string?> ConnecterAsync(string email, string motDePasse);
}

// Implémentation
public class ProduitApi : IProduitApi
{
    private readonly HttpClient _httpClient;

    public ProduitApi(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<List<Produit>> ObtenirTousAsync()
    {
        return await _httpClient.GetFromJsonAsync<List<Produit>>("produits")
            ?? new List<Produit>();
    }

    public async Task<Produit?> ObtenirParIdAsync(int id)
    {
        try
        {
            return await _httpClient.GetFromJsonAsync<Produit>($"produits/{id}");
        }
        catch (HttpRequestException)
        {
            return null;
        }
    }

    public async Task<Produit?> CreerAsync(Produit produit)
    {
        var reponse = await _httpClient.PostAsJsonAsync("produits", produit);
        reponse.EnsureSuccessStatusCode();
        return await reponse.Content.ReadFromJsonAsync<Produit>();
    }

    public async Task<bool> ModifierAsync(int id, Produit produit)
    {
        var reponse = await _httpClient.PutAsJsonAsync($"produits/{id}", produit);
        return reponse.IsSuccessStatusCode;
    }

    public async Task<bool> SupprimerAsync(int id)
    {
        var reponse = await _httpClient.DeleteAsync($"produits/{id}");
        return reponse.IsSuccessStatusCode;
    }
}

/*
─────────────────────────────────────────────────────────────────
Configuration dans Program.cs
─────────────────────────────────────────────────────────────────

builder.Services.AddHttpClient<IProduitApi, ProduitApi>(client =>
{
    client.BaseAddress = new Uri("https://api.monapp.com/api/");
});

builder.Services.AddHttpClient<IUtilisateurApi, UtilisateurApi>(client =>
{
    client.BaseAddress = new Uri("https://api.monapp.com/api/");
});
*/


// ----------------------------------------------------------------------------
// [SECURISE] AJOUTER LE JWT TOKEN AUX REQUÊTES
// ----------------------------------------------------------------------------

/*
PROBLÈME : Chaque requête API doit inclure le JWT token
SOLUTION : Handler HTTP personnalisé (intercepteur de requêtes)
*/

// Handlers/JwtAuthorizationHandler.cs
public class JwtAuthorizationHandler : DelegatingHandler
{
    private readonly ILocalStorageService _localStorage;

    public JwtAuthorizationHandler(ILocalStorageService localStorage)
    {
        _localStorage = localStorage;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        // Récupérer le token du stockage
        var token = await _localStorage.GetItemAsync<string>("jwt_token");

        // Ajouter le token si disponible
        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization =
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        }

        // Continuer l'envoi de la requête
        return await base.SendAsync(request, cancellationToken);
    }
}

/*
─────────────────────────────────────────────────────────────────
Enregistrer le handler dans Program.cs
─────────────────────────────────────────────────────────────────

builder.Services.AddTransient<JwtAuthorizationHandler>();

builder.Services.AddHttpClient<IProduitApi, ProduitApi>(client =>
{
    client.BaseAddress = new Uri("https://api.monapp.com/api/");
})
.AddHttpMessageHandler<JwtAuthorizationHandler>(); // <- Intercepteur JWT
*/


// ----------------------------------------------------------------------------
// [SYNC] RETRY POLICY — Réessayer en cas d'échec
// ----------------------------------------------------------------------------

/*
POURQUOI ?
Le réseau peut avoir des problèmes temporaires.
Retry automatique = meilleure expérience utilisateur.

PACKAGE : dotnet add package Microsoft.Extensions.Http.Resilience
(ou Polly pour les versions plus anciennes)
*/

/*
Program.cs avec Retry :

builder.Services.AddHttpClient<IProduitApi, ProduitApi>(client =>
{
    client.BaseAddress = new Uri("https://api.monapp.com/api/");
})
.AddStandardResilienceHandler()  // Retry + Circuit Breaker automatique
.Configure(options =>
{
    // Configurer la policy de retry
    options.Retry.MaxRetryAttempts = 3;
    options.Retry.Delay = TimeSpan.FromMilliseconds(500);
    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);
});
*/


// ----------------------------------------------------------------------------
// [RAPIDE] UTILISER HTTPCLIENT DANS UN COMPOSANT BLAZOR
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
Fichier : Pages/ListeProduits.razor — Exemple complet
─────────────────────────────────────────────────────────────────
*/

/*
@page "/produits"
@inject IProduitApi ProduitApi

<PageTitle>Produits</PageTitle>

<div class="container mt-4">
    <h1>[SHOPPING_BAGS] Produits</h1>

    <!-- État de chargement -->
    @if (_etat == EtatPage.Chargement)
    {
        <div class="text-center py-5">
            <div class="spinner-border text-primary" role="status"></div>
            <p class="mt-3">Chargement des produits...</p>
        </div>
    }
    else if (_etat == EtatPage.Erreur)
    {
        <div class="alert alert-danger">
            <strong>Erreur !</strong> @_erreur
            <button @onclick="ChargerProduits" class="btn btn-sm btn-danger ms-3">
                [SYNC] Réessayer
            </button>
        </div>
    }
    else if (_etat == EtatPage.Vide)
    {
        <div class="text-center py-5">
            <p class="text-muted">Aucun produit trouvé.</p>
        </div>
    }
    else  // EtatPage.Données
    {
        <!-- Barre d'outils -->
        <div class="d-flex justify-content-between mb-3">
            <span>@_produits.Count produit(s)</span>
            <button @onclick="OuvrirFormulaireCreation" class="btn btn-success">
                + Nouveau Produit
            </button>
        </div>

        <!-- Grille -->
        <div class="row row-cols-1 row-cols-md-3 g-4">
            @foreach (var produit in _produits)
            {
                <div class="col">
                    <div class="card h-100">
                        <div class="card-body">
                            <h5>@produit.Nom</h5>
                            <p class="text-primary fw-bold">@produit.Prix.ToString("C")</p>
                        </div>
                        <div class="card-footer d-flex gap-2">
                            <button @onclick="() => ModifierProduit(produit)"
                                    class="btn btn-sm btn-outline-primary">
                                [EDIT] Éditer
                            </button>
                            <button @onclick="() => ConfirmerSuppression(produit)"
                                    class="btn btn-sm btn-outline-danger">
                                [SUPPRIMER] Supprimer
                            </button>
                        </div>
                    </div>
                </div>
            }
        </div>
    }
</div>

@code {
    // États possibles de la page
    private enum EtatPage { Chargement, Données, Vide, Erreur }

    private List<Produit> _produits = new();
    private EtatPage _etat = EtatPage.Chargement;
    private string? _erreur;

    // Chargement au démarrage
    protected override async Task OnInitializedAsync()
    {
        await ChargerProduits();
    }

    private async Task ChargerProduits()
    {
        _etat = EtatPage.Chargement;
        _erreur = null;

        try
        {
            _produits = await ProduitApi.ObtenirTousAsync();
            _etat = _produits.Any() ? EtatPage.Données : EtatPage.Vide;
        }
        catch (HttpRequestException ex)
        {
            _erreur = $"Impossible de charger les produits: {ex.Message}";
            _etat = EtatPage.Erreur;
        }
    }

    private async Task ConfirmerSuppression(Produit produit)
    {
        // En vrai : afficher une modale de confirmation
        var succes = await ProduitApi.SupprimerAsync(produit.Id);

        if (succes)
        {
            _produits.Remove(produit);
            if (!_produits.Any()) _etat = EtatPage.Vide;
        }
    }

    private void OuvrirFormulaireCreation() { /* Ouvrir modal/page */ }
    private void ModifierProduit(Produit p) { /* Ouvrir modal/page */ }
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 13 : ASP.NET CORE + BLAZOR (FULL STACK)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer une solution complète (Client + API)
[OK] Partager des modèles entre projets
[OK] Créer des contrôleurs API
[OK] Utiliser les Minimal APIs
[OK] Configurer CORS
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] ARCHITECTURE FULL STACK
// ----------------------------------------------------------------------------

/*
STRUCTURE D'UNE SOLUTION COMPLÈTE :

MonApp/
├── MonApp.sln                  <- Solution
├── MonApp.Client/              <- Blazor WebAssembly (Frontend)
│   ├── Pages/
│   ├── Services/
│   ├── Program.cs
│   └── MonApp.Client.csproj
├── MonApp.Server/              <- ASP.NET Core (Backend + API)
│   ├── Controllers/
│   ├── Program.cs
│   └── MonApp.Server.csproj
└── MonApp.Shared/              <- Classes partagées Client & Serveur
    ├── Models/
    ├── DTOs/
    └── MonApp.Shared.csproj

CRÉER LA SOLUTION :

dotnet new blazorwasm --hosted -n MonApp
   -> Crée automatiquement Client, Server et Shared !


AVANTAGES DU MODÈLE HÉBERGÉ :
[OK] Modèles partagés (DRY principle)
[OK] Validation partagée (DataAnnotations)
[OK] Déploiement simplifié
[OK] Développement unifié
[OK] Pas de problèmes CORS (même domaine)
*/


// ----------------------------------------------------------------------------
// [PACKAGE] PROJET PARTAGÉ (MonApp.Shared)
// ----------------------------------------------------------------------------

// MonApp.Shared/Models/Produit.cs
// CE fichier est partagé entre Client et Server !
public class ProduitModel
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Le nom est obligatoire")]
    [StringLength(100, MinimumLength = 2)]
    public string Nom { get; set; } = string.Empty;

    [Required]
    [Range(0.01, 999999.99, ErrorMessage = "Prix doit être positif")]
    public decimal Prix { get; set; }

    [Required]
    public string Categorie { get; set; } = string.Empty;

    public string? Description { get; set; }

    public int Stock { get; set; }

    public DateTime DateCreation { get; set; } = DateTime.UtcNow;
}

// MonApp.Shared/DTOs/LoginDto.cs
public record LoginDto(
    [Required][EmailAddress] string Email,
    [Required][MinLength(8)] string MotDePasse);

public record LoginResponseDto(
    string Token,
    string Nom,
    string Email,
    string Role);

// MonApp.Shared/DTOs/PaginationDto.cs
public class PagedResultDto<T>
{
    public List<T> Items { get; set; } = new();
    public int Total { get; set; }
    public int Page { get; set; }
    public int PerPage { get; set; }
}


// ----------------------------------------------------------------------------
// [ECRAN] CONTRÔLEURS ASP.NET CORE
// ----------------------------------------------------------------------------

// MonApp.Server/Controllers/ProduitsController.cs
/*
[ApiController]
[Route("api/[controller]")]
public class ProduitsController : ControllerBase
{
    private readonly IProduitRepository _repository;
    private readonly ILogger<ProduitsController> _logger;

    public ProduitsController(
        IProduitRepository repository,
        ILogger<ProduitsController> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    // GET api/produits
    [HttpGet]
    public async Task<ActionResult<List<ProduitModel>>> ObtenirTous(
        [FromQuery] string? categorie = null,
        [FromQuery] string? terme = null,
        [FromQuery] int page = 1,
        [FromQuery] int parPage = 20)
    {
        try
        {
            var produits = await _repository.ObtenirTousAsync(categorie, terme, page, parPage);
            return Ok(produits);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erreur lors de la récupération des produits");
            return StatusCode(500, "Erreur interne du serveur");
        }
    }

    // GET api/produits/5
    [HttpGet("{id:int}")]
    public async Task<ActionResult<ProduitModel>> ObtenirParId(int id)
    {
        var produit = await _repository.ObtenirParIdAsync(id);

        if (produit is null)
            return NotFound($"Produit avec l'ID {id} non trouvé");

        return Ok(produit);
    }

    // POST api/produits
    [HttpPost]
    [Authorize]  // Requiert authentification
    public async Task<ActionResult<ProduitModel>> Creer([FromBody] ProduitModel produit)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        var cree = await _repository.CreerAsync(produit);

        // 201 Created avec l'URL de la nouvelle ressource
        return CreatedAtAction(
            nameof(ObtenirParId),
            new { id = cree.Id },
            cree);
    }

    // PUT api/produits/5
    [HttpPut("{id:int}")]
    [Authorize]
    public async Task<ActionResult<ProduitModel>> Modifier(int id, [FromBody] ProduitModel produit)
    {
        if (id != produit.Id)
            return BadRequest("L'ID dans l'URL ne correspond pas au corps");

        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        var existant = await _repository.ObtenirParIdAsync(id);
        if (existant is null)
            return NotFound();

        var modifie = await _repository.ModifierAsync(produit);
        return Ok(modifie);
    }

    // DELETE api/produits/5
    [HttpDelete("{id:int}")]
    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> Supprimer(int id)
    {
        var produit = await _repository.ObtenirParIdAsync(id);
        if (produit is null)
            return NotFound();

        await _repository.SupprimerAsync(id);
        return NoContent(); // 204 - Succès sans contenu
    }
}
*/


// ----------------------------------------------------------------------------
// [RAPIDE] MINIMAL APIS — Alternative moderne aux contrôleurs
// ----------------------------------------------------------------------------

/*
Minimal APIs = Façon concise de définir des endpoints
Parfait pour des petites APIs ou microservices
*/

/*
MonApp.Server/Program.cs (avec Minimal APIs) :

var builder = WebApplication.CreateBuilder(args);

// Services
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite("Data Source=app.db"));
builder.Services.AddScoped<IProduitRepository, ProduitRepository>();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

// CORS pour Blazor WebAssembly
builder.Services.AddCors(options =>
{
    options.AddPolicy("BlazorPolicy", policy =>
    {
        policy.WithOrigins("https://localhost:5001")  // URL de votre app Blazor
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

var app = builder.Build();

app.UseCors("BlazorPolicy");
app.UseAuthentication();
app.UseAuthorization();

// ─── Endpoints Produits ───────────────────────────────────────────────

// GET /api/produits
app.MapGet("/api/produits", async (IProduitRepository repo) =>
{
    return Results.Ok(await repo.ObtenirTousAsync());
});

// GET /api/produits/{id}
app.MapGet("/api/produits/{id:int}", async (int id, IProduitRepository repo) =>
{
    var produit = await repo.ObtenirParIdAsync(id);
    return produit is null
        ? Results.NotFound()
        : Results.Ok(produit);
});

// POST /api/produits
app.MapPost("/api/produits", async (ProduitModel produit, IProduitRepository repo) =>
{
    if (string.IsNullOrEmpty(produit.Nom))
        return Results.BadRequest("Nom requis");

    var cree = await repo.CreerAsync(produit);
    return Results.Created($"/api/produits/{cree.Id}", cree);
})
.RequireAuthorization();

// PUT /api/produits/{id}
app.MapPut("/api/produits/{id:int}", async (
    int id, ProduitModel produit, IProduitRepository repo) =>
{
    var existant = await repo.ObtenirParIdAsync(id);
    if (existant is null) return Results.NotFound();

    var modifie = await repo.ModifierAsync(produit);
    return Results.Ok(modifie);
})
.RequireAuthorization();

// DELETE /api/produits/{id}
app.MapDelete("/api/produits/{id:int}", async (int id, IProduitRepository repo) =>
{
    var existant = await repo.ObtenirParIdAsync(id);
    if (existant is null) return Results.NotFound();

    await repo.SupprimerAsync(id);
    return Results.NoContent();
})
.RequireAuthorization("Admin");

// ─── Endpoint Auth ────────────────────────────────────────────────────

app.MapPost("/api/auth/login", async (LoginDto dto, IAuthService authService) =>
{
    var result = await authService.ConnecterAsync(dto.Email, dto.MotDePasse);
    return result is null
        ? Results.Unauthorized()
        : Results.Ok(result);
});

app.Run();
*/


// ============================================================================
// [GUIDE] CHAPITRE 14 : BASE DE DONNÉES (ENTITY FRAMEWORK CORE)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Configurer Entity Framework Core
[OK] Créer et migrer la base de données
[OK] Implémenter le pattern Repository
[OK] Gérer les relations entre entités
[OK] Optimiser les requêtes
*/


// ----------------------------------------------------------------------------
// [OUTIL] CONFIGURATION EF CORE
// ----------------------------------------------------------------------------

/*
INSTALLATION :
dotnet add package Microsoft.EntityFrameworkCore.Sqlite      -> Développement
dotnet add package Microsoft.EntityFrameworkCore.SqlServer  -> Production SQL Server
dotnet add package Microsoft.EntityFrameworkCore.Npgsql     -> Production PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Tools      -> Migrations
*/

// Entités (modèles de base de données)
public class ProduitEntite
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public decimal Prix { get; set; }
    public string Categorie { get; set; } = string.Empty;
    public int Stock { get; set; }
    public DateTime DateCreation { get; set; } = DateTime.UtcNow;
    public DateTime? DateModification { get; set; }

    // Relations
    public int? CategorieId { get; set; }
    public CategorieEntite? CategorieNav { get; set; }
    public List<CommandeItemEntite> CommandeItems { get; set; } = new();
}

public class CategorieEntite
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public string? Description { get; set; }

    // Relation inverse
    public List<ProduitEntite> Produits { get; set; } = new();
}

public class CommandeEntite
{
    public int Id { get; set; }
    public DateTime DateCommande { get; set; } = DateTime.UtcNow;
    public string Statut { get; set; } = "En attente";
    public int UtilisateurId { get; set; }

    // Relations
    public List<CommandeItemEntite> Items { get; set; } = new();
}

public class CommandeItemEntite
{
    public int Id { get; set; }
    public int CommandeId { get; set; }
    public int ProduitId { get; set; }
    public int Quantite { get; set; }
    public decimal PrixUnitaire { get; set; }

    public CommandeEntite? Commande { get; set; }
    public ProduitEntite? Produit { get; set; }
}

// DbContext — Porte d'entrée vers la base de données
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    // Tables
    public DbSet<ProduitEntite> Produits => Set<ProduitEntite>();
    public DbSet<CategorieEntite> Categories => Set<CategorieEntite>();
    public DbSet<CommandeEntite> Commandes => Set<CommandeEntite>();
    public DbSet<CommandeItemEntite> CommandeItems => Set<CommandeItemEntite>();

    // Configuration des entités
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // ─── Produit ──────────────────────────────────────────────────
        modelBuilder.Entity<ProduitEntite>(entity =>
        {
            entity.HasKey(p => p.Id);

            entity.Property(p => p.Nom)
                .IsRequired()
                .HasMaxLength(100);

            entity.Property(p => p.Prix)
                .HasColumnType("decimal(18,2)");

            // Index pour les recherches fréquentes
            entity.HasIndex(p => p.Categorie);
            entity.HasIndex(p => p.Nom);

            // Relation avec Catégorie
            entity.HasOne(p => p.CategorieNav)
                .WithMany(c => c.Produits)
                .HasForeignKey(p => p.CategorieId)
                .OnDelete(DeleteBehavior.SetNull);
        });

        // ─── Données initiales (Seed) ─────────────────────────────────
        modelBuilder.Entity<CategorieEntite>().HasData(
            new CategorieEntite { Id = 1, Nom = "Électronique" },
            new CategorieEntite { Id = 2, Nom = "Mobilier" },
            new CategorieEntite { Id = 3, Nom = "Accessoires" }
        );
    }
}


// ----------------------------------------------------------------------------
// [SYNC] MIGRATIONS EF CORE
// ----------------------------------------------------------------------------

/*
COMMANDES DE MIGRATION :

Créer une migration :
dotnet ef migrations add NomMigration --project MonApp.Server

Appliquer les migrations :
dotnet ef database update --project MonApp.Server

Voir les migrations :
dotnet ef migrations list --project MonApp.Server

Annuler la dernière migration :
dotnet ef migrations remove --project MonApp.Server

Générer le script SQL :
dotnet ef migrations script --project MonApp.Server


CONFIGURER EF DANS Program.cs :

builder.Services.AddDbContext<AppDbContext>(options =>
{
    // Développement : SQLite
    options.UseSqlite(
        builder.Configuration.GetConnectionString("DefaultConnection"));

    // Production : SQL Server
    // options.UseSqlServer(
    //     builder.Configuration.GetConnectionString("DefaultConnection"));
});


appsettings.json :
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=app.db"  <- SQLite
  }
}


APPLIQUER MIGRATIONS AU DÉMARRAGE :
// Dans Program.cs, après avoir construit l'app :
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    db.Database.Migrate(); // Appliquer toutes les migrations en attente
}
*/


// ----------------------------------------------------------------------------
// [ARCHIVE] PATTERN REPOSITORY
// ----------------------------------------------------------------------------

/*
REPOSITORY = Abstraction de l'accès aux données
AVANTAGES :
- Séparer logique métier de l'accès aux données
- Faciliter les tests (mock du repository)
- Centraliser les requêtes EF
*/

// Interfaces
public interface IProduitRepository
{
    Task<List<ProduitEntite>> ObtenirTousAsync();
    Task<ProduitEntite?> ObtenirParIdAsync(int id);
    Task<List<ProduitEntite>> RechercherAsync(string? terme, string? categorie);
    Task<ProduitEntite> CreerAsync(ProduitEntite produit);
    Task<ProduitEntite> ModifierAsync(ProduitEntite produit);
    Task SupprimerAsync(int id);
    Task<bool> ExisteAsync(int id);
}

// Implémentation
public class ProduitRepository : IProduitRepository
{
    private readonly AppDbContext _context;

    public ProduitRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<List<ProduitEntite>> ObtenirTousAsync()
    {
        return await _context.Produits
            .Include(p => p.CategorieNav)  // Eager loading de la catégorie
            .OrderBy(p => p.Nom)
            .ToListAsync();
    }

    public async Task<ProduitEntite?> ObtenirParIdAsync(int id)
    {
        return await _context.Produits
            .Include(p => p.CategorieNav)
            .FirstOrDefaultAsync(p => p.Id == id);
    }

    public async Task<List<ProduitEntite>> RechercherAsync(
        string? terme,
        string? categorie)
    {
        // Construire la requête progressivement (Queryable)
        var query = _context.Produits
            .Include(p => p.CategorieNav)
            .AsQueryable();

        if (!string.IsNullOrWhiteSpace(terme))
        {
            query = query.Where(p =>
                p.Nom.Contains(terme) ||
                (p.CategorieNav != null && p.CategorieNav.Nom.Contains(terme)));
        }

        if (!string.IsNullOrWhiteSpace(categorie))
        {
            query = query.Where(p =>
                p.CategorieNav != null &&
                p.CategorieNav.Nom == categorie);
        }

        return await query
            .OrderBy(p => p.Nom)
            .ToListAsync();
    }

    public async Task<ProduitEntite> CreerAsync(ProduitEntite produit)
    {
        _context.Produits.Add(produit);
        await _context.SaveChangesAsync();
        return produit;
    }

    public async Task<ProduitEntite> ModifierAsync(ProduitEntite produit)
    {
        produit.DateModification = DateTime.UtcNow;
        _context.Produits.Update(produit);
        await _context.SaveChangesAsync();
        return produit;
    }

    public async Task SupprimerAsync(int id)
    {
        var produit = await _context.Produits.FindAsync(id);
        if (produit is not null)
        {
            _context.Produits.Remove(produit);
            await _context.SaveChangesAsync();
        }
    }

    public async Task<bool> ExisteAsync(int id)
    {
        return await _context.Produits.AnyAsync(p => p.Id == id);
    }
}

// Interface EF Core (simulée pour la compilation)
public interface IQueryable<T> { }
public class DbContext
{
    protected DbContext(object options) { }
    protected DbSet<T> Set<T>() where T : class => default!;
    public async Task<int> SaveChangesAsync() => 0;
}
public class DbSet<T> where T : class
{
    public void Add(T entity) { }
    public void Update(T entity) { }
    public void Remove(T entity) { }
    public Task<T?> FindAsync(params object?[] keys) => Task.FromResult<T?>(default);
    public Task<bool> AnyAsync(Func<T, bool> predicate) => Task.FromResult(false);
    public IQueryable<T> Include<TProperty>(Func<T, TProperty> include) => default!;
    public IQueryable<T> Where(Func<T, bool> predicate) => default!;
    public IQueryable<T> OrderBy<TKey>(Func<T, TKey> keySelector) => default!;
    public Task<List<T>> ToListAsync() => Task.FromResult(new List<T>());
    public Task<T?> FirstOrDefaultAsync(Func<T, bool> predicate) => Task.FromResult<T?>(default);
}
public class DbContextOptions<T> { }
public class ModelBuilder
{
    public EntityTypeBuilder<T> Entity<T>() where T : class => default!;
}
public class EntityTypeBuilder<T> where T : class
{
    public EntityTypeBuilder<T> HasKey(Func<T, object> key) => this;
    public PropertyBuilder<TProperty> Property<TProperty>(Func<T, TProperty> property) => default!;
    public IndexBuilder HasIndex(Func<T, object> index) => default!;
    public ReferenceNavigationBuilder<T, TRelated> HasOne<TRelated>(Func<T, TRelated?> navigation) where TRelated : class => default!;
    public EntityTypeBuilder<T> HasData(params T[] data) => this;
}
public class PropertyBuilder<T>
{
    public PropertyBuilder<T> IsRequired() => this;
    public PropertyBuilder<T> HasMaxLength(int maxLength) => this;
    public PropertyBuilder<T> HasColumnType(string typeName) => this;
}
public class IndexBuilder { }
public class ReferenceNavigationBuilder<TEntity, TRelated> where TEntity : class where TRelated : class
{
    public CollectionNavigationBuilder<TEntity, TRelated> WithMany(Func<TRelated, IEnumerable<TEntity>?> collection) => default!;
}
public class CollectionNavigationBuilder<TEntity, TRelated> where TEntity : class where TRelated : class
{
    public CollectionNavigationBuilder<TEntity, TRelated> HasForeignKey(Func<TEntity, object?> foreignKeyExpression) => this;
    public CollectionNavigationBuilder<TEntity, TRelated> OnDelete(DeleteBehavior behavior) => this;
}
public enum DeleteBehavior { Cascade, SetNull, Restrict }
public static class EfExtensions
{
    public static IQueryable<T> AsQueryable<T>(this DbSet<T> dbSet) where T : class => default!;
    public static Task<List<T>> ToListAsync<T>(this IQueryable<T> source) => Task.FromResult(new List<T>());
    public static Task<T?> FirstOrDefaultAsync<T>(this IQueryable<T> source, Func<T, bool> predicate) => Task.FromResult<T?>(default);
    public static Task<bool> AnyAsync<T>(this DbSet<T> source, Func<T, bool> predicate) where T : class => Task.FromResult(false);
    public static IQueryable<T> Include<T, TProperty>(this IQueryable<T> source, Func<T, TProperty> include) => source;
    public static IQueryable<T> Where<T>(this IQueryable<T> source, Func<T, bool> predicate) => source;
    public static IQueryable<T> OrderBy<T, TKey>(this IQueryable<T> source, Func<T, TKey> keySelector) => source;
    public static IQueryable<T> ThenBy<T, TKey>(this IQueryable<T> source, Func<T, TKey> keySelector) => source;
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE — PARTIE 4
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
EXERCICE : APPLICATION CRUD FULL STACK
═══════════════════════════════════════════════════════════════

OBJECTIF : Application complète avec API + EF Core + Blazor

ARCHITECTURE :
- MonApp.Shared : Modèles partagés
- MonApp.Server : ASP.NET Core + EF Core + SQLite
- MonApp.Client : Blazor WebAssembly

FONCTIONNALITÉS :

1. BACKEND (ASP.NET Core) :
   a) ProduitEntite : Id, Nom, Prix, Stock, CategorieId
   b) CategorieEntite : Id, Nom
   c) AppDbContext avec configuration
   d) ProduitRepository (CRUD complet)
   e) ProduitsController avec GET, POST, PUT, DELETE
   f) Seed data : 3 catégories et 5 produits

2. FRONTEND (Blazor) :
   a) IProduitApi + ProduitApi (HttpClient Typed)
   b) Page /produits :
      - Liste paginée
      - Filtre par catégorie
      - Bouton ajouter (modal)
      - Bouton éditer/supprimer
   c) Modal formulaire Produit (création + édition)
   d) Gestion d'erreurs (loading, error, vide)

COMPÉTENCES :
[OK] HttpClient Typed
[OK] EF Core + Migrations
[OK] Repository Pattern
[OK] Minimal API ou Controller
[OK] Modèles partagés
[OK] CRUD complet dans Blazor
[OK] Gestion des états (loading/error/data)
═══════════════════════════════════════════════════════════════
*/


// ============================================================================
// [OK] ÉLÉMENTS CLÉS DU CORRIGÉ
// ============================================================================

// ─── Shared/Models/ProduitCreerDto.cs ────────────────────────────────────────

public class ProduitCreerDto
{
    [Required(ErrorMessage = "Nom obligatoire")]
    [StringLength(100, MinimumLength = 2)]
    public string Nom { get; set; } = string.Empty;

    [Required]
    [Range(0.01, 99999.99)]
    public decimal Prix { get; set; }

    [Required]
    [Range(0, int.MaxValue)]
    public int Stock { get; set; }

    [Required]
    public int CategorieId { get; set; }

    public string? Description { get; set; }
}

// ─── Server : Migration initiale ─────────────────────────────────────────────

/*
dotnet ef migrations add InitialCreate --project MonApp.Server
dotnet ef database update --project MonApp.Server
*/

// ─── Client/Services/ProduitApiClient.cs ─────────────────────────────────────

public class ProduitApiClient
{
    private readonly HttpClient _httpClient;

    public ProduitApiClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<(List<ProduitEntite> Produits, string? Erreur)> ObtenirTousAsync()
    {
        try
        {
            var produits = await _httpClient
                .GetFromJsonAsync<List<ProduitEntite>>("api/produits");

            return (produits ?? new(), null);
        }
        catch (HttpRequestException ex)
        {
            return (new(), $"Erreur réseau: {ex.Message}");
        }
    }

    public async Task<(ProduitEntite? Produit, string? Erreur)> CreerAsync(
        ProduitCreerDto dto)
    {
        try
        {
            var reponse = await _httpClient.PostAsJsonAsync("api/produits", dto);

            if (!reponse.IsSuccessStatusCode)
            {
                var erreur = await reponse.Content.ReadAsStringAsync();
                return (null, $"Erreur {(int)reponse.StatusCode}: {erreur}");
            }

            var cree = await reponse.Content.ReadFromJsonAsync<ProduitEntite>();
            return (cree, null);
        }
        catch (HttpRequestException ex)
        {
            return (null, ex.Message);
        }
    }

    public async Task<bool> SupprimerAsync(int id)
    {
        var reponse = await _httpClient.DeleteAsync($"api/produits/{id}");
        return reponse.IsSuccessStatusCode;
    }
}


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 4

[OK] CHAPITRE 12 - HTTPCLIENT :
- GetFromJsonAsync / PostAsJsonAsync / PutAsJsonAsync
- Gestion d'erreurs HttpRequestException
- Query string avec QueryHelpers
- Typed HttpClient (interface + implémentation)
- JwtAuthorizationHandler (intercepteur JWT)
- Retry policies avec Resilience Handler

[OK] CHAPITRE 13 - FULL STACK :
- Architecture Client/Server/Shared
- Modèles partagés entre projets
- Contrôleurs ASP.NET Core (CRUD REST)
- Minimal APIs (alternative moderne)
- CORS pour Blazor WebAssembly

[OK] CHAPITRE 14 - EF CORE :
- DbContext et DbSet
- Entités et relations (One-to-Many, etc.)
- Migrations (create, update, remove)
- Seed data dans OnModelCreating
- Repository Pattern (interface + implémentation)
- Eager Loading avec Include()
- Requêtes optimisées avec IQueryable

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 5
- UI Professionnelle (MudBlazor)
- CSS Scoped et animations
- Composants dynamiques
- Architecture avancée (Clean Architecture, CQRS)
═══════════════════════════════════════════════════════════════
*/

// ============================================================================
// [LIVRE] BLAZOR - PARTIE 5 : UI / UX PROFESSIONNEL
// ============================================================================
//
// CHAPITRE 15 : CSS & Styling
// CHAPITRE 16 : Frameworks UI (MudBlazor, Bootstrap, Tailwind)
// CHAPITRE 17 : Composants dynamiques
//
// [TEMPS] TEMPS ESTIMÉ : ~8-10 heures
// [DOCS] PRÉREQUIS : Parties 1-4 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 15 : CSS & STYLING
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser le CSS Scoped (isolation CSS)
[OK] Créer des variables CSS et thèmes
[OK] Animer les composants Blazor
[OK] Gérer les classes CSS dynamiques
[OK] Appliquer les transitions de route
[OK] Créer un système de design cohérent
*/


// ----------------------------------------------------------------------------
// [DESIGN] CSS SCOPED — Isolation CSS par composant
// ----------------------------------------------------------------------------

/*
PROBLÈME SANS CSS SCOPED :
Les styles globaux s'appliquent à TOUT le DOM.
Un style dans ComposantA peut casser ComposantB !

SOLUTION : CSS Scoped (isolation CSS)
-> Chaque composant a son propre fichier .razor.css
-> Les styles ne "fuient" pas en dehors du composant
-> Blazor ajoute automatiquement un attribut unique : b-3xxtam6d2y

STRUCTURE DES FICHIERS :
CarteProduit.razor        <- Composant Blazor
CarteProduit.razor.css    <- CSS ISOLÉ pour ce composant SEULEMENT


COMMENT ÇA MARCHE EN COULISSES ?

Blazor transforme vos sélecteurs CSS :

    Votre CSS :       .carte { background: white; }
    CSS généré :      .carte[b-3xxtam6d2y] { background: white; }
    HTML généré :     <div class="carte" b-3xxtam6d2y>...</div>

-> Votre .carte n'affecte QUE les éléments de CE composant !
*/

/*
─────────────────────────────────────────────────────────────────
Fichier : Components/CarteProduit.razor
─────────────────────────────────────────────────────────────────

<div class="carte">
    <div class="carte-image">
        <img src="@Produit.ImageUrl" alt="@Produit.Nom" />
        @if (Produit.EstNouveau)
        {
            <span class="badge-nouveau">NOUVEAU</span>
        }
        @if (!Produit.EnStock)
        {
            <div class="overlay-rupture">RUPTURE DE STOCK</div>
        }
    </div>
    <div class="carte-contenu">
        <h3 class="titre">@Produit.Nom</h3>
        <p class="categorie">@Produit.Categorie</p>
        <div class="pied">
            <span class="prix">@Produit.Prix.ToString("C")</span>
            <button class="btn-ajouter"
                    disabled="@(!Produit.EnStock)"
                    @onclick="AjouterAuPanier">
                + Panier
            </button>
        </div>
    </div>
</div>

@code {
    [Parameter, EditorRequired] public Produit Produit { get; set; } = default!;
    [Parameter] public EventCallback<Produit> OnAjouter { get; set; }

    private async Task AjouterAuPanier()
    {
        await OnAjouter.InvokeAsync(Produit);
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Fichier : Components/CarteProduit.razor.css
─────────────────────────────────────────────────────────────────

Styles ISOLÉS - n'affectent que CarteProduit.razor

.carte {
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
    background: white;
}

.carte:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}

.carte-image {
    position: relative;
    overflow: hidden;
    height: 200px;
}

.carte-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.3s ease;
}

.carte:hover .carte-image img {
    transform: scale(1.05);
}

.badge-nouveau {
    position: absolute;
    top: 12px;
    right: 12px;
    background: #22c55e;
    color: white;
    padding: 4px 10px;
    border-radius: 20px;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.5px;
}

.overlay-rupture {
    position: absolute;
    inset: 0;
    background: rgba(0,0,0,0.5);
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    letter-spacing: 2px;
    font-size: 0.8rem;
}

.carte-contenu {
    padding: 16px;
}

.titre {
    font-size: 1rem;
    font-weight: 600;
    margin: 0 0 4px;
    color: #1e293b;
}

.categorie {
    font-size: 0.85rem;
    color: #94a3b8;
    margin: 0 0 12px;
}

.pied {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.prix {
    font-size: 1.25rem;
    font-weight: 700;
    color: #3b82f6;
}

.btn-ajouter {
    background: #3b82f6;
    color: white;
    border: none;
    padding: 8px 16px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 0.85rem;
    font-weight: 500;
    transition: background 0.2s;
}

.btn-ajouter:hover:not(:disabled) {
    background: #2563eb;
}

.btn-ajouter:disabled {
    background: #cbd5e1;
    cursor: not-allowed;
}

IMPORTANT : L'OPÉRATEUR ::deep

PROBLÈME : Le CSS scoped NE PEUT PAS styliser les composants enfants.

  <div class="ma-carte">
      <ComposantEnfant />  <- Je veux styliser ça, mais je ne peux pas !
  </div>

SOLUTION : ::deep perce l'isolation CSS pour les descendants

  .ma-carte ::deep .element-de-enfant {
      color: red; <- Maintenant ça fonctionne !
  }
*/


// ----------------------------------------------------------------------------
// [DESIGN] VARIABLES CSS ET SYSTÈME DE DESIGN
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
Fichier : wwwroot/css/variables.css — Design Tokens
─────────────────────────────────────────────────────────────────

Un "design token" = Variable CSS pour un aspect visuel précis.
En centralisant TOUT ici, changer le thème devient trivial.

:root {
    /* ─── Palette de couleurs ─────────────────────────────── */
    --color-primary-50:  #eff6ff;
    --color-primary-100: #dbeafe;
    --color-primary-500: #3b82f6;    /* Couleur principale */
    --color-primary-600: #2563eb;
    --color-primary-700: #1d4ed8;

    --color-success: #22c55e;
    --color-warning: #f59e0b;
    --color-danger:  #ef4444;
    --color-info:    #06b6d4;

    /* ─── Couleurs neutres ────────────────────────────────── */
    --color-gray-50:  #f8fafc;
    --color-gray-100: #f1f5f9;
    --color-gray-200: #e2e8f0;
    --color-gray-400: #94a3b8;
    --color-gray-600: #475569;
    --color-gray-800: #1e293b;
    --color-gray-900: #0f172a;

    /* ─── Typographie ─────────────────────────────────────── */
    --font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    --font-family-mono: 'JetBrains Mono', 'Fira Code', monospace;

    --font-size-xs:   0.75rem;   /* 12px */
    --font-size-sm:   0.875rem;  /* 14px */
    --font-size-base: 1rem;      /* 16px */
    --font-size-lg:   1.125rem;  /* 18px */
    --font-size-xl:   1.25rem;   /* 20px */
    --font-size-2xl:  1.5rem;    /* 24px */
    --font-size-3xl:  1.875rem;  /* 30px */
    --font-size-4xl:  2.25rem;   /* 36px */

    /* ─── Espacement ──────────────────────────────────────── */
    --space-1:  0.25rem;   /* 4px  */
    --space-2:  0.5rem;    /* 8px  */
    --space-4:  1rem;      /* 16px */
    --space-6:  1.5rem;    /* 24px */
    --space-8:  2rem;      /* 32px */
    --space-12: 3rem;      /* 48px */
    --space-16: 4rem;      /* 64px */

    /* ─── Bordures ────────────────────────────────────────── */
    --radius-sm:  4px;
    --radius-md:  8px;
    --radius-lg:  12px;
    --radius-xl:  16px;
    --radius-full: 9999px;

    /* ─── Ombres ──────────────────────────────────────────── */
    --shadow-sm:  0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow-md:  0 4px 6px rgba(0, 0, 0, 0.07);
    --shadow-lg:  0 10px 15px rgba(0, 0, 0, 0.1);
    --shadow-xl:  0 20px 25px rgba(0, 0, 0, 0.1);

    /* ─── Transitions ─────────────────────────────────────── */
    --transition-fast:   0.1s ease;
    --transition-normal: 0.2s ease;
    --transition-slow:   0.3s ease;

    /* ─── Z-index stack ───────────────────────────────────── */
    --z-dropdown: 1000;
    --z-sticky:   1020;
    --z-modal:    1050;
    --z-toast:    1090;

    /* ─── THÈME CLAIR (valeurs sémantiques) ───────────────── */
    --bg-primary:     #ffffff;
    --bg-secondary:   var(--color-gray-50);
    --bg-tertiary:    var(--color-gray-100);
    --text-primary:   var(--color-gray-900);
    --text-secondary: var(--color-gray-600);
    --text-disabled:  var(--color-gray-400);
    --border-color:   var(--color-gray-200);
}

/* ─── THÈME SOMBRE ──────────────────────────────────────────── */
[data-theme="dark"] {
    --bg-primary:     #0f172a;
    --bg-secondary:   #1e293b;
    --bg-tertiary:    #334155;
    --text-primary:   #f8fafc;
    --text-secondary: #94a3b8;
    --text-disabled:  #475569;
    --border-color:   #334155;

    --color-primary-500: #60a5fa;  /* Bleu plus clair sur fond sombre */
}

/* Utilisation des design tokens */
.ma-carte {
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-md);
    color: var(--text-primary);
    padding: var(--space-4);
    transition: box-shadow var(--transition-normal);
}
*/


// ----------------------------------------------------------------------------
// * ANIMATIONS BLAZOR
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
Fichier : wwwroot/css/animations.css
─────────────────────────────────────────────────────────────────

@keyframes fadeIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes fadeInUp {
    from { opacity: 0; transform: translateY(20px); }
    to   { opacity: 1; transform: translateY(0); }
}

@keyframes fadeInDown {
    from { opacity: 0; transform: translateY(-20px); }
    to   { opacity: 1; transform: translateY(0); }
}

@keyframes scaleIn {
    from { opacity: 0; transform: scale(0.9); }
    to   { opacity: 1; transform: scale(1); }
}

@keyframes slideInLeft {
    from { opacity: 0; transform: translateX(-30px); }
    to   { opacity: 1; transform: translateX(0); }
}

@keyframes slideInRight {
    from { opacity: 0; transform: translateX(30px); }
    to   { opacity: 1; transform: translateX(0); }
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50%       { opacity: 0.5; }
}

@keyframes spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

@keyframes shimmer {
    0%   { background-position: -1000px 0; }
    100% { background-position: 1000px 0; }
}

/* ─── Classes utilitaires ────────────────────────────────────── */
.anim-fade-in    { animation: fadeIn 0.3s ease forwards; }
.anim-fade-up    { animation: fadeInUp 0.4s ease forwards; }
.anim-fade-down  { animation: fadeInDown 0.4s ease forwards; }
.anim-scale-in   { animation: scaleIn 0.3s ease forwards; }
.anim-slide-left { animation: slideInLeft 0.4s ease forwards; }

/* Délais pour animer des listes en cascade */
.anim-delay-1 { animation-delay: 0.1s; opacity: 0; }
.anim-delay-2 { animation-delay: 0.2s; opacity: 0; }
.anim-delay-3 { animation-delay: 0.3s; opacity: 0; }
.anim-delay-4 { animation-delay: 0.4s; opacity: 0; }
.anim-delay-5 { animation-delay: 0.5s; opacity: 0; }

/* ─── Skeleton Loading ────────────────────────────────────────── */
.skeleton {
    background: linear-gradient(
        90deg,
        #f0f0f0 25%,
        #e0e0e0 50%,
        #f0f0f0 75%
    );
    background-size: 1000px 100%;
    animation: shimmer 2s infinite linear;
    border-radius: 4px;
}

.skeleton-text  { height: 1em; margin-bottom: 0.5em; }
.skeleton-title { height: 1.5em; width: 60%; }
.skeleton-image { height: 200px; border-radius: 8px; }
.skeleton-btn   { height: 38px; width: 120px; border-radius: 8px; }
*/

/*
─────────────────────────────────────────────────────────────────
Composant : SkeletonCarte.razor — Placeholder de chargement
─────────────────────────────────────────────────────────────────

<div class="card">
    <div class="skeleton skeleton-image mb-3"></div>
    <div class="card-body">
        <div class="skeleton skeleton-title mb-2"></div>
        <div class="skeleton skeleton-text" style="width: 80%"></div>
        <div class="skeleton skeleton-text" style="width: 60%"></div>
        <div class="d-flex justify-content-between mt-3">
            <div class="skeleton" style="height: 1.5em; width: 80px;"></div>
            <div class="skeleton skeleton-btn"></div>
        </div>
    </div>
</div>


UTILISATION dans une page :

@if (_chargement)
{
    <div class="row g-4">
        @for (int i = 0; i < 6; i++)
        {
            <div class="col-md-4"><SkeletonCarte /></div>
        }
    </div>
}
else
{
    <div class="row g-4">
        @foreach (var (produit, index) in _produits.Select((p, i) => (p, i)))
        {
            <!-- Animation en cascade : délai croissant par carte -->
            <div class="col-md-4 anim-fade-up anim-delay-@(Math.Min(index + 1, 5))">
                <CarteProduit Produit="produit" />
            </div>
        }
    </div>
}
*/


// ----------------------------------------------------------------------------
// [DESIGN] CLASSES CSS DYNAMIQUES DANS BLAZOR
// ----------------------------------------------------------------------------

/*
PLUSIEURS TECHNIQUES pour générer des classes CSS en C# :

1. Expression ternaire inline       -> Simple, lisible
2. Méthode C# calculée             -> Logique complexe
3. Dictionnaire d'attributs        -> Attributs multiples
4. StringBuilder                   -> Nombreuses conditions
5. ClassBuilder (pattern)          -> Approche fluent
*/

/*
─────────────────────────────────────────────────────────────────
Techniques de classes CSS dynamiques
─────────────────────────────────────────────────────────────────

@code {
    private bool _actif = true;
    private bool _loading = false;
    private string _statut = "success";
    private int _score = 85;
}

<!-- ─── 1. Expression ternaire ─────────────────────────────── -->
<div class="card @(_actif ? "border-primary" : "border-secondary")">
    ...
</div>

<!-- ─── 2. Conditions multiples concaténées ──────────────── -->
<button class="btn
               @(_actif  ? "btn-primary"  : "btn-outline-primary")
               @(_loading ? "disabled"     : "")
               @(_grand   ? "btn-lg"       : "btn-sm")">
    Envoyer
</button>

<!-- ─── 3. Méthode C# (plus propre pour logique complexe) ── -->
<div class="alert @ObtenirClasseStatut()">Message</div>
<span class="badge @ObtenirClasseScore(_score)">@_score pts</span>

@code {
    private string ObtenirClasseStatut() => _statut switch
    {
        "success" => "alert-success",
        "warning" => "alert-warning",
        "danger"  => "alert-danger",
        _         => "alert-info"
    };

    private string ObtenirClasseScore(int score) =>
        score >= 80 ? "bg-success text-white" :
        score >= 60 ? "bg-warning"            :
                      "bg-danger text-white";
}

<!-- ─── 4. Dictionnaire d'attributs ─────────────────────── -->
<div @attributes="@_attrs">Contenu</div>

@code {
    private Dictionary<string, object> _attrs => new()
    {
        ["class"]        = $"card {(_actif ? "active" : "")} {(_loading ? "loading" : "")}".Trim(),
        ["style"]        = $"--progress: {_score}%;",
        ["data-id"]      = "mon-composant",
        ["aria-expanded"] = _actif.ToString().ToLower()
    };
}

<!-- ─── 5. ClassBuilder Pattern ───────────────────────────── -->
@code {
    private string Classes => new ClassBuilder("btn")
        .Add("btn-primary", _actif)
        .Add("btn-lg", _grand)
        .Add("disabled", _loading)
        .Build();
}

<!-- Pattern ClassBuilder (à implémenter) -->
public class ClassBuilder
{
    private readonly List<string> _classes = new();

    public ClassBuilder(string baseClass) { _classes.Add(baseClass); }

    public ClassBuilder Add(string cssClass, bool condition = true)
    {
        if (condition && !string.IsNullOrEmpty(cssClass))
            _classes.Add(cssClass);
        return this;
    }

    public string Build() => string.Join(" ", _classes);
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 16 : FRAMEWORKS UI
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser MudBlazor (composants Material Design)
[OK] Créer des thèmes MudBlazor personnalisés
[OK] Utiliser les composants avancés (Table, Dialog, Form)
[OK] Intégrer Tailwind CSS avec Blazor
[OK] Choisir le bon framework selon le projet

COMPARATIF DES FRAMEWORKS :

╔═══════════════╦══════════╦═══════════╦═══════════════╗
║ Framework     ║ Composants║ Taille   ║ Courbe appr.  ║
╠═══════════════╬══════════╬═══════════╬═══════════════╣
║ MudBlazor     ║ 80+      ║ ~400KB   ║ Moyenne       ║
║ Blazorise     ║ 60+      ║ ~350KB   ║ Moyenne       ║
║ AntDesign     ║ 70+      ║ ~500KB   ║ Moyenne       ║
║ Radzen        ║ 80+      ║ ~450KB   ║ Facile        ║
║ FluentUI      ║ 40+      ║ ~300KB   ║ Difficile     ║
║ Bootstrap     ║ CSS seul ║ ~30KB    ║ Très facile   ║
║ Tailwind      ║ CSS seul ║ ~10KB    ║ Moyenne       ║
╚═══════════════╩══════════╩═══════════╩═══════════════╝

-> Pour projets professionnels SaaS : MudBlazor (recommandé)
-> Pour projets simples/rapides : Bootstrap
-> Pour contrôle total du design : Tailwind CSS
*/


// ----------------------------------------------------------------------------
// - MUDBLAZOR — Framework UI Professionnel
// ----------------------------------------------------------------------------

/*
INSTALLATION :
dotnet add package MudBlazor

CONFIGURATION COMPLÈTE :
*/

/*
1. _Imports.razor — Ajouter les usings globaux :
   @using MudBlazor

2. Program.cs — Enregistrer les services :
   builder.Services.AddMudServices(config =>
   {
       config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomRight;
       config.SnackbarConfiguration.ShowTransitionDuration = 200;
       config.SnackbarConfiguration.HideTransitionDuration = 400;
       config.SnackbarConfiguration.SnackbarVariant = Variant.Filled;
   });

3. index.html (WebAssembly) — CSS et JS :
   <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
   <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
   ...
   <script src="_content/MudBlazor/MudBlazor.min.js"></script>

4. MainLayout.razor — Providers :
   <MudThemeProvider Theme="@MonTheme" IsDarkMode="@_darkMode" />
   <MudDialogProvider FullWidth="true" MaxWidth="MaxWidth.Small" />
   <MudSnackbarProvider />
*/

/*
─────────────────────────────────────────────────────────────────
Thème MudBlazor Personnalisé
─────────────────────────────────────────────────────────────────
Fichier : Themes/AppTheme.cs
*/

/*
using MudBlazor;

public static class AppTheme
{
    public static MudTheme Theme => new()
    {
        Palette = new PaletteLight()
        {
            Primary         = "#3b82f6",
            PrimaryDarken   = "#2563eb",
            PrimaryLighten  = "#60a5fa",
            Secondary       = "#8b5cf6",
            Tertiary        = "#06b6d4",
            Success         = "#22c55e",
            Warning         = "#f59e0b",
            Error           = "#ef4444",
            Info            = "#06b6d4",

            Background      = "#f8fafc",
            BackgroundGrey  = "#f1f5f9",
            Surface         = "#ffffff",
            AppbarBackground = "#ffffff",
            AppbarText      = "#1e293b",
            DrawerBackground = "#ffffff",
            DrawerText      = "#1e293b",
            DrawerIcon      = "#64748b",

            TextPrimary     = "#1e293b",
            TextSecondary   = "#64748b",
            TextDisabled    = "#94a3b8",

            Divider         = "#e2e8f0",
            TableLines      = "#f1f5f9",
            TableStriped    = "#f8fafc",
            TableHover      = "#eff6ff",

            OverlayDark     = "rgba(15, 23, 42, 0.5)",
        },

        PaletteDark = new PaletteDark()
        {
            Primary         = "#60a5fa",
            Secondary       = "#a78bfa",
            Background      = "#0f172a",
            BackgroundGrey  = "#1e293b",
            Surface         = "#1e293b",
            AppbarBackground = "#1e293b",
            DrawerBackground = "#1e293b",
            TextPrimary     = "#f8fafc",
            TextSecondary   = "#94a3b8",
            TableLines      = "#334155",
            Divider         = "#334155",
        },

        Typography = new Typography()
        {
            Default = new Default()
            {
                FontFamily = new[] { "Inter", "Roboto", "sans-serif" },
                FontSize   = "0.875rem",
                LineHeight = 1.5,
                LetterSpacing = "normal",
            },
            H1  = new H1()  { FontSize = "2.5rem",   FontWeight = 700 },
            H2  = new H2()  { FontSize = "2rem",      FontWeight = 600 },
            H3  = new H3()  { FontSize = "1.5rem",    FontWeight = 600 },
            H4  = new H4()  { FontSize = "1.25rem",   FontWeight = 600 },
            H5  = new H5()  { FontSize = "1rem",      FontWeight = 600 },
            H6  = new H6()  { FontSize = "0.875rem",  FontWeight = 600 },
            Body1 = new Body1() { FontSize = "0.875rem", LineHeight = 1.6 },
            Button = new Button()
            {
                FontSize      = "0.875rem",
                FontWeight    = 500,
                TextTransform = "none",    // Pas de MAJUSCULES forcées !
                LetterSpacing = "0.01em"
            },
            Caption = new Caption() { FontSize = "0.75rem" },
        },

        LayoutProperties = new LayoutProperties()
        {
            DefaultBorderRadius = "8px",
            DrawerWidthLeft     = "260px",
            DrawerWidthRight    = "300px",
            DrawerMiniWidthLeft = "56px",
            AppbarHeight        = "64px",
        }
    };
}
*/

/*
─────────────────────────────────────────────────────────────────
MainLayout.razor avec MudBlazor complet
─────────────────────────────────────────────────────────────────
Fichier : Shared/MainLayout.razor

@inherits LayoutComponentBase
@inject AppState AppState
@implements IDisposable

<MudThemeProvider Theme="AppTheme.Theme" IsDarkMode="@(AppState.Theme == "dark")" />
<MudDialogProvider FullWidth="true" MaxWidth="MaxWidth.Small" CloseOnEscapeKey="true" />
<MudSnackbarProvider />

<MudLayout>

    <!-- ─── Barre de navigation ─────────────────────────── -->
    <MudAppBar Elevation="1" Dense="false" Fixed="true">
        <MudIconButton Icon="@Icons.Material.Filled.Menu"
                       Color="Color.Inherit" Edge="Edge.Start"
                       @onclick="BasculerDrawer" />

        <MudText Typo="Typo.h6" Class="ml-3 d-none d-md-block">
            [RAPIDE] MonApp
        </MudText>

        <MudSpacer />

        <!-- Recherche globale -->
        <MudTextField @bind-Value="_rechercheGlobale"
                      Placeholder="Rechercher..."
                      Variant="Variant.Outlined"
                      Margin="Margin.Dense"
                      Adornment="Adornment.End"
                      AdornmentIcon="@Icons.Material.Filled.Search"
                      Class="mr-2 d-none d-md-block"
                      Style="min-width: 250px;" />

        <!-- Notifications -->
        <MudBadge Content="@AppState.Notifications"
                  Color="Color.Error" Overlap="true"
                  Visible="@(AppState.Notifications > 0)">
            <MudIconButton Icon="@Icons.Material.Filled.Notifications"
                           Color="Color.Inherit" />
        </MudBadge>

        <!-- Bouton thème dark/light -->
        <MudIconButton Icon="@(AppState.Theme == "dark"
                               ? Icons.Material.Filled.LightMode
                               : Icons.Material.Filled.DarkMode)"
                       Color="Color.Inherit"
                       @onclick="AppState.BasculerTheme" />

        <!-- Menu utilisateur -->
        <MudMenu Direction="Direction.Bottom" OffsetY="true">
            <ActivatorContent>
                <MudAvatar Color="Color.Primary" Size="Size.Medium"
                           Class="ml-2 cursor-pointer">
                    @(AppState.NomUtilisateur?.Length > 0
                        ? AppState.NomUtilisateur[0].ToString().ToUpper()
                        : "?")
                </MudAvatar>
            </ActivatorContent>
            <ChildContent>
                <MudText Class="px-4 py-2" Typo="Typo.body2">
                    <strong>@AppState.NomUtilisateur</strong>
                </MudText>
                <MudDivider />
                <MudMenuItem Icon="@Icons.Material.Filled.Person"
                             Href="/profil">
                    Mon Profil
                </MudMenuItem>
                <MudMenuItem Icon="@Icons.Material.Filled.Settings"
                             Href="/parametres">
                    Paramètres
                </MudMenuItem>
                <MudDivider />
                <MudMenuItem Icon="@Icons.Material.Filled.Logout"
                             IconColor="Color.Error"
                             @onclick="SeDeconnecter">
                    Déconnexion
                </MudMenuItem>
            </ChildContent>
        </MudMenu>
    </MudAppBar>

    <!-- ─── Sidebar / Drawer ─────────────────────────────── -->
    <MudDrawer @bind-Open="_drawerOuvert"
               ClipMode="DrawerClipMode.Always"
               Variant="DrawerVariant.Mini"
               MiniWidth="56px" Width="260px"
               Elevation="2"
               @onmouseenter="() => _drawerHover = true"
               @onmouseleave="() => _drawerHover = false">

        <MudDrawerHeader Class="pa-4">
            @if (_drawerOuvert || _drawerHover)
            {
                <MudText Typo="Typo.h6">Navigation</MudText>
            }
        </MudDrawerHeader>

        <NavMenuMud EstOuvert="@(_drawerOuvert || _drawerHover)" />

    </MudDrawer>

    <!-- ─── Contenu principal ────────────────────────────── -->
    <MudMainContent Class="pt-16">
        <MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
            @Body
        </MudContainer>
    </MudMainContent>

</MudLayout>

@code {
    private bool _drawerOuvert = true;
    private bool _drawerHover = false;
    private string _rechercheGlobale = string.Empty;

    protected override void OnInitialized()
    {
        AppState.OnChange += () => InvokeAsync(StateHasChanged);
    }

    private void BasculerDrawer() => _drawerOuvert = !_drawerOuvert;

    private void SeDeconnecter()
    {
        AppState.Deconnecter();
        // NavigationManager.NavigateTo("/login");
    }

    public void Dispose()
    {
        AppState.OnChange -= () => InvokeAsync(StateHasChanged);
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
NavMenuMud.razor — Navigation sidebar MudBlazor
─────────────────────────────────────────────────────────────────

@inject NavigationManager NavManager

<MudNavMenu Dense="false" Rounded="true" Margin="Margin.Dense"
            Color="Color.Primary">

    <MudNavLink Icon="@Icons.Material.Filled.Dashboard"
                Href="/"
                Match="NavLinkMatch.All">
        @(EstOuvert ? "Dashboard" : "")
    </MudNavLink>

    <MudNavLink Icon="@Icons.Material.Filled.ShoppingBag"
                Href="/produits">
        @(EstOuvert ? "Produits" : "")
    </MudNavLink>

    <MudNavLink Icon="@Icons.Material.Filled.People"
                Href="/utilisateurs">
        @(EstOuvert ? "Utilisateurs" : "")
    </MudNavLink>

    <MudNavLink Icon="@Icons.Material.Filled.ShoppingCart"
                Href="/commandes">
        @(EstOuvert ? "Commandes" : "")
    </MudNavLink>

    <!-- Groupe avec sous-menu -->
    <MudNavGroup Icon="@Icons.Material.Filled.BarChart"
                 Title="@(EstOuvert ? "Rapports" : "")"
                 Expanded="false">
        <MudNavLink Href="/rapports/ventes">Ventes</MudNavLink>
        <MudNavLink Href="/rapports/clients">Clients</MudNavLink>
        <MudNavLink Href="/rapports/inventaire">Inventaire</MudNavLink>
    </MudNavGroup>

    <MudDivider Class="my-2" />

    <MudNavLink Icon="@Icons.Material.Filled.Settings"
                Href="/parametres">
        @(EstOuvert ? "Paramètres" : "")
    </MudNavLink>

</MudNavMenu>

@code {
    [Parameter] public bool EstOuvert { get; set; } = true;
}
*/

/*
─────────────────────────────────────────────────────────────────
Page Produits avec MudBlazor avancé
─────────────────────────────────────────────────────────────────
Fichier : Pages/GestionProduits.razor

@page "/produits"
@inject IProduitService ProduitService
@inject ISnackbar Snackbar
@inject IDialogService DialogService

<PageTitle>Gestion des Produits</PageTitle>

<MudText Typo="Typo.h4" Class="mb-4">[SHOPPING_BAGS] Gestion des Produits</MudText>

<!-- Statistiques rapides -->
<MudGrid Class="mb-6">
    <MudItem xs="12" sm="6" md="3">
        <MudPaper Class="pa-4 d-flex align-items-center gap-3" Elevation="2">
            <MudIcon Icon="@Icons.Material.Filled.Inventory"
                     Color="Color.Primary" Size="Size.Large" />
            <div>
                <MudText Typo="Typo.h5">@_produits.Count</MudText>
                <MudText Typo="Typo.caption" Color="Color.Secondary">Total produits</MudText>
            </div>
        </MudPaper>
    </MudItem>
    <MudItem xs="12" sm="6" md="3">
        <MudPaper Class="pa-4 d-flex align-items-center gap-3" Elevation="2">
            <MudIcon Icon="@Icons.Material.Filled.CheckCircle"
                     Color="Color.Success" Size="Size.Large" />
            <div>
                <MudText Typo="Typo.h5">@_produits.Count(p => p.Stock > 0)</MudText>
                <MudText Typo="Typo.caption" Color="Color.Secondary">En stock</MudText>
            </div>
        </MudPaper>
    </MudItem>
    <MudItem xs="12" sm="6" md="3">
        <MudPaper Class="pa-4 d-flex align-items-center gap-3" Elevation="2">
            <MudIcon Icon="@Icons.Material.Filled.Warning"
                     Color="Color.Warning" Size="Size.Large" />
            <div>
                <MudText Typo="Typo.h5">@_produits.Count(p => p.Stock > 0 && p.Stock <= 5)</MudText>
                <MudText Typo="Typo.caption" Color="Color.Secondary">Stock faible (≤5)</MudText>
            </div>
        </MudPaper>
    </MudItem>
    <MudItem xs="12" sm="6" md="3">
        <MudPaper Class="pa-4 d-flex align-items-center gap-3" Elevation="2">
            <MudIcon Icon="@Icons.Material.Filled.Cancel"
                     Color="Color.Error" Size="Size.Large" />
            <div>
                <MudText Typo="Typo.h5">@_produits.Count(p => p.Stock == 0)</MudText>
                <MudText Typo="Typo.caption" Color="Color.Secondary">Rupture</MudText>
            </div>
        </MudPaper>
    </MudItem>
</MudGrid>

<!-- Tableau principal -->
<MudPaper Elevation="2">
    <MudTable T="Produit"
              ServerData="ChargerDonnees"
              Dense="false"
              Hover="true"
              Striped="false"
              Bordered="false"
              @ref="_tableau"
              RowClassFunc="ClasseLigne"
              OnRowClick="RowClick">

        <ToolBarContent>
            <MudText Typo="Typo.h6">Catalogue</MudText>
            <MudSpacer />

            <!-- Filtre catégorie -->
            <MudSelect @bind-Value="_categorieFiltre"
                       Label="Catégorie"
                       Variant="Variant.Outlined"
                       Margin="Margin.Dense"
                       Style="min-width:180px"
                       Class="mr-3"
                       ValueChanged="FiltreChange">
                <MudSelectItem Value="@("")">Toutes</MudSelectItem>
                @foreach (var cat in _categories)
                {
                    <MudSelectItem Value="@cat">@cat</MudSelectItem>
                }
            </MudSelect>

            <!-- Recherche -->
            <MudTextField @bind-Value="_filtreRecherche"
                          Placeholder="Rechercher..."
                          Variant="Variant.Outlined"
                          Margin="Margin.Dense"
                          Adornment="Adornment.Start"
                          AdornmentIcon="@Icons.Material.Filled.Search"
                          Immediate="true"
                          DebounceInterval="300"
                          ValueChanged="FiltreChange"
                          Style="min-width:220px"
                          Class="mr-3" />

            <!-- Bouton ajout -->
            <MudButton Variant="Variant.Filled"
                       Color="Color.Success"
                       StartIcon="@Icons.Material.Filled.Add"
                       @onclick="OuvrirDialogAjout">
                Nouveau
            </MudButton>
        </ToolBarContent>

        <HeaderContent>
            <MudTh>
                <MudTableSortLabel SortLabel="nom" T="Produit">Nom</MudTableSortLabel>
            </MudTh>
            <MudTh>
                <MudTableSortLabel SortLabel="categorie" T="Produit">Catégorie</MudTableSortLabel>
            </MudTh>
            <MudTh>
                <MudTableSortLabel SortLabel="prix" T="Produit">Prix</MudTableSortLabel>
            </MudTh>
            <MudTh>Stock</MudTh>
            <MudTh>Statut</MudTh>
            <MudTh Style="width: 120px">Actions</MudTh>
        </HeaderContent>

        <RowTemplate>
            <MudTd DataLabel="Nom">
                <div class="d-flex align-items-center gap-2">
                    @if (context.EstNouveau)
                    {
                        <MudChip Size="Size.Small" Color="Color.Success"
                                 Variant="Variant.Filled">NEW</MudChip>
                    }
                    <MudText Typo="Typo.body2">@context.Nom</MudText>
                </div>
            </MudTd>
            <MudTd DataLabel="Catégorie">
                <MudChip Size="Size.Small" Color="Color.Info"
                         Variant="Variant.Outlined">
                    @context.Categorie
                </MudChip>
            </MudTd>
            <MudTd DataLabel="Prix">
                <MudText Typo="Typo.body2" Color="Color.Primary">
                    <strong>@context.Prix.ToString("C")</strong>
                </MudText>
            </MudTd>
            <MudTd DataLabel="Stock">@context.Stock</MudTd>
            <MudTd DataLabel="Statut">
                @{
                    var (couleur, icone, label) = context.Stock switch
                    {
                        0 => (Color.Error, Icons.Material.Filled.Cancel, "Rupture"),
                        <= 5 => (Color.Warning, Icons.Material.Filled.Warning, "Stock faible"),
                        _ => (Color.Success, Icons.Material.Filled.CheckCircle, "En stock")
                    };
                }
                <MudChip Color="couleur" Size="Size.Small"
                         Icon="@icone" Variant="Variant.Filled">
                    @label
                </MudChip>
            </MudTd>
            <MudTd>
                <MudIconButton Icon="@Icons.Material.Filled.Edit"
                               Color="Color.Primary" Size="Size.Small"
                               Title="Modifier"
                               @onclick="() => OuvrirDialogEdition(context)"
                               @onclick:stopPropagation="true" />
                <MudIconButton Icon="@Icons.Material.Filled.Delete"
                               Color="Color.Error" Size="Size.Small"
                               Title="Supprimer"
                               @onclick="() => ConfirmerSuppression(context)"
                               @onclick:stopPropagation="true" />
            </MudTd>
        </RowTemplate>

        <NoRecordsContent>
            <MudText Class="pa-4" Align="Align.Center" Color="Color.Secondary">
                Aucun produit trouvé.
            </MudText>
        </NoRecordsContent>

        <LoadingContent>
            <MudText Class="pa-4" Align="Align.Center">
                <MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-2" />
                Chargement...
            </MudText>
        </LoadingContent>

        <PagerContent>
            <MudTablePager PageSizeOptions="new int[]{10, 25, 50, 100}" />
        </PagerContent>

    </MudTable>
</MudPaper>

@code {
    private MudTable<Produit>? _tableau;
    private List<Produit> _produits = new();
    private List<string> _categories = new();
    private string _filtreRecherche = string.Empty;
    private string _categorieFiltre = string.Empty;

    protected override async Task OnInitializedAsync()
    {
        _produits = await ProduitService.ObtenirTousAsync();
        _categories = _produits.Select(p => p.Categorie).Distinct().Order().ToList();
    }

    // ServerData : méthode pour le tableau avec pagination/tri côté serveur
    private async Task<TableData<Produit>> ChargerDonnees(TableState state)
    {
        var filtres = _produits
            .Where(p => string.IsNullOrEmpty(_filtreRecherche) ||
                        p.Nom.Contains(_filtreRecherche, StringComparison.OrdinalIgnoreCase))
            .Where(p => string.IsNullOrEmpty(_categorieFiltre) ||
                        p.Categorie == _categorieFiltre);

        // Tri
        filtres = state.SortLabel switch
        {
            "nom"       => state.SortDirection == SortDirection.Ascending
                            ? filtres.OrderBy(p => p.Nom)
                            : filtres.OrderByDescending(p => p.Nom),
            "prix"      => state.SortDirection == SortDirection.Ascending
                            ? filtres.OrderBy(p => p.Prix)
                            : filtres.OrderByDescending(p => p.Prix),
            "categorie" => state.SortDirection == SortDirection.Ascending
                            ? filtres.OrderBy(p => p.Categorie)
                            : filtres.OrderByDescending(p => p.Categorie),
            _ => filtres.OrderBy(p => p.Nom)
        };

        var total = filtres.Count();
        var page = filtres.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList();

        return new TableData<Produit> { Items = page, TotalItems = total };
    }

    private void FiltreChange(string _) => _tableau?.ReloadServerData();

    private string ClasseLigne(Produit produit, int index)
    {
        if (produit.Stock == 0) return "table-row-rupture";
        if (produit.Stock <= 5) return "table-row-warning";
        return string.Empty;
    }

    private void RowClick(TableRowClickEventArgs<Produit> args) { }

    private async Task ConfirmerSuppression(Produit produit)
    {
        var resultat = await DialogService.ShowMessageBox(
            "[ATTENTION] Confirmation",
            $"Supprimer définitivement '{produit.Nom}' ?",
            yesText: "Supprimer",
            yesButton: new DialogOptions { },
            cancelText: "Annuler");

        if (resultat == true)
        {
            _produits.Remove(produit);
            _tableau?.ReloadServerData();
            Snackbar.Add($"'{produit.Nom}' supprimé.", Severity.Warning);
        }
    }

    private async Task OuvrirDialogAjout()
    {
        var dialog = await DialogService.ShowAsync<DialogProduit>(
            "Nouveau produit",
            new DialogParameters { ["Produit"] = new Produit() });

        var resultat = await dialog.Result;
        if (!resultat.Canceled && resultat.Data is Produit nouveau)
        {
            _produits.Add(nouveau);
            _tableau?.ReloadServerData();
            Snackbar.Add($"'{nouveau.Nom}' ajouté !", Severity.Success);
        }
    }

    private async Task OuvrirDialogEdition(Produit produit)
    {
        // Passer une copie pour ne pas modifier directement
        var copie = new Produit
        {
            Id = produit.Id, Nom = produit.Nom,
            Prix = produit.Prix, Categorie = produit.Categorie,
            Stock = produit.Stock
        };

        var dialog = await DialogService.ShowAsync<DialogProduit>(
            "Modifier le produit",
            new DialogParameters { ["Produit"] = copie });

        var resultat = await dialog.Result;
        if (!resultat.Canceled && resultat.Data is Produit modifie)
        {
            var index = _produits.FindIndex(p => p.Id == modifie.Id);
            if (index >= 0) _produits[index] = modifie;
            _tableau?.ReloadServerData();
            Snackbar.Add($"'{modifie.Nom}' modifié.", Severity.Info);
        }
    }
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 17 : COMPOSANTS DYNAMIQUES
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser DynamicComponent pour rendre des composants dynamiquement
[OK] Créer des systèmes de widgets configurables
[OK] Construire des formulaires schema-driven
[OK] Implémenter des tableaux de bord personnalisables
*/


// ----------------------------------------------------------------------------
// [MODULE] DYNAMICCOMPONENT — Composants dynamiques
// ----------------------------------------------------------------------------

/*
DynamicComponent = Rendre un composant dont le TYPE est connu seulement à l'exécution

SYNTAXE :
<DynamicComponent Type="@monType" Parameters="@mesParams" />

QUAND UTILISER ?
-> Dashboard avec widgets configurables par l'utilisateur
-> Système de plugins/extensions UI
-> CMS (Content Management System)
-> Formulaires/pages générées depuis une BDD/API
*/

// Modèle de configuration de widget
public record WidgetConfig(
    string Id,
    string Titre,
    Type ComposantType,
    Dictionary<string, object> Parametres,
    int Colonne,      // Position dans la grille (1-12)
    int Largeur,      // Largeur en colonnes (1-12)
    int Ligne,        // Position verticale
    int Hauteur       // Hauteur en lignes de la grille
);

/*
─────────────────────────────────────────────────────────────────
Dashboard avec DynamicComponent
─────────────────────────────────────────────────────────────────
Fichier : Pages/Dashboard.razor

@page "/dashboard"

<div class="dashboard-container">

    <!-- Barre d'outils -->
    <div class="d-flex justify-content-between mb-4">
        <MudText Typo="Typo.h4">Mon Dashboard</MudText>
        <MudMenu Label="+ Widget" Variant="Variant.Filled" Color="Color.Primary">
            <MudMenuItem @onclick="() => AjouterWidget(typeof(WidgetVentes))">
                [GRAPHIQUE] Widget Ventes
            </MudMenuItem>
            <MudMenuItem @onclick="() => AjouterWidget(typeof(WidgetUtilisateurs))">
                [UTILISATEURS] Widget Utilisateurs
            </MudMenuItem>
            <MudMenuItem @onclick="() => AjouterWidget(typeof(WidgetActivite))">
                [HAUSSE] Widget Activité
            </MudMenuItem>
            <MudMenuItem @onclick="() => AjouterWidget(typeof(WidgetNotes))">
                [NOTE] Widget Notes
            </MudMenuItem>
        </MudMenu>
    </div>

    <!-- Grille CSS pour les widgets -->
    <div class="widget-grid">
        @foreach (var widget in _widgets)
        {
            <div class="widget-cell"
                 style="grid-column: @widget.Colonne / span @widget.Largeur;
                        grid-row: @widget.Ligne / span @widget.Hauteur;">

                <!-- Conteneur du widget -->
                <MudPaper Elevation="2" Class="h-100 d-flex flex-column">
                    <!-- Header du widget -->
                    <div class="widget-header d-flex justify-content-between align-items-center pa-3">
                        <MudText Typo="Typo.subtitle1">
                            <strong>@widget.Titre</strong>
                        </MudText>
                        <MudIconButton Icon="@Icons.Material.Filled.Close"
                                       Size="Size.Small"
                                       @onclick="() => SupprimerWidget(widget.Id)" />
                    </div>
                    <MudDivider />

                    <!-- Corps du widget : composant dynamique -->
                    <div class="widget-body pa-3 flex-grow-1">
                        <DynamicComponent Type="@widget.ComposantType"
                                         Parameters="@widget.Parametres" />
                    </div>
                </MudPaper>

            </div>
        }
    </div>

    @if (!_widgets.Any())
    {
        <MudPaper Class="pa-8 text-center" Elevation="0" Outlined="true">
            <MudIcon Icon="@Icons.Material.Filled.DashboardCustomize"
                     Size="Size.Large" Color="Color.Secondary" />
            <MudText Typo="Typo.h6" Color="Color.Secondary" Class="mt-2">
                Dashboard vide
            </MudText>
            <MudText Color="Color.Secondary">
                Cliquez sur "+ Widget" pour ajouter des widgets.
            </MudText>
        </MudPaper>
    }
</div>

@code {
    private List<WidgetConfig> _widgets = new()
    {
        new("w1", "Ventes du mois", typeof(WidgetVentes),
            new() { ["Periode"] = "mois", ["Couleur"] = "primary" },
            1, 4, 1, 2),

        new("w2", "Utilisateurs actifs", typeof(WidgetUtilisateurs),
            new() { ["Limite"] = 5 },
            5, 4, 1, 2),

        new("w3", "Activité récente", typeof(WidgetActivite),
            new() { ["Jours"] = 7 },
            9, 4, 1, 2),

        new("w4", "Performance globale", typeof(WidgetVentes),
            new() { ["Periode"] = "annee", ["Couleur"] = "success" },
            1, 8, 3, 3),

        new("w5", "Notes rapides", typeof(WidgetNotes),
            new(),
            9, 4, 3, 3),
    };

    private int _prochaineLigne = 5;
    private int _prochaineColonne = 1;

    private void AjouterWidget(Type typeComposant)
    {
        var nom = typeComposant.Name.Replace("Widget", "");
        _widgets.Add(new WidgetConfig(
            Guid.NewGuid().ToString(),
            nom,
            typeComposant,
            new Dictionary<string, object>(),
            _prochaineColonne, 4,
            _prochaineLigne, 2));

        // Calculer la prochaine position
        _prochaineColonne += 4;
        if (_prochaineColonne > 9)
        {
            _prochaineColonne = 1;
            _prochaineLigne += 2;
        }
    }

    private void SupprimerWidget(string id)
    {
        _widgets.RemoveAll(w => w.Id == id);
    }
}


CSS pour la grille :
.widget-grid {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    grid-auto-rows: minmax(120px, auto);
    gap: 16px;
}
*/

/*
─────────────────────────────────────────────────────────────────
Composant Widget : WidgetVentes.razor
─────────────────────────────────────────────────────────────────

<div class="widget-stat h-100 d-flex flex-column justify-content-between">
    <div>
        <MudText Typo="Typo.h3" Color="@(Couleur == "success" ? Color.Success : Color.Primary)">
            @_total.ToString("C0")
        </MudText>
        <MudText Color="Color.Secondary">
            @(Periode == "mois" ? "Ce mois" : Periode == "annee" ? "Cette année" : "Cette semaine")
        </MudText>
    </div>

    <div class="d-flex align-items-center gap-1 mt-2">
        <MudIcon Icon="@(_evolution >= 0
                          ? Icons.Material.Filled.TrendingUp
                          : Icons.Material.Filled.TrendingDown)"
                 Color="@(_evolution >= 0 ? Color.Success : Color.Error)"
                 Size="Size.Small" />
        <MudText Typo="Typo.body2"
                 Color="@(_evolution >= 0 ? Color.Success : Color.Error)">
            @Math.Abs(_evolution)% vs période précédente
        </MudText>
    </div>

    <!-- Mini graphique simulé avec barres CSS -->
    <div class="mini-chart mt-3 d-flex align-items-end gap-1" style="height: 40px;">
        @foreach (var valeur in _serieSimulee)
        {
            <div class="mini-bar bg-primary opacity-@(valeur > 70 ? "100" : valeur > 40 ? "75" : "50")"
                 style="flex: 1; height: @(valeur)%;border-radius: 2px;"></div>
        }
    </div>
</div>

@code {
    [Parameter] public string Periode { get; set; } = "semaine";
    [Parameter] public string Couleur { get; set; } = "primary";

    private decimal _total;
    private double _evolution;
    private int[] _serieSimulee = Array.Empty<int>();

    protected override void OnParametersSet()
    {
        (_total, _evolution) = Periode switch
        {
            "mois"  => (48_320m,  12.5),
            "annee" => (580_400m, 8.3),
            _       => (12_480m,  -2.1)
        };

        // Simuler une série de données
        var rnd = new Random(Periode.GetHashCode());
        _serieSimulee = Enumerable.Range(0, 12)
            .Select(_ => rnd.Next(30, 100))
            .ToArray();
    }
}
*/


// ----------------------------------------------------------------------------
// [NOTE] FORMULAIRES DYNAMIQUES (Schema-Driven)
// ----------------------------------------------------------------------------

/*
Formulaire Schema-Driven = Généré depuis une CONFIGURATION,
sans coder manuellement chaque champ.

APPLICATIONS :
-> Systèmes CMS (Content Management)
-> Formulaires d'administration configurables
-> Questionnaires/surveys dynamiques
-> Configuration d'application via UI
*/

public class ChampFormulaire
{
    public string Nom { get; set; } = string.Empty;
    public string Label { get; set; } = string.Empty;
    public TypeChampFormulaire Type { get; set; } = TypeChampFormulaire.Texte;
    public bool Obligatoire { get; set; } = false;
    public string? Placeholder { get; set; }
    public string? TexteAide { get; set; }
    public object? ValeurDefaut { get; set; }
    public List<OptionChamp> Options { get; set; } = new();
    public int? Min { get; set; }
    public int? Max { get; set; }
    public int Ordre { get; set; } = 0;
    public string? GroupeSection { get; set; } // Pour regrouper les champs
    public string? RegexValidation { get; set; }
    public string? MessageErreurRegex { get; set; }
}

public enum TypeChampFormulaire
{
    Texte, Email, MotDePasse, Nombre, DecimalNombre,
    TexteLong, SelectSimple, SelectMultiple,
    Checkbox, Radio, Date, DateHeure, Couleur, Curseur, Fichier
}

public record OptionChamp(string Valeur, string Libelle, string? Icone = null);

/*
─────────────────────────────────────────────────────────────────
Composant FormulaireSchematise.razor
─────────────────────────────────────────────────────────────────

@foreach (var section in _sections)
{
    @if (!string.IsNullOrEmpty(section.Key))
    {
        <MudText Typo="Typo.subtitle1" Class="mt-4 mb-2">
            <strong>@section.Key</strong>
        </MudText>
        <MudDivider Class="mb-3" />
    }

    <MudGrid>
        @foreach (var champ in section.Value.OrderBy(c => c.Ordre))
        {
            <MudItem xs="12" sm="@(champ.Type == TypeChampFormulaire.TexteLong ? 12 : 6)">

                @switch (champ.Type)
                {
                    case TypeChampFormulaire.Texte:
                    case TypeChampFormulaire.Email:
                    case TypeChampFormulaire.MotDePasse:
                        <MudTextField @bind-Value="@_valeursStr[champ.Nom]"
                                      Label="@champ.Label"
                                      Placeholder="@champ.Placeholder"
                                      HelperText="@champ.TexteAide"
                                      Required="@champ.Obligatoire"
                                      InputType="@ObtenirInputType(champ.Type)"
                                      Variant="Variant.Outlined"
                                      FullWidth="true" />
                        break;

                    case TypeChampFormulaire.Nombre:
                        <MudNumericField @bind-Value="@_valeursInt[champ.Nom]"
                                         Label="@champ.Label"
                                         HelperText="@champ.TexteAide"
                                         Required="@champ.Obligatoire"
                                         Min="@(champ.Min ?? int.MinValue)"
                                         Max="@(champ.Max ?? int.MaxValue)"
                                         Variant="Variant.Outlined"
                                         FullWidth="true" />
                        break;

                    case TypeChampFormulaire.TexteLong:
                        <MudTextField @bind-Value="@_valeursStr[champ.Nom]"
                                      Label="@champ.Label"
                                      Placeholder="@champ.Placeholder"
                                      HelperText="@champ.TexteAide"
                                      Required="@champ.Obligatoire"
                                      Lines="4"
                                      Variant="Variant.Outlined"
                                      FullWidth="true" />
                        break;

                    case TypeChampFormulaire.SelectSimple:
                        <MudSelect @bind-Value="@_valeursStr[champ.Nom]"
                                   Label="@champ.Label"
                                   HelperText="@champ.TexteAide"
                                   Required="@champ.Obligatoire"
                                   Variant="Variant.Outlined"
                                   FullWidth="true">
                            <MudSelectItem Value="@("")">-- Choisir --</MudSelectItem>
                            @foreach (var opt in champ.Options)
                            {
                                <MudSelectItem Value="@opt.Valeur">@opt.Libelle</MudSelectItem>
                            }
                        </MudSelect>
                        break;

                    case TypeChampFormulaire.Checkbox:
                        <MudCheckBox @bind-Checked="@_valeursBool[champ.Nom]"
                                     Label="@champ.Label"
                                     Color="Color.Primary" />
                        break;

                    case TypeChampFormulaire.Date:
                        <MudDatePicker @bind-Date="@_valeursDate[champ.Nom]"
                                       Label="@champ.Label"
                                       HelperText="@champ.TexteAide"
                                       Required="@champ.Obligatoire"
                                       Variant="Variant.Outlined"
                                       FullWidth="true" />
                        break;

                    case TypeChampFormulaire.Curseur:
                        <div>
                            <MudText Typo="Typo.caption">
                                @champ.Label : @_valeursInt.GetValueOrDefault(champ.Nom)
                            </MudText>
                            <MudSlider @bind-Value="@_valeursInt[champ.Nom]"
                                       Min="@(champ.Min ?? 0)"
                                       Max="@(champ.Max ?? 100)"
                                       Color="Color.Primary"
                                       TickMarks="true" />
                        </div>
                        break;

                    case TypeChampFormulaire.Radio:
                        <div>
                            <MudText Typo="Typo.caption">@champ.Label</MudText>
                            <MudRadioGroup @bind-SelectedOption="@_valeursStr[champ.Nom]">
                                @foreach (var opt in champ.Options)
                                {
                                    <MudRadio Option="@opt.Valeur" Color="Color.Primary">
                                        @opt.Libelle
                                    </MudRadio>
                                }
                            </MudRadioGroup>
                        </div>
                        break;
                }

            </MudItem>
        }
    </MudGrid>
}

<MudButton Variant="Variant.Filled" Color="Color.Primary"
           Class="mt-4" @onclick="Soumettre">
    @TexteBouton
</MudButton>

@code {
    [Parameter, EditorRequired]
    public List<ChampFormulaire> Champs { get; set; } = new();

    [Parameter] public string TexteBouton { get; set; } = "Envoyer";

    [Parameter]
    public EventCallback<Dictionary<string, object?>> OnSoumis { get; set; }

    // Stockage des valeurs par type
    private Dictionary<string, string> _valeursStr = new();
    private Dictionary<string, int> _valeursInt = new();
    private Dictionary<string, bool> _valeursBool = new();
    private Dictionary<string, DateTime?> _valeursDate = new();

    // Champs groupés par section
    private Dictionary<string, List<ChampFormulaire>> _sections = new();

    protected override void OnInitialized()
    {
        // Initialiser les valeurs par défaut
        foreach (var champ in Champs)
        {
            var section = champ.GroupeSection ?? "";
            if (!_sections.ContainsKey(section))
                _sections[section] = new();
            _sections[section].Add(champ);

            // Initialiser selon le type
            switch (champ.Type)
            {
                case TypeChampFormulaire.Nombre:
                case TypeChampFormulaire.Curseur:
                    _valeursInt[champ.Nom] = champ.ValeurDefaut is int i ? i :
                                            champ.Min ?? 0;
                    break;
                case TypeChampFormulaire.Checkbox:
                    _valeursBool[champ.Nom] = champ.ValeurDefaut is bool b && b;
                    break;
                case TypeChampFormulaire.Date:
                    _valeursDate[champ.Nom] = champ.ValeurDefaut as DateTime?;
                    break;
                default:
                    _valeursStr[champ.Nom] = champ.ValeurDefaut?.ToString() ?? "";
                    break;
            }
        }
    }

    private InputType ObtenirInputType(TypeChampFormulaire type) => type switch
    {
        TypeChampFormulaire.Email      => InputType.Email,
        TypeChampFormulaire.MotDePasse => InputType.Password,
        _                              => InputType.Text
    };

    private async Task Soumettre()
    {
        // Assembler toutes les valeurs
        var resultat = new Dictionary<string, object?>();

        foreach (var (cle, val) in _valeursStr)
            resultat[cle] = val;
        foreach (var (cle, val) in _valeursInt)
            resultat[cle] = val;
        foreach (var (cle, val) in _valeursBool)
            resultat[cle] = val;
        foreach (var (cle, val) in _valeursDate)
            resultat[cle] = val;

        await OnSoumis.InvokeAsync(resultat);
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
UTILISATION du formulaire dynamique
─────────────────────────────────────────────────────────────────

@page "/contact"

<PageTitle>Contact</PageTitle>

<MudContainer MaxWidth="MaxWidth.Medium" Class="mt-4">
    <MudText Typo="Typo.h4" Class="mb-4">[EMAIL] Contactez-nous</MudText>

    <MudPaper Class="pa-6" Elevation="2">
        <FormulaireSchematise Champs="@_definition"
                              TexteBouton="Envoyer le message"
                              OnSoumis="TraiterFormulaire" />
    </MudPaper>

    @if (_soumis)
    {
        <MudAlert Severity="Severity.Success" Class="mt-4" ShowCloseIcon="true"
                  CloseIconClicked="() => _soumis = false">
            [OK] Message envoyé avec succès ! Nous vous répondrons sous 24h.
        </MudAlert>
    }
</MudContainer>

@code {
    private bool _soumis = false;

    private List<ChampFormulaire> _definition = new()
    {
        new()
        {
            Nom = "nom", Label = "Votre nom complet",
            Type = TypeChampFormulaire.Texte,
            Obligatoire = true, Placeholder = "Jean Dupont",
            GroupeSection = "Vos coordonnées", Ordre = 1
        },
        new()
        {
            Nom = "email", Label = "Adresse email",
            Type = TypeChampFormulaire.Email,
            Obligatoire = true, Placeholder = "jean@example.com",
            GroupeSection = "Vos coordonnées", Ordre = 2
        },
        new()
        {
            Nom = "telephone", Label = "Téléphone (optionnel)",
            Type = TypeChampFormulaire.Texte,
            Placeholder = "+33 6 00 00 00 00",
            GroupeSection = "Vos coordonnées", Ordre = 3
        },
        new()
        {
            Nom = "sujet", Label = "Sujet",
            Type = TypeChampFormulaire.SelectSimple,
            Obligatoire = true,
            GroupeSection = "Votre message", Ordre = 4,
            Options = new()
            {
                new("support", "Support technique"),
                new("devis", "Demande de devis"),
                new("partenariat", "Partenariat"),
                new("autre", "Autre")
            }
        },
        new()
        {
            Nom = "priorite", Label = "Niveau d'urgence",
            Type = TypeChampFormulaire.Curseur,
            Min = 1, Max = 5, ValeurDefaut = 3,
            TexteAide = "1 = Pas urgent, 5 = Critique",
            GroupeSection = "Votre message", Ordre = 5
        },
        new()
        {
            Nom = "message", Label = "Votre message",
            Type = TypeChampFormulaire.TexteLong,
            Obligatoire = true, Placeholder = "Décrivez votre besoin...",
            GroupeSection = "Votre message", Ordre = 6
        },
        new()
        {
            Nom = "newsletter", Label = "Je souhaite recevoir la newsletter",
            Type = TypeChampFormulaire.Checkbox,
            ValeurDefaut = false,
            Ordre = 7
        },
    };

    private async Task TraiterFormulaire(Dictionary<string, object?> valeurs)
    {
        // En vrai : envoyer à l'API
        await Task.Delay(500); // Simuler appel API
        _soumis = true;

        // Log des valeurs pour debug
        foreach (var (cle, val) in valeurs)
            Console.WriteLine($"  {cle}: {val}");
    }
}
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE — PARTIE 5
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
EXERCICE : DASHBOARD PROFESSIONNEL COMPLET
═══════════════════════════════════════════════════════════════

OBJECTIF : Application d'administration avec design professionnel

LIVRABLES :

1. LAYOUT MUDBLAZOR (Shared/MainLayout.razor) :
   [OK] AppBar avec logo, recherche globale, badge notifs
   [OK] Drawer mini (56px fermé, 260px ouvert)
   [OK] Navigation avec NavMenu MudBlazor
   [OK] Basculement thème dark/light
   [OK] Menu utilisateur avec avatar

2. PAGE DASHBOARD (/dashboard) :
   [OK] 4 cartes statistiques (Ventes, Users, Commandes, Revenue)
   [OK] Tableau des 5 dernières commandes
   [OK] Graphique activité (barres CSS simulées)
   [OK] Skeleton loading 1.5s avant affichage
   [OK] Animations fade-in en cascade

3. PAGE PRODUITS (/produits) :
   [OK] MudTable avec pagination côté client
   [OK] Filtre par catégorie + recherche
   [OK] Tri sur colonnes
   [OK] Dialog édition/ajout avec formulaire MudBlazor
   [OK] Confirmation de suppression avec MessageBox
   [OK] Snackbar de feedback

4. CSS SCOPED :
   [OK] CarteProduit.razor.css avec hover, transition
   [OK] Variables CSS (--color-primary, etc.)
   [OK] Animation shimmer pour skeleton

ÉTAPES :
1. dotnet new blazorwasm -n DashboardPro
2. dotnet add package MudBlazor
3. Configurer MudBlazor (index.html, Program.cs, _Imports.razor)
4. Créer le thème AppTheme.cs
5. Créer MainLayout.razor avec MudLayout
6. Créer les pages Dashboard et Produits
7. Ajouter CSS scoped et animations

TEMPS ESTIMÉ : 6-8 heures
═══════════════════════════════════════════════════════════════
*/


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 5

[OK] CHAPITRE 15 - CSS & STYLING :
- CSS Scoped -> Isolation par composant (.razor.css)
- Opérateur ::deep -> Cibler composants enfants
- Design Tokens -> Variables CSS centralisées dans :root
- Thème dark/light avec [data-theme="dark"]
- Animations CSS -> fadeIn, shimmer, slideIn, scaleIn
- Skeleton Loading -> Placeholder pendant chargement
- Classes CSS dynamiques -> Ternaire, méthode, ClassBuilder

[OK] CHAPITRE 16 - FRAMEWORKS UI :
- MudBlazor installation et configuration
- Thème personnalisé -> Palette, Typography, LayoutProperties
- MudLayout -> AppBar + Drawer + MainContent
- MudTable avancé -> ServerData, tri, filtre, pagination
- MudDialog -> Confirmation et formulaires
- MudSnackbar -> Feedback utilisateur
- MudBadge, MudAvatar, MudMenu -> Header professionnel
- Comparatif des frameworks UI Blazor

[OK] CHAPITRE 17 - COMPOSANTS DYNAMIQUES :
- DynamicComponent -> Rendre un type inconnu à compile-time
- Dashboard avec widgets configurables dynamiquement
- Ajout/suppression de widgets à l'exécution
- Formulaires Schema-Driven -> Générés depuis une config
- Gestion multi-type -> string, int, bool, DateTime par champ
- Groupes de sections -> Organisation des formulaires
- Applications : CMS, dashboards configurables, surveys

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 6
- Performance Blazor Server (SignalR, mémoire)
- Performance WebAssembly (lazy loading, AOT, taille)
- SEO et prerendering / streaming rendering
═══════════════════════════════════════════════════════════════
*/

// ============================================================================
// [LIVRE] BLAZOR - PARTIE 6 : PERFORMANCE & OPTIMISATION
// ============================================================================
//
// CHAPITRE 18 : Performance Blazor Server
// CHAPITRE 19 : Performance WebAssembly
// CHAPITRE 20 : SEO & Prerendering
//
// [TEMPS] TEMPS ESTIMÉ : ~8-10 heures
// [DOCS] PRÉREQUIS : Parties 1-5 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 18 : PERFORMANCE BLAZOR SERVER
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre l'architecture SignalR de Blazor Server
[OK] Optimiser la consommation mémoire par connexion
[OK] Gérer la scalabilité avec Redis backplane
[OK] Surveiller les métriques de performance
[OK] Éviter les pièges courants de performance
*/


// ----------------------------------------------------------------------------
// [RESEAU] SIGNALR ET BLAZOR SERVER — Comprendre la mécanique
// ----------------------------------------------------------------------------

/*
BLAZOR SERVER — COMMENT ÇA MARCHE ?

Navigateur                         Serveur ASP.NET Core
┌──────────────────────┐          ┌────────────────────────────┐
│  Blazor.js (minimal) │[BLACK_LEFT-POINTING_POINTER]────────[BLACK_RIGHT-POINTING_POINTER]│  Hub SignalR               │
│  DOM rendering       │          │  ┌──────────────────────┐  │
│                      │          │  │ Circuit (par client) │  │
│  -> Envoie events     │          │  │  - État composants   │  │
│  <- Reçoit diffs DOM  │          │  │  - Services Scoped   │  │
│                      │          │  │  - Composants .razor │  │
└──────────────────────┘          │  └──────────────────────┘  │
                                  └────────────────────────────┘

CIRCUIT = Instance par client connecté
-> Chaque utilisateur = 1 circuit = ~250KB de RAM (minimum)
-> 1000 utilisateurs = ~250MB RAM pour les circuits seuls !

CONSÉQUENCES :
[OK] Exécution C# côté serveur (accès direct BDD, fichiers, etc.)
[OK] Pas de bundle WASM volumineux
[X] Mémoire par utilisateur (scalabilité limitée)
[X] Latence réseau pour chaque interaction
[X] Connexion permanente requise
*/


// ----------------------------------------------------------------------------
// [RAPIDE] OPTIMISER LES RE-RENDUS — ShouldRender avancé
// ----------------------------------------------------------------------------

/*
PROBLÈME : Blazor Server re-rend les composants très fréquemment.
Chaque StateHasChanged() = calcul du VirtualDOM différentiel + SignalR message.

OBJECTIF : Réduire au minimum les re-rendus inutiles.
*/

// Composant optimisé avec ShouldRender
/*
@code {
    [Parameter] public Produit Produit { get; set; } = default!;

    private Produit? _dernierProduit;

    // ShouldRender retourne false -> Le composant NE SE RE-REND PAS
    // Économie : zéro diff DOM, zéro message SignalR
    protected override bool ShouldRender()
    {
        // Ne re-rendre que si le produit a réellement changé
        if (_dernierProduit == Produit) return false;

        // Comparaison par valeurs (si Produit est un record)
        if (_dernierProduit?.Id == Produit.Id &&
            _dernierProduit?.Nom == Produit.Nom &&
            _dernierProduit?.Prix == Produit.Prix)
        {
            return false;
        }

        _dernierProduit = Produit;
        return true;
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Pattern : Composant pur (Équivalent React.memo / PureComponent)
─────────────────────────────────────────────────────────────────

Un composant "pur" ne re-rend que si ses paramètres changent.
*/

// Classe de base pour composants purs
public abstract class ComposantPur<TParams> : ComponentBase
    where TParams : IEquatable<TParams>
{
    [Parameter] public TParams? Props { get; set; }

    private TParams? _dernierProps;

    protected override bool ShouldRender()
    {
        if (_dernierProps is null && Props is null) return false;
        if (_dernierProps is null || Props is null) return true;
        if (_dernierProps.Equals(Props)) return false;

        _dernierProps = Props;
        return true;
    }
}

// Utilisation : Mon composant hérite de ComposantPur
// public class MonComposant : ComposantPur<MonComposantProps> { ... }
// public record MonComposantProps(string Titre, int Valeur) : IEquatable<MonComposantProps>;


// ----------------------------------------------------------------------------
// [SYNC] STREAMING RENDER ET DEFER
// ----------------------------------------------------------------------------

/*
PROBLÈME :
Charger toutes les données avant d'afficher QUOI QUE CE SOIT
-> L'utilisateur attend... rien ne se passe.

SOLUTION 1 : Pattern manuel (afficher immédiatement, charger async)
SOLUTION 2 : Streaming Render (.NET 8+)
*/

/*
─────────────────────────────────────────────────────────────────
Pattern manuel : Afficher d'abord, charger ensuite
─────────────────────────────────────────────────────────────────

@page "/dashboard"

<!-- Afficher immédiatement la structure de la page -->
<div class="dashboard">
    <h1>Dashboard</h1>

    <!-- Stats : Afficher skeleton pendant chargement -->
    @if (_statsChargees)
    {
        <DashboardStats Stats="@_stats" />
    }
    else
    {
        <DashboardStatsSkeleton />
    }

    <!-- Tableau : Charger indépendamment des stats -->
    @if (_tableauCharge)
    {
        <TableauCommandes Commandes="@_commandes" />
    }
    else
    {
        <MudProgressLinear Color="Color.Primary" Indeterminate="true" />
    }
</div>

@code {
    private bool _statsChargees = false;
    private bool _tableauCharge = false;
    private DashboardStats? _stats;
    private List<Commande> _commandes = new();

    protected override async Task OnInitializedAsync()
    {
        // Charger en PARALLÈLE, mais mettre à jour séparément !
        var tacheStats = ChargerStats();
        var tacheTableau = ChargerTableau();

        // Attendre les deux mais traiter chaque résultat dès qu'il arrive
        await Task.WhenAll(tacheStats, tacheTableau);
    }

    private async Task ChargerStats()
    {
        // Simuler chargement rapide (200ms)
        await Task.Delay(200);
        _stats = new DashboardStats();
        _statsChargees = true;
        await InvokeAsync(StateHasChanged); // Mettre à jour immédiatement
    }

    private async Task ChargerTableau()
    {
        // Tableau plus lent (1s)
        await Task.Delay(1000);
        _commandes = new List<Commande>();
        _tableauCharge = true;
        await InvokeAsync(StateHasChanged);
    }
}
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] MÉMOIRE ET CIRCUITS — Optimisation serveur
// ----------------------------------------------------------------------------

/*
CIRCUIT BLAZOR SERVER : Ce qui consomme de la mémoire

1. L'arbre de composants rendu (VirtualDOM)
2. Les services Scoped (un par circuit)
3. Les subscriptions aux events (sources de fuites mémoire !)
4. Les states et données des composants

RÈGLES D'OR POUR ÉVITER LES FUITES MÉMOIRE :
*/

// [X] FUITE MÉMOIRE : Event non désabonné
/*
public class ComposantQuiFuitMemoire : ComponentBase
{
    [Inject] public MonService MonService { get; set; } = default!;

    protected override void OnInitialized()
    {
        // [X] S'abonner SANS se désabonner dans Dispose -> FUITE MÉMOIRE !
        // Le circuit reste en mémoire même si le composant est retiré
        MonService.OnDonneesMisesAJour += MettreAJourUI;
    }
    // PAS de Dispose -> MonService garde une référence -> Mémoire jamais libérée
}
*/

// [OK] PAS DE FUITE : IDisposable correctement implémenté
/*
@implements IDisposable

@code {
    [Inject] public MonService MonService { get; set; } = default!;

    protected override void OnInitialized()
    {
        // [OK] S'abonner
        MonService.OnDonneesMisesAJour += MettreAJourUI;
    }

    private void MettreAJourUI()
    {
        InvokeAsync(StateHasChanged); // Thread-safe !
    }

    // [OK] TOUJOURS se désabonner !
    public void Dispose()
    {
        MonService.OnDonneesMisesAJour -= MettreAJourUI;
    }
}
*/

// Configuration des timeouts des circuits dans Program.cs
/*
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents(options =>
    {
        // Temps avant de déconnecter un circuit inactif
        options.DisconnectedCircuitMaxRetained = 100;     // Max circuits déconnectés en mémoire
        options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3); // Délai
        options.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds(60);
        options.MaxBufferedUnacknowledgedRenderBatches = 10;
    });

// Configuration SignalR
builder.Services.AddSignalR(options =>
{
    options.MaximumReceiveMessageSize = 32 * 1024; // 32KB max par message
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});
*/


// ----------------------------------------------------------------------------
// [GRAPHIQUE] SCALABILITÉ — Redis Backplane pour multi-serveurs
// ----------------------------------------------------------------------------

/*
PROBLÈME DE SCALABILITÉ :

Serveur 1: Client A, Client B, Client C
Serveur 2: Client D, Client E

Si Client A veut envoyer un message à Client D :
-> Serveur 1 ne connaît pas Client D !
-> Les deux serveurs sont isolés.

SOLUTION : Redis Backplane
-> Message broker partagé entre tous les serveurs
-> Tous les serveurs "se parlent" via Redis

ARCHITECTURE :

Serveur 1 ──────────────────┐
             [BLACK_RIGHT-POINTING_POINTER] Redis Cluster [BLACK_LEFT-POINTING_POINTER] Tous les circuits
Serveur 2 ──────────────────┘
Serveur 3 ──────────────────┘

INSTALLATION :
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
*/

/*
Program.cs avec Redis :

builder.Services.AddSignalR()
    .AddStackExchangeRedis("localhost:6379,abortConnect=false", options =>
    {
        options.Configuration.ChannelPrefix = RedisChannel.Literal("MonApp_");
    });

// Ou avec Azure SignalR Service (encore mieux pour Azure)
builder.Services.AddSignalR()
    .AddAzureSignalR(builder.Configuration["Azure:SignalR:ConnectionString"]);
*/

/*
CONFIGURATION KUBERNETES / LOAD BALANCER :

Pour Blazor Server derrière un load balancer, OBLIGATOIRE :
-> Sticky Sessions (affinity) : Un client doit toujours aller au même serveur

Nginx config :
  upstream blazor_app {
      ip_hash;  <- Sticky sessions !
      server server1:5000;
      server server2:5000;
  }

Ou dans Azure App Service :
  ARR Affinity : ON (activé par défaut)
*/


// ----------------------------------------------------------------------------
// [HAUSSE] PROFILING ET MONITORING
// ----------------------------------------------------------------------------

/*
OUTILS DE MONITORING POUR BLAZOR SERVER :

1. DOTNET COUNTERS — Métriques .NET en temps réel
   dotnet-counters monitor --process-id <PID> --counters Microsoft.AspNetCore.Components

   Métriques utiles :
   -> blazor-server-active-circuits      : Circuits actifs
   -> blazor-server-disconnected-circuits: Circuits déconnectés
   -> blazor-server-total-circuits       : Total circuits créés

2. APPLICATION INSIGHTS — Monitoring complet
   dotnet add package Microsoft.ApplicationInsights.AspNetCore

3. DOTNET TRACE — Profiling fin
   dotnet trace collect --process-id <PID>

4. MEMORY DUMP — Analyser les fuites mémoire
   dotnet dump collect --process-id <PID>
   dotnet dump analyze <dump_file>
*/

// Middleware de monitoring personnalisé
public class CircuitMonitoringMiddleware : CircuitHandler
{
    private readonly ILogger<CircuitMonitoringMiddleware> _logger;
    private static int _circuitsActifs = 0;

    public CircuitMonitoringMiddleware(ILogger<CircuitMonitoringMiddleware> logger)
    {
        _logger = logger;
    }

    public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken ct)
    {
        Interlocked.Increment(ref _circuitsActifs);
        _logger.LogInformation(
            "Circuit ouvert: {CircuitId}. Circuits actifs: {Total}",
            circuit.Id,
            _circuitsActifs);
        return Task.CompletedTask;
    }

    public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken ct)
    {
        Interlocked.Decrement(ref _circuitsActifs);
        _logger.LogInformation(
            "Circuit fermé: {CircuitId}. Circuits actifs: {Total}",
            circuit.Id,
            _circuitsActifs);
        return Task.CompletedTask;
    }

    public static int CircuitsActifs => _circuitsActifs;
}

// Interface (simplifiée)
public abstract class CircuitHandler
{
    public virtual Task OnConnectionUpAsync(Circuit circuit, CancellationToken ct)
        => Task.CompletedTask;
    public virtual Task OnConnectionDownAsync(Circuit circuit, CancellationToken ct)
        => Task.CompletedTask;
}

public class Circuit { public string Id { get; set; } = string.Empty; }


// ============================================================================
// [GUIDE] CHAPITRE 19 : PERFORMANCE WEBASSEMBLY
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Mesurer et optimiser la taille du bundle
[OK] Implémenter le Lazy Loading des assemblies
[OK] Utiliser la compilation AOT (Ahead-of-Time)
[OK] Optimiser le temps de chargement initial
[OK] Implémenter le caching optimal
[OK] Mesurer les performances avec Lighthouse
*/


// ----------------------------------------------------------------------------
// [PACKAGE] TAILLE DU BUNDLE — Problème critique de WebAssembly
// ----------------------------------------------------------------------------

/*
BLAZOR WEBASSEMBLY = Téléchargement du runtime .NET dans le navigateur !

TAILLE TYPIQUE (non compressé) :
-> Runtime .NET WASM :    ~8-10 MB
-> Vos assemblies :       ~2-5 MB
-> Bibliothèques NuGet :  ~2-10 MB
TOTAL :                  ~12-25 MB !

AVEC COMPRESSION BROTLI :
-> Typiquement réduit à ~3-6 MB

COMMENT MESURER :
dotnet publish -c Release
-> Regarder wwwroot/_framework/ pour voir les fichiers

STRATÉGIES POUR RÉDUIRE :
1. Lazy Loading (charger les assemblies à la demande)
2. AOT Compilation (éliminer le runtime interpréteur)
3. IL Trimming (supprimer le code mort)
4. Compression serveur (Brotli / gzip)
*/

/*
─────────────────────────────────────────────────────────────────
Optimisations dans le fichier .csproj
─────────────────────────────────────────────────────────────────
*/

/*
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <!-- ─── OPTIMISATIONS PERFORMANCE ─────────────────────── -->

    <!-- 1. IL Trimming : Supprimer le code mort -->
    <!-- Réduit la taille du bundle de 30-50% -->
    <PublishTrimmed>true</PublishTrimmed>
    <TrimMode>full</TrimMode>

    <!-- 2. AOT Compilation : Compiler en code natif WebAssembly -->
    <!-- Améliore les performances d'exécution MAIS augmente la taille initiale -->
    <!-- À n'activer que si les performances d'exécution sont insuffisantes -->
    <RunAOTCompilation>true</RunAOTCompilation>

    <!-- 3. Réduire la taille du runtime -->
    <WasmStripILAfterAOT>true</WasmStripILAfterAOT>

    <!-- 4. Compression Brotli automatique à la publication -->
    <BlazorEnableCompression>true</BlazorEnableCompression>

  </PropertyGroup>

  <!-- ─── BIBLIOTHÈQUES LAZY LOADED ──────────────────────── -->
  <!-- Ces assemblies ne seront pas chargées au démarrage -->
  <ItemGroup>
    <BlazorWebAssemblyLazyLoad Include="MonApp.Rapports.dll" />
    <BlazorWebAssemblyLazyLoad Include="MonApp.Admin.dll" />
    <BlazorWebAssemblyLazyLoad Include="ChartJs.Blazor.dll" />
    <BlazorWebAssemblyLazyLoad Include="MudBlazor.dll" />
  </ItemGroup>

</Project>
*/


// ----------------------------------------------------------------------------
// [SLOTH] LAZY LOADING DES ASSEMBLIES
// ----------------------------------------------------------------------------

/*
LAZY LOADING = Charger les DLL seulement quand on en a besoin.

EXEMPLE :
- L'utilisateur arrive sur /home -> Charger SEULEMENT l'assembly principale
- L'utilisateur navigue vers /admin -> Charger l'assembly Admin
- L'utilisateur va sur /rapports -> Charger l'assembly Rapports

RÉSULTAT :
-> Chargement initial BEAUCOUP plus rapide
-> Assemblies lourdes chargées à la demande
*/

/*
─────────────────────────────────────────────────────────────────
Configuration Lazy Loading dans App.razor
─────────────────────────────────────────────────────────────────

@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="@typeof(App).Assembly"
        AdditionalAssemblies="@_assemblyChargees"
        OnNavigateAsync="@ChargerAssemblyPourRoute">

    <Found Context="routeData">
        @if (_chargement)
        {
            <!-- Écran de chargement pendant le lazy loading -->
            <div class="loading-overlay d-flex flex-column
                        align-items-center justify-content-center vh-100">
                <div class="spinner-border text-primary mb-3"></div>
                <p>Chargement du module...</p>
            </div>
        }
        else
        {
            <RouteView RouteData="@routeData"
                       DefaultLayout="@typeof(MainLayout)" />
        }
    </Found>

    <NotFound>
        <p>Page introuvable</p>
    </NotFound>

</Router>

@code {
    private List<Assembly> _assemblyChargees = new();
    private bool _chargement = false;

    // Dictionnaire : Route -> Assembly à charger
    private static readonly Dictionary<string, string[]> _assemblyParRoute = new()
    {
        ["/admin"]    = new[] { "MonApp.Admin.dll" },
        ["/rapports"] = new[] { "MonApp.Rapports.dll", "ChartJs.Blazor.dll" },
        ["/import"]   = new[] { "MonApp.Import.dll", "CsvHelper.dll" },
    };

    private async Task ChargerAssemblyPourRoute(NavigationContext context)
    {
        // Trouver les assemblies requises pour cette route
        var route = "/" + context.Path.Split('/')[1]; // Extraire le premier segment

        if (_assemblyParRoute.TryGetValue(route, out var assemblies))
        {
            _chargement = true;
            StateHasChanged();

            try
            {
                // Charger les assemblies en parallèle
                var tachesChargement = assemblies
                    .Where(a => !_assemblyChargees.Any(c => c.GetName().Name + ".dll" == a))
                    .Select(a => AssemblyLoader.LoadAssembliesAsync(new[] { a }));

                var resultats = await Task.WhenAll(tachesChargement);

                // Ajouter les assemblies chargées
                foreach (var assembliesChargees in resultats)
                {
                    _assemblyChargees.AddRange(assembliesChargees);
                }
            }
            finally
            {
                _chargement = false;
                StateHasChanged();
            }
        }
    }
}
*/


// ----------------------------------------------------------------------------
// [RACING_CAR] OPTIMISATION DU TEMPS DE CHARGEMENT
// ----------------------------------------------------------------------------

/*
STRATÉGIES POUR ACCÉLÉRER LE CHARGEMENT INITIAL :

1. SERVICE WORKER + PWA CACHING
-> Les fichiers WASM sont mis en cache après la 1ère visite
-> Visites suivantes : chargement depuis le cache (instantané !)

2. LOADING SCREEN PERSONNALISÉ
-> Afficher une belle page de chargement pendant le téléchargement WASM
-> Améliore la perception de performance

3. COMPRESSION SERVEUR OPTIMALE
-> Brotli > gzip (30% mieux que gzip)
-> À configurer côté serveur

4. CDN POUR LES FICHIERS STATIQUES
-> Servir les fichiers .dll et .wasm depuis un CDN
-> Réduit la latence de téléchargement
*/

/*
─────────────────────────────────────────────────────────────────
index.html optimisé avec loading screen
─────────────────────────────────────────────────────────────────

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MonApp</title>
    <base href="/" />

    <!-- Préchargement des polices (évite le Flash Of Unstyled Text) -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preload" as="style"
          href="https://fonts.googleapis.com/css?family=Inter:400,500,600,700">
    <link rel="stylesheet"
          href="https://fonts.googleapis.com/css?family=Inter:400,500,600,700">

    <link rel="stylesheet" href="css/app.css" />
    <link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
    <link rel="stylesheet" href="MonApp.styles.css" />

    <!-- PWA Manifest -->
    <link rel="manifest" href="manifest.webmanifest" />
    <meta name="theme-color" content="#3b82f6" />

    <!-- Styles de la loading screen (inline pour vitesse) -->
    <style>
        .loading-screen {
            position: fixed; inset: 0;
            display: flex; flex-direction: column;
            align-items: center; justify-content: center;
            background: #f8fafc;
            font-family: 'Inter', sans-serif;
            z-index: 9999;
            transition: opacity 0.3s ease;
        }
        .loading-logo { font-size: 3rem; margin-bottom: 1rem; }
        .loading-titre { font-size: 1.5rem; font-weight: 600;
                        color: #1e293b; margin-bottom: 0.5rem; }
        .loading-sous-titre { color: #64748b; margin-bottom: 2rem; }
        .loading-barre {
            width: 200px; height: 4px;
            background: #e2e8f0;
            border-radius: 2px; overflow: hidden;
        }
        .loading-barre-fill {
            height: 100%; width: 0;
            background: #3b82f6;
            border-radius: 2px;
            animation: progressBar 3s ease-in-out forwards;
        }
        @keyframes progressBar {
            0%   { width: 0; }
            50%  { width: 70%; }
            90%  { width: 90%; }
            100% { width: 95%; }
        }
        .loading-version { position: absolute; bottom: 1rem;
                          color: #94a3b8; font-size: 0.75rem; }
    </style>
</head>
<body>
    <!-- Element racine Blazor -->
    <div id="app">
        <!-- Loading screen visible pendant le téléchargement WASM -->
        <div class="loading-screen" id="loading">
            <div class="loading-logo">[RAPIDE]</div>
            <div class="loading-titre">MonApp</div>
            <div class="loading-sous-titre">Chargement en cours...</div>
            <div class="loading-barre">
                <div class="loading-barre-fill"></div>
            </div>
            <div class="loading-version">v2.4.1</div>
        </div>
    </div>

    <div id="blazor-error-ui">
        Une erreur inattendue s'est produite.
        <a href="">Recharger</a>
    </div>

    <script src="_content/MudBlazor/MudBlazor.min.js"></script>
    <script src="_framework/blazor.webassembly.js" autostart="false"></script>
    <script>
        // Démarrer Blazor et cacher le loading screen
        Blazor.start().then(() => {
            const loading = document.getElementById('loading');
            if (loading) {
                loading.style.opacity = '0';
                setTimeout(() => loading.remove(), 300);
            }
        });
    </script>

    <!-- Service Worker pour PWA (voir chapitre 30) -->
    <script>
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('service-worker.js');
        }
    </script>
</body>
</html>
*/


// ----------------------------------------------------------------------------
// [SPOOL_OF_THREAD] OPTIMISATION DES PERFORMANCES D'EXÉCUTION WASM
// ----------------------------------------------------------------------------

/*
VIRTUALIZATION — Afficher seulement les éléments visibles

PROBLÈME : Afficher 10 000 lignes dans un tableau = DOM énorme = lent !
SOLUTION : Virtualisation = afficher seulement les lignes visibles

Blazor inclut <Virtualize> depuis .NET 5 !
*/

/*
─────────────────────────────────────────────────────────────────
Virtualize : Listes de 100 000 éléments sans lag
─────────────────────────────────────────────────────────────────

@page "/liste-virtualisee"

<h1>Liste de @_totalItems éléments</h1>

<!-- SANS virtualisation : 100 000 DOM nodes = freeze -->
<!-- AVEC Virtualize : seulement ~20 DOM nodes visibles -->
<div style="height: 500px; overflow-y: auto;">
    <Virtualize Items="@_tousLesItems"
                Context="item"
                ItemSize="60"
                OverscanCount="5">

        <!-- Template de chaque ligne -->
        <div class="item-ligne d-flex align-items-center gap-3 px-3 py-2 border-bottom">
            <div class="avatar">@item.Nom[0]</div>
            <div>
                <div class="fw-semibold">@item.Nom</div>
                <div class="text-muted small">@item.Email</div>
            </div>
            <div class="ms-auto text-muted small">
                @item.DateInscription.ToString("dd/MM/yyyy")
            </div>
        </div>

        <!-- Placeholder pendant le scroll rapide -->
        <Placeholder>
            <div class="item-ligne d-flex align-items-center gap-3 px-3 py-2 border-bottom">
                <div class="skeleton" style="width: 40px; height: 40px; border-radius: 50%;"></div>
                <div>
                    <div class="skeleton skeleton-text" style="width: 150px;"></div>
                    <div class="skeleton skeleton-text" style="width: 200px;"></div>
                </div>
            </div>
        </Placeholder>

    </Virtualize>
</div>

@code {
    private List<Utilisateur> _tousLesItems = new();
    private int _totalItems = 100_000;

    protected override void OnInitialized()
    {
        // Générer 100 000 items en mémoire
        _tousLesItems = Enumerable.Range(1, _totalItems)
            .Select(i => new Utilisateur
            {
                Id = i,
                Nom = $"Utilisateur {i}",
                Email = $"user{i}@example.com",
                DateInscription = DateTime.Now.AddDays(-i)
            })
            .ToList();
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Virtualize avec chargement serveur (Items Provider)
─────────────────────────────────────────────────────────────────
Pour les très grandes listes où on ne peut pas tout charger en mémoire.

@page "/liste-serveur"

<div style="height: 600px; overflow-y: auto;">
    <Virtualize Context="item"
                ItemsProvider="ChargerItemsServeur"
                ItemSize="72"
                OverscanCount="3">
        <ItemContent>
            <div class="item-row">
                <strong>@item.Nom</strong>
                <span>@item.Prix.ToString("C")</span>
            </div>
        </ItemContent>
        <Placeholder>
            <div class="item-row skeleton-row">
                <div class="skeleton" style="width: 200px; height: 1em;"></div>
            </div>
        </Placeholder>
    </Virtualize>
</div>

@code {
    [Inject] public IProduitApi ProduitApi { get; set; } = default!;

    // Appelé automatiquement avec les paramètres de pagination
    private async ValueTask<ItemsProviderResult<Produit>> ChargerItemsServeur(
        ItemsProviderRequest requete)
    {
        // requete.StartIndex : Index de départ
        // requete.Count : Nombre d'items à charger
        // requete.CancellationToken : Annulation si scroll trop rapide

        var resultat = await ProduitApi.ObtenirPagineAsync(
            page: requete.StartIndex / requete.Count + 1,
            parPage: requete.Count,
            ct: requete.CancellationToken);

        return new ItemsProviderResult<Produit>(
            resultat.Items,   // Les items pour cette page
            resultat.Total);  // Total pour calculer la scrollbar
    }
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 20 : SEO & PRERENDERING
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le problème SEO des SPAs
[OK] Implémenter le prerendering statique
[OK] Utiliser le Streaming Rendering (.NET 8)
[OK] Gérer les balises <head> dynamiquement
[OK] Configurer un sitemap et robots.txt
*/


// ----------------------------------------------------------------------------
// [RECHERCHE] SEO AVEC BLAZOR — Le défi des SPAs
// ----------------------------------------------------------------------------

/*
PROBLÈME SEO CLASSIQUE DES SPAs :

Les moteurs de recherche reçoivent :
  <div id="app">Chargement...</div>

Ils ne voient PAS le contenu rendu par JavaScript !
-> Aucun contenu à indexer -> Mauvais référencement

SOLUTIONS BLAZOR :

1. PRERENDERING STATIQUE (.NET 8)
   -> HTML statique généré à la compilation
   -> Parfait pour le SEO + performance initiale

2. SERVER-SIDE RENDERING (SSR) + Blazor Server
   -> Chaque page générée côté serveur
   -> HTML complet envoyé au navigateur

3. BLAZOR WEBASSEMBLY AVEC PRERENDERING HÉBERGÉ
   -> Serveur génère le HTML initial
   -> WebAssembly prend le relais (hydratation)
*/


// ----------------------------------------------------------------------------
// [FICHIER] BALISES HEAD DYNAMIQUES
// ----------------------------------------------------------------------------

/*
CHAQUE PAGE DOIT AVOIR SON PROPRE :
-> <title> (titre dans les onglets et résultats Google)
-> meta description (description dans les résultats Google)
-> meta og:* (pour le partage sur réseaux sociaux)
-> link canonical (éviter le contenu dupliqué)
*/

/*
─────────────────────────────────────────────────────────────────
Composant SeoHead.razor — Balises SEO réutilisables
─────────────────────────────────────────────────────────────────

<PageTitle>@Titre - MonApp</PageTitle>
<HeadContent>
    <!-- Meta de base -->
    <meta name="description" content="@Description" />
    <meta name="keywords" content="@Mots_cles" />
    <meta name="author" content="MonApp" />

    <!-- Canonical URL (évite le contenu dupliqué) -->
    <link rel="canonical" href="@UrlCanonique" />

    <!-- Open Graph (Facebook, LinkedIn, etc.) -->
    <meta property="og:title" content="@Titre" />
    <meta property="og:description" content="@Description" />
    <meta property="og:image" content="@(ImageUrl ?? "/img/og-default.jpg")" />
    <meta property="og:url" content="@UrlCanonique" />
    <meta property="og:type" content="@TypeOG" />
    <meta property="og:site_name" content="MonApp" />
    <meta property="og:locale" content="fr_FR" />

    <!-- Twitter Card -->
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="@Titre" />
    <meta name="twitter:description" content="@Description" />
    <meta name="twitter:image" content="@(ImageUrl ?? "/img/og-default.jpg")" />

    <!-- JSON-LD Schema.org (données structurées pour Google) -->
    @if (SchemaJson is not null)
    {
        <script type="application/ld+json">@((MarkupString)SchemaJson)</script>
    }
</HeadContent>

@code {
    [Parameter] public string Titre { get; set; } = "MonApp";
    [Parameter] public string Description { get; set; } = "Description par défaut";
    [Parameter] public string Mots_cles { get; set; } = "blazor, dotnet, app";
    [Parameter] public string? ImageUrl { get; set; }
    [Parameter] public string? UrlCanonique { get; set; }
    [Parameter] public string TypeOG { get; set; } = "website";
    [Parameter] public string? SchemaJson { get; set; }

    [Inject] private NavigationManager NavManager { get; set; } = default!;

    protected override void OnInitialized()
    {
        UrlCanonique ??= NavManager.Uri;
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
UTILISATION dans une page produit
─────────────────────────────────────────────────────────────────

@page "/produits/{Slug}"
@inject IProduitService ProduitService

@if (_produit is not null)
{
    <!-- SEO optimisé par produit -->
    <SeoHead Titre="@_produit.Nom"
             Description="@($"Achetez {_produit.Nom} à {_produit.Prix:C}. {_produit.Description}")"
             Mots_cles="@($"{_produit.Nom}, {_produit.Categorie}, acheter en ligne")"
             ImageUrl="@_produit.ImageUrl"
             TypeOG="product"
             SchemaJson="@GenererSchemaJson()" />

    <!-- Contenu de la page -->
    <h1>@_produit.Nom</h1>
    <p>@_produit.Prix.ToString("C")</p>
}

@code {
    [Parameter] public string Slug { get; set; } = string.Empty;
    private Produit? _produit;

    protected override async Task OnParametersSetAsync()
    {
        _produit = await ProduitService.ObtenirParSlugAsync(Slug);
    }

    private string GenererSchemaJson()
    {
        if (_produit is null) return "{}";

        // JSON-LD Schema.org Product pour Google Shopping
        return System.Text.Json.JsonSerializer.Serialize(new
        {
            @context = "https://schema.org",
            @type = "Product",
            name = _produit.Nom,
            description = _produit.Description,
            image = _produit.ImageUrl,
            offers = new
            {
                @type = "Offer",
                price = _produit.Prix.ToString("F2"),
                priceCurrency = "EUR",
                availability = _produit.Stock > 0
                    ? "https://schema.org/InStock"
                    : "https://schema.org/OutOfStock"
            }
        }, new System.Text.Json.JsonSerializerOptions
        {
            WriteIndented = true
        });
    }
}
*/


// ----------------------------------------------------------------------------
// [RAPIDE] PRERENDERING BLAZOR (.NET 8)
// ----------------------------------------------------------------------------

/*
BLAZOR .NET 8 — Nouveaux modes de rendu :

1. Static SSR        -> HTML pur, pas d'interactivité
2. Interactive Server-> Blazor Server (SignalR)
3. Interactive WASM  -> Blazor WebAssembly
4. Interactive Auto  -> SSR d'abord, puis WASM (meilleur des deux !)

NOUVEAUTÉ .NET 8 : On peut MÉLANGER les modes par composant !

App.razor pour .NET 8 :
<Routes @rendermode="InteractiveAuto" />

Forcer le mode pour un composant spécifique :
@rendermode InteractiveServer
@rendermode InteractiveWebAssembly
@rendermode @(new InteractiveServerRenderMode(prerender: true))
*/

/*
─────────────────────────────────────────────────────────────────
Streaming Rendering — Afficher progressivement (.NET 8)
─────────────────────────────────────────────────────────────────

STREAMING RENDERING = Envoyer le HTML AU FUR ET À MESURE
-> Le navigateur peut afficher le skeleton IMMÉDIATEMENT
-> Le vrai contenu arrive ensuite (sans rechargement complet !)

@page "/produits"
@attribute [StreamRendering]  <- ACTIVER LE STREAMING

<h1>Catalogue Produits</h1>

@if (_produits is null)
{
    <!-- Affiché IMMÉDIATEMENT pendant le chargement -->
    <p>[HOURGLASS_WITH_FLOWING_SAND] Chargement du catalogue...</p>
    @for (int i = 0; i < 6; i++)
    {
        <div class="skeleton-card mb-3">
            <div class="skeleton skeleton-image"></div>
            <div class="skeleton skeleton-title mt-2"></div>
        </div>
    }
}
else if (!_produits.Any())
{
    <p>Aucun produit disponible.</p>
}
else
{
    <!-- Remplace le skeleton quand les données arrivent -->
    @foreach (var produit in _produits)
    {
        <CarteProduit Produit="produit" />
    }
}

@code {
    private List<Produit>? _produits; // null = pas encore chargé

    protected override async Task OnInitializedAsync()
    {
        // Le skeleton s'affiche AVANT cette ligne
        await Task.Delay(100); // Laisser le temps au streaming de commencer

        // Charger les données (depuis BDD ou API)
        _produits = await ProduitService.ObtenirTousAsync();
        // Le contenu réel remplace automatiquement le skeleton
    }
}
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE — PARTIE 6
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
EXERCICE : OPTIMISATION PERFORMANCE COMPLÈTE
═══════════════════════════════════════════════════════════════

OBJECTIF : Optimiser une application existante

TÂCHES :

1. PERFORMANCE WASM :
   a) Mesurer la taille du bundle initial (dotnet publish -c Release)
   b) Activer IL Trimming dans le .csproj
   c) Ajouter un loading screen HTML/CSS dans index.html
   d) Mesurer le gain de taille après Trimming

2. VIRTUALIZE :
   a) Créer une liste de 50 000 éléments simulés
   b) Mesurer FPS avant virtualisation (DevTools Performance)
   c) Implémenter <Virtualize> avec ItemSize et Placeholder
   d) Mesurer FPS après virtualisation

3. SHOULDRENDER :
   a) Identifier un composant qui se re-rend souvent
   b) Ajouter logging dans ShouldRender pour compter
   c) Implémenter la logique d'évitement du re-rendu
   d) Mesurer la réduction des re-rendus

4. SEO :
   a) Créer le composant SeoHead.razor
   b) L'utiliser sur 3 pages (Home, Produits, Détail Produit)
   c) Vérifier avec l'outil SEO de Chrome DevTools
   d) Ajouter un sitemap.xml dans wwwroot/

MÉTRIQUES À MESURER :
-> Taille du bundle avant/après Trimming
-> FPS avant/après Virtualize
-> Nombre de re-rendus avant/après ShouldRender
-> Score Lighthouse avant/après SEO
═══════════════════════════════════════════════════════════════
*/


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 6

[OK] CHAPITRE 18 - PERFORMANCE BLAZOR SERVER :
- Architecture SignalR : un circuit par utilisateur (~250KB RAM)
- ShouldRender() avancé pour éviter les re-rendus inutiles
- Pattern ComposantPur<TParams> : IEquatable pour comparaison
- Chargement progressif avec Task.WhenAll + InvokeAsync
- IDisposable OBLIGATOIRE pour éviter les fuites mémoire
- Redis Backplane pour multi-serveurs (scalabilité horizontale)
- Sticky Sessions pour load balancer
- CircuitHandler pour monitoring des connexions
- dotnet-counters pour les métriques temps réel

[OK] CHAPITRE 19 - PERFORMANCE WEBASSEMBLY :
- Taille du bundle : ~12-25MB non compressé -> 3-6MB Brotli
- IL Trimming : -30 à 50% de taille (PublishTrimmed=true)
- AOT Compilation : meilleures perfs d'exécution, bundle plus grand
- BlazorWebAssemblyLazyLoad : charger les DLL à la demande
- App.razor avec LazyAssemblyLoader et OnNavigateAsync
- Loading screen HTML/CSS inline avant le démarrage WASM
- Virtualize : afficher 100K items sans lag (VirtualDOM limité)
- Items Provider : pagination serveur avec Virtualize
- Compression Brotli côté serveur

[OK] CHAPITRE 20 - SEO & PRERENDERING :
- PageTitle et HeadContent pour balises <head> dynamiques
- Composant SeoHead.razor avec og:*, twitter:card, JSON-LD
- Modes de rendu .NET 8 : Static SSR, Interactive Server/WASM/Auto
- StreamRendering : envoyer HTML au fur et à mesure
- Schema.org Product pour Google Shopping
- Sitemap.xml et robots.txt dans wwwroot

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 7
- Clean Architecture (Domain / Application / Infrastructure)
- CQRS avec MediatR
- Modularisation et Feature Folders
- Multi-tenant SaaS
═══════════════════════════════════════════════════════════════
*/


// ============================================================================
// [LIVRE] BLAZOR - PARTIE 7 : ARCHITECTURE & DESIGN PATTERNS
// ============================================================================
//
// CHAPITRE 21 : Clean Architecture
// CHAPITRE 22 : Modularisation
// CHAPITRE 23 : Multi-tenant SaaS
//
// [TEMPS] TEMPS ESTIMÉ : ~10-12 heures
// [DOCS] PRÉREQUIS : Parties 1-6 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 21 : CLEAN ARCHITECTURE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Structurer une solution en couches Clean Architecture
[OK] Implémenter CQRS avec MediatR
[OK] Séparer les responsabilités correctement
[OK] Utiliser le pattern Repository + Unit of Work
[OK] Implémenter les Domain Events
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] CLEAN ARCHITECTURE — Séparation des responsabilités
// ----------------------------------------------------------------------------

/*
CLEAN ARCHITECTURE = Organiser le code en COUCHES INDÉPENDANTES

PRINCIPE : Les dépendances ne pointent que vers l'INTÉRIEUR

        ┌─────────────────────────────────────────┐
        │              Infrastructure              │  <- Implémentations
        │   ┌─────────────────────────────────┐   │     (EF, API, fichiers)
        │   │           Application           │   │
        │   │   ┌─────────────────────────┐   │   │
        │   │   │         Domain          │   │   │  <- Logique métier pure
        │   │   │   (Entities, Rules,     │   │   │     (0 dépendances !)
        │   │   │    Domain Events)       │   │   │
        │   │   └─────────────────────────┘   │   │
        │   │   Commands / Queries / Handlers  │   │
        │   └─────────────────────────────────┘   │
        │         UI (Blazor / API / Console)      │
        └─────────────────────────────────────────┘

STRUCTURE DE LA SOLUTION :

MonApp.sln
├── MonApp.Domain/           <- Aucune dépendance externe !
│   ├── Entities/            <- Classes métier
│   ├── ValueObjects/        <- Types valeur immuables
│   ├── Enums/
│   ├── Events/              <- Domain Events
│   ├── Exceptions/          <- Exceptions métier
│   └── Interfaces/          <- Contrats (IRepository, etc.)
│
├── MonApp.Application/      <- Dépend de Domain seulement
│   ├── Commands/            <- CQRS : Écriture
│   ├── Queries/             <- CQRS : Lecture
│   ├── Handlers/            <- Traitement des commandes/requêtes
│   ├── DTOs/                <- Transfert de données
│   ├── Validators/          <- Validation (FluentValidation)
│   ├── Interfaces/          <- Services abstraits
│   └── Mappings/            <- Mappage Entity -> DTO
│
├── MonApp.Infrastructure/   <- Dépend de Application + Domain
│   ├── Persistence/
│   │   ├── AppDbContext.cs
│   │   ├── Configurations/ <- Config EF par entité
│   │   └── Repositories/   <- Implémentations
│   ├── Services/            <- Email, Storage, etc.
│   └── DependencyInjection.cs
│
├── MonApp.Client/           <- Blazor WebAssembly
│   ├── Pages/
│   ├── Components/
│   └── Services/            <- Appels API (HttpClient)
│
└── MonApp.Server/           <- ASP.NET Core (API)
    ├── Controllers/
    └── Endpoints/
*/


// ----------------------------------------------------------------------------
// [ENTREPRISE] DOMAIN — Entités et logique métier
// ----------------------------------------------------------------------------

// Domain/Entities/Produit.cs
public class ProduitDomain
{
    // Propriétés privées (encapsulation totale)
    public int Id { get; private set; }
    public string Nom { get; private set; } = string.Empty;
    public decimal Prix { get; private set; }
    public int Stock { get; private set; }
    public string Categorie { get; private set; } = string.Empty;
    public bool EstActif { get; private set; } = true;
    public DateTime DateCreation { get; private set; }
    public DateTime? DateModification { get; private set; }

    // Domain Events (changements importants)
    private readonly List<IDomainEvent> _domainEvents = new();
    public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    // Constructeur privé -> Forcer l'utilisation de la méthode factory
    private ProduitDomain() { }

    // Factory method -> Valide les données à la création
    public static ProduitDomain Creer(string nom, decimal prix, string categorie)
    {
        // Validation des règles métier
        if (string.IsNullOrWhiteSpace(nom))
            throw new DomainException("Le nom du produit est obligatoire");

        if (nom.Length < 2 || nom.Length > 100)
            throw new DomainException("Le nom doit contenir entre 2 et 100 caractères");

        if (prix <= 0)
            throw new DomainException("Le prix doit être positif");

        if (string.IsNullOrWhiteSpace(categorie))
            throw new DomainException("La catégorie est obligatoire");

        var produit = new ProduitDomain
        {
            Nom = nom.Trim(),
            Prix = prix,
            Categorie = categorie,
            DateCreation = DateTime.UtcNow,
            EstActif = true
        };

        // Ajouter un domain event
        produit._domainEvents.Add(new ProduitCreeDomainEvent(produit));

        return produit;
    }

    // Méthodes métier (comportements)
    public void ModifierPrix(decimal nouveauPrix, string raisonChangement)
    {
        if (nouveauPrix <= 0)
            throw new DomainException("Le prix doit être positif");

        var ancienPrix = Prix;
        Prix = nouveauPrix;
        DateModification = DateTime.UtcNow;

        _domainEvents.Add(new PrixModifieDomainEvent(this, ancienPrix, nouveauPrix, raisonChangement));
    }

    public void AjusterStock(int quantite)
    {
        if (Stock + quantite < 0)
            throw new DomainException($"Stock insuffisant. Disponible: {Stock}");

        Stock += quantite;
        DateModification = DateTime.UtcNow;

        if (Stock == 0)
            _domainEvents.Add(new StockEpuiseDomainEvent(this));
        else if (Stock <= 5)
            _domainEvents.Add(new StockFaibleDomainEvent(this));
    }

    public void Desactiver()
    {
        if (!EstActif) throw new DomainException("Produit déjà désactivé");
        EstActif = false;
        DateModification = DateTime.UtcNow;
    }

    public void ClearDomainEvents() => _domainEvents.Clear();
}

// Domain/Exceptions/DomainException.cs
public class DomainException : Exception
{
    public DomainException(string message) : base(message) { }
}

// Domain/Events/IDomainEvent.cs
public interface IDomainEvent { DateTime OccurredOn { get; } }

// Domain Events concrets
public record ProduitCreeDomainEvent(ProduitDomain Produit)
    : IDomainEvent { public DateTime OccurredOn => DateTime.UtcNow; }

public record PrixModifieDomainEvent(
    ProduitDomain Produit,
    decimal AncienPrix,
    decimal NouveauPrix,
    string Raison)
    : IDomainEvent { public DateTime OccurredOn => DateTime.UtcNow; }

public record StockEpuiseDomainEvent(ProduitDomain Produit)
    : IDomainEvent { public DateTime OccurredOn => DateTime.UtcNow; }

public record StockFaibleDomainEvent(ProduitDomain Produit)
    : IDomainEvent { public DateTime OccurredOn => DateTime.UtcNow; }

// Domain/Interfaces/IProduitRepository.cs
public interface IProduitDomainRepository
{
    Task<ProduitDomain?> ObtenirParIdAsync(int id, CancellationToken ct = default);
    Task<IReadOnlyList<ProduitDomain>> ObtenirTousAsync(CancellationToken ct = default);
    Task AjouterAsync(ProduitDomain produit, CancellationToken ct = default);
    void Modifier(ProduitDomain produit);
    void Supprimer(ProduitDomain produit);
    Task<bool> ExisteAsync(int id, CancellationToken ct = default);
    Task<bool> ExisteNomAsync(string nom, CancellationToken ct = default);
}

// Domain/Interfaces/IUnitOfWork.cs
public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken ct = default);
}


// ----------------------------------------------------------------------------
// [SYNC] CQRS AVEC MEDIATR
// ----------------------------------------------------------------------------

/*
CQRS = Command Query Responsibility Segregation

PRINCIPE : Séparer les opérations de LECTURE et d'ÉCRITURE

COMMAND -> Modifie l'état (Create, Update, Delete)
QUERY   -> Lit l'état (GetById, GetAll, Search)

AVANTAGES :
[OK] Lisibilité : Chaque classe a UNE responsabilité
[OK] Testabilité : Tester chaque Command/Query indépendamment
[OK] Scalabilité : Optimiser lecture et écriture séparément
[OK] Pipeline : Ajouter validation, logging, caching sur chaque opération

INSTALLATION MEDIATR :
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions
*/

// Application/Commands/Produits/CreerProduitCommand.cs
public record CreerProduitCommand(
    string Nom,
    decimal Prix,
    string Categorie,
    string? Description,
    int StockInitial
) : IRequest<CreerProduitResult>;

public record CreerProduitResult(int Id, string Nom, decimal Prix);

// Application/Commands/Produits/CreerProduitCommandHandler.cs
public class CreerProduitCommandHandler
    : IRequestHandler<CreerProduitCommand, CreerProduitResult>
{
    private readonly IProduitDomainRepository _repository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger<CreerProduitCommandHandler> _logger;

    public CreerProduitCommandHandler(
        IProduitDomainRepository repository,
        IUnitOfWork unitOfWork,
        ILogger<CreerProduitCommandHandler> logger)
    {
        _repository = repository;
        _unitOfWork = unitOfWork;
        _logger = logger;
    }

    public async Task<CreerProduitResult> Handle(
        CreerProduitCommand command,
        CancellationToken ct)
    {
        // 1. Vérifier que le nom n'existe pas déjà
        if (await _repository.ExisteNomAsync(command.Nom, ct))
            throw new DomainException($"Un produit avec le nom '{command.Nom}' existe déjà");

        // 2. Créer le produit via la factory (valide les règles métier)
        var produit = ProduitDomain.Creer(command.Nom, command.Prix, command.Categorie);

        // 3. Ajuster le stock initial
        if (command.StockInitial > 0)
            produit.AjusterStock(command.StockInitial);

        // 4. Sauvegarder
        await _repository.AjouterAsync(produit, ct);
        await _unitOfWork.SaveChangesAsync(ct);

        _logger.LogInformation(
            "Produit créé: {Nom} (Id: {Id})", produit.Nom, produit.Id);

        return new CreerProduitResult(produit.Id, produit.Nom, produit.Prix);
    }
}

// Application/Commands/Produits/CreerProduitCommandValidator.cs
// (FluentValidation)
public class CreerProduitCommandValidator : AbstractValidator<CreerProduitCommand>
{
    public CreerProduitCommandValidator()
    {
        RuleFor(c => c.Nom)
            .NotEmpty().WithMessage("Le nom est obligatoire")
            .Length(2, 100).WithMessage("Le nom doit faire entre 2 et 100 caractères")
            .Matches(@"^[a-zA-ZÀ-ÿ0-9\s\-_\.]+$")
                .WithMessage("Le nom contient des caractères non autorisés");

        RuleFor(c => c.Prix)
            .GreaterThan(0).WithMessage("Le prix doit être positif")
            .LessThanOrEqualTo(999999.99m).WithMessage("Prix trop élevé");

        RuleFor(c => c.Categorie)
            .NotEmpty().WithMessage("La catégorie est obligatoire");

        RuleFor(c => c.StockInitial)
            .GreaterThanOrEqualTo(0).WithMessage("Le stock ne peut pas être négatif");
    }
}

// Interfaces MediatR (simplifiées)
public interface IRequest<TResponse> { }
public interface IRequestHandler<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    Task<TResponse> Handle(TRequest request, CancellationToken ct);
}
public abstract class AbstractValidator<T>
{
    protected IRuleBuilderInitial<T, TProperty> RuleFor<TProperty>(
        Func<T, TProperty> propertyExpression) => default!;
}
public interface IRuleBuilderInitial<T, TProperty>
{
    IRuleBuilderInitial<T, TProperty> NotEmpty();
    IRuleBuilderInitial<T, TProperty> Length(int min, int max);
    IRuleBuilderInitial<T, TProperty> GreaterThan(decimal value);
    IRuleBuilderInitial<T, TProperty> GreaterThanOrEqualTo(int value);
    IRuleBuilderInitial<T, TProperty> LessThanOrEqualTo(decimal value);
    IRuleBuilderInitial<T, TProperty> Matches(string pattern);
    IRuleBuilderInitial<T, TProperty> WithMessage(string message);
}

// Application/Queries/Produits/ObtenirProduitsQuery.cs
public record ObtenirProduitsQuery(
    string? Terme = null,
    string? Categorie = null,
    int Page = 1,
    int ParPage = 20,
    string? Tri = "nom"
) : IRequest<PagedResultDto<ProduitDto>>;

public record ProduitDto(
    int Id,
    string Nom,
    decimal Prix,
    string Categorie,
    int Stock,
    bool EstActif,
    bool EnStock,
    DateTime DateCreation
);

// Application/Queries/Produits/ObtenirProduitsQueryHandler.cs
public class ObtenirProduitsQueryHandler
    : IRequestHandler<ObtenirProduitsQuery, PagedResultDto<ProduitDto>>
{
    // Note : Les queries peuvent utiliser DIRECTEMENT le DbContext
    // (pas besoin de passer par le Domain Repository)
    // C'est l'avantage de CQRS : les lectures peuvent être optimisées différemment
    private readonly AppDbContext _context;

    public ObtenirProduitsQueryHandler(AppDbContext context)
    {
        _context = context;
    }

    public async Task<PagedResultDto<ProduitDto>> Handle(
        ObtenirProduitsQuery query,
        CancellationToken ct)
    {
        var q = _context.Produits.AsQueryable();

        // Filtres
        if (!string.IsNullOrWhiteSpace(query.Terme))
            q = q.Where(p => p.Nom.Contains(query.Terme));

        if (!string.IsNullOrWhiteSpace(query.Categorie))
            q = q.Where(p => p.Categorie == query.Categorie);

        // Tri
        q = query.Tri switch
        {
            "prix_asc"   => q.OrderBy(p => p.Prix),
            "prix_desc"  => q.OrderByDescending(p => p.Prix),
            "date"       => q.OrderByDescending(p => p.DateCreation),
            _            => q.OrderBy(p => p.Nom)
        };

        // Compter le total (avant pagination)
        var total = await q.CountAsync(ct);

        // Pagination
        var items = await q
            .Skip((query.Page - 1) * query.ParPage)
            .Take(query.ParPage)
            .Select(p => new ProduitDto(
                p.Id, p.Nom, p.Prix, p.Categorie,
                p.Stock, p.EstActif, p.Stock > 0, p.DateCreation))
            .ToListAsync(ct);

        return new PagedResultDto<ProduitDto>
        {
            Items = items,
            Total = total,
            Page = query.Page,
            PerPage = query.ParPage
        };
    }
}

// Méthodes EF (simulées)
public static class QueryableExtensions2
{
    public static Task<int> CountAsync<T>(this IQueryable<T> source, CancellationToken ct = default)
        => Task.FromResult(0);
    public static Task<List<T>> ToListAsync<T>(this IQueryable<T> source, CancellationToken ct = default)
        => Task.FromResult(new List<T>());
    public static IQueryable<T> Where<T>(this IQueryable<T> source, Func<T, bool> predicate) => source;
    public static IQueryable<T> OrderBy<T, TKey>(this IQueryable<T> source, Func<T, TKey> keySelector) => source;
    public static IQueryable<T> OrderByDescending<T, TKey>(this IQueryable<T> source, Func<T, TKey> keySelector) => source;
    public static IQueryable<TResult> Select<T, TResult>(this IQueryable<T> source, Func<T, TResult> selector) => default!;
    public static IQueryable<T> Skip<T>(this IQueryable<T> source, int count) => source;
    public static IQueryable<T> Take<T>(this IQueryable<T> source, int count) => source;
}


// ----------------------------------------------------------------------------
// [PLUGIN] PIPELINE BEHAVIOURS MEDIATR
// ----------------------------------------------------------------------------

/*
PIPELINE BEHAVIOUR = Middleware pour MediatR
-> Exécuté AVANT et APRÈS chaque Handler

EXEMPLES DE BEHAVIOURS COURANTS :
- ValidationBehaviour    : Valider la commande avec FluentValidation
- LoggingBehaviour       : Logger les commandes/queries
- PerformanceBehaviour   : Alerter si Handler trop lent
- CachingBehaviour       : Mettre en cache les queries
- TransactionBehaviour   : Envelopper dans une transaction DB
*/

// Application/Behaviours/ValidationBehaviour.cs
public class ValidationBehaviour<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        if (!_validators.Any())
            return await next(); // Pas de validateur -> Continuer

        // Exécuter tous les validateurs
        var contexte = new ValidationContext<TRequest>(request);
        var resultats = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(contexte, ct)));

        // Collecter les erreurs
        var erreurs = resultats
            .SelectMany(r => r.Errors)
            .Where(e => e is not null)
            .ToList();

        if (erreurs.Any())
        {
            // Lever une exception avec toutes les erreurs
            throw new ValidationException(erreurs);
        }

        return await next(); // Continuer vers le Handler
    }
}

// Application/Behaviours/LoggingBehaviour.cs
public class LoggingBehaviour<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly ILogger<LoggingBehaviour<TRequest, TResponse>> _logger;

    public LoggingBehaviour(ILogger<LoggingBehaviour<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        var nomRequete = typeof(TRequest).Name;

        _logger.LogDebug("Traitement de {NomRequete}: {@Requete}", nomRequete, request);

        var debut = DateTime.UtcNow;
        var reponse = await next();
        var duree = DateTime.UtcNow - debut;

        if (duree.TotalMilliseconds > 500)
        {
            _logger.LogWarning(
                "Requête lente : {NomRequete} ({Duree}ms)",
                nomRequete, duree.TotalMilliseconds);
        }
        else
        {
            _logger.LogDebug(
                "{NomRequete} traité en {Duree}ms",
                nomRequete, duree.TotalMilliseconds);
        }

        return reponse;
    }
}

// Interfaces MediatR Pipeline (simplifiées)
public interface IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct);
}
public delegate Task<TResponse> RequestHandlerDelegate<TResponse>();
public interface IValidator<T>
{
    Task<ValidationResult> ValidateAsync(ValidationContext<T> context, CancellationToken ct);
}
public class ValidationContext<T> { public ValidationContext(T instance) { } }
public class ValidationResult { public List<ValidationFailure> Errors { get; set; } = new(); }
public class ValidationFailure { }
public class ValidationException : Exception
{
    public ValidationException(List<ValidationFailure> errors) : base("Validation échouée") { }
}

/*
─────────────────────────────────────────────────────────────────
Utiliser MediatR dans un Contrôleur ou Composant Blazor
─────────────────────────────────────────────────────────────────

Dans un Contrôleur ASP.NET Core :

[ApiController]
[Route("api/[controller]")]
public class ProduitsController : ControllerBase
{
    private readonly IMediator _mediator;

    public ProduitsController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet]
    public async Task<ActionResult<PagedResultDto<ProduitDto>>> ObtenirTous(
        [FromQuery] ObtenirProduitsQuery query)
    {
        var resultat = await _mediator.Send(query);
        return Ok(resultat);
    }

    [HttpPost]
    public async Task<ActionResult<CreerProduitResult>> Creer(
        [FromBody] CreerProduitCommand command)
    {
        try
        {
            var resultat = await _mediator.Send(command);
            return CreatedAtAction(nameof(ObtenirParId), new { id = resultat.Id }, resultat);
        }
        catch (ValidationException ex)
        {
            return BadRequest(ex.Message);
        }
        catch (DomainException ex)
        {
            return Conflict(ex.Message);
        }
    }
}

Dans un Composant Blazor (si serveur) :

@inject IMediator Mediator

@code {
    protected override async Task OnInitializedAsync()
    {
        var query = new ObtenirProduitsQuery(Page: 1, ParPage: 20);
        var resultat = await Mediator.Send(query);
        _produits = resultat.Items;
    }

    private async Task AjouterProduit()
    {
        var command = new CreerProduitCommand(_nom, _prix, _categorie, null, 0);
        var resultat = await Mediator.Send(command);
        // ...
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Enregistrement MediatR dans Program.cs
─────────────────────────────────────────────────────────────────

using MediatR;
using FluentValidation;

// MediatR avec tous les handlers de l'assembly Application
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(CreerProduitCommand).Assembly);

    // Enregistrer les Pipeline Behaviours (ordre important !)
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>));
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
});

// FluentValidation : Tous les validateurs de l'assembly Application
builder.Services.AddValidatorsFromAssembly(typeof(CreerProduitCommandValidator).Assembly);
*/


// ============================================================================
// [GUIDE] CHAPITRE 22 : MODULARISATION
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Organiser les fonctionnalités en Feature Folders
[OK] Créer des Razor Class Libraries réutilisables
[OK] Structurer un projet en modules indépendants
[OK] Partager des composants entre projets
*/


// ----------------------------------------------------------------------------
// [DOSSIER] FEATURE FOLDERS — Organisation par fonctionnalité
// ----------------------------------------------------------------------------

/*
ORGANISATION TRADITIONNELLE (par type) :
Controllers/
  ProduitsController.cs
  UsersController.cs
  CommandesController.cs
Services/
  ProduitService.cs
  UserService.cs
  CommandeService.cs
Models/
  Produit.cs
  User.cs
  Commande.cs

PROBLÈME : Pour modifier la fonctionnalité "Produits",
on touche à PLUSIEURS dossiers différents. C'est fragmenté.


FEATURE FOLDERS (par fonctionnalité) :
Features/
  Produits/
    ProduitModel.cs
    IProduitService.cs
    ProduitService.cs
    ProduitsController.cs
    ListeProduits.razor
    CarteProduit.razor
    FormulaireAjoutProduit.razor
    ProduitValidator.cs
  Utilisateurs/
    UtilisateurModel.cs
    IUtilisateurService.cs
    UtilisateurService.cs
    UtilisateursController.cs
    ListeUtilisateurs.razor
  Commandes/
    CommandeModel.cs
    ICommandeService.cs
    CommandeService.cs

AVANTAGE : Tout ce qui concerne "Produits" est ensemble !
-> Cohésion élevée : Les fichiers liés sont proches
-> Couplage faible : Les modules sont indépendants
-> Facilité de navigation et de modification
*/


// ----------------------------------------------------------------------------
// [DOCS] RAZOR CLASS LIBRARIES — Composants partageables
// ----------------------------------------------------------------------------

/*
RAZOR CLASS LIBRARY (RCL) = Bibliothèque de composants Blazor
Réutilisable entre plusieurs applications Blazor !

UTILITÉ :
-> Design System de votre entreprise
-> Composants partagés entre des applications
-> Vendre des composants UI (bibliothèque commerciale)

CRÉER UNE RCL :
dotnet new razorclasslib -n MonDesignSystem

STRUCTURE D'UNE RCL :
MonDesignSystem/
├── Components/
│   ├── Button/
│   │   ├── Button.razor
│   │   ├── Button.razor.css
│   │   └── ButtonVariant.cs
│   ├── Card/
│   │   ├── Card.razor
│   │   └── Card.razor.css
│   ├── Modal/
│   ├── DataGrid/
│   └── Charts/
├── Styles/
│   ├── variables.css
│   └── utilities.css
├── wwwroot/
│   ├── css/
│   └── js/
└── MonDesignSystem.csproj

UTILISER LA RCL DANS UN PROJET :
dotnet add reference ../MonDesignSystem/MonDesignSystem.csproj

Dans _Imports.razor :
@using MonDesignSystem.Components
*/

/*
─────────────────────────────────────────────────────────────────
Exemple : Composant Button réutilisable dans une RCL
─────────────────────────────────────────────────────────────────
Fichier : MonDesignSystem/Components/Button/Button.razor

<button class="ds-btn ds-btn--@Variante ds-btn--@Taille @(ChargementEnCours ? "ds-btn--loading" : "") @(_cssClasse)"
        type="@Type"
        disabled="@(Desactive || ChargementEnCours)"
        @onclick="GererClic"
        @attributes="AttributsSupplementaires">

    @if (ChargementEnCours)
    {
        <span class="ds-btn__spinner" aria-hidden="true"></span>
    }

    @if (IconeGauche is not null && !ChargementEnCours)
    {
        <span class="ds-btn__icon ds-btn__icon--gauche">@IconeGauche</span>
    }

    <span class="ds-btn__label">@ChildContent</span>

    @if (IconeDroite is not null)
    {
        <span class="ds-btn__icon ds-btn__icon--droite">@IconeDroite</span>
    }
</button>

@code {
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [Parameter] public RenderFragment? IconeGauche { get; set; }
    [Parameter] public RenderFragment? IconeDroite { get; set; }
    [Parameter] public string Variante { get; set; } = "primary"; // primary, secondary, ghost, danger
    [Parameter] public string Taille { get; set; } = "md";        // sm, md, lg
    [Parameter] public string Type { get; set; } = "button";
    [Parameter] public bool Desactive { get; set; } = false;
    [Parameter] public bool ChargementEnCours { get; set; } = false;
    [Parameter] public string? CssClasse { get; set; }
    [Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object>? AttributsSupplementaires { get; set; }

    private string _cssClasse => CssClasse ?? "";

    private async Task GererClic(MouseEventArgs args)
    {
        if (!Desactive && !ChargementEnCours)
            await OnClick.InvokeAsync(args);
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Fichier CSS de la RCL : MonDesignSystem/Components/Button/Button.razor.css
─────────────────────────────────────────────────────────────────

.ds-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    border: none;
    cursor: pointer;
    font-weight: 500;
    transition: all 0.2s ease;
    white-space: nowrap;
    text-decoration: none;
    border-radius: 8px;
    font-family: inherit;
}

/* Variantes */
.ds-btn--primary   { background: #3b82f6; color: white; }
.ds-btn--secondary { background: #f1f5f9; color: #1e293b; }
.ds-btn--ghost     { background: transparent; color: #3b82f6;
                     border: 1px solid #3b82f6; }
.ds-btn--danger    { background: #ef4444; color: white; }

.ds-btn--primary:hover:not(:disabled)   { background: #2563eb; }
.ds-btn--secondary:hover:not(:disabled) { background: #e2e8f0; }
.ds-btn--ghost:hover:not(:disabled)     { background: #eff6ff; }
.ds-btn--danger:hover:not(:disabled)    { background: #dc2626; }

/* Tailles */
.ds-btn--sm { padding: 6px 12px;  font-size: 0.75rem; }
.ds-btn--md { padding: 10px 18px; font-size: 0.875rem; }
.ds-btn--lg { padding: 14px 24px; font-size: 1rem; }

/* États */
.ds-btn:disabled, .ds-btn--loading {
    opacity: 0.6;
    cursor: not-allowed;
}

.ds-btn__spinner {
    width: 1em; height: 1em;
    border: 2px solid transparent;
    border-top-color: currentColor;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 23 : MULTI-TENANT SAAS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les stratégies multi-tenant
[OK] Implémenter l'isolation des données par tenant
[OK] Gérer les sous-domaines dynamiquement
[OK] Personnaliser l'UI par tenant
[OK] Gérer la facturation par tenant
*/


// ----------------------------------------------------------------------------
// [ENTREPRISE] STRATÉGIES MULTI-TENANT
// ----------------------------------------------------------------------------

/*
MULTI-TENANT = Une seule application, plusieurs clients (tenants)

STRATÉGIES D'ISOLATION DES DONNÉES :

1. BASE DE DONNÉES PAR TENANT
   Chaque client -> Sa propre BDD
   [OK] Isolation maximale, performances, conformité RGPD
   [X] Migrations complexes, coût infrastructure

2. SCHÉMA PAR TENANT (PostgreSQL, SQL Server)
   Une BDD, un schéma par tenant
   [OK] Bon compromis isolation/coût
   [X] Complexité de configuration

3. COLONNE TenantId PAR TABLE
   Une BDD, une table partagée, colonne TenantId
   [OK] Simple, facile à déployer
   [X] Risque de fuite de données si query incorrecte
   -> C'est l'approche qu'on va implémenter ici
*/


// ----------------------------------------------------------------------------
// [CLE] TENANT RESOLVER — Identifier le tenant actuel
// ----------------------------------------------------------------------------

// Services/ITenantResolver.cs
public interface ITenantResolver
{
    string? ObtenirTenantActuel();
}

// Services/TenantResolver.cs — Résolution par sous-domaine
public class TenantResolver : ITenantResolver
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantResolver(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string? ObtenirTenantActuel()
    {
        var host = _httpContextAccessor.HttpContext?.Request.Host.Host;
        if (host is null) return null;

        // Extraire le sous-domaine : "acme.monapp.com" -> "acme"
        var parties = host.Split('.');

        // Si localhost ou pas de sous-domaine -> retourner null
        if (parties.Length < 3) return null;

        var sousdomaine = parties[0];

        // Ignorer "www" et "app"
        return sousdomaine is "www" or "app" ? null : sousdomaine;
    }
}

// Fallback : Résolution par header HTTP
public class TenantResolverParHeader : ITenantResolver
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantResolverParHeader(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string? ObtenirTenantActuel()
    {
        // Le client envoie X-Tenant-Id dans chaque requête
        return _httpContextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault();
    }
}

// Interface HttpContext (simplifiée)
public interface IHttpContextAccessor
{
    HttpContext? HttpContext { get; }
}
public class HttpContext
{
    public HttpRequest Request { get; set; } = new();
}
public class HttpRequest
{
    public HostString Host { get; set; }
    public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
}
public struct HostString { public string Host { get; set; } }
public interface IHeaderDictionary
{
    StringValues this[string key] { get; }
}
public class HeaderDictionary : Dictionary<string, StringValues>, IHeaderDictionary
{
    public StringValues this[string key] => ContainsKey(key) ? base[key] : StringValues.Empty;
}
public struct StringValues
{
    public static StringValues Empty = new();
    public string? FirstOrDefault() => null;
}


// ----------------------------------------------------------------------------
// [ARCHIVE] DBCONTEXT MULTI-TENANT — Filtrage automatique par TenantId
// ----------------------------------------------------------------------------

public class MultiTenantDbContext : DbContext
{
    private readonly ITenantResolver _tenantResolver;

    public string? TenantActuel => _tenantResolver.ObtenirTenantActuel();

    public MultiTenantDbContext(
        DbContextOptions<MultiTenantDbContext> options,
        ITenantResolver tenantResolver)
        : base(options)
    {
        _tenantResolver = tenantResolver;
    }

    // Tables multi-tenant
    // public DbSet<ProduitTenant> Produits => Set<ProduitTenant>();
    // public DbSet<CommandeTenant> Commandes => Set<CommandeTenant>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Filtrage automatique par TenantId sur TOUTES les entités qui implémentent ITenantEntity
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType))
            {
                // Ajouter un filtre global automatique
                // EF Core applique ce filtre à TOUTES les requêtes
                var method = typeof(MultiTenantDbContext)
                    .GetMethod(nameof(ConfigurerFiltreTenant),
                        System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!
                    .MakeGenericMethod(entityType.ClrType);

                method.Invoke(null, new object[] { modelBuilder, this });
            }
        }
    }

    private static void ConfigurerFiltreTenant<T>(ModelBuilder builder, MultiTenantDbContext ctx)
        where T : class, ITenantEntity
    {
        // Filtre global : EF ajoutera automatiquement WHERE TenantId = @tenantActuel
        builder.Entity<T>().HasQueryFilter(e => e.TenantId == ctx.TenantActuel);
    }

    // Override SaveChanges pour injecter TenantId automatiquement
    public override Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        // Avant de sauvegarder, injecter le TenantId sur les nouvelles entités
        var nouvelles = ChangeTracker.Entries<ITenantEntity>()
            .Where(e => e.State == EntityState.Added)
            .Select(e => e.Entity);

        foreach (var entite in nouvelles)
        {
            entite.TenantId = TenantActuel ?? throw new InvalidOperationException("Tenant non identifié");
        }

        return base.SaveChangesAsync(ct);
    }
}

// Interface que toutes les entités multi-tenant doivent implémenter
public interface ITenantEntity
{
    string TenantId { get; set; }
}

// Entité exemple
public class ProduitTenant : ITenantEntity
{
    public int Id { get; set; }
    public string TenantId { get; set; } = string.Empty; // Isolé par tenant
    public string Nom { get; set; } = string.Empty;
    public decimal Prix { get; set; }
}

// Enum EF (simulé)
public enum EntityState { Added, Modified, Deleted, Unchanged, Detached }

// ChangeTracker simulé
public class ChangeTracker
{
    public IEnumerable<EntityEntry<T>> Entries<T>() where T : class
        => Enumerable.Empty<EntityEntry<T>>();
}
public class EntityEntry<T> where T : class
{
    public EntityState State { get; set; }
    public T Entity { get; set; } = default!;
}

// ModelBuilder.Model simulé
public static class ModelBuilderExtensions
{
    public static object GetEntityTypes(this object model) => new object();
}


// ----------------------------------------------------------------------------
// [DESIGN] PERSONNALISATION PAR TENANT
// ----------------------------------------------------------------------------

// Services/TenantConfigService.cs
public class TenantConfig
{
    public string TenantId { get; set; } = string.Empty;
    public string NomEntreprise { get; set; } = string.Empty;
    public string LogoUrl { get; set; } = string.Empty;
    public string CouleurPrimaire { get; set; } = "#3b82f6";
    public string CouleurSecondaire { get; set; } = "#8b5cf6";
    public string PlanAbonnement { get; set; } = "free"; // free, pro, enterprise
    public bool FonctionnaliteAvancee { get; set; } = false;
    public int LimiteUtilisateurs { get; set; } = 5;
    public string Domaine { get; set; } = string.Empty;
    public DateTime DateExpiration { get; set; }
}

public interface ITenantConfigService
{
    Task<TenantConfig?> ObtenirConfigAsync(string tenantId);
}

public class TenantConfigService : ITenantConfigService
{
    private readonly AppDbContext _context;
    private readonly IMemoryCache _cache;

    public TenantConfigService(AppDbContext context, IMemoryCache cache)
    {
        _context = context;
        _cache = cache;
    }

    public async Task<TenantConfig?> ObtenirConfigAsync(string tenantId)
    {
        // Cache pour éviter les requêtes BDD à chaque request
        var cacheKey = $"tenant_config_{tenantId}";

        if (_cache.TryGetValue<TenantConfig>(cacheKey, out var cached))
            return cached;

        // Charger depuis la BDD
        // var config = await _context.Tenants
        //     .Where(t => t.TenantId == tenantId)
        //     .FirstOrDefaultAsync();

        // Simulation
        var config = new TenantConfig
        {
            TenantId = tenantId,
            NomEntreprise = $"Entreprise {tenantId}",
            LogoUrl = $"/logos/{tenantId}.png",
            CouleurPrimaire = "#3b82f6",
            PlanAbonnement = "pro",
            LimiteUtilisateurs = 50
        };

        if (config is not null)
        {
            _cache.Set(cacheKey, config, TimeSpan.FromMinutes(15));
        }

        return config;
    }
}

// Interface IMemoryCache (simplifiée)
public interface IMemoryCache
{
    bool TryGetValue<T>(string key, out T? value);
    void Set<T>(string key, T value, TimeSpan expiry);
}

/*
─────────────────────────────────────────────────────────────────
Composant TenantTheme.razor — Appliquer le thème du tenant
─────────────────────────────────────────────────────────────────

@implements IDisposable
@inject ITenantResolver TenantResolver
@inject ITenantConfigService TenantConfigService

@if (_config is not null)
{
    <!-- Injecter les variables CSS du tenant -->
    <style>
        :root {
            --color-primary: @_config.CouleurPrimaire;
            --color-secondary: @_config.CouleurSecondaire;
        }
    </style>
}

@ChildContent

@code {
    [Parameter] public RenderFragment? ChildContent { get; set; }

    private TenantConfig? _config;

    protected override async Task OnInitializedAsync()
    {
        var tenantId = TenantResolver.ObtenirTenantActuel();
        if (tenantId is not null)
        {
            _config = await TenantConfigService.ObtenirConfigAsync(tenantId);
        }
    }

    public void Dispose() { }
}
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE — PARTIE 7
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
EXERCICE : APPLICATION CLEAN ARCHITECTURE + CQRS
═══════════════════════════════════════════════════════════════

OBJECTIF : Refactoriser le catalogue produits en Clean Architecture

STRUCTURE À CRÉER :
1. MonCatalogue.Domain/
   - ProduitDomain.cs (entité avec règles métier)
   - IProduitRepository.cs (interface)
   - DomainException.cs
   - Events/ProduitCreeDomainEvent.cs

2. MonCatalogue.Application/
   - Commands/CreerProduitCommand.cs + Handler
   - Commands/SupprimerProduitCommand.cs + Handler
   - Queries/ObtenirProduitsQuery.cs + Handler
   - Queries/ObtenirProduitParIdQuery.cs + Handler
   - Validators/CreerProduitCommandValidator.cs
   - DTOs/ProduitDto.cs

3. MonCatalogue.Infrastructure/
   - AppDbContext.cs avec EF Core
   - ProduitRepository.cs (implémentation)
   - DependencyInjection.cs (extension pour Program.cs)

4. MonCatalogue.Client (Blazor)
   - Page /produits : Utilise IMediator (via HttpClient vers API)
   - Page /produits/ajouter : Formulaire -> CreerProduitCommand

5. MonCatalogue.Server (ASP.NET Core)
   - ProduitsController.cs avec IMediator
   - ValidationExceptionMiddleware.cs

COMPÉTENCES :
[OK] Entités Domain avec règles métier encapsulées
[OK] Factory Method et Domain Events
[OK] CQRS (Commands + Queries séparées)
[OK] MediatR avec Pipeline Behaviours
[OK] FluentValidation
[OK] Repository Pattern propre
[OK] Clean Architecture (0 dépendance vers l'intérieur)
═══════════════════════════════════════════════════════════════
*/


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 7

[OK] CHAPITRE 21 - CLEAN ARCHITECTURE :
- Structure en couches : Domain -> Application -> Infrastructure -> UI
- Domain : Entités, règles métier, Domain Events, interfaces
- ProduitDomain avec factory method Creer() et méthodes métier
- DomainException pour les violations de règles métier
- Application : Commands, Queries, Handlers, DTOs, Validators
- CQRS : Séparer lecture (Query) et écriture (Command)
- MediatR : IRequest<T> + IRequestHandler<TRequest, TResponse>
- Pipeline Behaviours : ValidationBehaviour, LoggingBehaviour
- FluentValidation pour validation des commandes
- Infrastructure : DbContext, Repository, implémentations

[OK] CHAPITRE 22 - MODULARISATION :
- Feature Folders : Organisation par fonctionnalité (cohésion élevée)
- Avantage vs organisation par type : tout en un seul endroit
- Razor Class Library (RCL) : Composants partageables entre projets
- dotnet new razorclasslib -n MonDesignSystem
- Button.razor dans la RCL : variantes, tailles, états, icons
- CaptureUnmatchedValues pour passer des attributs HTML arbitraires
- CSS Scoped dans la RCL pour isolation des styles

[OK] CHAPITRE 23 - MULTI-TENANT SAAS :
- Stratégies : BDD par tenant / Schéma par tenant / TenantId column
- ITenantResolver : identifier le tenant par sous-domaine ou header
- MultiTenantDbContext : HasQueryFilter global + SaveChanges auto
- ITenantEntity : interface que toutes les entités partagent
- Filtrage automatique EF Core par TenantId (impossible d'oublier !)
- TenantConfig : couleurs, logo, plan, limites par tenant
- Cache mémoire pour les configs tenant (15 minutes)
- TenantTheme.razor : injecter les variables CSS du tenant

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 8
- Sécurité avancée (CSRF, XSS, Headers)
- Protection JWT et Refresh Tokens
- Sécurité spécifique WebAssembly
- Secrets management (Azure Key Vault, HashiCorp)
═══════════════════════════════════════════════════════════════
*/

// ============================================================================
// [LIVRE] BLAZOR - PARTIE 8 : SÉCURITÉ
// ============================================================================
//
// CHAPITRE 24 : Sécurité avancée (CSRF, XSS, Headers, JWT, Rate Limiting)
// CHAPITRE 25 : Sécurité WebAssembly (limitations, protection code client)
//
// [TEMPS] TEMPS ESTIMÉ : ~8-10 heures
// [DOCS] PRÉREQUIS : Parties 1-7 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 24 : SÉCURITÉ AVANCÉE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Configurer les en-têtes de sécurité HTTP
[OK] Protéger contre XSS dans Blazor
[OK] Implémenter les Refresh Tokens JWT
[OK] Gérer les secrets (Azure Key Vault, variables d'environnement)
[OK] Implémenter le Rate Limiting
[OK] Configurer HTTPS et CORS correctement
[OK] Auditer la sécurité de l'application
*/


// ----------------------------------------------------------------------------
// [SECURITE] EN-TÊTES DE SÉCURITÉ HTTP
// ----------------------------------------------------------------------------

/*
LES EN-TÊTES DE SÉCURITÉ = Première ligne de défense côté serveur

SANS ces en-têtes, votre app est vulnérable à :
-> Clickjacking        : Un iframe malveillant superpose votre site
-> XSS réfléchi        : Scripts injectés via URL ou formulaires
-> MIME Sniffing       : Le navigateur exécute du JS déguisé en image
-> Downgrade HTTPS     : Forcer HTTP sur une connexion supposément sécurisée
-> Information leakage : Le serveur révèle sa technologie (X-Powered-By)

COMMENT MESURER ? -> https://securityheaders.com
OBJECTIF : Score A ou A+
*/

/*
─────────────────────────────────────────────────────────────────
Middleware SecurityHeaders pour ASP.NET Core
─────────────────────────────────────────────────────────────────
Fichier : Middleware/SecurityHeadersMiddleware.cs
*/

/*
public class SecurityHeadersMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IWebHostEnvironment _env;

    public SecurityHeadersMiddleware(RequestDelegate next, IWebHostEnvironment env)
    {
        _next = next;
        _env = env;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var headers = context.Response.Headers;

        // ─── 1. X-Frame-Options : Empêcher le clickjacking ─────────────
        // DENY = Jamais dans une iframe
        // SAMEORIGIN = Seulement si même domaine
        headers["X-Frame-Options"] = "DENY";

        // ─── 2. X-Content-Type-Options : Empêcher le MIME sniffing ─────
        // nosniff = Le navigateur respecte le Content-Type déclaré
        headers["X-Content-Type-Options"] = "nosniff";

        // ─── 3. X-XSS-Protection : Protection XSS basique ─────────────
        headers["X-XSS-Protection"] = "1; mode=block";

        // ─── 4. Referrer-Policy : Contrôler les informations partagées ─
        // strict-origin-when-cross-origin = URL complète seulement même origine
        headers["Referrer-Policy"] = "strict-origin-when-cross-origin";

        // ─── 5. Permissions-Policy : Restreindre les API navigateur ────
        // Désactiver les fonctionnalités non nécessaires
        headers["Permissions-Policy"] =
            "geolocation=(), microphone=(), camera=(), " +
            "payment=(), usb=(), magnetometer=(), gyroscope=()";

        // ─── 6. Supprimer les en-têtes qui révèlent la technologie ────
        headers.Remove("X-Powered-By");
        headers.Remove("Server");

        // ─── 7. HSTS : Forcer HTTPS (seulement en production) ─────────
        if (!_env.IsDevelopment())
        {
            // max-age=31536000 = 1 an en secondes
            // includeSubDomains = S'applique à tous les sous-domaines
            // preload = Peut être ajouté à la liste de preload des navigateurs
            headers["Strict-Transport-Security"] =
                "max-age=31536000; includeSubDomains; preload";
        }

        // ─── 8. Content-Security-Policy (CSP) ─────────────────────────
        // La directive la plus puissante contre XSS
        // ATTENTION : Blazor WebAssembly nécessite des règles spécifiques !
        var csp = BuildCspHeader(_env.IsDevelopment());
        headers["Content-Security-Policy"] = csp;

        await _next(context);
    }

    private static string BuildCspHeader(bool isDevelopment)
    {
        // En développement : plus permissif (pour Hot Reload, etc.)
        if (isDevelopment)
        {
            return string.Join("; ",
                "default-src 'self'",
                "script-src 'self' 'unsafe-inline' 'unsafe-eval'",   // Hot Reload nécessite unsafe
                "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
                "font-src 'self' https://fonts.gstatic.com",
                "img-src 'self' data: blob: https:",
                "connect-src 'self' wss: ws:",  // WebSocket pour SignalR
                "frame-ancestors 'none'",
                "base-uri 'self'",
                "form-action 'self'"
            );
        }

        // En production : plus strict
        return string.Join("; ",
            "default-src 'self'",
            "script-src 'self' 'wasm-unsafe-eval'",  // 'wasm-unsafe-eval' requis pour Blazor WASM
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
            "font-src 'self' https://fonts.gstatic.com",
            "img-src 'self' data: blob: https://votre-cdn.com",
            "connect-src 'self' https://api.monapp.com wss://monapp.com",
            "frame-ancestors 'none'",
            "base-uri 'self'",
            "form-action 'self'",
            "upgrade-insecure-requests",  // Forcer HTTPS pour toutes les ressources
            "block-all-mixed-content"     // Bloquer contenu HTTP sur page HTTPS
        );
    }
}
*/

/*
Enregistrement dans Program.cs :
app.UseMiddleware<SecurityHeadersMiddleware>();

ORDRE IMPORTANT dans Program.cs :
app.UseHsts();                          <- 1
app.UseHttpsRedirection();              <- 2
app.UseMiddleware<SecurityHeadersMiddleware>(); <- 3
app.UseStaticFiles();                   <- 4
app.UseRouting();                       <- 5
app.UseCors();                          <- 6
app.UseAuthentication();                <- 7
app.UseAuthorization();                 <- 8
app.MapControllers();                   <- 9
*/


// ----------------------------------------------------------------------------
// [SECURISE] PROTECTION XSS DANS BLAZOR
// ----------------------------------------------------------------------------

/*
BLAZOR EST NATURELLEMENT RÉSISTANT AU XSS !

POURQUOI ? Blazor encode automatiquement toutes les variables.

    @_texteUtilisateur
    -> Affiché comme texte brut, jamais exécuté comme HTML

MAIS IL Y A DES EXCEPTIONS À SURVEILLER :

1. MarkupString : Inject du HTML brut (DANGEREUX sans sanitisation)
2. JavaScript Interop : Passer des données non sanitisées à JS
3. innerHTML via JS Interop : Équivalent à innerHTML en JS classique
*/

/*
─────────────────────────────────────────────────────────────────
EXEMPLES : Ce qui est sûr vs ce qui est dangereux
─────────────────────────────────────────────────────────────────

// [OK] SÛR : Blazor encode automatiquement
@_commentaireUtilisateur
// Si _commentaireUtilisateur = "<script>alert('xss')</script>"
// Affiché comme texte : &lt;script&gt;alert('xss')&lt;/script&gt;

// [X] DANGEREUX : MarkupString affiche HTML brut
@((MarkupString)_commentaireUtilisateur)
// EXÉCUTE le script JavaScript !

// [OK] SÛR avec MarkupString : Sanitiser d'abord
@((MarkupString)SanitiserHtml(_commentaireUtilisateur))
*/

// Services/HtmlSanitizerService.cs
public class HtmlSanitizerService
{
    // Utiliser la bibliothèque HtmlSanitizer
    // dotnet add package HtmlSanitizer

    public string Sanitiser(string html)
    {
        if (string.IsNullOrEmpty(html)) return string.Empty;

        // La bibliothèque HtmlSanitizer de Ganss est recommandée
        // Elle supprime les scripts, event handlers dangereux, etc.
        // var sanitizer = new HtmlSanitizer();

        // Configurer les balises et attributs autorisés
        // sanitizer.AllowedTags.Clear();
        // sanitizer.AllowedTags.Add("p");
        // sanitizer.AllowedTags.Add("b");
        // sanitizer.AllowedTags.Add("i");
        // sanitizer.AllowedTags.Add("u");
        // sanitizer.AllowedTags.Add("strong");
        // sanitizer.AllowedTags.Add("em");
        // sanitizer.AllowedTags.Add("ul");
        // sanitizer.AllowedTags.Add("ol");
        // sanitizer.AllowedTags.Add("li");
        // sanitizer.AllowedTags.Add("a");
        // sanitizer.AllowedAttributes.Add("href");
        // sanitizer.AllowedAttributes.Add("class");

        // return sanitizer.Sanitize(html);

        // Implémentation basique (utiliser HtmlSanitizer en vrai !)
        return System.Web.HttpUtility.HtmlEncode(html);
    }
}

/*
─────────────────────────────────────────────────────────────────
Composant RichTextDisplay.razor — Affichage HTML sécurisé
─────────────────────────────────────────────────────────────────

@inject HtmlSanitizerService Sanitizer

<div class="rich-text-content">
    @((MarkupString)_contenuSanitise)
</div>

@code {
    [Parameter]
    public string? ContenuHtml { get; set; }

    private string _contenuSanitise = string.Empty;

    protected override void OnParametersSet()
    {
        // TOUJOURS sanitiser avant d'afficher comme MarkupString !
        _contenuSanitise = ContenuHtml is not null
            ? Sanitizer.Sanitiser(ContenuHtml)
            : string.Empty;
    }
}
*/


// ----------------------------------------------------------------------------
// [CLE] JWT + REFRESH TOKENS — Authentification robuste
// ----------------------------------------------------------------------------

/*
PROBLÈME DES JWT SIMPLES :
-> Access Token de courte durée (15 min) -> Utilisateur déconnecté souvent
-> Access Token de longue durée -> Si volé, utilisable longtemps

SOLUTION : Refresh Token Pattern

ACCESS TOKEN  : Durée courte (15 min), utilisé pour les requêtes API
REFRESH TOKEN : Durée longue (30 jours), utilisé pour obtenir un nouvel Access Token
                Stocké côté serveur (révocable !)

FLUX :
1. Login -> Obtenir Access Token (15 min) + Refresh Token (30 jours)
2. Requête API -> Utiliser Access Token
3. Access Token expiré -> Utiliser Refresh Token -> Obtenir nouveau Access Token
4. Déconnexion -> Révoquer Refresh Token côté serveur
*/

// Models/AuthTokens.cs
public record AuthTokens(
    string AccessToken,
    DateTime AccessTokenExpiration,
    string RefreshToken,
    DateTime RefreshTokenExpiration
);

// Models/RefreshTokenEntity.cs (côté serveur, en BDD)
public class RefreshTokenEntity
{
    public int Id { get; set; }
    public string Token { get; set; } = string.Empty;
    public int UtilisateurId { get; set; }
    public DateTime ExpirationDate { get; set; }
    public bool EstRevoque { get; set; } = false;
    public DateTime CreeLe { get; set; } = DateTime.UtcNow;
    public string? RemplacePar { get; set; } // Token de rotation
    public string? IpAdresse { get; set; }
    public string? UserAgent { get; set; }
}

// Services/TokenService.cs (côté serveur)
public class TokenService
{
    private readonly IConfiguration _config;
    private readonly AppDbContext _context;

    public TokenService(IConfiguration config, AppDbContext context)
    {
        _config = config;
        _context = context;
    }

    // Générer un Access Token JWT
    public string GenererAccessToken(UtilisateurModel utilisateur)
    {
        var cle = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!));

        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, utilisateur.Id.ToString()),
            new Claim(JwtRegisteredClaimNames.Email, utilisateur.Email),
            new Claim(ClaimTypes.Name, utilisateur.Nom),
            new Claim(ClaimTypes.Role, utilisateur.Role),
            new Claim("tenant_id", utilisateur.TenantId ?? ""),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        };

        var token = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(15), // Courte durée !
            signingCredentials: new SigningCredentials(
                cle,
                SecurityAlgorithms.HmacSha256));

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    // Générer un Refresh Token sécurisé
    public string GenererRefreshToken()
    {
        // 64 bytes aléatoires cryptographiquement sûrs
        var randomBytes = new byte[64];
        using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
        rng.GetBytes(randomBytes);
        return Convert.ToBase64String(randomBytes);
    }

    // Sauvegarder le Refresh Token en BDD
    public async Task<RefreshTokenEntity> SauvegarderRefreshTokenAsync(
        int utilisateurId,
        string token,
        string? ipAdresse = null,
        string? userAgent = null)
    {
        var refreshToken = new RefreshTokenEntity
        {
            Token = token,
            UtilisateurId = utilisateurId,
            ExpirationDate = DateTime.UtcNow.AddDays(30),
            IpAdresse = ipAdresse,
            UserAgent = userAgent
        };

        // _context.RefreshTokens.Add(refreshToken);
        // await _context.SaveChangesAsync();

        return refreshToken;
    }

    // Valider et rotation du Refresh Token
    public async Task<AuthTokens?> RafraichirTokensAsync(
        string refreshToken,
        string? ipAdresse = null)
    {
        // 1. Chercher le refresh token en BDD
        // var tokenEntite = await _context.RefreshTokens
        //     .Include(t => t.Utilisateur)
        //     .FirstOrDefaultAsync(t => t.Token == refreshToken);

        // Simulation
        RefreshTokenEntity? tokenEntite = null;

        if (tokenEntite is null) return null;

        // 2. Vérifier si révoqué ou expiré
        if (tokenEntite.EstRevoque)
        {
            // ALERTE SÉCURITÉ : Tentative d'utilisation d'un token révoqué !
            // Peut indiquer un vol de token -> Révoquer TOUTE la chaîne
            await RevoquerTousLesTokensUtilisateur(tokenEntite.UtilisateurId);
            return null;
        }

        if (tokenEntite.ExpirationDate < DateTime.UtcNow) return null;

        // 3. Marquer l'ancien token comme révoqué (rotation)
        tokenEntite.EstRevoque = true;
        tokenEntite.RemplacePar = "nouveau_token";

        // 4. Générer de nouveaux tokens
        // var utilisateur = tokenEntite.Utilisateur;
        // var nouvelAccessToken = GenererAccessToken(utilisateur);
        // var nouveauRefreshToken = GenererRefreshToken();

        // await SauvegarderRefreshTokenAsync(utilisateur.Id, nouveauRefreshToken, ipAdresse);
        // await _context.SaveChangesAsync();

        return new AuthTokens(
            "nouvel_access_token",
            DateTime.UtcNow.AddMinutes(15),
            "nouveau_refresh_token",
            DateTime.UtcNow.AddDays(30));
    }

    // Révoquer tous les tokens d'un utilisateur (après compromission)
    public async Task RevoquerTousLesTokensUtilisateur(int utilisateurId)
    {
        // var tokens = await _context.RefreshTokens
        //     .Where(t => t.UtilisateurId == utilisateurId && !t.EstRevoque)
        //     .ToListAsync();

        // foreach (var token in tokens)
        //     token.EstRevoque = true;

        // await _context.SaveChangesAsync();
        await Task.CompletedTask;
    }
}

// Imports nécessaires (simulés)
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

public class SymmetricSecurityKey { public SymmetricSecurityKey(byte[] key) { } }
public class JwtSecurityToken
{
    public JwtSecurityToken(string? issuer, string? audience, Claim[] claims,
        DateTime expires, SigningCredentials signingCredentials) { }
}
public class JwtSecurityTokenHandler
{
    public string WriteToken(JwtSecurityToken token) => "jwt_token";
}
public static class JwtRegisteredClaimNames
{
    public const string Sub = "sub";
    public const string Email = "email";
    public const string Jti = "jti";
}
public class Claim
{
    public Claim(string type, string value) { }
}
public static class ClaimTypes
{
    public const string Name = "name";
    public const string Role = "role";
}
public class SigningCredentials
{
    public SigningCredentials(SymmetricSecurityKey key, string algorithm) { }
}
public static class SecurityAlgorithms { public const string HmacSha256 = "HS256"; }
public class UtilisateurModel
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Role { get; set; } = string.Empty;
    public string? TenantId { get; set; }
}

/*
─────────────────────────────────────────────────────────────────
Endpoint de rafraîchissement dans Program.cs (Minimal API)
─────────────────────────────────────────────────────────────────

app.MapPost("/api/auth/refresh", async (
    RefreshRequest req,
    TokenService tokenService,
    HttpContext ctx) =>
{
    var ipAdresse = ctx.Connection.RemoteIpAddress?.ToString();

    var tokens = await tokenService.RafraichirTokensAsync(req.RefreshToken, ipAdresse);

    return tokens is null
        ? Results.Unauthorized()
        : Results.Ok(tokens);
});

public record RefreshRequest(string RefreshToken);
*/

/*
─────────────────────────────────────────────────────────────────
Côté Client : Auto-refresh du token avant expiration
─────────────────────────────────────────────────────────────────
Fichier : Services/TokenRefreshService.cs (Blazor Client)
*/

public class TokenRefreshService : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly JwtAuthStateProvider _authProvider;
    private readonly ILocalStorageService _localStorage;
    private Timer? _timer;
    private const string CLE_REFRESH = "refresh_token";
    private const string CLE_ACCESS = "access_token";
    private const string CLE_EXPIRATION = "token_expiration";

    public TokenRefreshService(
        HttpClient httpClient,
        JwtAuthStateProvider authProvider,
        ILocalStorageService localStorage)
    {
        _httpClient = httpClient;
        _authProvider = authProvider;
        _localStorage = localStorage;
    }

    // Démarrer le rafraîchissement automatique
    public async Task DemarrerAsync()
    {
        var expirationStr = await _localStorage.GetItemAsync<string>(CLE_EXPIRATION);
        if (expirationStr is null) return;

        if (!DateTime.TryParse(expirationStr, out var expiration)) return;

        // Calculer quand rafraîchir : 2 minutes avant expiration
        var delaiAvantRefresh = expiration - DateTime.UtcNow - TimeSpan.FromMinutes(2);

        if (delaiAvantRefresh <= TimeSpan.Zero)
        {
            // Token déjà expiré ou sur le point d'expirer -> Rafraîchir immédiatement
            await RafraichirAsync();
            return;
        }

        // Programmer le rafraîchissement automatique
        _timer = new Timer(
            async _ => await RafraichirAsync(),
            null,
            delaiAvantRefresh,      // Délai avant premier déclenchement
            Timeout.InfiniteTimeSpan); // Pas de répétition (replanifié après refresh)
    }

    private async Task RafraichirAsync()
    {
        var refreshToken = await _localStorage.GetItemAsync<string>(CLE_REFRESH);
        if (string.IsNullOrEmpty(refreshToken)) return;

        try
        {
            var reponse = await _httpClient.PostAsJsonAsync(
                "api/auth/refresh",
                new { RefreshToken = refreshToken });

            if (!reponse.IsSuccessStatusCode)
            {
                // Refresh Token invalide ou expiré -> Déconnecter
                await _authProvider.DeconnecterAsync();
                return;
            }

            var tokens = await reponse.Content.ReadFromJsonAsync<AuthTokens>();
            if (tokens is null) return;

            // Sauvegarder les nouveaux tokens
            await _localStorage.SetItemAsync(CLE_ACCESS, tokens.AccessToken);
            await _localStorage.SetItemAsync(CLE_REFRESH, tokens.RefreshToken);
            await _localStorage.SetItemAsync(CLE_EXPIRATION,
                tokens.AccessTokenExpiration.ToString("O"));

            // Mettre à jour l'état d'authentification
            await _authProvider.ConnecterAsync(tokens.AccessToken);

            // Replanifier le prochain rafraîchissement
            await DemarrerAsync();
        }
        catch (Exception)
        {
            // En cas d'erreur réseau -> Déconnecter l'utilisateur
            await _authProvider.DeconnecterAsync();
        }
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}


// ----------------------------------------------------------------------------
// [SIGNAL] RATE LIMITING — Protéger l'API contre les abus
// ----------------------------------------------------------------------------

/*
RATE LIMITING = Limiter le nombre de requêtes par période

POURQUOI ?
-> Protection contre les attaques brute-force (login)
-> Protection DDoS basique
-> Garantir la qualité de service pour tous
-> Prévenir le scraping

SCÉNARIOS :
-> Login : Max 5 tentatives / 15 minutes par IP
-> API générale : Max 100 requêtes / minute par utilisateur
-> Export PDF : Max 10 / heure par utilisateur

PACKAGE (.NET 7+) :
Inclus dans ASP.NET Core ! Pas de NuGet supplémentaire.
*/

/*
─────────────────────────────────────────────────────────────────
Configuration du Rate Limiting dans Program.cs
─────────────────────────────────────────────────────────────────

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // ─── Policy 1 : Limite globale (tous les endpoints) ──────────
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
    {
        // Partitionner par IP + Utilisateur (si connecté)
        var partitionKey = context.User.Identity?.IsAuthenticated == true
            ? $"user_{context.User.FindFirst("sub")?.Value}"
            : $"ip_{context.Connection.RemoteIpAddress}";

        return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ =>
            new FixedWindowRateLimiterOptions
            {
                PermitLimit = 200,              // 200 requêtes
                Window = TimeSpan.FromMinutes(1), // par minute
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 10
            });
    });

    // ─── Policy 2 : Login (brute-force protection) ───────────────
    options.AddPolicy("login", context =>
    {
        var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        return RateLimitPartition.GetFixedWindowLimiter(ip, _ =>
            new FixedWindowRateLimiterOptions
            {
                PermitLimit = 5,                      // 5 tentatives
                Window = TimeSpan.FromMinutes(15),    // par 15 minutes
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 0
            });
    });

    // ─── Policy 3 : API authentifiée (sliding window) ────────────
    options.AddPolicy("api_auth", context =>
    {
        var userId = context.User.FindFirst("sub")?.Value ?? "anonymous";
        return RateLimitPartition.GetSlidingWindowLimiter(userId, _ =>
            new SlidingWindowRateLimiterOptions
            {
                PermitLimit = 100,                  // 100 requêtes
                Window = TimeSpan.FromMinutes(1),   // par minute (fenêtre glissante)
                SegmentsPerWindow = 6,              // Fenêtre divisée en 6 segments de 10s
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 5
            });
    });

    // ─── Policy 4 : Export (token bucket) ────────────────────────
    options.AddPolicy("export", context =>
    {
        var userId = context.User.FindFirst("sub")?.Value ?? "anonymous";
        return RateLimitPartition.GetTokenBucketLimiter(userId, _ =>
            new TokenBucketRateLimiterOptions
            {
                TokenLimit = 10,                        // 10 exports max
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 0,
                ReplenishmentPeriod = TimeSpan.FromHours(1), // Recharge toutes les heures
                TokensPerPeriod = 10,                        // 10 tokens rechargés
                AutoReplenishment = true
            });
    });

    // ─── Réponse personnalisée quand limite atteinte ─────────────
    options.OnRejected = async (context, ct) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.HttpContext.Response.Headers["Retry-After"] = "60";

        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            error = "Trop de requêtes",
            message = "Vous avez dépassé la limite de requêtes. Réessayez dans 60 secondes.",
            retryAfter = 60
        }, ct);
    };
});

// Activer le Rate Limiting AVANT les routes
app.UseRateLimiter();

// Appliquer la policy sur un endpoint spécifique
app.MapPost("/api/auth/login", LoginHandler)
   .RequireRateLimiting("login");

app.MapGet("/api/produits", ObtenirProduitsHandler)
   .RequireRateLimiting("api_auth");

app.MapGet("/api/export", ExportHandler)
   .RequireRateLimiting("export");

// Ou via attribut sur un Controller
[EnableRateLimiting("api_auth")]
public class ProduitsController : ControllerBase { ... }
*/


// ----------------------------------------------------------------------------
// [WEB] CORS — Configurer correctement les origines croisées
// ----------------------------------------------------------------------------

/*
CORS = Cross-Origin Resource Sharing

PROBLÈME :
Blazor WebAssembly (sur https://app.monapp.com) appelle une API (sur https://api.monapp.com)
-> Origines DIFFÉRENTES -> Le navigateur bloque la requête !

SOLUTION : L'API doit AUTORISER explicitement l'origine Blazor

RÈGLES D'OR CORS :
[OK] Ne JAMAIS utiliser AllowAnyOrigin() en production !
[OK] Lister explicitement les origines autorisées
[OK] Être précis sur les méthodes et headers autorisés
[OK] AllowCredentials() SEULEMENT si vous utilisez les cookies
*/

/*
─────────────────────────────────────────────────────────────────
Configuration CORS dans Program.cs
─────────────────────────────────────────────────────────────────

builder.Services.AddCors(options =>
{
    // ─── Policy Production ─────────────────────────────────────
    options.AddPolicy("Production", policy =>
    {
        policy
            .WithOrigins(
                "https://monapp.com",
                "https://app.monapp.com",
                "https://www.monapp.com"
            )
            .WithMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
            .WithHeaders(
                "Authorization",
                "Content-Type",
                "X-Tenant-Id",
                "X-Request-Id"
            )
            .AllowCredentials()     // Seulement si vous utilisez les cookies
            .SetPreflightMaxAge(TimeSpan.FromHours(1)); // Cache preflight 1h
    });

    // ─── Policy Développement ──────────────────────────────────
    options.AddPolicy("Developpement", policy =>
    {
        policy
            .WithOrigins(
                "http://localhost:5000",
                "https://localhost:5001",
                "http://localhost:3000"  // Si vous testez avec un autre outil
            )
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials();
    });
});

// Appliquer selon l'environnement
if (app.Environment.IsDevelopment())
    app.UseCors("Developpement");
else
    app.UseCors("Production");
*/


// ----------------------------------------------------------------------------
// [ARCHIVE] GESTION DES SECRETS
// ----------------------------------------------------------------------------

/*
RÈGLE N°1 DE SÉCURITÉ : JAMAIS de secrets dans le code source !

[X] INTERDIT :
public class ApiService
{
    private const string CleApi = "sk_live_abc123secretkey"; // <- Dans Git !
    private const string ConnexionBd = "Server=prod;Password=MonMotDePasse123!";
}

[X] INTERDIT :
appsettings.json :
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=prod-server;Password=prod_password!"
  }
}

SOLUTIONS CORRECTES :

1. Variables d'environnement       -> Développement simple
2. User Secrets (.NET)             -> Développement local
3. Azure Key Vault                 -> Production sur Azure
4. AWS Secrets Manager             -> Production sur AWS
5. HashiCorp Vault                 -> Multi-cloud, self-hosted
6. Docker Secrets                  -> Conteneurs Docker

COMMENT ACCÉDER AUX SECRETS EN .NET :

Tous ces providers sont accessibles via IConfiguration !
Pas de changement de code nécessaire.
*/

/*
─────────────────────────────────────────────────────────────────
1. User Secrets — Développement local
─────────────────────────────────────────────────────────────────

Initialiser User Secrets :
dotnet user-secrets init --project MonApp.Server

Ajouter des secrets :
dotnet user-secrets set "Jwt:Secret" "ma_cle_secrete_locale_longue"
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;..."
dotnet user-secrets set "Stripe:SecretKey" "sk_test_..."

Lister les secrets :
dotnet user-secrets list

Supprimer un secret :
dotnet user-secrets remove "Jwt:Secret"

Les secrets sont stockés dans :
~/.microsoft/usersecrets/<guid>/secrets.json
-> Jamais commités dans Git !
*/

/*
─────────────────────────────────────────────────────────────────
2. Azure Key Vault — Production sur Azure
─────────────────────────────────────────────────────────────────

INSTALLATION :
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
dotnet add package Azure.Identity

Program.cs :

if (!builder.Environment.IsDevelopment())
{
    // En production : charger les secrets depuis Azure Key Vault
    var keyVaultUri = new Uri(builder.Configuration["Azure:KeyVaultUri"]!);

    // Utiliser Managed Identity (recommandé) :
    // -> Pas de credentials dans le code !
    // -> L'application Azure a un accès Key Vault via son identité managée
    builder.Configuration.AddAzureKeyVault(
        keyVaultUri,
        new DefaultAzureCredential());
}

// Dans les secrets Azure Key Vault, le nom "ConnectionStrings--DefaultConnection"
// correspond à builder.Configuration["ConnectionStrings:DefaultConnection"]
// (Les -- remplacent les : qui ne sont pas autorisés dans les noms Key Vault)

Configuration Azure Key Vault :
- Créer un Key Vault dans Azure Portal
- Ajouter les secrets (ConnectionStrings--DefaultConnection, Jwt--Secret, etc.)
- Assigner le rôle "Key Vault Secrets User" à l'App Service (Managed Identity)
*/

/*
─────────────────────────────────────────────────────────────────
3. Variables d'environnement — Docker / Kubernetes
─────────────────────────────────────────────────────────────────

docker-compose.yml :
services:
  api:
    image: monapp-api
    environment:
      - ConnectionStrings__DefaultConnection=Server=db;Password=prodpwd123!
      - Jwt__Secret=cle_secrete_tres_longue_en_production
      - Stripe__SecretKey=sk_live_...

Kubernetes Secret :
apiVersion: v1
kind: Secret
metadata:
  name: monapp-secrets
type: Opaque
data:
  jwt-secret: Y2xlX3NlY3JldGVfYmFzZTY0  # base64 de la valeur
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: api
        env:
        - name: Jwt__Secret
          valueFrom:
            secretKeyRef:
              name: monapp-secrets
              key: jwt-secret

Les variables d'environnement remplacent les configurations :
ConnectionStrings:DefaultConnection -> ConnectionStrings__DefaultConnection
(Les __ remplacent les : dans les noms de variables d'environnement)
*/


// ============================================================================
// [GUIDE] CHAPITRE 25 : SÉCURITÉ WEBASSEMBLY
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les limitations de sécurité WASM
[OK] Éviter d'exposer des secrets côté client
[OK] Implémenter la sécurité côté client ET serveur
[OK] Protéger contre la manipulation des données côté client
[OK] Auditer une application Blazor WASM
*/


// ----------------------------------------------------------------------------
// [ATTENTION] LIMITATIONS FONDAMENTALES DE WEBASSEMBLY
// ----------------------------------------------------------------------------

/*
RÈGLE D'OR : TOUT LE CODE CLIENT EST VISIBLE ET MODIFIABLE PAR L'UTILISATEUR !

En Blazor WebAssembly :
-> Les DLL .NET sont téléchargées dans le navigateur
-> Elles peuvent être décompilées avec des outils comme ILSpy, dnSpy
-> Le code C# peut être lu et analysé
-> Les variables JavaScript sont accessibles via la console
-> Les requêtes réseau sont visibles dans DevTools

CONSÉQUENCES :

[X] NE JAMAIS METTRE CÔTÉ CLIENT :
- Clés API secrètes (Stripe, SendGrid, etc.)
- Mots de passe de base de données
- Clés de chiffrement
- Logique d'autorisation sans validation serveur
- Chaînes de connexion
- Credentials de services tiers

[OK] CE QUI PEUT ÊTRE CÔTÉ CLIENT :
- Clés publiques (Stripe publishable key, Google Maps API key publique)
- URLs d'API
- Configuration d'affichage
- Logique métier NON SENSIBLE
- Validation côté client (toujours DOUBLER côté serveur !)
*/

/*
─────────────────────────────────────────────────────────────────
ANTI-PATTERN : Ce qu'il ne faut JAMAIS faire
─────────────────────────────────────────────────────────────────

[X] MAUVAIS : Validation d'autorisation seulement côté client

// Programme Blazor WASM (visible par l'utilisateur !)
@page "/admin"

@if (_estAdmin)  // <- L'utilisateur peut modifier cette variable !
{
    <div>Contenu admin ultra-secret</div>
}

@code {
    private bool _estAdmin = false;  // L'utilisateur peut changer cette valeur !

    protected override async Task OnInitializedAsync()
    {
        var state = await AuthProvider.GetAuthenticationStateAsync();
        _estAdmin = state.User.IsInRole("Admin");
        // Un utilisateur malveillant peut manipuler le state pour se faire passer pour Admin
    }
}

[X] MAUVAIS : Appel API sans vérification côté serveur

@code {
    // L'utilisateur peut modifier cet ID dans DevTools !
    private int _userIdCible = 123;

    private async Task ChargerDonneesUtilisateur()
    {
        // Si le serveur ne vérifie pas que l'utilisateur courant peut
        // accéder à cet ID, c'est une faille IDOR !
        var donnees = await Api.ObtenirUtilisateur(_userIdCible);
    }
}

[OK] BON : Vérification côté serveur OBLIGATOIRE

// Côté serveur (Controller ou Minimal API)
[HttpGet("/api/utilisateurs/{id}")]
[Authorize]
public async Task<IActionResult> ObtenirUtilisateur(int id)
{
    // Le serveur vérifie que l'utilisateur peut accéder à CET id
    var idUtilisateurConnecte = int.Parse(User.FindFirst("sub")!.Value);

    if (id != idUtilisateurConnecte && !User.IsInRole("Admin"))
        return Forbid(); // 403 Forbidden

    var utilisateur = await _service.ObtenirAsync(id);
    return Ok(utilisateur);
}
*/


// ----------------------------------------------------------------------------
// [SECURISE] STOCKER LES TOKENS CÔTÉ CLIENT — Securement
// ----------------------------------------------------------------------------

/*
OÙ STOCKER LES JWT TOKENS EN BLAZOR WASM ?

OPTION 1 : localStorage
POUR :  Simple à utiliser, persiste après fermeture du navigateur
CONTRE : Accessible via JavaScript -> Vulnérable au XSS
         Si XSS possible, un attaquant vole les tokens

OPTION 2 : sessionStorage
POUR :  Limité à l'onglet, meilleure que localStorage contre certaines attaques
CONTRE : Toujours accessible via JavaScript -> Toujours vulnérable au XSS

OPTION 3 : Cookie HttpOnly + SameSite=Strict
POUR :  JavaScript NE PEUT PAS lire les cookies HttpOnly !
         Protection XSS native
CONTRE : Nécessite un serveur pour gérer les cookies
         CSRF possible (mitiger avec SameSite=Strict)

OPTION 4 : Mémoire JavaScript (variable in-memory)
POUR :  Pas accessible depuis d'autres onglets
         Disparu à la fermeture de l'onglet (sécurité)
CONTRE : Perdu au refresh de la page (UX moins bonne)

RECOMMANDATION POUR BLAZOR WASM :
-> localStorage pour le Refresh Token (longue durée)
-> Mémoire in-app pour l'Access Token (courte durée)
-> Activer Content-Security-Policy pour minimiser le risque XSS
*/

// Services/SecureTokenStorage.cs
public class SecureTokenStorage
{
    private readonly ILocalStorageService _localStorage;
    private string? _accessTokenEnMemoire; // Stocké en mémoire JS (non persistent)

    public SecureTokenStorage(ILocalStorageService localStorage)
    {
        _localStorage = localStorage;
    }

    // Access Token : En mémoire seulement (perdu au refresh)
    public string? AccessToken
    {
        get => _accessTokenEnMemoire;
        set => _accessTokenEnMemoire = value;
    }

    // Refresh Token : Dans localStorage (persisté)
    public async Task<string?> ObtenirRefreshTokenAsync()
        => await _localStorage.GetItemAsync<string>("rt");

    public async Task SauvegarderRefreshTokenAsync(string token)
        => await _localStorage.SetItemAsync("rt", token);

    public async Task EffacerTousLesTokensAsync()
    {
        _accessTokenEnMemoire = null;
        await _localStorage.RemoveItemAsync("rt");
    }
}


// ----------------------------------------------------------------------------
// [RECHERCHE] AUDIT DE SÉCURITÉ BLAZOR WASM
// ----------------------------------------------------------------------------

/*
CHECKLIST DE SÉCURITÉ BLAZOR WEBASSEMBLY :

APPLICATION :
[WHITE_SQUARE] Aucun secret dans le code C# ou JavaScript
[WHITE_SQUARE] Aucun secret dans appsettings.json côté client
[WHITE_SQUARE] Toutes les autorisations vérifiées côté SERVEUR
[WHITE_SQUARE] Les IDs ne peuvent pas être manipulés (IDOR protection côté serveur)
[WHITE_SQUARE] Validation des inputs côté serveur (pas seulement client)
[WHITE_SQUARE] Paramètres d'URL non utilisés sans validation

AUTHENTIFICATION :
[WHITE_SQUARE] JWT avec durée courte (max 15-30 minutes)
[WHITE_SQUARE] Refresh Tokens stockés côté serveur + révocables
[WHITE_SQUARE] Logout révoque le Refresh Token côté serveur
[WHITE_SQUARE] Protection brute-force sur login (Rate Limiting)
[WHITE_SQUARE] HTTPS obligatoire (HSTS activé)

HEADERS HTTP :
[WHITE_SQUARE] Content-Security-Policy configuré
[WHITE_SQUARE] X-Frame-Options = DENY
[WHITE_SQUARE] X-Content-Type-Options = nosniff
[WHITE_SQUARE] Strict-Transport-Security activé
[WHITE_SQUARE] X-Powered-By supprimé

DONNÉES :
[WHITE_SQUARE] MarkupString sanitisé avant affichage
[WHITE_SQUARE] Paramètres d'URL encodés
[WHITE_SQUARE] Inputs utilisateur validés avant utilisation
[WHITE_SQUARE] Pas de données sensibles dans les logs

RÉSEAU :
[WHITE_SQUARE] CORS restrictif (pas AllowAnyOrigin en production)
[WHITE_SQUARE] Rate Limiting sur les endpoints sensibles
[WHITE_SQUARE] TLS 1.2+ seulement
[WHITE_SQUARE] Certificats à jour

OUTILS D'AUDIT :
-> OWASP ZAP (scan automatique)
-> Burp Suite (test manuel)
-> dotnet-security-audit (scan des packages)
-> Snyk (vulnérabilités dans les dépendances)
-> SecurityHeaders.com (vérifier les headers)
-> SSLLabs.com (vérifier la configuration TLS)
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE — PARTIE 8
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
EXERCICE : SÉCURISER UNE APPLICATION BLAZOR
═══════════════════════════════════════════════════════════════

OBJECTIF : Auditer et sécuriser une application existante

TÂCHES :

1. EN-TÊTES DE SÉCURITÉ :
   a) Implémenter SecurityHeadersMiddleware
   b) Configurer Content-Security-Policy pour Blazor WASM
   c) Vérifier le score sur securityheaders.com (objectif A+)
   d) Supprimer les en-têtes qui révèlent la technologie

2. JWT + REFRESH TOKENS :
   a) Implémenter TokenService côté serveur
   b) Endpoint POST /api/auth/login retourne Access + Refresh Token
   c) Endpoint POST /api/auth/refresh valide et rotate le Refresh Token
   d) Endpoint POST /api/auth/logout révoque le Refresh Token
   e) Implémenter TokenRefreshService côté client (auto-refresh)
   f) Stocker Refresh Token dans localStorage, Access Token en mémoire

3. RATE LIMITING :
   a) Configurer la policy "login" (5 tentatives / 15 min par IP)
   b) Configurer la policy globale (200 req / min par utilisateur)
   c) Retourner 429 avec Retry-After header
   d) Tester avec un script ou Postman

4. AUDIT DE SÉCURITÉ :
   a) Scanner les packages avec dotnet list package --vulnerable
   b) Vérifier qu'aucun secret n'est dans le code (grep pour "password", "secret", "key")
   c) Tester la protection IDOR (accéder à une ressource d'un autre user)
   d) Vérifier que [Authorize] est sur tous les endpoints sensibles

COMPÉTENCES :
[OK] SecurityHeadersMiddleware + CSP
[OK] JWT Access + Refresh Token pattern
[OK] Rate Limiting .NET 8
[OK] Audit de sécurité
[OK] Principes Never Trust The Client
═══════════════════════════════════════════════════════════════
*/


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 8

[OK] CHAPITRE 24 - SÉCURITÉ AVANCÉE :
- SecurityHeadersMiddleware : X-Frame-Options, X-Content-Type-Options,
  Strict-Transport-Security, Permissions-Policy
- Content-Security-Policy : Prévention XSS, 'wasm-unsafe-eval' pour Blazor WASM
- suppression Server, X-Powered-By : Cacher la technologie
- XSS : Blazor encode automatiquement, MAIS MarkupString nécessite sanitisation
- HtmlSanitizerService avec bibliothèque HtmlSanitizer (Ganss)
- JWT + Refresh Tokens : Access Token 15min + Refresh Token 30 jours révocable
- Rotation des Refresh Tokens : Détecter et révoquer toute la chaîne si réutilisation
- TokenRefreshService côté client : Auto-refresh 2 minutes avant expiration
- Rate Limiting .NET 7+ : FixedWindow, SlidingWindow, TokenBucket
- Policies : login (5/15min), api_auth (100/min), export (10/heure)
- CORS restrictif : WithOrigins explicite, jamais AllowAnyOrigin en prod
- Gestion des secrets : User Secrets, Azure Key Vault, Variables d'env

[OK] CHAPITRE 25 - SÉCURITÉ WEBASSEMBLY :
- Règle d'or : Tout le code client est visible et modifiable !
- Ne jamais mettre de secrets dans le code WASM
- Validation obligatoire côté serveur (même si validé côté client)
- Protection IDOR : Vérifier que l'utilisateur peut accéder à la ressource
- Stockage des tokens : Access Token en mémoire, Refresh Token en localStorage
- SecureTokenStorage : Pattern recommandé pour Blazor WASM
- Checklist de sécurité complète (headers, auth, données, réseau)
- Outils d'audit : OWASP ZAP, Burp Suite, dotnet-security-audit, Snyk

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 9
- Dockerisation de l'application Blazor
- CI/CD avec GitHub Actions
- Build et déploiement automatisé
- Monitoring et observabilité (Serilog, Application Insights)
═══════════════════════════════════════════════════════════════
*/

// ============================================================================
// [LIVRE] BLAZOR - PARTIE 9 : DEVOPS & PRODUCTION
// ============================================================================
//
// CHAPITRE 26 : Dockerisation
// CHAPITRE 27 : CI/CD (GitHub Actions, Azure DevOps)
// CHAPITRE 28 : Observabilité (Logging, Serilog, Monitoring)
//
// [TEMPS] TEMPS ESTIMÉ : ~10-12 heures
// [DOCS] PRÉREQUIS : Parties 1-8 complétées
// ============================================================================

/*
[OBJECTIF] PHILOSOPHIE DE CETTE PARTIE

POURQUOI DEVOPS ?
-> Votre application peut être parfaite localement...
  ...mais inutile si elle n'est pas déployée !
-> DevOps = Automatiser le chemin du code à la production
-> Zéro erreur humaine, déploiement reproductible, retour arrière facile

AVANT DEVOPS (manuel) :
  Développeur -> Compile sur sa machine -> Copie les fichiers sur le serveur ->
  Croise les doigts -> Problème de version -> Panic...

AVEC DEVOPS (automatisé) :
  Développeur -> git push -> Tests automatiques -> Build Docker -> Deploy -> [OK]

CE QUE VOUS ALLEZ CONSTRUIRE :
  - Dockeriser votre application Blazor + API
  - Pipeline CI/CD qui déploie automatiquement à chaque push
  - Monitoring avec Serilog + Application Insights
  - Alertes quand quelque chose se passe mal
*/


// ============================================================================
// [GUIDE] CHAPITRE 26 : DOCKERISATION
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre Docker et pourquoi l'utiliser
[OK] Créer un Dockerfile optimisé pour Blazor WASM
[OK] Créer un Dockerfile optimisé pour ASP.NET Core (API)
[OK] Orchestrer avec Docker Compose
[OK] Configurer un reverse proxy avec Nginx
[OK] Optimiser les images Docker (taille, sécurité)
*/


// ----------------------------------------------------------------------------
// [DOCKER] DOCKER — Comprendre les concepts fondamentaux
// ----------------------------------------------------------------------------

/*
QU'EST-CE QUE DOCKER ?

SANS DOCKER :
"Ça marche sur ma machine !"
-> Chaque serveur a des versions différentes (.NET, OS, bibliothèques)
-> Ce qui fonctionne chez le dev ne fonctionne pas en prod
-> Configuration manuelle de chaque serveur

AVEC DOCKER :
-> Un CONTENEUR = Boîte hermétique avec TOUT ce dont l'app a besoin
-> Identique partout (dev, staging, production)
-> Déployable en secondes
-> Isolation totale entre applications

ANALOGIE PARFAITE :
Conteneur maritime = Contient des marchandises emballées
-> Peut être chargé sur n'importe quel bateau
-> Le bateau ne sait pas ce qu'il y a dedans
-> Contenu identique peu importe le port de destination

VOCABULAIRE DOCKER :

IMAGE :     Le "moule" (template) d'un conteneur
            Fichier read-only avec l'app + dépendances + config
            Exemple : "monapp:v1.2.3"

CONTENEUR : L'instance en cours d'exécution d'une image
            Comme un processus isolé
            Peut être démarré, arrêté, copié, supprimé

REGISTRY :  Dépôt d'images Docker
            -> Docker Hub (public)
            -> Azure Container Registry (privé, recommandé)
            -> GitHub Container Registry

DOCKERFILE : Script qui décrit comment construire une image
             Chaque ligne = une couche de l'image

DOCKER COMPOSE : Outil pour orchestrer PLUSIEURS conteneurs
                 Exemple : App + BDD + Redis + Nginx

COMMANDES ESSENTIELLES :
docker build -t monapp:latest .          -> Construire une image
docker run -p 8080:80 monapp:latest      -> Lancer un conteneur
docker ps                                 -> Lister les conteneurs actifs
docker logs monconteneur                  -> Voir les logs
docker exec -it monconteneur bash        -> Entrer dans un conteneur
docker stop monconteneur                  -> Arrêter un conteneur
docker rm monconteneur                    -> Supprimer un conteneur
docker images                             -> Lister les images
docker rmi monapp:latest                  -> Supprimer une image
docker pull nginx:alpine                  -> Télécharger une image
docker push registry.io/monapp:latest    -> Envoyer vers un registry
*/


// ----------------------------------------------------------------------------
// [FICHIER] DOCKERFILE BLAZOR WEBASSEMBLY + ASP.NET CORE (HOSTED)
// ----------------------------------------------------------------------------

/*
STRATÉGIE MULTI-STAGE BUILD :

POURQUOI ?
-> Le build .NET nécessite le SDK (2 GB+)
-> L'exécution ne nécessite que le runtime (200 MB)
-> Multi-stage = Build avec SDK, copie uniquement le résultat dans l'image finale

IMAGE FINALE RÉSULTANTE :
-> ~150-200 MB au lieu de 2+ GB
-> Pas d'outils de build dans l'image de prod (sécurité)
-> Moins de surface d'attaque

SCHÉMA :

  Stage 1 "build" (SDK ~2GB)   Stage 2 "final" (~150MB)
  ┌──────────────────────┐     ┌──────────────────────┐
  │ .NET SDK             │     │ .NET Runtime seulement│
  │ dotnet restore       │ ->->-> │ Fichiers publiés      │
  │ dotnet publish       │     │ Configuration         │
  └──────────────────────┘     └──────────────────────┘
*/

/*
─────────────────────────────────────────────────────────────────
Dockerfile pour application Blazor WASM hébergée (Client + Server)
─────────────────────────────────────────────────────────────────
Fichier : Dockerfile (à la racine de la solution)
*/

/*
# ╔══════════════════════════════════════════════════════════════╗
# ║  STAGE 1 : BUILD                                             ║
# ║  Utiliser l'image SDK complète pour compiler l'application   ║
# ╚══════════════════════════════════════════════════════════════╝
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

# Définir le répertoire de travail dans le conteneur
WORKDIR /src

# ─── Optimisation du cache Docker ─────────────────────────────
# Copier UNIQUEMENT les fichiers .csproj en premier
# Docker cache chaque couche : si les .csproj n'ont pas changé,
# la restauration NuGet est mise en cache -> Build plus rapide !

# Copier les projets (adapter selon votre structure)
COPY ["MonApp.Server/MonApp.Server.csproj", "MonApp.Server/"]
COPY ["MonApp.Client/MonApp.Client.csproj", "MonApp.Client/"]
COPY ["MonApp.Shared/MonApp.Shared.csproj", "MonApp.Shared/"]

# Restaurer les dépendances NuGet (couche cachée si .csproj inchangé)
RUN dotnet restore "MonApp.Server/MonApp.Server.csproj"

# ─── Build de l'application ───────────────────────────────────
# Copier TOUT le reste du code source
COPY . .

# Compiler en Release (optimisations activées)
WORKDIR "/src/MonApp.Server"
RUN dotnet build "MonApp.Server.csproj" -c Release -o /app/build

# ─── Publication ──────────────────────────────────────────────
# Publier = Préparer les fichiers pour la production
# --no-restore : Pas besoin de restaurer à nouveau
RUN dotnet publish "MonApp.Server.csproj" \
    -c Release \
    -o /app/publish \
    --no-restore \
    /p:UseAppHost=false

# ╔══════════════════════════════════════════════════════════════╗
# ║  STAGE 2 : RUNTIME FINAL                                     ║
# ║  Image légère pour l'exécution en production                 ║
# ╚══════════════════════════════════════════════════════════════╝
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final

# ─── Configuration de sécurité ────────────────────────────────
# Ne PAS exécuter en tant que root (bonne pratique de sécurité)
# L'utilisateur "app" est créé par l'image aspnet de Microsoft
USER app

WORKDIR /app

# Exposer le port HTTP (pas HTTPS : le reverse proxy gère TLS)
EXPOSE 8080

# Copier les fichiers publiés depuis l'étape build
COPY --from=build /app/publish .

# Variables d'environnement
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:8080

# Commande de démarrage
ENTRYPOINT ["dotnet", "MonApp.Server.dll"]
*/


/*
─────────────────────────────────────────────────────────────────
.dockerignore — Fichiers à exclure (comme .gitignore)
─────────────────────────────────────────────────────────────────
Fichier : .dockerignore

**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
*/


// ----------------------------------------------------------------------------
// [OUTIL] DOCKER COMPOSE — Orchestrer plusieurs services
// ----------------------------------------------------------------------------

/*
DOCKER COMPOSE = Définir et lancer plusieurs conteneurs ensemble

NOTRE STACK DE PRODUCTION :

┌─────────────────────────────────────────────────────────┐
│                    Internet                             │
└──────────────────┬──────────────────────────────────────┘
                   │ HTTPS :443
┌──────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────────┐
│                 Nginx (Reverse Proxy)                   │
│  -> Terminaison TLS (HTTPS)                              │
│  -> Compression Gzip/Brotli                             │
│  -> Caching des fichiers statiques                       │
│  -> Load balancing (si plusieurs instances)              │
└──────────┬──────────────────────────────────────────────┘
           │ HTTP interne
┌──────────[BLACK_DOWN-POINTING_TRIANGLE]──────────┐    ┌──────────────────┐
│  ASP.NET Core API   │    │  PostgreSQL       │
│  + Blazor WASM      │[BLACK_LEFT-POINTING_POINTER]──[BLACK_RIGHT-POINTING_POINTER]│  (Base de données)│
└─────────────────────┘    └──────────────────┘
           │
┌──────────[BLACK_DOWN-POINTING_TRIANGLE]──────────┐
│  Redis              │
│  (Cache + Sessions) │
└─────────────────────┘
*/

/*
─────────────────────────────────────────────────────────────────
docker-compose.yml — Stack complète
─────────────────────────────────────────────────────────────────

version: '3.9'

# ─── Réseau interne (les services se parlent par leur nom) ────
networks:
  monapp_network:
    driver: bridge

# ─── Volumes persistants (données survivent aux redémarrages) ─
volumes:
  postgres_data:
  redis_data:
  nginx_certs:

services:

  # ══════════════════════════════════════════════════════════
  # APPLICATION ASP.NET CORE + BLAZOR WASM
  # ══════════════════════════════════════════════════════════
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: final
    image: monapp:latest
    container_name: monapp_api
    restart: unless-stopped         # Redémarre automatiquement si crash

    # Variables d'environnement (NE PAS METTRE LES SECRETS ICI !)
    # Utiliser un fichier .env ou Docker Secrets pour les secrets
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ASPNETCORE_URLS=http://+:8080

    # Fichier de secrets (NON commité dans Git !)
    env_file:
      - .env.production             # Contient les vrais secrets

    # Santé du conteneur : Docker redémarre si /health répond en erreur
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

    # Dépendances : Démarrer après la BDD et Redis
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

    networks:
      - monapp_network

    # Ressources (limiter pour éviter qu'une app monopolise le serveur)
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
        reservations:
          memory: 256M

  # ══════════════════════════════════════════════════════════
  # POSTGRESQL — Base de données
  # ══════════════════════════════════════════════════════════
  postgres:
    image: postgres:16-alpine       # Alpine = image légère (~30MB)
    container_name: monapp_postgres
    restart: unless-stopped

    environment:
      POSTGRES_USER_FILE: /run/secrets/db_user
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: monapp_prod
      PGDATA: /var/lib/postgresql/data/pgdata  # Sous-dossier pour les données

    volumes:
      - postgres_data:/var/lib/postgresql/data  # Persister les données

    # NE PAS exposer vers l'extérieur ! Seulement interne au réseau Docker
    # ports:
    #   - "5432:5432"  <- INTERDIT en production !

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U monapp -d monapp_prod"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

    networks:
      - monapp_network

    deploy:
      resources:
        limits:
          memory: 256M

  # ══════════════════════════════════════════════════════════
  # REDIS — Cache et sessions
  # ══════════════════════════════════════════════════════════
  redis:
    image: redis:7-alpine
    container_name: monapp_redis
    restart: unless-stopped

    command: >
      redis-server
      --requirepass ${REDIS_PASSWORD}
      --maxmemory 128mb
      --maxmemory-policy allkeys-lru
      --appendonly yes

    volumes:
      - redis_data:/data

    healthcheck:
      test: ["CMD", "redis-cli", "--no-auth-warning",
             "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

    networks:
      - monapp_network

    deploy:
      resources:
        limits:
          memory: 128M

  # ══════════════════════════════════════════════════════════
  # NGINX — Reverse Proxy
  # ══════════════════════════════════════════════════════════
  nginx:
    image: nginx:1.25-alpine
    container_name: monapp_nginx
    restart: unless-stopped

    ports:
      - "80:80"       # HTTP (redirige vers HTTPS)
      - "443:443"     # HTTPS

    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro      # Config Nginx
      - ./nginx/conf.d:/etc/nginx/conf.d:ro               # Sites
      - nginx_certs:/etc/nginx/ssl                        # Certificats TLS
      - ./nginx/www:/var/www/certbot:ro                   # Let's Encrypt

    depends_on:
      app:
        condition: service_healthy

    networks:
      - monapp_network

# ─── Docker Secrets (alternative sécurisée aux variables d'env) ─
secrets:
  db_user:
    file: ./secrets/db_user.txt
  db_password:
    file: ./secrets/db_password.txt
*/

/*
─────────────────────────────────────────────────────────────────
.env.production — Variables d'environnement (JAMAIS dans Git !)
─────────────────────────────────────────────────────────────────
Ajouter .env.production dans .gitignore !

ConnectionStrings__DefaultConnection=Host=postgres;Database=monapp_prod;Username=monapp;Password=VotreMotDePasse!
Jwt__Secret=VotreCleSecreteTresLonguePourLaProduction_MinimumSeize_Caracteres!
Jwt__Issuer=https://monapp.com
Jwt__Audience=https://app.monapp.com
Redis__ConnectionString=redis:6379,password=VotreMotDePasse
REDIS_PASSWORD=VotreMotDePasse
Stripe__SecretKey=sk_live_votre_cle
SendGrid__ApiKey=SG.votre_cle
ApplicationInsights__ConnectionString=InstrumentationKey=...
*/

/*
─────────────────────────────────────────────────────────────────
nginx/conf.d/monapp.conf — Configuration Nginx
─────────────────────────────────────────────────────────────────

# ─── Redirection HTTP -> HTTPS ─────────────────────────────────
server {
    listen 80;
    server_name monapp.com www.monapp.com app.monapp.com;

    # Let's Encrypt validation
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Tout le reste -> HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

# ─── Serveur HTTPS principal ───────────────────────────────────
server {
    listen 443 ssl http2;
    server_name app.monapp.com;

    # Certificats TLS (Let's Encrypt via Certbot)
    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    # Configuration TLS sécurisée
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP Stapling (vérifie validité du certificat)
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;

    # ─── Compression ──────────────────────────────────────────
    gzip on;
    gzip_vary on;
    gzip_min_length 256;
    gzip_types
        application/javascript
        application/wasm
        application/octet-stream
        text/css
        text/plain
        application/json;

    # Compression Brotli (si module installé - recommandé pour WASM !)
    # brotli on;
    # brotli_types application/wasm application/octet-stream text/css;

    # ─── Fichiers statiques Blazor WASM ───────────────────────
    # Cache agressif pour les fichiers avec hash dans le nom
    location ~* \.(wasm|dll|dat)$ {
        proxy_pass http://app:8080;
        add_header Cache-Control "public, max-age=604800, immutable";
        add_header Content-Encoding identity;  # Ne pas re-compresser
        gzip off;  # Blazor gère sa propre compression
    }

    # Cache moyen pour CSS/JS
    location ~* \.(css|js|svg|png|jpg|ico|woff2)$ {
        proxy_pass http://app:8080;
        add_header Cache-Control "public, max-age=86400";
    }

    # ─── Proxy vers l'application ─────────────────────────────
    location / {
        proxy_pass http://app:8080;
        proxy_http_version 1.1;

        # Headers pour que l'app connaisse l'IP réelle du client
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket (pour Blazor Server SignalR)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts
        proxy_connect_timeout 30s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;

        # Buffer (pour les grosses réponses)
        proxy_buffer_size          128k;
        proxy_buffers              4 256k;
        proxy_busy_buffers_size    256k;
    }

    # ─── Sécurité ──────────────────────────────────────────────
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Cacher la version Nginx
    server_tokens off;

    # ─── Rate Limiting Nginx ───────────────────────────────────
    # Défini dans nginx.conf : limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    location /api/auth/login {
        limit_req zone=api burst=5 nodelay;
        proxy_pass http://app:8080;
    }
}
*/


// ----------------------------------------------------------------------------
// [RAPIDE] COMMANDES DOCKER UTILES EN PRODUCTION
// ----------------------------------------------------------------------------

/*
CONSTRUIRE ET DÉMARRER L'APPLICATION :

# Construire les images et démarrer tous les services
docker compose up --build -d

# Voir les logs en temps réel
docker compose logs -f

# Voir les logs d'un service spécifique
docker compose logs -f app

# Redémarrer un service (après un déploiement)
docker compose up -d --no-deps --build app

# Arrêter tous les services
docker compose down

# Arrêter ET supprimer les volumes ([ATTENTION] supprime les données !)
docker compose down -v

# Vérifier l'état des services
docker compose ps

# Exécuter une commande dans un conteneur
docker compose exec app dotnet ef database update

# Ouvrir un shell dans un conteneur
docker compose exec app bash

# Appliquer les migrations EF Core
docker compose exec app dotnet ef database update \
  --project MonApp.Infrastructure \
  --startup-project MonApp.Server

# Backup de la base de données
docker compose exec postgres pg_dump \
  -U monapp monapp_prod > backup_$(date +%Y%m%d_%H%M%S).sql

# Mise à jour sans downtime (rolling update)
docker compose pull
docker compose up -d --no-deps --build app
*/


// ============================================================================
// [GUIDE] CHAPITRE 27 : CI/CD
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les concepts CI/CD
[OK] Créer un pipeline GitHub Actions complet
[OK] Automatiser les tests, build et déploiement
[OK] Gérer les secrets dans les pipelines
[OK] Déployer sur Azure App Service et VPS
[OK] Implémenter des stratégies de déploiement (Blue/Green, Rolling)
*/


// ----------------------------------------------------------------------------
// [SYNC] CI/CD — Les concepts fondamentaux
// ----------------------------------------------------------------------------

/*
CI = Continuous Integration (Intégration Continue)
CD = Continuous Delivery/Deployment (Livraison/Déploiement Continu)

POURQUOI CI/CD ?

SANS CI/CD :
  Développeur -> Compile localement -> Envoie par FTP -> Espère que ça marche
  -> Merge conflicts ignorés -> Bugs en prod -> "Fonctionne chez moi !"
  -> Déploiements stressants, souvent la nuit ou le weekend

AVEC CI/CD :
  git push -> Tests automatiques -> Build Docker -> Déploiement -> Notification
  -> Erreurs détectées immédiatement -> Rollback automatique si problème
  -> Déploiements quotidiens sans stress

FLUX CI/CD TYPIQUE :

  ┌────────────────────────────────────────────────────────────┐
  │                    PIPELINE CI/CD                          │
  │                                                            │
  │  git push       ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐   │
  │  ──────────────[BLACK_RIGHT-POINTING_POINTER]│Tests │─[BLACK_RIGHT-POINTING_POINTER]│Build │─[BLACK_RIGHT-POINTING_POINTER]│ Push │─[BLACK_RIGHT-POINTING_POINTER]│Deploy│   │
  │                 │Unit  │  │Docker│  │Regis.│  │ Prod │   │
  │                 │Integ.│  │Image │  │ ACR  │  │      │   │
  │                 └──────┘  └──────┘  └──────┘  └──────┘   │
  │                    │                              │        │
  │                    └──── Notification Email/Slack ┘        │
  └────────────────────────────────────────────────────────────┘

ÉTAPES DÉTAILLÉES :

1. TRIGGER : Push sur main/develop -> Pipeline démarre
2. LINT    : Vérifier le style de code (dotnet format)
3. BUILD   : Compiler l'application (dotnet build)
4. TEST    : Lancer les tests unitaires et d'intégration
5. ANALYSE : Analyse de sécurité des packages (vulnérabilités)
6. DOCKER  : Construire l'image Docker
7. PUSH    : Envoyer l'image vers le registry
8. DEPLOY  : Déployer sur le serveur de staging
9. SMOKE   : Tests de fumée (l'app répond-elle ?)
10. PROD   : Déployer en production (si staging OK)
11. NOTIFY : Notification de succès/échec
*/


// ----------------------------------------------------------------------------
// [OCTOPUS] GITHUB ACTIONS — Pipeline CI/CD complet
// ----------------------------------------------------------------------------

/*
GITHUB ACTIONS = CI/CD intégré directement dans GitHub
-> Gratuit pour les repos publics
-> 2000 minutes/mois pour les repos privés (plan gratuit)
-> Fichiers YAML dans .github/workflows/

CONCEPTS GITHUB ACTIONS :

WORKFLOW     : Pipeline complet (fichier .yml)
JOB          : Ensemble d'étapes exécutées sur la même machine
STEP         : Une action ou commande dans un Job
RUNNER       : La machine virtuelle qui exécute les Jobs
  ubuntu-latest -> Ubuntu Linux (le plus utilisé)
  windows-latest -> Windows Server
  macos-latest  -> macOS

TRIGGER (ON) : Événement qui déclenche le workflow
  push         -> À chaque push
  pull_request -> À chaque PR
  schedule     -> Périodiquement (cron)
  workflow_dispatch -> Manuel
*/

/*
─────────────────────────────────────────────────────────────────
.github/workflows/ci-cd.yml — Pipeline complet
─────────────────────────────────────────────────────────────────

name: [RAPIDE] CI/CD Pipeline

# ─── TRIGGERS : Quand ce pipeline se déclenche ────────────────
on:
  # Sur push vers les branches principales
  push:
    branches:
      - main          # Production
      - develop       # Staging
    paths-ignore:
      - '**.md'       # Ignorer les changements de documentation
      - '.gitignore'

  # Sur Pull Request (pour valider avant merge)
  pull_request:
    branches:
      - main
      - develop

  # Déclenchement manuel depuis l'interface GitHub
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environnement cible'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

# ─── VARIABLES GLOBALES ────────────────────────────────────────
env:
  DOTNET_VERSION: '8.0.x'
  REGISTRY: ghcr.io                           # GitHub Container Registry
  IMAGE_NAME: ${{ github.repository }}         # owner/repo-name

# ─── JOBS ──────────────────────────────────────────────────────
jobs:

  # ══════════════════════════════════════════════════════════════
  # JOB 1 : Tests et qualité du code
  # ══════════════════════════════════════════════════════════════
  tests:
    name: [TEST] Tests & Qualité
    runs-on: ubuntu-latest

    # Service PostgreSQL pour les tests d'intégration
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_PASSWORD: test_password
          POSTGRES_DB: monapp_test
        ports:
          - 5432:5432
        # Attendre que Postgres soit prêt
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      # ─── 1. Récupérer le code source ───────────────────────
      - name: [ENTREE] Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0   # Historique complet (pour l'analyse SonarCloud)

      # ─── 2. Configurer .NET ─────────────────────────────────
      - name: [OUTIL] Configurer .NET ${{ env.DOTNET_VERSION }}
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      # ─── 3. Cache NuGet ────────────────────────────────────
      # Cache les packages NuGet pour accélérer les builds suivants
      - name: [PACKAGE] Cache NuGet
        uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
          restore-keys: |
            ${{ runner.os }}-nuget-

      # ─── 4. Restaurer les packages ─────────────────────────
      - name: [PACKAGE] Restaurer les packages NuGet
        run: dotnet restore MonApp.sln

      # ─── 5. Vérifier le formatage ──────────────────────────
      - name: [DESIGN] Vérifier le style de code
        run: dotnet format MonApp.sln --verify-no-changes --severity warn
        continue-on-error: true  # Ne pas bloquer sur le style

      # ─── 6. Compiler ────────────────────────────────────────
      - name: [OUTIL] Compiler la solution
        run: >
          dotnet build MonApp.sln
          --no-restore
          -c Release
          -warnaserror   # Traiter les warnings comme des erreurs

      # ─── 7. Tests unitaires ─────────────────────────────────
      - name: [TEST] Exécuter les tests unitaires
        run: >
          dotnet test MonApp.Tests.Unit/MonApp.Tests.Unit.csproj
          --no-build
          -c Release
          --logger trx
          --results-directory TestResults/Unit
          --collect:"XPlat Code Coverage"
          /p:CollectCoverage=true
          /p:CoverletOutputFormat=opencover

      # ─── 8. Tests d'intégration ─────────────────────────────
      - name: [LIEN] Exécuter les tests d'intégration
        run: >
          dotnet test MonApp.Tests.Integration/MonApp.Tests.Integration.csproj
          --no-build
          -c Release
          --logger trx
          --results-directory TestResults/Integration
        env:
          ConnectionStrings__DefaultConnection: >-
            Host=localhost;
            Database=monapp_test;
            Username=postgres;
            Password=test_password

      # ─── 9. Publier les résultats de tests ──────────────────
      - name: [GRAPHIQUE] Publier les résultats de tests
        uses: dorny/test-reporter@v1
        if: success() || failure()   # Toujours publier (même si tests échouent)
        with:
          name: Tests .NET
          path: TestResults/**/*.trx
          reporter: dotnet-trx

      # ─── 10. Couverture du code ─────────────────────────────
      - name: [HAUSSE] Rapport de couverture
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: TestResults/**/*.xml

      # ─── 11. Analyse des vulnérabilités ─────────────────────
      - name: [VERROUILLE] Scanner les vulnérabilités NuGet
        run: dotnet list package --vulnerable --include-transitive
        continue-on-error: true  # Informationnel

  # ══════════════════════════════════════════════════════════════
  # JOB 2 : Build de l'image Docker
  # ══════════════════════════════════════════════════════════════
  build-docker:
    name: [DOCKER] Build & Push Docker
    runs-on: ubuntu-latest
    needs: tests          # N'exécuter QUE si les tests passent !

    # Seulement sur main et develop (pas sur les PRs)
    if: github.event_name != 'pull_request'

    outputs:
      # Passer le tag de l'image aux jobs suivants
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
      - name: [ENTREE] Checkout code
        uses: actions/checkout@v4

      # ─── 1. Se connecter au Container Registry ──────────────
      - name: [CLE] Login GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}   # Automatique !

      # ─── 2. Métadonnées de l'image (tags, labels) ───────────
      - name: [LABEL] Générer les métadonnées Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            # Tag avec le SHA court du commit (ex: sha-abc1234)
            type=sha,prefix=sha-,format=short
            # Tag avec le nom de la branche (ex: main, develop)
            type=ref,event=branch
            # Tag latest uniquement sur main
            type=raw,value=latest,enable={{is_default_branch}}
            # Tag avec la date (ex: 20240101)
            type=raw,value={{date 'YYYYMMDD'}},enable={{is_default_branch}}

      # ─── 3. Build et cache Docker ───────────────────────────
      - name: [OUTIL] Configurer Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: [DOCKER] Build et Push l'image Docker
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          # Cache des couches Docker entre les builds -> Build plus rapide !
          cache-from: type=gha
          cache-to: type=gha,mode=max
          # Build pour plusieurs architectures (linux/amd64 pour les serveurs)
          platforms: linux/amd64

      # ─── 4. Scan de sécurité de l'image ─────────────────────
      - name: [VERROUILLE] Scanner l'image avec Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          format: table
          severity: CRITICAL,HIGH
          exit-code: 0   # Ne pas bloquer (informationnel)

  # ══════════════════════════════════════════════════════════════
  # JOB 3 : Déploiement Staging
  # ══════════════════════════════════════════════════════════════
  deploy-staging:
    name: [WEB] Deploy -> Staging
    runs-on: ubuntu-latest
    needs: build-docker
    if: github.ref == 'refs/heads/develop'

    # Environnement GitHub (pour les règles d'approbation et les secrets)
    environment:
      name: staging
      url: https://staging.monapp.com

    steps:
      - name: [ENTREE] Checkout code
        uses: actions/checkout@v4

      # ─── Déploiement SSH sur VPS ─────────────────────────────
      - name: [RAPIDE] Déployer sur le VPS de staging
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.STAGING_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          port: 22
          script: |
            # Se placer dans le répertoire de l'app
            cd /opt/monapp-staging

            # Récupérer la nouvelle image
            echo ${{ secrets.GITHUB_TOKEN }} | \
              docker login ghcr.io -u ${{ github.actor }} --password-stdin

            # Mettre à jour la variable d'image
            export IMAGE_TAG=${{ needs.build-docker.outputs.image-tag }}

            # Mettre à jour et redémarrer (sans downtime)
            docker compose pull app
            docker compose up -d --no-deps app

            # Vérifier que l'app est démarrée
            sleep 15
            docker compose ps

            # Test de santé
            curl -f https://staging.monapp.com/health || \
              (docker compose logs app && exit 1)

      # ─── Tests de fumée sur staging ─────────────────────────
      - name: [HOT] Tests de fumée
        run: |
          # Vérifier que les pages principales répondent
          curl -sf https://staging.monapp.com | grep -q "MonApp" || exit 1
          curl -sf https://staging.monapp.com/api/health | grep -q '"status":"Healthy"' || exit 1
          echo "[OK] Tests de fumée réussis !"

  # ══════════════════════════════════════════════════════════════
  # JOB 4 : Déploiement Production
  # ══════════════════════════════════════════════════════════════
  deploy-production:
    name: [OBJECTIF] Deploy -> Production
    runs-on: ubuntu-latest
    needs: build-docker
    if: github.ref == 'refs/heads/main'

    # Protection : Nécessite approbation manuelle !
    environment:
      name: production
      url: https://monapp.com

    steps:
      - name: [ENTREE] Checkout code
        uses: actions/checkout@v4

      # ─── Déploiement Azure App Service ──────────────────────
      # Option A : Azure App Service (via GitHub Action officielle)
      - name: [CLE] Login Azure
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: [RAPIDE] Déployer sur Azure App Service
        uses: azure/webapps-deploy@v3
        with:
          app-name: monapp-production
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

      # ─── Tests de fumée en production ───────────────────────
      - name: [HOT] Tests de fumée production
        run: |
          sleep 30  # Attendre le démarrage
          curl -sf https://monapp.com/api/health | grep -q '"status":"Healthy"' || exit 1
          echo "[OK] Production en bonne santé !"

      # ─── Notification Slack ──────────────────────────────────
      - name: [SPEECH_BALLOON] Notification Slack
        if: always()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          channel-id: 'C1234567890'
          slack-message: |
            ${{ job.status == 'success' && '[OK]' || '[X]' }} Déploiement Production
            *Commit:* ${{ github.sha }}
            *Auteur:* ${{ github.actor }}
            *Message:* ${{ github.event.head_commit.message }}
            *Status:* ${{ job.status }}
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
*/


// ----------------------------------------------------------------------------
// [LISTE] CONFIGURER LES SECRETS GITHUB
// ----------------------------------------------------------------------------

/*
SECRETS GITHUB = Variables secrètes stockées dans GitHub
-> Jamais exposées dans les logs
-> Chiffrées au repos
-> Accessible via ${{ secrets.NOM_DU_SECRET }}

COMMENT AJOUTER DES SECRETS :
GitHub -> Repo -> Settings -> Secrets and variables -> Actions -> New repository secret

SECRETS NÉCESSAIRES POUR NOTRE PIPELINE :

Pour le VPS staging :
  STAGING_HOST       : IP ou domaine du serveur (ex: 192.168.1.100)
  STAGING_USER       : Utilisateur SSH (ex: ubuntu, deploy)
  STAGING_SSH_KEY    : Clé privée SSH (contenu du fichier ~/.ssh/id_rsa)

Pour Azure :
  AZURE_CREDENTIALS  : JSON des credentials Azure (via az ad sp create-for-rbac)

Pour les notifications :
  SLACK_BOT_TOKEN    : Token du bot Slack

GÉNÉRER UNE CLÉ SSH POUR LE DÉPLOIEMENT :
  ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key -N ""
  -> deploy_key      : Clé privée -> Mettre dans STAGING_SSH_KEY
  -> deploy_key.pub  : Clé publique -> Ajouter dans ~/.ssh/authorized_keys sur le serveur
*/


// ----------------------------------------------------------------------------
// [OBJECTIF] AZURE DEVOPS — Alternative enterprise à GitHub Actions
// ----------------------------------------------------------------------------

/*
AZURE DEVOPS = Suite Microsoft pour CI/CD, GitOps, gestion de projets
-> Idéal pour les équipes utilisant déjà l'écosystème Azure
-> Pipelines YAML ou classique (UI)

STRUCTURE D'UN PIPELINE AZURE DEVOPS :
*/

/*
─────────────────────────────────────────────────────────────────
azure-pipelines.yml — Pipeline Azure DevOps
─────────────────────────────────────────────────────────────────

# Déclencheurs
trigger:
  branches:
    include:
      - main
      - develop
  paths:
    exclude:
      - '**/*.md'

variables:
  buildConfiguration: 'Release'
  dotnetVersion: '8.0.x'
  imageRepository: 'monapp'
  containerRegistry: 'monappacr.azurecr.io'
  dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
  tag: '$(Build.BuildId)'

# Pool d'agents Microsoft (Ubuntu)
pool:
  vmImage: 'ubuntu-latest'

stages:

  # ═══════════════════════════════════════════════════════════
  # STAGE 1 : CI (Build + Tests)
  # ═══════════════════════════════════════════════════════════
  - stage: CI
    displayName: '[TEST] Build & Tests'
    jobs:
      - job: BuildAndTest
        displayName: 'Build et Tests .NET'
        steps:

          - task: UseDotNet@2
            displayName: 'Configurer .NET $(dotnetVersion)'
            inputs:
              version: $(dotnetVersion)

          - task: DotNetCoreCLI@2
            displayName: '[PACKAGE] Restaurer les packages'
            inputs:
              command: 'restore'
              projects: '**/*.sln'

          - task: DotNetCoreCLI@2
            displayName: '[OUTIL] Compiler'
            inputs:
              command: 'build'
              projects: '**/*.sln'
              arguments: '--configuration $(buildConfiguration) --no-restore'

          - task: DotNetCoreCLI@2
            displayName: '[TEST] Tests unitaires'
            inputs:
              command: 'test'
              projects: '**/*Tests.Unit.csproj'
              arguments: >-
                --configuration $(buildConfiguration)
                --no-build
                --collect "Code Coverage"
                --logger trx
                --results-directory $(Agent.TempDirectory)/TestResults

          - task: PublishTestResults@2
            displayName: '[GRAPHIQUE] Publier résultats tests'
            inputs:
              testResultsFormat: 'VSTest'
              testResultsFiles: '$(Agent.TempDirectory)/TestResults/*.trx'

  # ═══════════════════════════════════════════════════════════
  # STAGE 2 : Docker Build
  # ═══════════════════════════════════════════════════════════
  - stage: Docker
    displayName: '[DOCKER] Docker'
    dependsOn: CI
    condition: succeeded()
    jobs:
      - job: DockerBuild
        steps:

          - task: Docker@2
            displayName: '[CLE] Login Azure Container Registry'
            inputs:
              command: 'login'
              containerRegistry: 'AzureContainerRegistryServiceConnection'

          - task: Docker@2
            displayName: '[DOCKER] Build et Push image'
            inputs:
              command: 'buildAndPush'
              repository: $(imageRepository)
              dockerfile: $(dockerfilePath)
              containerRegistry: 'AzureContainerRegistryServiceConnection'
              tags: |
                $(tag)
                latest

  # ═══════════════════════════════════════════════════════════
  # STAGE 3 : Déploiement Staging
  # ═══════════════════════════════════════════════════════════
  - stage: DeployStaging
    displayName: '[WEB] Deploy Staging'
    dependsOn: Docker
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
    jobs:
      - deployment: DeployToStaging
        environment: 'staging'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebAppContainer@1
                  displayName: '[RAPIDE] Déployer Azure App Service'
                  inputs:
                    azureSubscription: 'AzureServiceConnection'
                    appName: 'monapp-staging'
                    containers: '$(containerRegistry)/$(imageRepository):$(tag)'

  # ═══════════════════════════════════════════════════════════
  # STAGE 4 : Déploiement Production (avec approbation)
  # ═══════════════════════════════════════════════════════════
  - stage: DeployProduction
    displayName: '[OBJECTIF] Deploy Production'
    dependsOn: DeployStaging
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployToProduction
        environment: 'production'  # Approbation manuelle configurée dans Azure DevOps
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebAppContainer@1
                  displayName: '[RAPIDE] Déployer Production'
                  inputs:
                    azureSubscription: 'AzureServiceConnection'
                    appName: 'monapp-production'
                    containers: '$(containerRegistry)/$(imageRepository):$(tag)'
*/


// ============================================================================
// [GUIDE] CHAPITRE 28 : OBSERVABILITÉ
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les 3 piliers de l'observabilité
[OK] Configurer Serilog (logging structuré)
[OK] Implémenter le health checking
[OK] Utiliser Application Insights (Azure)
[OK] Créer des dashboards de monitoring
[OK] Configurer des alertes
*/


// ----------------------------------------------------------------------------
// [EYE] LES 3 PILIERS DE L'OBSERVABILITÉ
// ----------------------------------------------------------------------------

/*
OBSERVABILITÉ = Comprendre l'état interne d'un système à partir de ses sorties

PILIER 1 : LOGS (Traces d'événements)
  QUOI ? Enregistrement textuel des événements qui se produisent
  QUAND ? Pour déboguer, auditer, suivre les actions
  EXEMPLE : "2024-01-15 14:32:01 INFO  Utilisateur 42 s'est connecté"

PILIER 2 : MÉTRIQUES (Données numériques)
  QUOI ? Mesures numériques agrégées dans le temps
  QUAND ? Pour monitorer la santé, détecter les tendances
  EXEMPLE : "CPU: 45%, RAM: 2.1GB, Requêtes/sec: 127, Erreurs: 0.2%"

PILIER 3 : TRACES DISTRIBUÉES (Suivi des requêtes)
  QUOI ? Suivi d'une requête à travers plusieurs services
  QUAND ? Pour identifier les goulots d'étranglement
  EXEMPLE : "/api/commandes -> (12ms) -> DB query -> (45ms) -> Email -> (200ms)"

ANALOGIE :
  Logs    = Journal de bord du capitaine
  Métriques = Tableau de bord de l'avion (altitude, vitesse, carburant)
  Traces  = GPS tracking du vol (où exactement à chaque instant)
*/


// ----------------------------------------------------------------------------
// [NOTE] SERILOG — Logging structuré professionnel
// ----------------------------------------------------------------------------

/*
POURQUOI SERILOG ET PAS MICROSOFT.EXTENSIONS.LOGGING SEUL ?

LOGS CLASSIQUES (non structurés) :
  logger.LogInformation($"Utilisateur {userId} a acheté {productName} pour {price}€");
  -> Sortie : "Utilisateur 42 a acheté Laptop pour 999.99€"
  -> Impossible de chercher/filtrer par userId ou price !

LOGS STRUCTURÉS (Serilog) :
  Log.Information("Achat effectué par {UserId} : {ProductName} à {Price}€",
                  userId, productName, price);
  -> Sortie JSON : { "UserId": 42, "ProductName": "Laptop", "Price": 999.99, ... }
  -> Requêtes Elasticsearch/Seq : WHERE UserId = 42 AND Price > 500 !

INSTALLATION :
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq                   -> Dashboard local
dotnet add package Serilog.Sinks.ApplicationInsights   -> Azure
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Process
*/

// Program.cs avec Serilog complet
public static class SerilogConfiguration
{
    public static void ConfigurerSerilog(WebApplicationBuilder builder)
    {
        /*
        Log.Logger = new LoggerConfiguration()

            // ─── Sources de configuration ──────────────────────────────
            // Serilog lit la config depuis appsettings.json
            .ReadFrom.Configuration(builder.Configuration)

            // ─── Enrichissement des logs ───────────────────────────────
            // Ajouter des propriétés supplémentaires à TOUS les logs
            .Enrich.FromLogContext()              // Properties poussées via LogContext
            .Enrich.WithMachineName()             // Nom du serveur
            .Enrich.WithEnvironmentName()         // Production/Staging/Development
            .Enrich.WithThreadId()                // ID du thread
            .Enrich.WithProcessId()               // ID du processus
            .Enrich.WithProperty("Application", "MonApp") // Nom de l'app
            .Enrich.WithProperty("Version", Assembly.GetExecutingAssembly()
                .GetName().Version?.ToString() ?? "0.0.0")

            // ─── Niveau minimum de log ──────────────────────────────────
            // Selon l'environnement
            .MinimumLevel.Is(builder.Environment.IsDevelopment()
                ? LogEventLevel.Debug
                : LogEventLevel.Information)

            // Réduire le bruit des logs Microsoft
            .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
            .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
            .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
            .MinimumLevel.Override("System", LogEventLevel.Warning)

            // ─── SINKS : Où écrire les logs ────────────────────────────

            // Console (développement) : Colorée et lisible par les humains
            .WriteTo.Console(
                theme: Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code,
                outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} " +
                                "{Properties:j}{NewLine}{Exception}")

            // Fichier rotatif (tous les jours, garder 30 jours)
            .WriteTo.File(
                path: "logs/monapp-.log",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30,
                outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} " +
                                "[{Level:u3}] {Message:lj}{NewLine}{Exception}",
                fileSizeLimitBytes: 10 * 1024 * 1024, // 10 MB max par fichier
                rollOnFileSizeLimit: true)

            // Fichier JSON (pour ingestion par des outils)
            .WriteTo.File(
                new CompactJsonFormatter(),
                path: "logs/monapp-json-.log",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 7)

            // Seq (dashboard local de développement)
            .WriteTo.Seq(
                serverUrl: builder.Configuration["Seq:ServerUrl"] ?? "http://localhost:5341",
                apiKey: builder.Configuration["Seq:ApiKey"],
                restrictedToMinimumLevel: LogEventLevel.Debug)

            // Application Insights (Azure, production)
            .WriteTo.ApplicationInsights(
                connectionString: builder.Configuration["ApplicationInsights:ConnectionString"],
                telemetryConverter: TelemetryConverter.Traces,
                restrictedToMinimumLevel: LogEventLevel.Information)

            .CreateLogger();

        builder.Host.UseSerilog();
        */
    }
}

/*
─────────────────────────────────────────────────────────────────
appsettings.json avec configuration Serilog
─────────────────────────────────────────────────────────────────

{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning",
        "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/monapp-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
    "Properties": {
      "Application": "MonApp",
      "Environment": "Production"
    }
  }
}
*/


// ----------------------------------------------------------------------------
// [GRAPHIQUE] LOGGING AVANCÉ — Patterns et bonnes pratiques
// ----------------------------------------------------------------------------

// Services/CommandeService.cs — Exemple de logging professionnel
public class CommandeServiceAvecLogs
{
    private readonly ILogger<CommandeServiceAvecLogs> _logger;
    private readonly AppDbContext _context;

    public CommandeServiceAvecLogs(
        ILogger<CommandeServiceAvecLogs> logger,
        AppDbContext context)
    {
        _logger = logger;
        _context = context;
    }

    public async Task<int> CreerCommandeAsync(int utilisateurId, List<int> produitIds)
    {
        // ─── LogInformation : Événement normal ──────────────────────────
        _logger.LogInformation(
            "Création commande pour utilisateur {UtilisateurId} avec {NombreProduits} produits",
            utilisateurId,
            produitIds.Count);

        // Les paramètres nommés ({UtilisateurId}) sont des propriétés structurées !
        // Pas de concaténation de string -> Plus performant + Cherchable

        var stopwatch = System.Diagnostics.Stopwatch.StartNew();

        try
        {
            // Simuler la création de commande
            await Task.Delay(100);
            var commandeId = new Random().Next(1, 1000);

            stopwatch.Stop();

            // ─── LogInformation : Succès avec durée ─────────────────────
            _logger.LogInformation(
                "Commande {CommandeId} créée en {DureeMs}ms pour utilisateur {UtilisateurId}",
                commandeId,
                stopwatch.ElapsedMilliseconds,
                utilisateurId);

            return commandeId;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            // ─── LogError : Erreur avec exception complète ───────────────
            _logger.LogError(
                ex,  // L'exception est le PREMIER paramètre
                "Échec création commande pour utilisateur {UtilisateurId} après {DureeMs}ms. " +
                "Produits demandés: {ProduitIds}",
                utilisateurId,
                stopwatch.ElapsedMilliseconds,
                produitIds);

            throw;
        }
    }

    public async Task<bool> TraiterPaiementAsync(int commandeId, decimal montant)
    {
        // ─── LogWarning : Événement inhabituel mais non critique ─────────
        if (montant > 10_000)
        {
            _logger.LogWarning(
                "Paiement élevé détecté : {Montant}€ pour commande {CommandeId}. " +
                "Vérification manuelle recommandée",
                montant,
                commandeId);
        }

        // ─── LogDebug : Informations de débogage ────────────────────────
        // NE s'affiche qu'en développement (MinimumLevel.Debug)
        _logger.LogDebug(
            "Tentative paiement : CommandeId={CommandeId}, Montant={Montant}, " +
            "Provider=Stripe",
            commandeId,
            montant);

        try
        {
            await Task.Delay(200); // Simuler appel Stripe
            return true;
        }
        catch (Exception ex) when (ex.Message.Contains("insufficient_funds"))
        {
            // ─── LogWarning : Erreur métier attendue ────────────────────
            _logger.LogWarning(
                "Paiement refusé (fonds insuffisants) : CommandeId={CommandeId}, Montant={Montant}",
                commandeId,
                montant);
            return false;
        }
    }
}

// Middleware de logging des requêtes HTTP
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(
        RequestDelegate next,
        ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        var requestId = Guid.NewGuid().ToString("N")[..8]; // ID court

        // Ajouter le requestId à tous les logs de cette requête
        using var scope = _logger.BeginScope(new Dictionary<string, object>
        {
            ["RequestId"] = requestId,
            ["ClientIp"] = context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        });

        _logger.LogInformation(
            "-> {Method} {Path}{QueryString}",
            context.Request.Method,
            context.Request.Path,
            context.Request.QueryString);

        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();

            var niveau = context.Response.StatusCode >= 500
                ? LogLevel.Error
                : context.Response.StatusCode >= 400
                ? LogLevel.Warning
                : LogLevel.Information;

            _logger.Log(
                niveau,
                "<- {StatusCode} {Method} {Path} ({DureeMs}ms)",
                context.Response.StatusCode,
                context.Request.Method,
                context.Request.Path,
                stopwatch.ElapsedMilliseconds);
        }
    }
}

// LogLevel (simulé)
public enum LogLevel { Trace, Debug, Information, Warning, Error, Critical }


// ----------------------------------------------------------------------------
// [HEAVY_BLACK_HEART] HEALTH CHECKS — Surveiller la santé de l'application
// ----------------------------------------------------------------------------

/*
HEALTH CHECKS = Endpoints qui indiquent si l'app est "en bonne santé"

UTILITÉ :
-> Docker : Redémarrer le conteneur si unhealthy
-> Kubernetes : Ne pas envoyer de trafic si not ready
-> Load Balancer : Retirer une instance défaillante
-> Monitoring : Alerter si unhealthy

ENDPOINTS TYPIQUES :
/health         -> Santé complète (pour monitoring humain)
/health/ready   -> L'app est prête à recevoir du trafic (Kubernetes readiness)
/health/live    -> L'app est vivante (Kubernetes liveness)

INSTALLATION :
dotnet add package AspNetCore.HealthChecks.NpgSql          -> PostgreSQL
dotnet add package AspNetCore.HealthChecks.Redis           -> Redis
dotnet add package AspNetCore.HealthChecks.Uris            -> URLs externes
dotnet add package AspNetCore.HealthChecks.UI              -> Dashboard web
*/

/*
─────────────────────────────────────────────────────────────────
Configuration des Health Checks dans Program.cs
─────────────────────────────────────────────────────────────────

builder.Services
    .AddHealthChecks()

    // ─── Base de données ────────────────────────────────────
    .AddNpgSql(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        name: "postgresql",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "ready" })

    // ─── Redis ──────────────────────────────────────────────
    .AddRedis(
        redisConnectionString: builder.Configuration["Redis:ConnectionString"]!,
        name: "redis",
        failureStatus: HealthStatus.Degraded,  // Dégradé mais pas mort
        tags: new[] { "cache", "ready" })

    // ─── URL externe (API Stripe, etc.) ─────────────────────
    .AddUrlGroup(
        uri: new Uri("https://api.stripe.com/v1/"),
        name: "stripe-api",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "external" })

    // ─── Check personnalisé ─────────────────────────────────
    .AddCheck<QueueHealthCheck>(
        name: "email-queue",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "queue" })

    // ─── Vérification de l'espace disque ────────────────────
    .AddDiskStorageHealthCheck(
        setup => setup.AddDrive(
            driveName: "/",
            minimumFreeMegabytes: 500),
        name: "disk-space",
        failureStatus: HealthStatus.Degraded)

    // ─── UI Web des health checks ────────────────────────────
    .AddHealthChecksUI(settings =>
    {
        settings.SetEvaluationTimeInSeconds(30);       // Vérifier toutes les 30s
        settings.MaximumHistoryEntriesPerEndpoint(60); // Garder 60 entrées
        settings.AddHealthCheckEndpoint(
            name: "MonApp",
            uri: "https://monapp.com/health");
    })
    .AddInMemoryStorage();

// Configurer les endpoints
app.MapHealthChecks("/health", new HealthCheckOptions
{
    // Afficher les détails complets en JSON
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
    // Inclure tous les checks
    Predicate = _ => true,
});

// Endpoint "ready" : Seulement les checks de démarrage (BDD, dépendances)
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready"),
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
});

// Endpoint "live" : Vérification minimale (l'app tourne-t-elle ?)
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false, // Aucun check = juste vérifier que l'app répond
});

// Dashboard UI
app.MapHealthChecksUI(options =>
{
    options.UIPath = "/health-ui";    // Accessible sur /health-ui
    options.ApiPath = "/health-api";  // API JSON pour le dashboard
});
*/

// Health Check personnalisé
public class QueueHealthCheck : IHealthCheck
{
    private readonly IEmailQueueService _queueService;

    public QueueHealthCheck(IEmailQueueService queueService)
    {
        _queueService = queueService;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        try
        {
            var taille = await _queueService.ObtenirTailleQueueAsync();

            var data = new Dictionary<string, object>
            {
                ["queue_size"] = taille,
                ["checked_at"] = DateTime.UtcNow,
            };

            if (taille > 10_000)
            {
                return HealthCheckResult.Degraded(
                    description: $"Queue email en retard : {taille} messages en attente",
                    data: data);
            }

            return HealthCheckResult.Healthy(
                description: $"Queue email OK : {taille} messages",
                data: data);
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                description: "Impossible de contacter la queue email",
                exception: ex);
        }
    }
}

// Interfaces (simulées)
public interface IHealthCheck
{
    Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct);
}
public class HealthCheckContext { }
public class HealthCheckResult
{
    public static HealthCheckResult Healthy(string? description = null, Dictionary<string, object>? data = null) => new();
    public static HealthCheckResult Degraded(string? description = null, Dictionary<string, object>? data = null) => new();
    public static HealthCheckResult Unhealthy(string? description = null, Exception? exception = null) => new();
}
public interface IEmailQueueService
{
    Task<int> ObtenirTailleQueueAsync();
}
public enum HealthStatus { Healthy, Degraded, Unhealthy }


// ----------------------------------------------------------------------------
// [GRAPHIQUE] APPLICATION INSIGHTS — Monitoring Azure complet
// ----------------------------------------------------------------------------

/*
APPLICATION INSIGHTS = Plateforme de monitoring Microsoft Azure

FONCTIONNALITÉS :
-> Logs centralisés (Serilog sink)
-> Métriques de performance (temps de réponse, taux d'erreur)
-> Traces distribuées (suivi end-to-end d'une requête)
-> Alertes (email/SMS si erreur > X% ou temps réponse > Y ms)
-> Live Metrics (métriques en temps réel)
-> Application Map (carte visuelle des dépendances)
-> Failure Analysis (analyse des erreurs)
-> Smart Detection (alertes intelligentes)

INSTALLATION :
dotnet add package Microsoft.ApplicationInsights.AspNetCore
dotnet add package Microsoft.ApplicationInsights.WorkerService

CONFIGURATION :
*/

/*
Program.cs avec Application Insights :

// Enregistrer Application Insights
builder.Services.AddApplicationInsightsTelemetry(options =>
{
    options.ConnectionString =
        builder.Configuration["ApplicationInsights:ConnectionString"];

    // Activer la détection des dépendances (HTTP, SQL, Redis, etc.)
    options.EnableDependencyTrackingTelemetryModule = true;

    // Activer le profilage des performances
    options.EnableAdaptiveSampling = true;
    options.EnableDiagnosticsTelemetryModule = true;
});

// Personnaliser le télémétrie
builder.Services.AddSingleton<ITelemetryInitializer, MonTelemetryInitializer>();

// Configurer le sampling (pour réduire le coût)
builder.Services.Configure<TelemetryConfiguration>(config =>
{
    config.DefaultTelemetrySink.TelemetryProcessorChainBuilder
        .UseAdaptiveSampling(
            maxTelemetryItemsPerSecond: 5,   // Max 5 items/sec
            excludedTypes: "Event");         // Ne pas sampler les events
});
*/

// Initializer personnalisé pour enrichir toutes les télémétries
public class MonTelemetryInitializer : ITelemetryInitializer
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public MonTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        var context = _httpContextAccessor.HttpContext;
        if (context is null) return;

        // Ajouter des propriétés à TOUS les événements Application Insights
        telemetry.Context.GlobalProperties["TenantId"] =
            context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "unknown";

        telemetry.Context.GlobalProperties["UserId"] =
            context.User.FindFirst("sub")?.Value ?? "anonymous";

        // Rôle du serveur (utile en multi-instance)
        telemetry.Context.Cloud.RoleName = "monapp-api";
        telemetry.Context.Cloud.RoleInstance = System.Environment.MachineName;
    }
}

// Interfaces Application Insights (simplifiées)
public interface ITelemetryInitializer
{
    void Initialize(ITelemetry telemetry);
}
public interface ITelemetry
{
    TelemetryContext Context { get; }
}
public class TelemetryContext
{
    public IDictionary<string, string> GlobalProperties { get; set; } = new Dictionary<string, string>();
    public CloudContext Cloud { get; set; } = new();
}
public class CloudContext
{
    public string? RoleName { get; set; }
    public string? RoleInstance { get; set; }
}

/*
─────────────────────────────────────────────────────────────────
Utiliser Application Insights dans le code
─────────────────────────────────────────────────────────────────
*/

/*
@inject TelemetryClient TelemetryClient

@code {
    private void TrackAchat(Produit produit, decimal montant)
    {
        // ─── Event personnalisé ─────────────────────────────
        TelemetryClient.TrackEvent("AchatEffectue", new Dictionary<string, string>
        {
            ["ProduitId"] = produit.Id.ToString(),
            ["Categorie"] = produit.Categorie,
        },
        new Dictionary<string, double>
        {
            ["Montant"] = (double)montant,
        });

        // ─── Métrique ────────────────────────────────────────
        TelemetryClient.TrackMetric("ValeurPanier", (double)montant);
    }

    private void TrackErreur(Exception ex, string contexte)
    {
        TelemetryClient.TrackException(ex, new Dictionary<string, string>
        {
            ["Contexte"] = contexte,
            ["UtilisateurId"] = _utilisateurId.ToString(),
        });
    }
}
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE — PARTIE 9
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
EXERCICE : DÉPLOIEMENT COMPLET EN PRODUCTION
═══════════════════════════════════════════════════════════════

OBJECTIF : Déployer une application Blazor complète en production

ÉTAPES :

1. DOCKERISATION (2h) :
   a) Créer le Dockerfile multi-stage
   b) Créer le .dockerignore
   c) Créer docker-compose.yml avec app + postgres + redis + nginx
   d) Créer nginx.conf avec reverse proxy + compression + headers sécurité
   e) Créer .env.production (jamais commité !)
   f) Tester localement : docker compose up --build

2. HEALTH CHECKS (1h) :
   a) Installer les packages healthchecks
   b) Configurer PostgreSQL + Redis checks
   c) Créer un check personnalisé pour la queue email
   d) Configurer les endpoints /health, /health/ready, /health/live
   e) Vérifier : curl http://localhost:8080/health

3. LOGGING SERILOG (1h) :
   a) Installer Serilog + sinks (Console + File)
   b) Configurer dans Program.cs et appsettings.json
   c) Remplacer tous les ILogger.LogInformation par des logs structurés
   d) Ajouter RequestLoggingMiddleware
   e) Vérifier les logs structurés dans la console

4. CI/CD GITHUB ACTIONS (2h) :
   a) Créer .github/workflows/ci-cd.yml
   b) Job tests : build + tests unitaires
   c) Job docker : build + push vers GHCR
   d) Job staging : déploiement sur VPS de staging
   e) Configurer les secrets GitHub
   f) Pousser sur develop -> Vérifier que le pipeline passe

5. MONITORING (1h) :
   a) Créer un compte Application Insights (Azure Portal)
   b) Configurer le SDK dans l'application
   c) Déclencher quelques requêtes -> Vérifier dans Azure Portal
   d) Créer une alerte : "Notifier si taux d'erreur > 5%"

COMPÉTENCES :
[OK] Docker multi-stage build
[OK] Docker Compose avec stack complète
[OK] Nginx reverse proxy
[OK] Health Checks ASP.NET Core
[OK] Serilog logging structuré
[OK] GitHub Actions CI/CD
[OK] Application Insights
═══════════════════════════════════════════════════════════════
*/


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 9

[OK] CHAPITRE 26 - DOCKERISATION :
- Concepts : Image, Conteneur, Registry, Dockerfile, Compose
- Multi-stage build : Étape build (SDK ~2GB) -> Runtime final (~150MB)
- Dockerfile optimisé : Copier .csproj d'abord -> Cache NuGet
- USER app : Ne pas exécuter en root (sécurité)
- EXPOSE 8080 : Port HTTP interne (Nginx gère HTTPS)
- docker-compose.yml : App + PostgreSQL + Redis + Nginx
- Healthcheck Docker : Redémarrer si /health échoue
- Networks Docker : Services se parlent par leur nom
- Volumes persistants : Les données survivent aux redémarrages
- .dockerignore : Exclure bin, obj, .git pour builds plus rapides
- .env.production : Secrets JAMAIS dans Git

[OK] CHAPITRE 27 - CI/CD :
- CI = Intégration Continue : Tests + Build automatiques à chaque push
- CD = Déploiement Continu : Deploy automatique si CI passe
- GitHub Actions : Fichiers YAML dans .github/workflows/
- Workflow : Trigger -> Jobs -> Steps
- Cache NuGet : ${{ hashFiles('**/*.csproj') }} -> Build 5x plus rapide
- Container Registry : ghcr.io (GitHub) ou ACR (Azure)
- Metadata Action : Tags automatiques (sha, branche, latest)
- Deploy SSH : appleboy/ssh-action pour déployer sur VPS
- Deploy Azure : azure/webapps-deploy pour Azure App Service
- Environments : Staging + Production avec approbation manuelle
- Secrets GitHub : Jamais en clair dans le YAML
- Azure DevOps : Alternative enterprise avec stages et approbations

[OK] CHAPITRE 28 - OBSERVABILITÉ :
- 3 piliers : Logs (événements), Métriques (nombres), Traces (suivi)
- Serilog : Logging structuré avec propriétés nommées {UserId}
- Enrichissement : MachineName, Environment, ThreadId, ApplicationName
- Minimum Level Override : Réduire le bruit Microsoft/System
- Sinks : Console (colorée), File (JSON + texte), Seq, Application Insights
- LogContext.BeginScope : Ajouter des propriétés temporaires
- Health Checks : /health, /health/ready, /health/live
- PostgreSQL/Redis/URL checks + check personnalisé
- Application Insights : SDK, ITelemetryInitializer, TrackEvent, TrackMetric
- Alertes : Taux d'erreur, temps de réponse, disponibilité

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 10
- Blazor Hybrid avec .NET MAUI
- PWA et Service Workers
- WebSockets et temps réel avec SignalR
- Micro-frontends
- Librairie de composants commerciale
═══════════════════════════════════════════════════════════════
*/


// ============================================================================
// [LIVRE] BLAZOR - PARTIE 10 : AVANCÉ & EXPERT
// ============================================================================
//
// CHAPITRE 29 : Blazor Hybrid (.NET MAUI)
// CHAPITRE 30 : PWA avec Blazor
// CHAPITRE 31 : WebSockets & Temps réel (SignalR avancé)
// CHAPITRE 32 : Micro-frontends avec Blazor
// CHAPITRE 33 : Créer une bibliothèque de composants commerciale
// CHAPITRE 34 : Contribution open-source Blazor
//
// [TEMPS] TEMPS ESTIMÉ : ~15-20 heures
// [DOCS] PRÉREQUIS : Parties 1-9 complétées
// ============================================================================

/*
[OBJECTIF] PHILOSOPHIE DE CETTE PARTIE

Vous êtes maintenant un développeur Blazor avancé.
Cette partie vous amène au niveau EXPERT :
-> Applications desktop/mobile avec .NET MAUI
-> Applications offline-first avec les PWA
-> Communications temps réel (chat, notifications live)
-> Architecture micro-frontends pour les grandes équipes
-> Construire et vendre une bibliothèque de composants

À la fin de cette partie, vous serez capable de :
-> Créer n'importe quel type d'application avec Blazor
-> Choisir la bonne architecture pour chaque situation
-> Contribuer à l'écosystème Blazor open-source
*/


// ============================================================================
// [GUIDE] CHAPITRE 29 : BLAZOR HYBRID (.NET MAUI)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre ce qu'est Blazor Hybrid
[OK] Créer une app .NET MAUI avec Blazor
[OK] Partager du code entre Web et Desktop/Mobile
[OK] Accéder aux APIs natives (GPS, caméra, fichiers)
[OK] Distribuer sur Windows, macOS, iOS, Android
*/


// ----------------------------------------------------------------------------
// [WEB] BLAZOR HYBRID — Un seul code, toutes les plateformes
// ----------------------------------------------------------------------------

/*
QU'EST-CE QUE BLAZOR HYBRID ?

SANS HYBRID (Avant) :
  Web -> Blazor WASM                     (C#)
  Desktop -> WPF, WinForms               (C#, mais UI différente)
  Mobile -> Xamarin, MAUI                (C#, mais code différent)
  -> 3 bases de code différentes pour 3 plateformes !

AVEC BLAZOR HYBRID (.NET MAUI) :
  Web + Desktop + Mobile -> UN SEUL code Blazor !
  -> Composants Razor réutilisés partout
  -> Accès aux APIs natives via .NET MAUI

COMMENT ÇA MARCHE ?

  MAUI App (natif)
  ├── BlazorWebView        <- Composant MAUI qui héberge Blazor
  │   ├── Rendu dans WebView natif (WKWebView iOS, WebView2 Windows, etc.)
  │   ├── Exécution .NET LOCALE (pas de WebAssembly !)
  │   └── Accès directs aux ressources système
  └── Reste de l'UI MAUI (natif)
      └── Peut mixer Blazor et MAUI !

DIFFÉRENCES BLAZOR HYBRID vs WASM :

  WASM :   Code dans le navigateur, sandboxé, accès limité au système
  HYBRID : Code natif .NET, accès TOTAL au système (fichiers, GPS, etc.)

PLATEFORMES SUPPORTÉES :
  -> Windows 10/11 (WinUI 3)
  -> macOS 12+ (Catalyst)
  -> iOS 15+
  -> Android 7+

INSTALLATION :
  Visual Studio 2022+ avec la charge de travail "Mobile development"
  dotnet new maui-blazor -n MonHybridApp

STRUCTURE DU PROJET MAUI BLAZOR :

  MonHybridApp/
  ├── MauiProgram.cs         <- Configuration MAUI (comme Program.cs)
  ├── MainPage.xaml          <- Page principale avec BlazorWebView
  ├── MainPage.xaml.cs
  ├── Platforms/             <- Code spécifique par plateforme
  │   ├── Android/
  │   ├── iOS/
  │   ├── MacCatalyst/
  │   └── Windows/
  ├── Resources/             <- Images, fonts, splash screen
  ├── wwwroot/               <- Assets web (CSS, JS)
  ├── Pages/                 <- Vos composants Blazor (réutilisables !)
  └── Shared/
*/

/*
─────────────────────────────────────────────────────────────────
MauiProgram.cs — Configuration de l'application MAUI Hybrid
─────────────────────────────────────────────────────────────────

using Microsoft.AspNetCore.Components.WebView.Maui;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                fonts.AddFont("Inter-Regular.ttf", "Inter");
            });

        // ─── Enregistrer les services MAUI + Blazor ────────────────
        builder.Services.AddMauiBlazorWebView();

        #if DEBUG
        // Activer DevTools en développement (F12 dans l'app !)
        builder.Services.AddBlazorWebViewDeveloperTools();
        builder.Logging.AddDebug();
        #endif

        // ─── Vos services habituels ────────────────────────────────
        builder.Services.AddScoped<IProduitService, ProduitServiceHybrid>();
        builder.Services.AddSingleton<AppState>();

        // ─── Services MAUI (APIs natives) ─────────────────────────
        // Ces services accèdent au matériel réel de l'appareil
        builder.Services.AddSingleton<ICameraService, MauiCameraService>();
        builder.Services.AddSingleton<ILocationService, MauiLocationService>();
        builder.Services.AddSingleton<IFilePickerService, MauiFilePickerService>();
        builder.Services.AddSingleton<INotificationService, MauiNotificationService>();

        // HttpClient configuré pour l'API distante
        builder.Services.AddHttpClient("api", client =>
        {
            client.BaseAddress = new Uri("https://api.monapp.com/");
        });

        return builder.Build();
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
MainPage.xaml — Intégrer BlazorWebView dans MAUI
─────────────────────────────────────────────────────────────────

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MonHybridApp"
             x:Class="MonHybridApp.MainPage">

    <Grid>
        <!--
          BlazorWebView = Le composant qui héberge tout Blazor
          HostPage -> Le fichier HTML de base (dans wwwroot/)
        -->
        <BlazorWebView x:Name="blazorWebView"
                       HostPage="wwwroot/index.html">
            <BlazorWebView.RootComponents>
                <!--
                  ComponentType -> Le composant Blazor racine
                  Selector -> L'élément HTML où le monter (#app dans index.html)
                -->
                <RootComponent Selector="#app"
                               ComponentType="{x:Type local:Routes}" />
            </BlazorWebView.RootComponents>
        </BlazorWebView>
    </Grid>

</ContentPage>
*/


// ----------------------------------------------------------------------------
// [MOBILE] ACCÈS AUX APIS NATIVES DEPUIS BLAZOR
// ----------------------------------------------------------------------------

/*
MAGIE DE BLAZOR HYBRID :
Vos composants Blazor peuvent utiliser des services qui accèdent
au matériel physique de l'appareil !

Pattern :
1. Définir une interface (dans le projet partagé)
2. Implémenter pour MAUI (accès natif)
3. Implémenter pour le Web (si supporté)
4. Injecter dans les composants Blazor
*/

// Interfaces partagées (dans MonApp.Shared)
public interface ICameraService
{
    Task<byte[]?> PrendrePhotoAsync();
    Task<bool> EstDisponibleAsync();
}

public interface ILocationService
{
    Task<(double Latitude, double Longitude)?> ObtenirPositionAsync();
    Task<bool> EstAutoriseeAsync();
}

public interface IFilePickerService
{
    Task<(string Nom, byte[] Contenu)?> ChoisirFichierAsync(string[] types);
    Task SauvegarderFichierAsync(string nom, byte[] contenu);
}

public interface INotificationService
{
    Task EnvoyerNotificationLocaleAsync(string titre, string message);
    Task<bool> DemanderPermissionAsync();
}

/*
─────────────────────────────────────────────────────────────────
Implémentation MAUI de ICameraService
─────────────────────────────────────────────────────────────────
Fichier : Services/MauiCameraService.cs
*/

/*
// Ce fichier N'EXISTE QUE dans le projet MAUI (pas dans Shared ni Client Web)
public class MauiCameraService : ICameraService
{
    public async Task<bool> EstDisponibleAsync()
    {
        return MediaPicker.Default.IsCaptureSupported;
    }

    public async Task<byte[]?> PrendrePhotoAsync()
    {
        try
        {
            // Demander la permission si nécessaire
            var status = await Permissions.RequestAsync<Permissions.Camera>();
            if (status != PermissionStatus.Granted)
                return null;

            // Ouvrir la caméra native
            var photo = await MediaPicker.Default.CapturePhotoAsync(
                new MediaPickerOptions
                {
                    Title = "Prendre une photo"
                });

            if (photo is null) return null;

            // Lire les bytes de l'image
            await using var stream = await photo.OpenReadAsync();
            using var memoryStream = new MemoryStream();
            await stream.CopyToAsync(memoryStream);
            return memoryStream.ToArray();
        }
        catch (PermissionException)
        {
            return null;
        }
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Composant Blazor qui utilise la caméra (FONCTIONNE SUR MAUI !)
─────────────────────────────────────────────────────────────────
Fichier : Pages/ScannerProduit.razor (partagé Web + MAUI)

@page "/scanner"
@inject ICameraService CameraService
@inject IProduitService ProduitService

<div class="scanner-container">
    <h2>[CAMERA_WITH_FLASH] Scanner un produit</h2>

    @if (_photo is not null)
    {
        <!-- Afficher la photo prise -->
        <img src="@($"data:image/jpeg;base64,{Convert.ToBase64String(_photo)}")"
             class="photo-preview"
             alt="Photo scannée" />
        <button @onclick="AnalyserPhoto" class="btn btn-primary">
            Analyser
        </button>
    }
    else
    {
        <button @onclick="PrendrePhoto"
                disabled="@(!_cameraDisponible)"
                class="btn btn-success">
            [CAMERA] Prendre une photo
        </button>

        @if (!_cameraDisponible)
        {
            <p class="text-muted">Caméra non disponible sur cette plateforme</p>
        }
    }

    @if (_produitTrouve is not null)
    {
        <div class="produit-trouve mt-4">
            <h3>[OK] Produit trouvé</h3>
            <p><strong>@_produitTrouve.Nom</strong></p>
            <p>Prix: @_produitTrouve.Prix.ToString("C")</p>
        </div>
    }
</div>

@code {
    private byte[]? _photo;
    private bool _cameraDisponible = false;
    private Produit? _produitTrouve;

    protected override async Task OnInitializedAsync()
    {
        _cameraDisponible = await CameraService.EstDisponibleAsync();
    }

    private async Task PrendrePhoto()
    {
        _photo = await CameraService.PrendrePhotoAsync();
        // Sur desktop sans caméra -> _photo = null
    }

    private async Task AnalyserPhoto()
    {
        if (_photo is null) return;
        // Envoyer la photo à l'API pour reconnaissance
        _produitTrouve = await ProduitService.ReconnaitreParImageAsync(_photo);
    }
}
*/


// ----------------------------------------------------------------------------
// [SYNC] PARTAGE DE CODE WEB <-> MAUI
// ----------------------------------------------------------------------------

/*
STRATÉGIE DE PARTAGE DE CODE :

                     ┌─────────────────────────┐
                     │   MonApp.Shared          │
                     │   - Modèles (DTOs)       │
                     │   - Interfaces services  │
                     │   - Composants Blazor    │ <- Réutilisés partout !
                     │   - Validation           │
                     └─────────┬───────────────┘
                               │
               ┌───────────────┼───────────────────┐
               │               │                   │
  ┌────────────[BLACK_DOWN-POINTING_TRIANGLE]─────┐  ┌──────[BLACK_DOWN-POINTING_TRIANGLE]──────┐  ┌────────[BLACK_DOWN-POINTING_TRIANGLE]─────────┐
  │ MonApp.Client    │  │ MonApp.MAUI │  │ MonApp.Server    │
  │ (Blazor WASM)    │  │ (Desktop +  │  │ (ASP.NET Core)   │
  │                  │  │ Mobile)     │  │                  │
  │ Services via     │  │ Services    │  │ Services BDD     │
  │ HttpClient       │  │ natifs MAUI │  │ directs          │
  └──────────────────┘  └─────────────┘  └──────────────────┘

Les COMPOSANTS BLAZOR sont partagés entre Web et MAUI !
Seulement les IMPLÉMENTATIONS DES SERVICES diffèrent.
*/

// Exemple : Service produit adapté pour chaque plateforme

// Version Web (via HttpClient)
public class ProduitServiceWeb : IProduitService
{
    private readonly HttpClient _httpClient;

    public ProduitServiceWeb(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<List<Produit>> ObtenirTousAsync()
        => await _httpClient.GetFromJsonAsync<List<Produit>>("api/produits")
           ?? new List<Produit>();

    public Task<Produit?> ObtenirParIdAsync(int id) => throw new NotImplementedException();
    public Task<Produit?> ReconnaitreParImageAsync(byte[] image) => throw new NotImplementedException();
}

// Version MAUI (accès direct à une BDD locale SQLite + sync)
public class ProduitServiceHybrid : IProduitService
{
    // SQLite local pour mode offline
    // private readonly SQLiteConnection _localDb;
    private readonly HttpClient? _httpClient; // Pour sync si connecté

    public ProduitServiceHybrid(IHttpClientFactory? factory = null)
    {
        _httpClient = factory?.CreateClient("api");
        // _localDb = new SQLiteConnection(Path.Combine(
        //     FileSystem.AppDataDirectory, "produits.db"));
    }

    public async Task<List<Produit>> ObtenirTousAsync()
    {
        // 1. Essayer de synchroniser depuis l'API
        try
        {
            if (_httpClient is not null && await VerifierConnexionAsync())
            {
                var produitsDistants = await _httpClient
                    .GetFromJsonAsync<List<Produit>>("api/produits");
                // SauvegarderLocalementAsync(produitsDistants);
                return produitsDistants ?? new();
            }
        }
        catch { /* Pas de connexion */ }

        // 2. Fallback : Données locales (SQLite)
        // return _localDb.Table<ProduitLocal>()
        //     .Select(p => p.ToModel()).ToList();

        return new List<Produit>(); // Simulation
    }

    private async Task<bool> VerifierConnexionAsync()
    {
        return Connectivity.Current.NetworkAccess == NetworkAccess.Internet;
    }

    public Task<Produit?> ObtenirParIdAsync(int id) => throw new NotImplementedException();
    public Task<Produit?> ReconnaitreParImageAsync(byte[] image) => throw new NotImplementedException();
}

// Enum MAUI (simulé)
public static class Connectivity
{
    public static ConnectivityCurrent Current { get; } = new();
}
public class ConnectivityCurrent
{
    public NetworkAccess NetworkAccess { get; } = NetworkAccess.Internet;
}
public enum NetworkAccess { None, Local, ConstrainedInternet, Internet, Unknown }


// ============================================================================
// [GUIDE] CHAPITRE 30 : PWA AVEC BLAZOR
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre ce qu'est une PWA
[OK] Configurer le Service Worker Blazor
[OK] Implémenter le mode offline
[OK] Gérer les stratégies de cache
[OK] Mettre à jour l'application automatiquement
[OK] Permettre l'installation sur l'écran d'accueil
*/


// ----------------------------------------------------------------------------
// [MOBILE] PWA — Application Web Progressive
// ----------------------------------------------------------------------------

/*
QU'EST-CE QU'UNE PWA ?

PWA = Progressive Web App
-> Site web qui SE COMPORTE COMME UNE APPLICATION NATIVE

CAPACITÉS D'UNE PWA :
[OK] Installable sur l'écran d'accueil (sans app store !)
[OK] Fonctionne HORS LIGNE
[OK] Notifications push
[OK] Accès aux fichiers locaux
[OK] Mise à jour automatique en arrière-plan
[OK] Splash screen au démarrage
[OK] Plein écran sans barre d'URL
[OK] Icône sur le bureau (Windows, macOS, Android, iOS)

POURQUOI UNE PWA PLUTÔT QU'UNE APP NATIVE ?
-> PAS de soumission à l'App Store
-> Mise à jour instantanée (l'utilisateur a toujours la dernière version)
-> Un seul code pour toutes les plateformes
-> Plus facile à maintenir
-> Partageable via une simple URL

BLAZOR WEBASSEMBLY = PARFAIT POUR LES PWA !
-> Code exécuté localement (pas de serveur requis hors ligne)
-> Service Worker inclus par défaut si vous activez l'option PWA

ACTIVER PWA LORS DE LA CRÉATION :
dotnet new blazorwasm --pwa -n MonAppPWA
*/

/*
─────────────────────────────────────────────────────────────────
manifest.webmanifest — Définir l'application installable
─────────────────────────────────────────────────────────────────
Fichier : wwwroot/manifest.webmanifest

{
  "name": "MonApp - Gestion Produits",
  "short_name": "MonApp",
  "description": "Application de gestion de produits et commandes",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#0f172a",
  "theme_color": "#3b82f6",
  "prefer_related_applications": false,
  "orientation": "any",

  "icons": [
    {
      "src": "icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ],

  "screenshots": [
    {
      "src": "screenshots/dashboard.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide",
      "label": "Dashboard principal"
    }
  ],

  "shortcuts": [
    {
      "name": "Nouveau Produit",
      "url": "/produits/ajouter",
      "icons": [{ "src": "icons/add.png", "sizes": "192x192" }]
    },
    {
      "name": "Commandes",
      "url": "/commandes",
      "icons": [{ "src": "icons/order.png", "sizes": "192x192" }]
    }
  ],

  "categories": ["business", "productivity"],
  "lang": "fr",
  "dir": "ltr"
}
*/

/*
─────────────────────────────────────────────────────────────────
service-worker.js — Service Worker personnalisé
─────────────────────────────────────────────────────────────────
Fichier : wwwroot/service-worker.js

// Ce fichier est publié sous forme de service-worker.published.js
// en production (avec la liste des fichiers à cacher)

// ─── Stratégies de cache ──────────────────────────────────────
//
// 1. CACHE FIRST (pour les assets statiques)
//    -> Chercher dans le cache d'abord
//    -> Aller au réseau seulement si absent du cache
//    -> Idéal pour : CSS, JS, images, polices
//
// 2. NETWORK FIRST (pour les données API)
//    -> Chercher sur le réseau d'abord (données fraîches)
//    -> Fallback sur le cache si réseau indisponible
//    -> Idéal pour : /api/produits, /api/commandes
//
// 3. STALE WHILE REVALIDATE (pour le contenu semi-dynamique)
//    -> Retourner le cache immédiatement (rapide !)
//    -> Mettre à jour le cache en arrière-plan
//    -> Idéal pour : pages HTML, contenus qui changent peu
//
// 4. CACHE ONLY (pour le mode offline strict)
//    -> Seulement depuis le cache
//    -> Jamais de requête réseau
//
// 5. NETWORK ONLY (pour les opérations critiques)
//    -> Jamais depuis le cache (POST, DELETE)
//    -> Échec si pas de réseau

// NOM ET VERSION DU CACHE
const CACHE_VERSION = 'v1.2.3';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const API_CACHE = `api-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`;

// Liste des fichiers essentiels pour le mode offline
const OFFLINE_FALLBACK_URL = '/offline.html';
const ESSENTIAL_FILES = [
  '/',
  '/index.html',
  '/css/app.css',
  '/_framework/blazor.webassembly.js',
  OFFLINE_FALLBACK_URL,
];

// ─── Installation du Service Worker ───────────────────────────
self.addEventListener('install', event => {
  console.log('[SW] Installation du service worker v' + CACHE_VERSION);

  event.waitUntil(
    caches.open(STATIC_CACHE).then(cache => {
      // Mettre en cache les fichiers essentiels
      return cache.addAll(ESSENTIAL_FILES);
    }).then(() => {
      // Prendre le contrôle immédiatement (sans attendre reload)
      self.skipWaiting();
    })
  );
});

// ─── Activation (nettoyage des anciens caches) ────────────────
self.addEventListener('activate', event => {
  console.log('[SW] Activation de la nouvelle version');

  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys
          // Supprimer les caches des ANCIENNES versions
          .filter(key => key !== STATIC_CACHE &&
                         key !== API_CACHE &&
                         key !== DYNAMIC_CACHE)
          .map(key => {
            console.log('[SW] Suppression ancien cache:', key);
            return caches.delete(key);
          })
      );
    }).then(() => {
      // Prendre le contrôle de tous les clients
      return self.clients.claim();
    })
  );
});

// ─── Interception des requêtes ────────────────────────────────
self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // Ne pas intercepter les requêtes POST/PUT/DELETE (mutations)
  if (event.request.method !== 'GET') {
    return; // Laisser passer normalement
  }

  // Stratégie selon le type de ressource
  if (url.pathname.startsWith('/api/')) {
    // API -> Network First (données fraîches en priorité)
    event.respondWith(networkFirst(event.request, API_CACHE));

  } else if (url.pathname.includes('/_framework/') ||
             url.pathname.includes('.wasm') ||
             url.pathname.includes('.dll')) {
    // Fichiers Blazor -> Cache First (changent avec la version de l'app)
    event.respondWith(cacheFirst(event.request, STATIC_CACHE));

  } else if (url.pathname.match(/\.(css|js|png|jpg|svg|ico|woff2)$/)) {
    // Assets statiques -> Cache First
    event.respondWith(cacheFirst(event.request, STATIC_CACHE));

  } else {
    // Pages HTML -> Stale While Revalidate
    event.respondWith(staleWhileRevalidate(event.request, DYNAMIC_CACHE));
  }
});

// ─── Stratégie : Network First ────────────────────────────────
async function networkFirst(request, cacheName) {
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open(cacheName);
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    // Réseau indisponible -> Chercher dans le cache
    const cachedResponse = await caches.match(request);
    if (cachedResponse) return cachedResponse;

    // Rien dans le cache -> Page offline
    if (request.destination === 'document') {
      return caches.match(OFFLINE_FALLBACK_URL);
    }
    throw error;
  }
}

// ─── Stratégie : Cache First ──────────────────────────────────
async function cacheFirst(request, cacheName) {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) return cachedResponse;

  // Pas dans le cache -> Aller au réseau ET mettre en cache
  const networkResponse = await fetch(request);
  if (networkResponse.ok) {
    const cache = await caches.open(cacheName);
    cache.put(request, networkResponse.clone());
  }
  return networkResponse;
}

// ─── Stratégie : Stale While Revalidate ──────────────────────
async function staleWhileRevalidate(request, cacheName) {
  const cache = await caches.open(cacheName);
  const cachedResponse = await cache.match(request);

  // Revalider en arrière-plan
  const networkFetch = fetch(request).then(networkResponse => {
    if (networkResponse.ok) {
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  });

  // Retourner le cache immédiatement si disponible, sinon attendre le réseau
  return cachedResponse || networkFetch;
}

// ─── Notifications Push ───────────────────────────────────────
self.addEventListener('push', event => {
  if (!event.data) return;

  const data = event.data.json();

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192x192.png',
      badge: '/icons/badge-72x72.png',
      vibrate: [200, 100, 200],
      data: { url: data.url || '/' },
      actions: data.actions || [],
    })
  );
});

// Clic sur une notification -> Ouvrir l'app à la bonne URL
self.addEventListener('notificationclick', event => {
  event.notification.close();

  const url = event.notification.data?.url || '/';

  event.waitUntil(
    clients.matchAll({ type: 'window' }).then(clientList => {
      // Si l'app est déjà ouverte, naviguer dans l'onglet existant
      for (const client of clientList) {
        if (client.url === url && 'focus' in client) {
          return client.focus();
        }
      }
      // Sinon ouvrir un nouvel onglet
      return clients.openWindow(url);
    })
  );
});
*/


// ----------------------------------------------------------------------------
// [SYNC] MISE À JOUR DE L'APPLICATION — ServiceWorkerUpdateManager
// ----------------------------------------------------------------------------

/*
PROBLÈME :
L'utilisateur a la version 1.0 de votre PWA en cache.
Vous déployez la version 1.1.
Comment l'utilisateur obtient-il la nouvelle version ?

RÉPONSE : Le Service Worker détecte automatiquement les changements
et propose à l'utilisateur de mettre à jour.

BLAZOR FOURNIT : ServiceWorkerUpdateManager
*/

/*
─────────────────────────────────────────────────────────────────
Composant AppUpdateNotifier.razor — Proposition de mise à jour
─────────────────────────────────────────────────────────────────

@implements IDisposable
@inject ServiceWorkerUpdateManager SwUpdateManager
@inject NavigationManager NavManager

@if (_miseAJourDisponible)
{
    <!-- Bandeau de mise à jour (discret mais visible) -->
    <div class="update-banner position-fixed bottom-0 start-0 end-0 z-1050 p-3"
         style="background: #1e293b; color: white; border-top: 3px solid #3b82f6;">
        <div class="container d-flex align-items-center justify-content-between">
            <div>
                <strong>🆕 Nouvelle version disponible !</strong>
                <span class="ms-2 text-muted">Rechargez pour obtenir les dernières améliorations.</span>
            </div>
            <div class="d-flex gap-2">
                <button @onclick="AppliquerMiseAJour"
                        class="btn btn-primary btn-sm">
                    (sync) Mettre à jour maintenant
                </button>
                <button @onclick="() => _miseAJourDisponible = false"
                        class="btn btn-outline-secondary btn-sm">
                    Plus tard
                </button>
            </div>
        </div>
    </div>
}

@code {
    private bool _miseAJourDisponible = false;

    protected override async Task OnInitializedAsync()
    {
        // S'abonner aux notifications de mise à jour
        SwUpdateManager.OnUpdateAvailable += NotifierMiseAJour;

        // Vérifier immédiatement s'il y a une mise à jour en attente
        await SwUpdateManager.CheckForUpdateAsync();
    }

    private void NotifierMiseAJour()
    {
        _miseAJourDisponible = true;
        InvokeAsync(StateHasChanged);
    }

    private async Task AppliquerMiseAJour()
    {
        // Demander au Service Worker d'activer la nouvelle version
        await SwUpdateManager.ActivateUpdateAsync();

        // Recharger la page pour appliquer la mise à jour
        NavManager.NavigateTo(NavManager.Uri, forceLoad: true);
    }

    public void Dispose()
    {
        SwUpdateManager.OnUpdateAvailable -= NotifierMiseAJour;
    }
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 31 : WEBSOCKETS & TEMPS RÉEL
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre SignalR avancé
[OK] Implémenter un chat en temps réel
[OK] Créer un système de notifications live
[OK] Gérer les groupes d'utilisateurs
[OK] Implémenter les indicateurs de présence
[OK] Optimiser les performances SignalR
*/


// ----------------------------------------------------------------------------
// [RESEAU] SIGNALR — Communication bidirectionnelle en temps réel
// ----------------------------------------------------------------------------

/*
QU'EST-CE QUE SIGNALR ?

HTTP classique :     Client -> Serveur (une seule direction, pull)
                     Client demande -> Serveur répond -> Connexion fermée

WebSocket / SignalR : Client <-> Serveur (bidirectionnel, push)
                      Connexion persistante
                      Serveur peut envoyer à n'importe quel moment !

POURQUOI SIGNALR PLUTÔT QUE WEBSOCKET RAW ?
-> SignalR est une abstraction au-dessus des WebSockets
-> Fallback automatique : WebSocket -> Server-Sent Events -> Long Polling
-> Reconnexion automatique
-> Groupes d'utilisateurs
-> Hub côté serveur (comme des contrôleurs pour les WebSockets)

EXEMPLES D'APPLICATIONS TEMPS RÉEL :
-> Chat / messagerie en temps réel
-> Notifications (email reçu, commande livrée, paiement reçu)
-> Tableaux de bord avec données en direct
-> Collaboration en temps réel (Google Docs-like)
-> Jeux multijoueurs
-> Mises à jour de prix (bourse, crypto)
-> Suivi de livraison en temps réel

INSTALLATION :
dotnet add package Microsoft.AspNetCore.SignalR.Client (côté client)
Les Hubs sont inclus dans ASP.NET Core (pas de package supplémentaire)
*/


// ----------------------------------------------------------------------------
// [ACCUEIL] HUB SIGNALR — Le serveur temps réel
// ----------------------------------------------------------------------------

// Hubs/ChatHub.cs (côté serveur)
public class ChatHub : Hub
{
    private readonly ILogger<ChatHub> _logger;
    private static readonly Dictionary<string, InfoUtilisateur> _utilisateursConnectes = new();

    public ChatHub(ILogger<ChatHub> logger)
    {
        _logger = logger;
    }

    // ─── Connexion/Déconnexion ──────────────────────────────────────────────

    // Appelé automatiquement quand un client se connecte
    public override async Task OnConnectedAsync()
    {
        _logger.LogInformation("Client connecté: {ConnectionId}", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    // Appelé automatiquement à la déconnexion
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        if (_utilisateursConnectes.TryGetValue(Context.ConnectionId, out var user))
        {
            _utilisateursConnectes.Remove(Context.ConnectionId);

            // Notifier TOUS les clients : Cet utilisateur s'est déconnecté
            await Clients.All.SendAsync("UtilisateurDeconnecte", user);
            _logger.LogInformation("Utilisateur déconnecté: {Nom}", user.Nom);
        }

        await base.OnDisconnectedAsync(exception);
    }

    // ─── Méthodes invocables par les clients ────────────────────────────────

    // Le client appelle cette méthode pour rejoindre le chat
    public async Task Rejoindre(string nomUtilisateur, string? salonId = null)
    {
        var utilisateur = new InfoUtilisateur(
            Context.ConnectionId,
            nomUtilisateur,
            DateTime.UtcNow);

        _utilisateursConnectes[Context.ConnectionId] = utilisateur;

        // Rejoindre un salon spécifique (groupe SignalR)
        var salon = salonId ?? "general";
        await Groups.AddToGroupAsync(Context.ConnectionId, salon);

        // Envoyer la liste des utilisateurs en ligne au nouveau venu
        var tousLesUsers = _utilisateursConnectes.Values.ToList();
        await Clients.Caller.SendAsync("InitialisationChat", new
        {
            Salon = salon,
            UtilisateursCourants = tousLesUsers,
            Historique = new List<MessageChat>() // Charger depuis BDD en vrai
        });

        // Notifier TOUT LE SALON : Quelqu'un vient d'arriver
        await Clients.Group(salon).SendAsync("UtilisateurRejoins", utilisateur);

        _logger.LogInformation("{Nom} a rejoint le salon {Salon}", nomUtilisateur, salon);
    }

    // Envoyer un message dans un salon
    public async Task EnvoyerMessage(string salon, string texte)
    {
        if (!_utilisateursConnectes.TryGetValue(Context.ConnectionId, out var expediteur))
            return; // Pas encore connecté avec un nom

        // Valider le message
        if (string.IsNullOrWhiteSpace(texte) || texte.Length > 2000)
            return;

        var message = new MessageChat(
            Id: Guid.NewGuid().ToString(),
            Salon: salon,
            Expediteur: expediteur,
            Texte: texte.Trim(),
            Horodatage: DateTime.UtcNow,
            Type: TypeMessage.Texte);

        // Envoyer à TOUS les membres du salon (y compris l'expéditeur)
        await Clients.Group(salon).SendAsync("NouveauMessage", message);

        // Sauvegarder dans la BDD en arrière-plan
        // await _messageRepository.SauvegarderAsync(message);
    }

    // Indicateur "est en train de taper..."
    public async Task SignalerEcritureProlongee(string salon, bool estEnTrainDeTaper)
    {
        if (!_utilisateursConnectes.TryGetValue(Context.ConnectionId, out var user))
            return;

        // Notifier TOUS SAUF l'expéditeur lui-même
        await Clients.GroupExcept(salon, Context.ConnectionId)
            .SendAsync("IndicateurEcriture", new
            {
                Utilisateur = user.Nom,
                EstEnTrainDeTaper = estEnTrainDeTaper
            });
    }

    // Envoyer une réaction sur un message ([BIEN], [HEAVY_BLACK_HEART], etc.)
    public async Task ReagirAuMessage(string salon, string messageId, string emoji)
    {
        if (!_utilisateursConnectes.TryGetValue(Context.ConnectionId, out var user))
            return;

        await Clients.Group(salon).SendAsync("NouvelleReaction", new
        {
            MessageId = messageId,
            Emoji = emoji,
            UtilisateurId = user.Id,
            UtilisateurNom = user.Nom
        });
    }

    // Créer ou rejoindre un salon privé (entre 2 personnes)
    public async Task OuvrirConversationPrivee(string destinataireConnectionId)
    {
        // Créer un ID de salon unique pour ces 2 utilisateurs
        var ids = new[] { Context.ConnectionId, destinataireConnectionId }.OrderBy(x => x);
        var salonPrive = $"prive_{string.Join("_", ids)}";

        // Ajouter les deux dans le salon privé
        await Groups.AddToGroupAsync(Context.ConnectionId, salonPrive);
        await Groups.AddToGroupAsync(destinataireConnectionId, salonPrive);

        var user = _utilisateursConnectes.GetValueOrDefault(Context.ConnectionId);
        await Clients.Group(salonPrive).SendAsync("ConversationPriveeOuverte", new
        {
            SalonId = salonPrive,
            Initiateur = user?.Nom
        });
    }
}

// Modèles
public record InfoUtilisateur(string Id, string Nom, DateTime ConnecteLe);

public record MessageChat(
    string Id,
    string Salon,
    InfoUtilisateur Expediteur,
    string Texte,
    DateTime Horodatage,
    TypeMessage Type);

public enum TypeMessage { Texte, Image, Fichier, Systeme }

// Hub SignalR (simulé)
public abstract class Hub
{
    protected IHubCallerClients Clients { get; } = default!;
    protected IGroupManager Groups { get; } = default!;
    protected HubCallerContext Context { get; } = default!;

    public virtual Task OnConnectedAsync() => Task.CompletedTask;
    public virtual Task OnDisconnectedAsync(Exception? exception) => Task.CompletedTask;
}

public interface IHubCallerClients
{
    IClientProxy All { get; }
    IClientProxy Caller { get; }
    IClientProxy Group(string groupName);
    IClientProxy GroupExcept(string groupName, string excludedConnectionId);
    IClientProxy Client(string connectionId);
}

public interface IClientProxy
{
    Task SendAsync(string method, object? arg1 = null, CancellationToken ct = default);
}

public interface IGroupManager
{
    Task AddToGroupAsync(string connectionId, string groupName, CancellationToken ct = default);
    Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken ct = default);
}

public class HubCallerContext
{
    public string ConnectionId { get; } = string.Empty;
}


// ----------------------------------------------------------------------------
// [SPEECH_BALLOON] COMPOSANT CHAT — Interface Blazor temps réel
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
Fichier : Pages/Chat.razor — Interface de chat complète
─────────────────────────────────────────────────────────────────

@page "/chat"
@implements IAsyncDisposable
@inject NavigationManager NavManager
@inject AppState AppState

<div class="chat-container d-flex flex-column vh-100">

    <!-- En-tête du chat -->
    <div class="chat-header bg-primary text-white p-3 d-flex align-items-center gap-3">
        <h5 class="mb-0">[SPEECH_BALLOON] Chat - Salon #@_salonActuel</h5>
        <span class="badge bg-success ms-auto">@_utilisateursEnLigne.Count en ligne</span>

        <!-- Indicateur d'écriture -->
        @if (_utilisateursQuiEcrivent.Any())
        {
            <small class="text-white-50 fst-italic">
                @string.Join(", ", _utilisateursQuiEcrivent) écrit...
            </small>
        }
    </div>

    <!-- Corps du chat (messages) -->
    <div class="chat-messages flex-grow-1 overflow-auto p-3" @ref="_messagesDiv">
        @foreach (var message in _messages)
        {
            var estMoi = message.Expediteur.Nom == AppState.NomUtilisateur;

            <div class="message-container mb-3 @(estMoi ? "text-end" : "")">
                @if (!estMoi)
                {
                    <small class="text-muted d-block mb-1">@message.Expediteur.Nom</small>
                }
                <div class="message-bubble d-inline-block px-3 py-2 rounded-3
                            @(estMoi ? "bg-primary text-white" : "bg-light")">
                    @message.Texte
                </div>
                <small class="text-muted d-block mt-1">
                    @message.Horodatage.ToString("HH:mm")
                </small>
            </div>
        }
    </div>

    <!-- Zone de saisie -->
    <div class="chat-input border-top p-3">
        <div class="input-group">
            <input @bind="_texteMessage"
                   @bind:event="oninput"
                   @onkeyup="GererTouche"
                   @oninput="SignalerEcriture"
                   class="form-control"
                   placeholder="Écrire un message..."
                   maxlength="2000" />
            <button @onclick="EnvoyerMessage"
                    disabled="@(!_estConnecte || string.IsNullOrWhiteSpace(_texteMessage))"
                    class="btn btn-primary">
                Envoyer [DOWNWARDS_ARROW_WITH_CORNER_LEFTWARDS]
            </button>
        </div>
        <small class="text-muted">@_texteMessage.Length / 2000</small>
    </div>

    <!-- Panneau latéral : Utilisateurs en ligne -->
    <div class="utilisateurs-panel position-fixed end-0 top-0 h-100 bg-light border-start p-3"
         style="width: 200px; top: 64px !important;">
        <h6 class="border-bottom pb-2">[UTILISATEURS] En ligne</h6>
        @foreach (var user in _utilisateursEnLigne)
        {
            <div class="d-flex align-items-center gap-2 mb-2">
                <div class="rounded-circle bg-success" style="width: 8px; height: 8px;"></div>
                <span class="small @(user.Nom == AppState.NomUtilisateur ? "fw-bold" : "")">
                    @user.Nom
                </span>
            </div>
        }
    </div>

</div>

@code {
    private HubConnection? _hubConnection;
    private List<MessageChat> _messages = new();
    private List<InfoUtilisateur> _utilisateursEnLigne = new();
    private List<string> _utilisateursQuiEcrivent = new();
    private string _salonActuel = "general";
    private string _texteMessage = string.Empty;
    private bool _estConnecte = false;
    private ElementReference _messagesDiv;
    private Timer? _timerEcriture;

    protected override async Task OnInitializedAsync()
    {
        await ConnecterSignalRAsync();
    }

    private async Task ConnecterSignalRAsync()
    {
        // Créer la connexion SignalR
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(NavManager.ToAbsoluteUri("/hubs/chat"))
            .WithAutomaticReconnect(new[] { // Intervalles de reconnexion
                TimeSpan.Zero,
                TimeSpan.FromSeconds(2),
                TimeSpan.FromSeconds(5),
                TimeSpan.FromSeconds(10),
                TimeSpan.FromSeconds(30)
            })
            .Build();

        // ─── S'abonner aux événements serveur ─────────────────────

        // Nouveau message reçu
        _hubConnection.On<MessageChat>("NouveauMessage", async message =>
        {
            _messages.Add(message);
            await InvokeAsync(StateHasChanged);
            // Scroller vers le bas
            await ScrollerVersBas();
        });

        // Quelqu'un a rejoint le salon
        _hubConnection.On<InfoUtilisateur>("UtilisateurRejoins", async user =>
        {
            if (!_utilisateursEnLigne.Any(u => u.Id == user.Id))
                _utilisateursEnLigne.Add(user);

            // Message système
            _messages.Add(new MessageChat(
                Guid.NewGuid().ToString(), _salonActuel,
                new InfoUtilisateur("system", "Système", DateTime.UtcNow),
                $"[OK] {user.Nom} a rejoint le salon",
                DateTime.UtcNow, TypeMessage.Systeme));

            await InvokeAsync(StateHasChanged);
        });

        // Quelqu'un s'est déconnecté
        _hubConnection.On<InfoUtilisateur>("UtilisateurDeconnecte", async user =>
        {
            _utilisateursEnLigne.RemoveAll(u => u.Id == user.Id);

            _messages.Add(new MessageChat(
                Guid.NewGuid().ToString(), _salonActuel,
                new InfoUtilisateur("system", "Système", DateTime.UtcNow),
                $"[WAVING_HAND_SIGN] {user.Nom} a quitté le salon",
                DateTime.UtcNow, TypeMessage.Systeme));

            await InvokeAsync(StateHasChanged);
        });

        // Initialisation : Liste des utilisateurs et historique
        _hubConnection.On<object>("InitialisationChat", async data =>
        {
            // Charger l'historique et les utilisateurs en ligne
            await InvokeAsync(StateHasChanged);
        });

        // Indicateur "est en train de taper"
        _hubConnection.On<object>("IndicateurEcriture", async data =>
        {
            // Gérer l'affichage de l'indicateur d'écriture
            await InvokeAsync(StateHasChanged);
        });

        // Événements de reconnexion
        _hubConnection.Reconnecting += error =>
        {
            _estConnecte = false;
            InvokeAsync(StateHasChanged);
            return Task.CompletedTask;
        };

        _hubConnection.Reconnected += connectionId =>
        {
            _estConnecte = true;
            InvokeAsync(StateHasChanged);
            return Task.CompletedTask;
        };

        // Démarrer la connexion
        await _hubConnection.StartAsync();
        _estConnecte = true;

        // Rejoindre le salon par défaut
        var nomUtilisateur = AppState.NomUtilisateur ?? "Anonyme";
        await _hubConnection.InvokeAsync("Rejoindre", nomUtilisateur, _salonActuel);
    }

    private async Task EnvoyerMessage()
    {
        if (string.IsNullOrWhiteSpace(_texteMessage) || _hubConnection is null)
            return;

        var texte = _texteMessage;
        _texteMessage = string.Empty; // Vider l'input immédiatement

        await _hubConnection.InvokeAsync("EnvoyerMessage", _salonActuel, texte);
    }

    private async Task GererTouche(KeyboardEventArgs e)
    {
        if (e.Key == "Enter" && !e.ShiftKey)
            await EnvoyerMessage();
    }

    private async Task SignalerEcriture()
    {
        if (_hubConnection is null) return;

        // Notifier "est en train de taper"
        await _hubConnection.InvokeAsync("SignalerEcritureProlongee",
            _salonActuel, true);

        // Arrêter l'indicateur après 2 secondes d'inactivité
        _timerEcriture?.Dispose();
        _timerEcriture = new Timer(async _ =>
        {
            if (_hubConnection?.State == HubConnectionState.Connected)
            {
                await _hubConnection.InvokeAsync("SignalerEcritureProlongee",
                    _salonActuel, false);
            }
        }, null, TimeSpan.FromSeconds(2), Timeout.InfiniteTimeSpan);
    }

    private async Task ScrollerVersBas()
    {
        // Scroller la div des messages vers le bas
        // await JSRuntime.InvokeVoidAsync("scrollerVersBas", _messagesDiv);
    }

    public async ValueTask DisposeAsync()
    {
        _timerEcriture?.Dispose();
        if (_hubConnection is not null)
        {
            await _hubConnection.DisposeAsync();
        }
    }
}
*/

// HubConnection (simulée pour la compilation)
public class HubConnectionBuilder
{
    public HubConnectionBuilder WithUrl(Uri url) => this;
    public HubConnectionBuilder WithAutomaticReconnect(TimeSpan[] intervals) => this;
    public HubConnection Build() => new();
}

public class HubConnection : IAsyncDisposable
{
    public HubConnectionState State => HubConnectionState.Connected;

    public event Func<Exception?, Task>? Reconnecting;
    public event Func<string?, Task>? Reconnected;

    public IDisposable On<T>(string methodName, Func<T, Task> handler) => default!;

    public async Task StartAsync() => await Task.CompletedTask;
    public async Task InvokeAsync(string methodName, object? arg1 = null, object? arg2 = null)
        => await Task.CompletedTask;

    public async ValueTask DisposeAsync() => await ValueTask.CompletedTask;
}

public enum HubConnectionState { Disconnected, Connecting, Connected, Reconnecting }


// ----------------------------------------------------------------------------
// [NOTIF] NOTIFICATIONS PUSH SERVEUR -> CLIENTS SPÉCIFIQUES
// ----------------------------------------------------------------------------

// Services/NotificationService.cs (côté serveur)
public class NotificationHubService
{
    private readonly IHubContext<NotificationHub> _hubContext;
    private readonly ILogger<NotificationHubService> _logger;

    public NotificationHubService(
        IHubContext<NotificationHub> hubContext,
        ILogger<NotificationHubService> logger)
    {
        _hubContext = hubContext;
        _logger = logger;
    }

    // Notifier UN utilisateur spécifique
    public async Task NotifierUtilisateurAsync(
        string userId,
        string titre,
        string message,
        string? url = null)
    {
        await _hubContext.Clients
            .Group($"user_{userId}")  // Groupe par userId
            .SendAsync("NouvelleNotification", new
            {
                Id = Guid.NewGuid().ToString(),
                Titre = titre,
                Message = message,
                Url = url,
                Horodatage = DateTime.UtcNow,
                EstLue = false
            });

        _logger.LogInformation(
            "Notification envoyée à l'utilisateur {UserId}: {Titre}",
            userId, titre);
    }

    // Notifier TOUS les utilisateurs d'un tenant
    public async Task NotifierTenantAsync(
        string tenantId,
        string titre,
        string message)
    {
        await _hubContext.Clients
            .Group($"tenant_{tenantId}")
            .SendAsync("NouvelleNotification", new { Titre = titre, Message = message });
    }

    // Notifier TOUS les utilisateurs connectés (maintenance, etc.)
    public async Task NotifierTousAsync(string message)
    {
        await _hubContext.Clients.All
            .SendAsync("MessageSysteme", new
            {
                Message = message,
                Type = "warning",
                Horodatage = DateTime.UtcNow
            });
    }
}

// Hub pour les notifications
public class NotificationHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        // Ajouter l'utilisateur dans son groupe personnel
        var userId = Context.GetHttpContext()?.User.FindFirst("sub")?.Value;
        var tenantId = Context.GetHttpContext()?.Request.Headers["X-Tenant-Id"].FirstOrDefault();

        if (userId is not null)
            await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}");

        if (tenantId is not null)
            await Groups.AddToGroupAsync(Context.ConnectionId, $"tenant_{tenantId}");

        await base.OnConnectedAsync();
    }
}

// IHubContext (simulé)
public interface IHubContext<THub> where THub : Hub
{
    IHubClients Clients { get; }
}
public interface IHubClients
{
    IClientProxy All { get; }
    IClientProxy Group(string groupName);
    IClientProxy Client(string connectionId);
}


// ============================================================================
// [LIVRE] BLAZOR - PARTIE 10 (SUITE) : AVANCÉ & EXPERT
// ============================================================================
//
// CHAPITRE 32 : Micro-frontends avec Blazor (suite complète)
// CHAPITRE 33 : Créer une bibliothèque de composants commerciale
// CHAPITRE 34 : Contribution open-source Blazor
//
// [TEMPS] TEMPS ESTIMÉ : ~8-10 heures
// [DOCS] PRÉREQUIS : Chapitres 1-31 complétés
// ============================================================================

/*
[OBJECTIF] PHILOSOPHIE DE CETTE SECTION

Vous avez maintenant toutes les bases pour construire des applications
professionnelles avec Blazor. Cette section vous amène au niveau EXPERT :

-> Chapitre 32 : Architectures pour les GRANDES ÉQUIPES (micro-frontends)
-> Chapitre 33 : Créer et VENDRE une bibliothèque de composants
-> Chapitre 34 : Contribuer à l'OPEN-SOURCE Blazor lui-même

Après cette partie, vous serez capable de :
-> Architecturer des systèmes Blazor pour des équipes de 50+ développeurs
-> Créer des produits commerciaux basés sur Blazor
-> Contribuer au framework Blazor lui-même
*/


// ============================================================================
// [GUIDE] CHAPITRE 32 : MICRO-FRONTENDS AVEC BLAZOR (SUITE COMPLÈTE)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre POURQUOI les micro-frontends existent
[OK] Choisir la bonne approche pour votre contexte
[OK] Implémenter les micro-frontends avec Razor Class Libraries
[OK] Gérer le lazy loading des modules
[OK] Partager l'état entre micro-apps
[OK] Orchestrer le routage
[OK] Éviter les pièges classiques
*/


// ----------------------------------------------------------------------------
// [MODULE] POURQUOI LES MICRO-FRONTENDS ?
// ----------------------------------------------------------------------------

/*
ANALOGIE PARFAITE : Le Restaurant vs la Cantine

RESTAURANT (Monolithe Frontend) :
-> Un seul chef cuisine TOUT le menu
-> Si le chef est absent, TOUT s'arrête
-> Impossible de changer un plat sans refaire tout le menu
-> Fonctionne bien pour 1-5 personnes

BRIGADE ÉTOILÉE (Micro-frontends) :
-> Chef des entrées, Chef des plats, Chef des desserts (équipes séparées)
-> Si le chef des desserts est absent, les entrées continuent
-> On peut changer le dessert sans toucher les entrées
-> Nécessaire à partir de 10+ personnes

PROBLÈMES RÉELS QUE LES MICRO-FRONTENDS RÉSOLVENT :

Problème 1 : "Merge Hell"
  SANS : 3 équipes modifient le même fichier App.razor
  -> Conflits Git à chaque merge
  -> Réunions de coordination interminables
  AVEC : Chaque équipe a son module séparé -> Zéro conflit

Problème 2 : "Le déploiement est bloqué par l'équipe X"
  SANS : Pour déployer une petite correction, toute l'app doit être testée
  -> Délai de 2 semaines pour un bug fix de 5 lignes
  AVEC : L'équipe déploie son module indépendamment -> Bug fix en 1 heure

Problème 3 : "Tout casser en voulant bien faire"
  SANS : Une modification dans un composant partagé casse 5 autres équipes
  AVEC : Interfaces contractuelles + versioning -> Changements isolés

Problème 4 : "Impossible de choisir la bonne tech"
  SANS : Toute l'app en Blazor, même si une partie bénéficierait d'Angular
  AVEC : Chaque module peut utiliser sa propre tech (Blazor, React, Svelte)

QUAND NE PAS UTILISER LES MICRO-FRONTENDS ?

[X] Équipe de 1-5 personnes (overhead inutile)
[X] Application simple (pas de frontières de domaine claires)
[X] Deadline serrée (architecture prend du temps à mettre en place)
[X] Si vous ne comprenez pas encore bien Blazor (maîtrisez le monolithe d'abord)

QUAND LES UTILISER ?

[OK] Équipe 10+ développeurs
[OK] Plusieurs équipes sur le même frontend
[OK] Domaines métier clairement séparés (Catalogue, Commandes, Finance...)
[OK] Besoin de déploiements indépendants
[OK] Différentes vélocités de développement par équipe
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] ARCHITECTURE DÉTAILLÉE — Razor Class Libraries (RCL)
// ----------------------------------------------------------------------------

/*
APPROCHE RECOMMANDÉE POUR BLAZOR : Razor Class Libraries (RCL)

AVANTAGES RCL :
-> Compilation .NET standard (pas de magie)
-> Typage fort entre modules
-> Lazy loading natif Blazor
-> Partage via NuGet privé (Azure Artifacts, GitHub Packages)
-> Outillage IDE complet (IntelliSense, debugging)

STRUCTURE DE SOLUTION COMPLÈTE :

Solution MonEntreprise.sln
│
├── [ACCUEIL] src/Apps/
│   └── ShellApp/                        <- Orchestrateur principal
│       ├── Program.cs
│       ├── App.razor                    <- Router avec lazy loading
│       ├── Shared/
│       │   ├── ShellLayout.razor        <- Layout principal
│       │   ├── NavigationMenu.razor     <- Menu global
│       │   └── NotificationBar.razor    <- Notifications globales
│       ├── Pages/
│       │   ├── Index.razor              <- Page d'accueil shell
│       │   └── Erreur404.razor
│       └── ShellApp.csproj
│
├── [PACKAGE] src/Modules/
│   ├── Module.Catalogue/
│   │   ├── Pages/
│   │   │   ├── ListeProduits.razor      <- @page "/catalogue"
│   │   │   └── DetailProduit.razor      <- @page "/produits/{Id:int}"
│   │   ├── Components/
│   │   │   ├── CarteProduit.razor
│   │   │   └── FiltreProduits.razor
│   │   ├── Services/
│   │   │   └── CatalogueService.cs
│   │   └── Module.Catalogue.csproj
│   │
│   ├── Module.Commandes/
│   │   ├── Pages/
│   │   │   ├── ListeCommandes.razor     <- @page "/commandes"
│   │   │   └── DetailCommande.razor     <- @page "/commandes/{Id:int}"
│   │   ├── Services/
│   │   └── Module.Commandes.csproj
│   │
│   └── Module.Finance/
│       ├── Pages/
│       │   └── Facturation.razor        <- @page "/finance"
│       └── Module.Finance.csproj
│
└── [OUTIL] src/Shared/
    └── MonEntreprise.Shared/            <- Contrats partagés (PAS de logique UI !)
        ├── Models/                      <- DTOs partagés entre modules
        ├── Interfaces/                  <- Contrats de service
        ├── Events/                      <- Événements inter-modules
        ├── Abstractions/                <- Classes de base
        └── MonEntreprise.Shared.csproj
*/


// ----------------------------------------------------------------------------
// [FICHIER] CONTRATS PARTAGÉS — MonEntreprise.Shared
// ----------------------------------------------------------------------------

// MonEntreprise.Shared/Events/IntegrationEvents.cs
// Ce fichier définit les "messages" que les modules peuvent s'envoyer
// C'est le CONTRAT entre les modules - ne jamais le changer sans versioning !
namespace MonEntreprise.Shared.Events
{
    /*
    POURQUOI DES ÉVÉNEMENTS D'INTÉGRATION ?

    SANS événements :
    Module Commandes appelle directement Module Catalogue -> COUPLAGE FORT !
    Si Module Catalogue change son API -> Module Commandes se casse !

    AVEC événements :
    Module Commandes publie "ProduitConsultéEvent"
    Module Catalogue écoute cet événement s'il veut
    -> COUPLAGE FAIBLE ! Chaque module reste indépendant
    */

    // Événement : Un produit a été ajouté au panier
    // Publié par : Module Catalogue
    // Écouté par : Module Commandes, Module Analytique
    public record ProduitAjoutePanierEvent(
        int ProduitId,
        string NomProduit,
        decimal Prix,
        int Quantite,
        string SessionId,       // Pour identifier le visiteur
        DateTime Horodatage);

    // Événement : Une commande a été passée
    // Publié par : Module Commandes
    // Écouté par : Module Finance, Module Analytique, Module Notifications
    public record CommandePasseeEvent(
        int CommandeId,
        string UserId,
        decimal MontantTotal,
        List<LigneCommandeEvent> Lignes,
        DateTime Horodatage);

    public record LigneCommandeEvent(
        int ProduitId,
        string NomProduit,
        decimal PrixUnitaire,
        int Quantite);

    // Événement : Paiement confirmé
    // Publié par : Module Finance
    // Écouté par : Module Commandes (pour passer la commande en "Payée")
    public record PaiementConfirmeEvent(
        int CommandeId,
        string TransactionId,
        decimal Montant,
        string MethodePaiement,
        DateTime Horodatage);

    // Événement : Utilisateur authentifié
    // Publié par : Shell App
    // Écouté par : TOUS les modules (pour mettre à jour leur contexte)
    public record UtilisateurAuthentifieEvent(
        string UserId,
        string Email,
        string NomComplet,
        List<string> Roles,
        string TenantId);
}

// MonEntreprise.Shared/Interfaces/IModuleRegistration.cs
namespace MonEntreprise.Shared.Interfaces
{
    /*
    POURQUOI UNE INTERFACE D'ENREGISTREMENT ?

    Chaque module doit pouvoir :
    1. S'enregistrer ses services dans le DI container
    2. Configurer son routage
    3. Déclarer ses permissions requises

    Sans cette interface, le Shell devrait connaître
    les détails internes de chaque module -> couplage fort !
    */

    public interface IModuleRegistration
    {
        // Identifiant unique du module (ex: "catalogue", "commandes")
        string ModuleId { get; }

        // Nom affiché dans l'UI (ex: "Catalogue Produits")
        string NomAffichage { get; }

        // Icône pour le menu (classe CSS, ex: "bi bi-box-seam")
        string Icone { get; }

        // Préfixes de routes gérés par ce module (ex: ["/catalogue", "/produits"])
        string[] RoutesPrefixes { get; }

        // Assemblies à charger en lazy loading
        string[] AssembliesLazy { get; }

        // Permissions requises pour accéder au module (null = public)
        string[]? PermissionsRequises { get; }

        // Ordre d'affichage dans le menu (10, 20, 30...)
        int OrdreMenu { get; }

        // Enregistrer les services du module dans le DI container
        void EnregistrerServices(IServiceCollection services, IConfiguration config);
    }
}

// MonEntreprise.Shared/Services/IntegrationEventBus.cs
namespace MonEntreprise.Shared.Services
{
    /*
    BUS D'ÉVÉNEMENTS D'INTÉGRATION

    C'est le système de communication entre les modules.
    Fonctionne comme une boîte aux lettres :
    - Module A dépose un message ("ProduitAjouté")
    - Module B et C qui ont demandé à être notifiés reçoivent le message

    THREAD SAFETY : Utiliser ConcurrentDictionary car Blazor peut appeler
    depuis plusieurs threads (rare mais possible)
    */

    using System.Collections.Concurrent;

    public class IntegrationEventBus
    {
        // Dictionnaire : Type d'événement -> Liste des abonnés
        private readonly ConcurrentDictionary<Type, List<Delegate>> _abonnes = new();
        private readonly ILogger<IntegrationEventBus> _logger;

        public IntegrationEventBus(ILogger<IntegrationEventBus> logger)
        {
            _logger = logger;
        }

        // S'abonner à un type d'événement
        public void Souscrire<TEvent>(Func<TEvent, Task> handler) where TEvent : class
        {
            var type = typeof(TEvent);
            _abonnes.GetOrAdd(type, _ => new List<Delegate>()).Add(handler);

            _logger.LogDebug(
                "Nouveau abonné pour {EventType}: total={Count}",
                type.Name,
                _abonnes[type].Count);
        }

        // Se désabonner (IMPORTANT : toujours se désabonner dans Dispose !)
        public void Desouscrire<TEvent>(Func<TEvent, Task> handler) where TEvent : class
        {
            var type = typeof(TEvent);
            if (_abonnes.TryGetValue(type, out var handlers))
            {
                handlers.Remove(handler);
            }
        }

        // Publier un événement à tous les abonnés
        public async Task PublierAsync<TEvent>(TEvent evenement) where TEvent : class
        {
            var type = typeof(TEvent);

            _logger.LogInformation(
                "Événement publié: {EventType} -> {SubscriberCount} abonnés",
                type.Name,
                _abonnes.GetValueOrDefault(type)?.Count ?? 0);

            if (!_abonnes.TryGetValue(type, out var handlers)) return;

            // Copier la liste pour éviter les modifications pendant l'itération
            var handlersCopie = handlers.ToList();

            foreach (var handler in handlersCopie)
            {
                try
                {
                    if (handler is Func<TEvent, Task> asyncHandler)
                    {
                        await asyncHandler(evenement);
                    }
                }
                catch (Exception ex)
                {
                    // NE PAS laisser l'erreur d'un abonné bloquer les autres !
                    _logger.LogError(ex,
                        "Erreur dans un abonné de {EventType}",
                        type.Name);
                }
            }
        }
    }
}


// ----------------------------------------------------------------------------
// [ACCUEIL] SHELL APP — L'orchestrateur principal
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
ShellApp/App.razor — Routing avec Lazy Loading
─────────────────────────────────────────────────────────────────

LAZY LOADING = Charger le code d'un module SEULEMENT quand on en a besoin

POURQUOI ?
-> L'app shell de base télécharge seulement ~2MB
-> Quand l'utilisateur navigue vers /catalogue, on charge Module.Catalogue.dll
-> Si l'utilisateur ne va JAMAIS sur /finance, ce code n'est JAMAIS téléchargé
-> RÉSULTAT : Démarrage 3-5x plus rapide pour les gros projets

@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using MonEntreprise.Shared.Services
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="@typeof(App).Assembly"
        AdditionalAssemblies="@_modulesCharges"
        OnNavigateAsync="@ChargeurModules">

    <Found Context="routeData">
        <!-- Afficher un spinner pendant le chargement du module -->
        @if (_chargementEnCours)
        {
            <div class="shell-loading-overlay">
                <div class="text-center">
                    <div class="spinner-border text-primary" role="status"></div>
                    <p class="mt-3 text-muted">Chargement du module @_moduleEnChargement...</p>
                </div>
            </div>
        }
        else
        {
            <RouteView RouteData="@routeData"
                       DefaultLayout="@typeof(ShellLayout)" />
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        }
    </Found>

    <NotFound>
        <PageTitle>Page non trouvée - MonEntreprise</PageTitle>
        <ShellLayout>
            <div class="text-center py-5">
                <h1 class="display-1">404</h1>
                <p>Cette page n'existe pas.</p>
                <a href="/" class="btn btn-primary">Retour à l'accueil</a>
            </div>
        </ShellLayout>
    </NotFound>
</Router>

@code {
    // Liste des assemblies de modules chargés (ajoutés dynamiquement)
    private List<Assembly> _modulesCharges = new();
    private bool _chargementEnCours = false;
    private string _moduleEnChargement = "";

    // TABLE DE ROUTAGE : Préfixe URL -> Fichiers DLL à charger
    // Cette table est la clé : elle dit "pour cette URL, charge CES DLLs"
    private static readonly Dictionary<string, (string[] Dlls, string NomModule)> _routesModules = new()
    {
        // Format : "/prefixe" -> (["Dll1.dll", "Dll2.dll"], "Nom affiché")
        ["/catalogue"]  = (new[] { "Module.Catalogue.dll" }, "Catalogue"),
        ["/produits"]   = (new[] { "Module.Catalogue.dll" }, "Catalogue"),
        ["/commandes"]  = (new[] { "Module.Commandes.dll" }, "Commandes"),
        ["/commande"]   = (new[] { "Module.Commandes.dll" }, "Commandes"),
        ["/finance"]    = (new[] { "Module.Finance.dll",
                                   "Syncfusion.Charts.dll" }, "Finance"),  // Avec dépendance externe !
        ["/analytique"] = (new[] { "Module.Analytique.dll",
                                   "ChartJs.Blazor.dll" }, "Analytique"),
    };

    // Appelé par le Router AVANT chaque navigation
    private async Task ChargeurModules(NavigationContext ctx)
    {
        // Extraire le préfixe de la route demandée
        // Ex: "/catalogue/produits/42" -> "/catalogue"
        var prefixe = "/" + ctx.Path.TrimStart('/').Split('/').First();

        // Chercher si ce préfixe correspond à un module
        if (!_routesModules.TryGetValue(prefixe, out var moduleInfo))
            return; // Route inconnue, le Router 404 gérera

        var (dlls, nomModule) = moduleInfo;

        // Vérifier si les DLLs sont déjà chargées (éviter double chargement)
        var dllsACharger = dlls
            .Where(dll => !_modulesCharges
                .Any(a => a.GetName().Name + ".dll" == dll))
            .ToArray();

        if (!dllsACharger.Any()) return; // Déjà chargé !

        // Afficher le spinner de chargement
        _chargementEnCours = true;
        _moduleEnChargement = nomModule;
        StateHasChanged();

        try
        {
            Logger.LogInformation(
                "Chargement du module {Module} ({Dlls})",
                nomModule,
                string.Join(", ", dllsACharger));

            var chrono = System.Diagnostics.Stopwatch.StartNew();

            // CHARGER LES DLLs en lazy loading
            var assemblies = await AssemblyLoader.LoadAssembliesAsync(dllsACharger);

            // Ajouter les nouvelles assemblies à la liste
            _modulesCharges.AddRange(assemblies.Where(a => a is not null)!);

            chrono.Stop();
            Logger.LogInformation(
                "Module {Module} chargé en {Ms}ms",
                nomModule,
                chrono.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "Échec du chargement du module {Module}", nomModule);
            // Afficher un message d'erreur à l'utilisateur
        }
        finally
        {
            _chargementEnCours = false;
            StateHasChanged();
        }
    }
}
*/


// ----------------------------------------------------------------------------
// [PACKAGE] MODULE CATALOGUE — Exemple de module complet
// ----------------------------------------------------------------------------

// Module.Catalogue/ModuleCatalogueRegistration.cs
/*
CHAQUE MODULE s'enregistre lui-même via cette classe.
Le Shell App récupère tous les IModuleRegistration et les configure.
C'est le "plugin pattern" : le module dit au shell ce dont il a besoin.
*/
public class ModuleCatalogueRegistration // : IModuleRegistration
{
    public string ModuleId => "catalogue";
    public string NomAffichage => "Catalogue Produits";
    public string Icone => "bi bi-box-seam";
    public string[] RoutesPrefixes => new[] { "/catalogue", "/produits" };
    public string[] AssembliesLazy => new[] { "Module.Catalogue.dll" };
    public string[]? PermissionsRequises => null; // Public
    public int OrdreMenu => 10;

    public void EnregistrerServices(IServiceCollection services, IConfiguration config)
    {
        // Services spécifiques au module Catalogue
        services.AddScoped<ICatalogueService, CatalogueService>();
        services.AddScoped<IPanierService, PanierService>();

        // HTTP client pour l'API Catalogue
        services.AddHttpClient<ICatalogueService, CatalogueService>(client =>
        {
            client.BaseAddress = new Uri(
                config["Api:CatalogueUrl"] ?? "https://api.monentreprise.com/catalogue/");
        });

        // Cache pour les produits (TTL: 5 minutes)
        services.AddMemoryCache();
    }
}

// Services du module
public interface ICatalogueService
{
    Task<List<Produit>> ObtenirProduitsAsync(FiltresProduits filtres);
    Task<Produit?> ObtenirParIdAsync(int id);
    Task<List<Categorie>> ObtenirCategoriesAsync();
}

public interface IPanierService
{
    Task<Panier> ObtenirPanierAsync();
    Task AjouterProduitAsync(int produitId, int quantite);
    Task SupprimerProduitAsync(int produitId);
    Task ViderPanierAsync();
    event Action? PanierModifie;
}

// Modèles du module (local au module, différent des DTOs partagés)
public class Produit
{
    public int Id { get; set; }
    public string Nom { get; set; } = "";
    public string Description { get; set; } = "";
    public decimal Prix { get; set; }
    public string ImageUrl { get; set; } = "";
    public int Stock { get; set; }
    public string Categorie { get; set; } = "";
    public List<string> Tags { get; set; } = new();
    public double NoteMoyenne { get; set; }
    public int NombreAvis { get; set; }
}

public class Categorie
{
    public int Id { get; set; }
    public string Nom { get; set; } = "";
    public string IconeUrl { get; set; } = "";
    public int NombreProduits { get; set; }
}

public class FiltresProduits
{
    public string? Recherche { get; set; }
    public int? CategorieId { get; set; }
    public decimal? PrixMin { get; set; }
    public decimal? PrixMax { get; set; }
    public bool SeulementEnStock { get; set; }
    public string TriPar { get; set; } = "nom";      // nom, prix, note
    public bool TriDecroissant { get; set; }
    public int Page { get; set; } = 1;
    public int ParPage { get; set; } = 24;
}

public class Panier
{
    public List<LignePanier> Lignes { get; set; } = new();
    public decimal Total => Lignes.Sum(l => l.Total);
    public int NombreArticles => Lignes.Sum(l => l.Quantite);
}

public class LignePanier
{
    public int ProduitId { get; set; }
    public string NomProduit { get; set; } = "";
    public decimal PrixUnitaire { get; set; }
    public int Quantite { get; set; }
    public decimal Total => PrixUnitaire * Quantite;
}

// Implémentation du service Catalogue
public class CatalogueService : ICatalogueService
{
    private readonly HttpClient _http;
    private readonly IMemoryCache _cache;
    private readonly ILogger<CatalogueService> _logger;

    public CatalogueService(
        HttpClient http,
        IMemoryCache cache,
        ILogger<CatalogueService> logger)
    {
        _http = http;
        _cache = cache;
        _logger = logger;
    }

    public async Task<List<Produit>> ObtenirProduitsAsync(FiltresProduits filtres)
    {
        // Les listes filtrées ne sont PAS mises en cache (trop de combinaisons)
        var queryString = BuildQueryString(filtres);

        try
        {
            var produits = await _http.GetFromJsonAsync<List<Produit>>(
                $"produits?{queryString}");
            return produits ?? new();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erreur lors de la récupération des produits");
            return new(); // Retourner une liste vide plutôt que crasher
        }
    }

    public async Task<Produit?> ObtenirParIdAsync(int id)
    {
        // Les produits individuels SONT mis en cache (TTL: 5 minutes)
        var cacheKey = $"produit_{id}";

        if (_cache.TryGetValue<Produit>(cacheKey, out var produitCache))
        {
            _logger.LogDebug("Produit {Id} servi depuis le cache", id);
            return produitCache;
        }

        var produit = await _http.GetFromJsonAsync<Produit>($"produits/{id}");

        if (produit is not null)
        {
            _cache.Set(cacheKey, produit, TimeSpan.FromMinutes(5));
        }

        return produit;
    }

    public async Task<List<Categorie>> ObtenirCategoriesAsync()
    {
        // Les catégories changent rarement -> Cache de 30 minutes
        return await _cache.GetOrCreateAsync("categories", async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
            return await _http.GetFromJsonAsync<List<Categorie>>("categories")
                   ?? new List<Categorie>();
        }) ?? new();
    }

    private static string BuildQueryString(FiltresProduits f)
    {
        var params_ = new List<string>();
        if (!string.IsNullOrEmpty(f.Recherche)) params_.Add($"q={Uri.EscapeDataString(f.Recherche)}");
        if (f.CategorieId.HasValue)             params_.Add($"categorieId={f.CategorieId}");
        if (f.PrixMin.HasValue)                 params_.Add($"prixMin={f.PrixMin}");
        if (f.PrixMax.HasValue)                 params_.Add($"prixMax={f.PrixMax}");
        if (f.SeulementEnStock)                 params_.Add("enStock=true");
        params_.Add($"tri={f.TriPar}");
        params_.Add($"desc={f.TriDecroissant}");
        params_.Add($"page={f.Page}");
        params_.Add($"parPage={f.ParPage}");
        return string.Join("&", params_);
    }
}

// Implémentation du panier (stocké en LocalStorage)
public class PanierService : IPanierService
{
    private Panier _panier = new();
    private readonly IntegrationEventBus _eventBus;
    private readonly ILogger<PanierService> _logger;

    // Événement pour notifier les composants (ex: badge dans le menu)
    public event Action? PanierModifie;

    public PanierService(
        IntegrationEventBus eventBus,
        ILogger<PanierService> logger)
    {
        _eventBus = eventBus;
        _logger = logger;
    }

    public Task<Panier> ObtenirPanierAsync() => Task.FromResult(_panier);

    public async Task AjouterProduitAsync(int produitId, int quantite)
    {
        var ligne = _panier.Lignes.FirstOrDefault(l => l.ProduitId == produitId);

        if (ligne is not null)
        {
            ligne.Quantite += quantite;
        }
        else
        {
            _panier.Lignes.Add(new LignePanier
            {
                ProduitId = produitId,
                Quantite = quantite,
                // En réalité, récupérer le prix depuis le service catalogue
                PrixUnitaire = 0,
                NomProduit = ""
            });
        }

        // Notifier tous les modules qu'un produit a été ajouté
        // Module Commandes pourra l'utiliser pour préparer la commande
        // Module Analytique pourra enregistrer l'action
        await _eventBus.PublierAsync(new ProduitAjouteAuPanierEvent(
            produitId, quantite, 0,
            $"session_{DateTime.UtcNow.Ticks}",
            DateTime.UtcNow));

        PanierModifie?.Invoke();

        _logger.LogInformation(
            "Produit {ProduitId} ajouté au panier (quantité: {Quantite})",
            produitId, quantite);
    }

    public Task SupprimerProduitAsync(int produitId)
    {
        _panier.Lignes.RemoveAll(l => l.ProduitId == produitId);
        PanierModifie?.Invoke();
        return Task.CompletedTask;
    }

    public Task ViderPanierAsync()
    {
        _panier = new Panier();
        PanierModifie?.Invoke();
        return Task.CompletedTask;
    }
}

// Stubs pour la compilation
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class IntegrationEventBus
{
    public Task PublierAsync<T>(T evt) where T : class => Task.CompletedTask;
    public void Souscrire<T>(Func<T, Task> h) where T : class { }
    public void Desouscrire<T>(Func<T, Task> h) where T : class { }
}

public record ProduitAjouteAuPanierEvent(int ProduitId, int Quantite, decimal Prix, string SessionId, DateTime Horodatage);
public record CommandePasseeEvent(int CommandeId, string UserId, decimal Total, List<object> Lignes, DateTime Horodatage);
public record PaiementConfirmeEvent(int CommandeId, string TransactionId, decimal Montant, string Methode, DateTime Horodatage);

public static class CacheExtensions
{
    public static T? GetOrCreate<T>(this IMemoryCache cache, object key, Func<ICacheEntry, T> factory) => default;
    public static T Set<T>(this IMemoryCache cache, object key, T value, TimeSpan ttl) => value;
    public static bool TryGetValue<T>(this IMemoryCache cache, object key, out T? value)
    {
        value = default;
        return false;
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 33 : CRÉER UNE BIBLIOTHÈQUE DE COMPOSANTS COMMERCIALE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Structurer une bibliothèque de composants professionnelle
[OK] Appliquer les principes de design API
[OK] Écrire une documentation interactive (Storybook-like)
[OK] Tester les composants de manière exhaustive
[OK] Publier sur NuGet
[OK] Gérer les versions (SemVer)
[OK] Monétiser votre bibliothèque
*/


// ----------------------------------------------------------------------------
// [OBJECTIF] POURQUOI CRÉER UNE BIBLIOTHÈQUE DE COMPOSANTS ?
// ----------------------------------------------------------------------------

/*
MOTIVATIONS :

1. RÉUTILISATION INTERNE
   -> Plusieurs projets dans votre entreprise utilisent les mêmes composants
   -> Correction de bug en 1 endroit -> Tous les projets bénéficient
   -> Cohérence UI/UX garantie

2. PRODUIT COMMERCIAL
   -> MudBlazor, Radzen, Syncfusion, Telerik ont des milliers de clients
   -> Modèle économique : Licence par développeur (500-2000€/an)
   -> Si votre bibliothèque résout un problème réel -> Revenus récurrents

3. RÉPUTATION ET VISIBILITÉ
   -> Une bibliothèque populaire sur GitHub = Reconnu comme expert
   -> Opportunités d'emploi, speaking, consulting

EXEMPLES DE BIBLIOTHÈQUES BLAZOR RÉUSSIES :
-> MudBlazor : 4000+ stars, gratuite, référence du marché
-> Radzen Blazor : Composants payants, client entreprise
-> Syncfusion Blazor : Suite complète (2000+$/développeur/an)
-> Telerik UI for Blazor : Très performant, écosystème Progress

COMMENT SE DIFFÉRENCIER ?
-> Focus sur une NICHE (ex: "composants pour les applications financières")
-> Performance exceptionnelle (benchmark contre les autres)
-> API ultra-simple ("zero config, works out of the box")
-> Accessibilité parfaite (WCAG 2.1 AA)
-> Support exceptionnel (réponse < 24h)
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] STRUCTURE DE LA BIBLIOTHÈQUE
// ----------------------------------------------------------------------------

/*
Structure de projet recommandée :

MonUI/                                <- Dossier racine
│
├── src/
│   ├── MonUI.Core/                   <- Bibliothèque principale (RCL)
│   │   ├── Components/               <- Composants Blazor
│   │   │   ├── Button/
│   │   │   │   ├── MonButton.razor
│   │   │   │   ├── MonButton.razor.css
│   │   │   │   └── ButtonVariant.cs   <- Enum des variantes
│   │   │   ├── DataGrid/
│   │   │   │   ├── MonDataGrid.razor
│   │   │   │   ├── MonDataGridColumn.razor
│   │   │   │   └── DataGridOptions.cs
│   │   │   ├── Modal/
│   │   │   ├── Notification/
│   │   │   └── ...
│   │   ├── Services/
│   │   │   ├── INotificationService.cs
│   │   │   └── NotificationService.cs
│   │   ├── Extensions/
│   │   │   └── ServiceCollectionExtensions.cs  <- AddMonUI()
│   │   ├── wwwroot/
│   │   │   ├── css/
│   │   │   │   ├── mon-ui.css          <- Styles de base
│   │   │   │   └── themes/
│   │   │   │       ├── light.css
│   │   │   │       └── dark.css
│   │   │   └── js/
│   │   │       └── mon-ui.js           <- Interop JS minimal
│   │   └── MonUI.Core.csproj
│   │
│   └── MonUI.Icons/                   <- Package optionnel d'icônes
│       └── MonUI.Icons.csproj
│
├── tests/
│   ├── MonUI.Tests.Unit/              <- Tests de la logique
│   │   └── MonUI.Tests.Unit.csproj
│   └── MonUI.Tests.Components/        <- Tests des composants (bUnit)
│       └── MonUI.Tests.Components.csproj
│
├── docs/
│   └── MonUI.Docs/                    <- Site de documentation interactif
│       ├── Pages/
│       │   ├── Components/
│       │   │   └── ButtonPage.razor    <- Page de démo du Button
│       │   └── ...
│       └── MonUI.Docs.csproj
│
└── MonUI.sln
*/


// ----------------------------------------------------------------------------
// [DESIGN] DESIGN API — La clé du succès d'une bibliothèque
// ----------------------------------------------------------------------------

/*
RÈGLES D'OR POUR L'API D'UNE BIBLIOTHÈQUE :

RÈGLE 1 : PROGRESSIVE DISCLOSURE (Divulgation progressive)
-> Le cas simple doit être TRIVIAL
-> Le cas avancé doit être POSSIBLE
-> Exemple :

    Cas simple (80% des utilisateurs) :
    <MonButton>Cliquer ici</MonButton>

    Cas moyen (15%) :
    <MonButton Variant="ButtonVariant.Danger" Size="Size.Large" Disabled="@_chargement">
        Supprimer
    </MonButton>

    Cas avancé (5%) :
    <MonButton OnClick="@GérerClick"
               Icon="@Icons.Trash"
               Loading="@_chargement"
               LoadingText="Suppression..."
               class="my-custom-class"
               style="margin-top: 10px">
        Supprimer définitivement
    </MonButton>

RÈGLE 2 : CONVENTION OVER CONFIGURATION
-> Des valeurs par défaut sensées (jamais de config obligatoire)
-> Exemple : Le bouton est Variant.Primary par défaut (le plus utilisé)

RÈGLE 3 : CONSISTANCE
-> Tous vos composants partagent les mêmes patterns
-> Si un paramètre s'appelle "Disabled" dans Button, pas "IsDisabled" dans Input
-> Si "Size" accepte Small/Medium/Large, TOUS les composants utilisent les mêmes valeurs

RÈGLE 4 : FAIL FAST
-> Si l'utilisateur utilise mal votre composant -> Erreur claire immédiatement
-> Pas d'erreurs silencieuses
-> Messages d'erreur UTILES (pas "Null reference exception à la ligne 42")

RÈGLE 5 : COMPOSABILITÉ
-> Les composants fonctionnent bien ensemble
-> <MonDialog> peut contenir <MonButton>, <MonDataGrid>, etc.
-> Éviter les composants "tout-en-un" impossible à customiser
*/


// ----------------------------------------------------------------------------
// [PACKAGE] COMPOSANT BOUTON — Exemple de composant professionnel
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
MonButton.razor — Composant Button professionnel et accessible
─────────────────────────────────────────────────────────────────

Objectifs :
-> WCAG 2.1 AA (accessibilité)
-> Support du clavier
-> État de chargement intégré
-> Support des icônes
-> Toutes les variantes (Primary, Secondary, Danger, Ghost...)
-> Tous les tailles (Small, Medium, Large)
-> Support du type (button, submit, reset)
-> Attributs HTML additionnels (via splatting)
-> Référence au DOM exposée

@namespace MonUI.Components

<!-- Tous les attributs HTML non reconnus sont transmis à l'élément -->
@attribute [CaptureUnmatchedValues]  <!-- Permet le splatting d'attributs -->

<button
    @attributes="AttributsSupplementaires"
    type="@TypeBouton"
    disabled="@(Disabled || Loading)"
    aria-busy="@Loading.ToString().ToLower()"
    aria-label="@(Loading ? (LoadingText ?? "Chargement...") : AriaLabel)"
    class="@ClasseCSS"
    @onclick="GererClick"
    @ref="ElementRef">

    <!-- Icône à gauche (optionnelle) -->
    @if (IconeGauche is not null && !Loading)
    {
        <span class="mon-btn-icon-left" aria-hidden="true">
            @IconeGauche
        </span>
    }

    <!-- Spinner de chargement -->
    @if (Loading)
    {
        <span class="mon-btn-spinner" aria-hidden="true">
            <svg class="spinning" viewBox="0 0 24 24" fill="none" ...>
                <!-- SVG du spinner -->
            </svg>
        </span>
    }

    <!-- Texte du bouton -->
    <span class="mon-btn-text">
        @if (Loading && LoadingText is not null)
        {
            @LoadingText
        }
        else
        {
            @ChildContent
        }
    </span>

    <!-- Icône à droite (optionnelle) -->
    @if (IconeDropite is not null && !Loading)
    {
        <span class="mon-btn-icon-right" aria-hidden="true">
            @IconeDropite
        </span>
    }
</button>

@code {
    // ─── PARAMÈTRES PRINCIPAUX ───────────────────────────────────────

    /// Contenu du bouton (texte, icônes, composants)
    [Parameter, EditorRequired]
    public RenderFragment? ChildContent { get; set; }

    /// Gestionnaire de clic
    [Parameter]
    public EventCallback<MouseEventArgs> OnClick { get; set; }

    /// Variante visuelle du bouton
    [Parameter]
    public ButtonVariant Variant { get; set; } = ButtonVariant.Primary;

    /// Taille du bouton
    [Parameter]
    public ButtonSize Size { get; set; } = ButtonSize.Medium;

    /// Désactiver le bouton
    [Parameter]
    public bool Disabled { get; set; }

    /// Afficher l'état de chargement
    [Parameter]
    public bool Loading { get; set; }

    /// Texte affiché pendant le chargement (null = garder le texte original)
    [Parameter]
    public string? LoadingText { get; set; }

    /// Type HTML du bouton (button, submit, reset)
    [Parameter]
    public string TypeBouton { get; set; } = "button";

    /// Prendre toute la largeur disponible
    [Parameter]
    public bool FullWidth { get; set; }

    // ─── ICÔNES ──────────────────────────────────────────────────────

    [Parameter] public RenderFragment? IconeGauche { get; set; }
    [Parameter] public RenderFragment? IconeDropite { get; set; }

    // ─── ACCESSIBILITÉ ───────────────────────────────────────────────

    /// Label pour les lecteurs d'écran (si le contenu n'est pas explicite)
    [Parameter]
    public string? AriaLabel { get; set; }

    // ─── AVANCÉ ──────────────────────────────────────────────────────

    /// Classes CSS supplémentaires
    [Parameter]
    public string? Class { get; set; }

    /// Référence à l'élément DOM (pour focus programmatique)
    [Parameter]
    public ElementReference? ElementRef { get; set; }

    /// Tous les autres attributs HTML (id, data-*, style, ...)
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object>? AttributsSupplementaires { get; set; }

    // ─── CALCUL DES CLASSES CSS ──────────────────────────────────────
    private string ClasseCSS => new CssBuilder("mon-btn")
        .AddClass($"mon-btn-{Variant.ToString().ToLower()}")  // mon-btn-primary
        .AddClass($"mon-btn-{Size.ToString().ToLower()}")     // mon-btn-medium
        .AddClass("mon-btn-loading", Loading)                 // mon-btn-loading
        .AddClass("mon-btn-fullwidth", FullWidth)             // mon-btn-fullwidth
        .AddClass("mon-btn-icon-only",                        // Bouton icône seul
            IconeGauche is not null && ChildContent is null)
        .AddClass(Class)                                      // Classes custom
        .Build();

    // ─── GESTION DU CLIC ─────────────────────────────────────────────
    private async Task GererClick(MouseEventArgs e)
    {
        // Ne pas déclencher si disabled ou loading
        if (Disabled || Loading) return;

        await OnClick.InvokeAsync(e);
    }
}
*/

// Enums pour les variantes et tailles
public enum ButtonVariant
{
    Primary,     // Action principale (bleu)
    Secondary,   // Action secondaire (gris)
    Success,     // Confirmation (vert)
    Danger,      // Destructif (rouge)
    Warning,     // Attention (orange)
    Info,        // Information (cyan)
    Ghost,       // Transparent avec bordure
    Link,        // Apparence de lien
}

public enum ButtonSize
{
    Small,       // 32px de hauteur
    Medium,      // 40px de hauteur (défaut)
    Large,       // 48px de hauteur
    ExtraLarge,  // 56px de hauteur
}

// Helper pour construire les classes CSS (inspiré de BlazorClassBuilder)
public class CssBuilder
{
    private readonly List<string> _classes = new();

    public CssBuilder(string? baseClass = null)
    {
        if (!string.IsNullOrWhiteSpace(baseClass))
            _classes.Add(baseClass);
    }

    public CssBuilder AddClass(string? cssClass)
    {
        if (!string.IsNullOrWhiteSpace(cssClass))
            _classes.Add(cssClass);
        return this;
    }

    public CssBuilder AddClass(string? cssClass, bool condition)
    {
        if (condition && !string.IsNullOrWhiteSpace(cssClass))
            _classes.Add(cssClass);
        return this;
    }

    public string Build() => string.Join(" ", _classes.Distinct());
}


// ----------------------------------------------------------------------------
// [GRAPHIQUE] DATA GRID — Composant avancé avec performances
// ----------------------------------------------------------------------------

/*
Le DataGrid est LE composant qui fait ou défait une bibliothèque.
C'est le plus demandé et le plus complexe à bien faire.

FONCTIONNALITÉS ESSENTIELLES :
-> Tri par colonne (côté client et côté serveur)
-> Pagination (côté client et côté serveur)
-> Filtrage (côté client et côté serveur)
-> Sélection (simple, multiple)
-> Virtualisation (afficher 10000 lignes sans lag)
-> Colonnes redimensionnables
-> Groupement
-> Export (CSV, Excel)
-> Édition inline
*/

/*
─────────────────────────────────────────────────────────────────
MonDataGrid.razor — DataGrid générique et performant
─────────────────────────────────────────────────────────────────

@namespace MonUI.Components
@typeparam TItem        <- Générique : fonctionne avec N'IMPORTE QUEL type

@* Le DataGrid parent - contient les colonnes via ChildContent *@
<div class="mon-datagrid-wrapper @(FullHeight ? "full-height" : "")">

    @* Barre d'outils optionnelle *@
    @if (Toolbar is not null)
    {
        <div class="mon-datagrid-toolbar">
            @Toolbar
        </div>
    }

    @* Zone de filtrage global *@
    @if (ShowSearch)
    {
        <div class="mon-datagrid-search">
            <input type="search"
                   placeholder="@SearchPlaceholder"
                   @bind="_recherche"
                   @bind:event="oninput"
                   @oninput="() => ActualiserDonnees()" />
        </div>
    }

    @* Table principale *@
    <div class="mon-datagrid-scroll">
        <table class="mon-datagrid @Class" role="grid" aria-label="@AriaLabel">

            @* En-tête avec colonnes triables *@
            <thead>
                <tr role="row">
                    @* Colonne de sélection (si sélection activée) *@
                    @if (SelectionMode != SelectionMode.None)
                    {
                        <th class="mon-col-select" role="columnheader">
                            @if (SelectionMode == SelectionMode.Multiple)
                            {
                                <input type="checkbox"
                                       @bind="_toutSelectionne"
                                       @bind:after="TouteSelectionToggle"
                                       aria-label="Sélectionner tout" />
                            }
                        </th>
                    }

                    @* Colonnes définies par l'utilisateur via CascadingValue *@
                    <CascadingValue Value="this" IsFixed="true">
                        @ChildContent
                    </CascadingValue>
                </tr>
            </thead>

            @* Corps de la table *@
            <tbody>
                @if (_chargement)
                {
                    @* Skeleton loading (meilleure UX que spinner) *@
                    @for (int i = 0; i < PageSize; i++)
                    {
                        <tr class="mon-row-skeleton">
                            @for (int j = 0; j < _colonnes.Count; j++)
                            {
                                <td><div class="skeleton-line"></div></td>
                            }
                        </tr>
                    }
                }
                else if (!_donneesAffichees.Any())
                {
                    @* Message "aucun résultat" *@
                    <tr class="mon-row-empty">
                        <td colspan="@_colonnes.Count" class="text-center py-5">
                            @if (EmptyContent is not null)
                            {
                                @EmptyContent
                            }
                            else
                            {
                                <div>
                                    <p>@EmptyMessage</p>
                                </div>
                            }
                        </td>
                    </tr>
                }
                else
                {
                    @foreach (var (item, index) in _donneesAffichees.Select((x, i) => (x, i)))
                    {
                        var estSelectionne = _selection.Contains(item);
                        <tr class="mon-row @(estSelectionne ? "selected" : "") @(index % 2 == 0 ? "even" : "odd")"
                            @onclick="() => GererClicLigne(item)"
                            @key="GetItemKey?.Invoke(item) ?? item"
                            role="row"
                            aria-selected="@estSelectionne.ToString().ToLower()">

                            @if (SelectionMode != SelectionMode.None)
                            {
                                <td class="mon-col-select">
                                    <input type="@(SelectionMode == SelectionMode.Single ? "radio" : "checkbox")"
                                           checked="@estSelectionne"
                                           @onchange="() => BasculerSelection(item)"
                                           aria-label="@($"Sélectionner la ligne {index + 1}")" />
                                </td>
                            }

                            @* Rendu des cellules *@
                            @foreach (var colonne in _colonnes)
                            {
                                <td class="@colonne.ClasseCell" style="@colonne.StyleCell" role="gridcell">
                                    @if (colonne.Template is not null)
                                    {
                                        @colonne.Template(item)  @* Template personnalisé *@
                                    }
                                    else
                                    {
                                        @colonne.ObtenirValeurTexte(item) @* Valeur par défaut *@
                                    }
                                </td>
                            }
                        </tr>
                    }
                }
            </tbody>
        </table>
    </div>

    @* Pagination *@
    @if (ShowPagination && !ServerSide)
    {
        <div class="mon-datagrid-footer">
            <span class="mon-datagrid-info">
                Affichage @((_pageCourante - 1) * PageSize + 1)-@(Math.Min(_pageCourante * PageSize, _totalItems))
                sur @_totalItems résultats
            </span>
            <MonPagination
                PageCourante="@_pageCourante"
                TotalPages="@_totalPages"
                OnPageChange="@ChangerPage" />
        </div>
    }
</div>

@code {
    // ─── PARAMÈTRES DE BASE ──────────────────────────────────────────

    /// Source de données (côté client)
    [Parameter]
    public IEnumerable<TItem>? Items { get; set; }

    /// Fonction de chargement (côté serveur)
    /// Appelée avec les paramètres de tri, filtre, pagination
    [Parameter]
    public Func<DataGridRequest, Task<DataGridResponse<TItem>>>? LoadData { get; set; }

    /// Définition des colonnes
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    // ─── PARAMÈTRES D'AFFICHAGE ──────────────────────────────────────

    [Parameter] public int PageSize { get; set; } = 25;
    [Parameter] public bool ShowPagination { get; set; } = true;
    [Parameter] public bool ShowSearch { get; set; } = false;
    [Parameter] public string SearchPlaceholder { get; set; } = "Rechercher...";
    [Parameter] public string EmptyMessage { get; set; } = "Aucun résultat";
    [Parameter] public RenderFragment? EmptyContent { get; set; }
    [Parameter] public RenderFragment? Toolbar { get; set; }
    [Parameter] public bool FullHeight { get; set; } = false;
    [Parameter] public SelectionMode SelectionMode { get; set; } = SelectionMode.None;
    [Parameter] public bool Striped { get; set; } = true;
    [Parameter] public bool Hoverable { get; set; } = true;
    [Parameter] public string? Class { get; set; }
    [Parameter] public string? AriaLabel { get; set; } = "Tableau de données";

    // ─── SERVEUR-SIDE ────────────────────────────────────────────────

    /// Mode serveur : tri/filtre/pagination envoyés au serveur
    [Parameter] public bool ServerSide { get; set; } = false;

    // ─── CALLBACKS ───────────────────────────────────────────────────

    [Parameter] public EventCallback<TItem> OnRowClick { get; set; }
    [Parameter] public EventCallback<List<TItem>> OnSelectionChange { get; set; }
    [Parameter] public Func<TItem, object>? GetItemKey { get; set; }

    // ─── ÉTAT INTERNE ────────────────────────────────────────────────
    private List<MonDataGridColumn<TItem>> _colonnes = new();
    private List<TItem> _donneesAffichees = new();
    private HashSet<TItem> _selection = new();
    private bool _toutSelectionne = false;
    private bool _chargement = false;
    private string _recherche = "";
    private int _pageCourante = 1;
    private int _totalItems = 0;
    private int _totalPages => (int)Math.Ceiling(_totalItems / (double)PageSize);
    private string? _triColonne = null;
    private bool _triDecroissant = false;

    // Appelé par les colonnes enfants pour s'enregistrer
    internal void EnregistrerColonne(MonDataGridColumn<TItem> colonne)
    {
        _colonnes.Add(colonne);
        StateHasChanged();
    }

    protected override async Task OnParametersSetAsync()
    {
        await ActualiserDonnees();
    }

    private async Task ActualiserDonnees()
    {
        _chargement = true;
        StateHasChanged();

        try
        {
            if (ServerSide && LoadData is not null)
            {
                // Mode serveur : déléguer au callback
                var requete = new DataGridRequest
                {
                    Page = _pageCourante,
                    ParPage = PageSize,
                    Recherche = _recherche,
                    TriColonne = _triColonne,
                    TriDecroissant = _triDecroissant,
                };

                var reponse = await LoadData(requete);
                _donneesAffichees = reponse.Items.ToList();
                _totalItems = reponse.Total;
            }
            else if (Items is not null)
            {
                // Mode client : filtrer/trier/paginer en mémoire
                var query = Items.AsEnumerable();

                // Filtrage
                if (!string.IsNullOrEmpty(_recherche))
                {
                    query = query.Where(item =>
                        _colonnes.Any(col =>
                            col.ObtenirValeurTexte(item)
                                ?.Contains(_recherche, StringComparison.OrdinalIgnoreCase) == true));
                }

                // Tri
                if (_triColonne is not null)
                {
                    var colonne = _colonnes.FirstOrDefault(c => c.Champ == _triColonne);
                    if (colonne?.SortFunc is not null)
                    {
                        query = _triDecroissant
                            ? query.OrderByDescending(colonne.SortFunc.Compile())
                            : query.OrderBy(colonne.SortFunc.Compile());
                    }
                }

                _totalItems = query.Count();

                // Pagination
                _donneesAffichees = query
                    .Skip((_pageCourante - 1) * PageSize)
                    .Take(PageSize)
                    .ToList();
            }
        }
        finally
        {
            _chargement = false;
            StateHasChanged();
        }
    }

    private async Task TrierParColonne(string champ)
    {
        if (_triColonne == champ)
            _triDecroissant = !_triDecroissant;
        else
        {
            _triColonne = champ;
            _triDecroissant = false;
        }

        _pageCourante = 1;
        await ActualiserDonnees();
    }

    private async Task ChangerPage(int page)
    {
        _pageCourante = page;
        await ActualiserDonnees();
    }

    private async Task GererClicLigne(TItem item)
    {
        if (SelectionMode != SelectionMode.None)
            await BasculerSelection(item);

        await OnRowClick.InvokeAsync(item);
    }

    private async Task BasculerSelection(TItem item)
    {
        if (SelectionMode == SelectionMode.Single)
        {
            _selection.Clear();
            _selection.Add(item);
        }
        else
        {
            if (!_selection.Remove(item))
                _selection.Add(item);
        }

        _toutSelectionne = _selection.Count == _donneesAffichees.Count;
        await OnSelectionChange.InvokeAsync(_selection.ToList());
    }

    private async Task TouteSelectionToggle()
    {
        if (_toutSelectionne)
            _donneesAffichees.ForEach(i => _selection.Add(i));
        else
            _selection.Clear();

        await OnSelectionChange.InvokeAsync(_selection.ToList());
    }

    // Export CSV
    public string ExporterCSV()
    {
        var sb = new System.Text.StringBuilder();

        // En-têtes
        sb.AppendLine(string.Join(",", _colonnes.Select(c => $"\"{c.Titre}\"")));

        // Données
        foreach (var item in _donneesAffichees)
        {
            var valeurs = _colonnes.Select(c =>
                $"\"{c.ObtenirValeurTexte(item)?.Replace("\"", "\"\"") ?? ""}\"");
            sb.AppendLine(string.Join(",", valeurs));
        }

        return sb.ToString();
    }
}
*/

// Modèles du DataGrid
public class DataGridRequest
{
    public int Page { get; set; } = 1;
    public int ParPage { get; set; } = 25;
    public string? Recherche { get; set; }
    public string? TriColonne { get; set; }
    public bool TriDecroissant { get; set; }
    public Dictionary<string, string> Filtres { get; set; } = new();
}

public class DataGridResponse<T>
{
    public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
    public int Total { get; set; }
}

public enum SelectionMode { None, Single, Multiple }

// Colonne du DataGrid (simplified)
public class MonDataGridColumn<TItem>
{
    public string Champ { get; set; } = "";
    public string Titre { get; set; } = "";
    public string? ClasseCell { get; set; }
    public string? StyleCell { get; set; }
    public RenderFragment<TItem>? Template { get; set; }
    public System.Linq.Expressions.Expression<Func<TItem, object>>? SortFunc { get; set; }

    public string? ObtenirValeurTexte(TItem item)
    {
        if (SortFunc is null) return null;
        try { return SortFunc.Compile()(item)?.ToString(); }
        catch { return null; }
    }
}


// ----------------------------------------------------------------------------
// [PLUGIN] EXTENSIBILITÉ — Comment rendre votre bibliothèque customisable
// ----------------------------------------------------------------------------

/*
PATTERN : OPTIONS + THEME + CSS VARIABLES

Le secret des grandes bibliothèques : permettre la customisation
sans forcer à modifier les composants eux-mêmes.

3 NIVEAUX DE CUSTOMISATION :

Niveau 1 - CSS Variables (le plus simple, 90% des cas)
-> L'utilisateur change --mon-ui-primary-color en une ligne de CSS
-> Appliqué globalement, instantanément
-> Idéal pour : couleurs, typographie, arrondis, espacement

Niveau 2 - Options (configuration .NET)
-> builder.Services.AddMonUI(options => options.Theme = MonTheme.Dark)
-> Appliqué à la configuration globale
-> Idéal pour : langue par défaut, comportements, icônes

Niveau 3 - Templates (personnalisation profonde)
-> <MonDataGrid><RowTemplate>...</RowTemplate></MonDataGrid>
-> L'utilisateur remplace entièrement le rendu d'une partie
-> Idéal pour : cas très spécifiques, apparences uniques
*/

// Options de la bibliothèque
public class MonUIOptions
{
    // Theme global
    public string Theme { get; set; } = "light";

    // Langue pour les textes automatiques
    public string Locale { get; set; } = "fr-FR";

    // Taille de pagination par défaut
    public int DefaultPageSize { get; set; } = 25;

    // Activer les animations (peut être désactivé pour l'accessibilité)
    public bool EnableAnimations { get; set; } = true;

    // Icônes à utiliser (Bootstrap Icons par défaut)
    public string IconLibrary { get; set; } = "bootstrap";

    // Position par défaut des notifications
    public NotificationPosition DefaultNotificationPosition { get; set; }
        = NotificationPosition.TopRight;

    // Délai avant fermeture automatique des notifications (ms)
    public int DefaultNotificationDuration { get; set; } = 4000;

    // Callback pour les erreurs non gérées dans les composants
    public Action<Exception, string>? OnComponentError { get; set; }
}

public enum NotificationPosition
{
    TopLeft, TopCenter, TopRight,
    BottomLeft, BottomCenter, BottomRight
}

// Extension pour enregistrer la bibliothèque
public static class MonUIServiceExtensions
{
    public static IServiceCollection AddMonUI(
        this IServiceCollection services,
        Action<MonUIOptions>? configure = null)
    {
        // Options
        var options = new MonUIOptions();
        configure?.Invoke(options);
        services.AddSingleton(options);

        // Services
        services.AddScoped<INotificationService, NotificationService>();
        services.AddScoped<IModalService, ModalService>();
        services.AddScoped<IThemeService, ThemeService>();

        // JavaScript Interop
        services.AddScoped<MonUIJSInterop>();

        return services;
    }
}

// Interfaces des services
public interface INotificationService
{
    Task AfficherSuccesAsync(string message, string? titre = null);
    Task AfficherErreurAsync(string message, string? titre = null);
    Task AfficherAvertissementAsync(string message, string? titre = null);
    Task AfficherInfoAsync(string message, string? titre = null);
    Task<bool> DemanderConfirmationAsync(string message, string? titre = null);
}

public interface IModalService
{
    Task<TResult?> AfficherAsync<TComponent, TResult>(
        string titre,
        Dictionary<string, object>? parametres = null)
        where TComponent : ComponentBase;
    Task FermerAsync();
}

public interface IThemeService
{
    string ThemeActuel { get; }
    Task ChangerThemeAsync(string theme);
    event Action<string>? ThemeChange;
}

// Stubs pour compilation
public class NotificationService : INotificationService
{
    public Task AfficherSuccesAsync(string message, string? titre = null) => Task.CompletedTask;
    public Task AfficherErreurAsync(string message, string? titre = null) => Task.CompletedTask;
    public Task AfficherAvertissementAsync(string message, string? titre = null) => Task.CompletedTask;
    public Task AfficherInfoAsync(string message, string? titre = null) => Task.CompletedTask;
    public Task<bool> DemanderConfirmationAsync(string message, string? titre = null) => Task.FromResult(true);
}

public class ModalService : IModalService
{
    public Task<TResult?> AfficherAsync<TComponent, TResult>(string titre, Dictionary<string, object>? parametres = null)
        where TComponent : ComponentBase => Task.FromResult<TResult?>(default);
    public Task FermerAsync() => Task.CompletedTask;
}

public class ThemeService : IThemeService
{
    public string ThemeActuel => "light";
    public event Action<string>? ThemeChange;
    public Task ChangerThemeAsync(string theme) { ThemeChange?.Invoke(theme); return Task.CompletedTask; }
}

public class MonUIJSInterop
{
    // Interop JavaScript minimal
}


// ----------------------------------------------------------------------------
// [TEST] TESTS DES COMPOSANTS — Avec bUnit
// ----------------------------------------------------------------------------

/*
bUnit = Bibliothèque de tests spécialisée pour Blazor
-> Rend les composants dans un contexte de test
-> Permet d'interagir avec l'UI de façon programmatique
-> Vérifie le rendu HTML, les états, les événements

INSTALLATION :
dotnet add package bunit
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.NET.Test.Sdk

COMMANDE POUR LANCER LES TESTS :
dotnet test MonUI.Tests.Components/
*/

/*
─────────────────────────────────────────────────────────────────
MonButtonTests.cs — Tests du composant Button
─────────────────────────────────────────────────────────────────

using Bunit;
using Xunit;
using MonUI.Components;

public class MonButtonTests : TestContext
{
    [Fact]
    public void Button_RendCorrectement_ParDefaut()
    {
        // ARRANGE : Rendre le composant
        var cut = RenderComponent<MonButton>(params_ => params_
            .AddChildContent("Cliquer ici"));

        // ACT : Trouver l'élément
        var button = cut.Find("button");

        // ASSERT : Vérifications
        Assert.Contains("mon-btn", button.GetAttribute("class") ?? "");
        Assert.Contains("mon-btn-primary", button.GetAttribute("class") ?? "");
        Assert.Contains("mon-btn-medium", button.GetAttribute("class") ?? "");
        Assert.Equal("button", button.GetAttribute("type"));
        Assert.Null(button.GetAttribute("disabled"));
        Assert.Equal("Cliquer ici", button.TextContent.Trim());
    }

    [Fact]
    public async Task Button_DeclencheOnClick_QuandClique()
    {
        // ARRANGE
        var clicCount = 0;
        var cut = RenderComponent<MonButton>(params_ => params_
            .Add(p => p.OnClick, _ => { clicCount++; return Task.CompletedTask; })
            .AddChildContent("Cliquer"));

        // ACT
        await cut.Find("button").ClickAsync(new MouseEventArgs());

        // ASSERT
        Assert.Equal(1, clicCount);
    }

    [Fact]
    public async Task Button_NeDeclenchePas_SiDisabled()
    {
        // ARRANGE
        var clicCount = 0;
        var cut = RenderComponent<MonButton>(params_ => params_
            .Add(p => p.Disabled, true)
            .Add(p => p.OnClick, _ => { clicCount++; return Task.CompletedTask; })
            .AddChildContent("Cliquer"));

        // ACT : Tenter de cliquer (ne devrait pas marcher)
        var button = cut.Find("button");
        // Un bouton disabled ne déclenche pas onclick -> pas besoin de try/catch

        // ASSERT
        Assert.NotNull(button.GetAttribute("disabled"));
        Assert.Equal(0, clicCount);
    }

    [Fact]
    public void Button_AfficheSpinner_EnEtatLoading()
    {
        // ARRANGE & ACT
        var cut = RenderComponent<MonButton>(params_ => params_
            .Add(p => p.Loading, true)
            .AddChildContent("Sauvegarder"));

        // ASSERT : Spinner visible, texte préservé
        var spinner = cut.Find(".mon-btn-spinner");
        Assert.NotNull(spinner);
        // Bouton désactivé pendant le chargement
        Assert.NotNull(cut.Find("button").GetAttribute("disabled"));
    }

    [Fact]
    public void Button_AfficheTexteLoading_SiDefini()
    {
        // ARRANGE & ACT
        var cut = RenderComponent<MonButton>(params_ => params_
            .Add(p => p.Loading, true)
            .Add(p => p.LoadingText, "Sauvegarde en cours...")
            .AddChildContent("Sauvegarder"));

        // ASSERT
        Assert.Contains("Sauvegarde en cours...", cut.Markup);
        Assert.DoesNotContain("Sauvegarder", cut.Find(".mon-btn-text").TextContent);
    }

    [Theory]  // Test paramétrique : tester plusieurs variantes automatiquement
    [InlineData(ButtonVariant.Primary, "mon-btn-primary")]
    [InlineData(ButtonVariant.Danger, "mon-btn-danger")]
    [InlineData(ButtonVariant.Success, "mon-btn-success")]
    [InlineData(ButtonVariant.Ghost, "mon-btn-ghost")]
    public void Button_AppliqueClasseVariante_Correctement(
        ButtonVariant variante,
        string classeAttendue)
    {
        var cut = RenderComponent<MonButton>(params_ => params_
            .Add(p => p.Variant, variante)
            .AddChildContent("Test"));

        Assert.Contains(classeAttendue,
            cut.Find("button").GetAttribute("class") ?? "");
    }

    [Fact]
    public void Button_EstAccessible_WCAG()
    {
        // Test d'accessibilité basique
        var cut = RenderComponent<MonButton>(params_ => params_
            .Add(p => p.Loading, true)
            .Add(p => p.AriaLabel, "Sauvegarder le document")
            .AddChildContent("[SAUVEGARDE]"));

        var button = cut.Find("button");

        // aria-busy pendant loading
        Assert.Equal("true", button.GetAttribute("aria-busy"));
        // aria-label explicite pour les icônes seules
        Assert.Equal("Sauvegarder le document", button.GetAttribute("aria-label"));
    }
}
*/


// ----------------------------------------------------------------------------
// [SORTIE] PUBLICATION SUR NUGET
// ----------------------------------------------------------------------------

/*
ÉTAPES POUR PUBLIER SUR NUGET.ORG :

1. PRÉPARER LE .csproj avec les métadonnées NuGet :

<Project Sdk="Microsoft.NET.Sdk.Razor">
  <PropertyGroup>
    <!-- Métadonnées NuGet obligatoires -->
    <PackageId>MonUI.Components</PackageId>
    <Version>1.0.0</Version>
    <Authors>Votre Nom</Authors>
    <Description>
      Bibliothèque de composants Blazor professionnels.
      Boutons, DataGrid, Modal, Notifications et plus encore.
    </Description>

    <!-- Métadonnées optionnelles mais recommandées -->
    <PackageTags>blazor;components;ui;webassembly;server</PackageTags>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageProjectUrl>https://github.com/votrelogin/mon-ui</PackageProjectUrl>
    <RepositoryUrl>https://github.com/votrelogin/mon-ui.git</RepositoryUrl>
    <PackageIcon>icon.png</PackageIcon>
    <PackageReadmeFile>README.md</PackageReadmeFile>
    <PackageReleaseNotes>
      v1.0.0 : Version initiale
      - Composant Button avec 8 variantes
      - Composant DataGrid avec tri et pagination
      - Composant Modal
      - Composant Notification
    </PackageReleaseNotes>

    <!-- Configuration de build -->
    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
    <EmbedAllSources>true</EmbedAllSources>
    <Deterministic>true</Deterministic>
  </PropertyGroup>

  <!-- Inclure les fichiers statiques -->
  <ItemGroup>
    <None Include="README.md" Pack="true" PackagePath="\" />
    <None Include="icon.png" Pack="true" PackagePath="\" />
  </ItemGroup>
</Project>

2. CRÉER UN COMPTE NUGET.ORG ET OBTENIR UN API KEY
   -> https://www.nuget.org/account/apikeys

3. BUILD ET PACK :
   dotnet build -c Release
   dotnet pack -c Release -o ./nupkgs

4. PUBLIER :
   dotnet nuget push ./nupkgs/*.nupkg \
     --api-key VotreApiKey \
     --source https://api.nuget.org/v3/index.json \
     --skip-duplicate

VERSIONING (SemVer) :
  1.0.0  -> Version majeure : changements incompatibles (breaking changes)
  1.1.0  -> Version mineure : nouvelles fonctionnalités (compatible)
  1.1.1  -> Correctif : bug fixes seulement

AUTOMATISER AVEC GITHUB ACTIONS :
  (Voir chapitre CI/CD - ajouter un job "publish-nuget" sur tag)

MONÉTISATION :
-> Tier 1 (Gratuit) : Composants de base, usage non-commercial
-> Tier 2 (49€/mois) : Tous les composants, projets commerciaux
-> Tier 3 (199€/mois) : Support prioritaire, fonctionnalités avancées
-> Utiliser : Gumroad, Paddle, Stripe pour la gestion des licences
*/


// ============================================================================
// [GUIDE] CHAPITRE 34 : CONTRIBUTION OPEN-SOURCE BLAZOR
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre POURQUOI contribuer à l'open-source
[OK] Naviguer dans le code source de Blazor
[OK] Contribuer efficacement (issues, PRs)
[OK] Rédiger des propositions de fonctionnalités
[OK] Gérer les interactions avec les maintainers
[OK] Construire votre réputation open-source
*/


// ----------------------------------------------------------------------------
// [MONDE] POURQUOI CONTRIBUER À L'OPEN-SOURCE ?
// ----------------------------------------------------------------------------

/*
BÉNÉFICES PERSONNELS :
-> Améliorer vos compétences (travailler sur une base de code réelle et complexe)
-> Visibilité (votre travail est vu par des milliers de développeurs)
-> Réseau professionnel (vous connaissez les mainteneurs)
-> CV : "Contributeur à ASP.NET Core / Blazor" -> Très valorisé
-> Confiance en vous (votre code est validé par des experts Microsoft)

BÉNÉFICES POUR LA COMMUNAUTÉ :
-> Blazor s'améliore grâce à vos contributions
-> Vous corrigez des bugs que vous avez rencontrés
-> Vous ajoutez des fonctionnalités dont vous avez besoin
-> Documentation plus claire -> Tout le monde progresse plus vite

TYPES DE CONTRIBUTIONS (du plus facile au plus complexe) :

* Niveau 1 : DOCUMENTATION (commencer ici !)
  -> Corriger des fautes de frappe
  -> Clarifier des explications confuses
  -> Ajouter des exemples de code
  -> Traduire la documentation
  Avantage : Toujours accepté si correct, peu de risque de rejet

** Niveau 2 : BUG REPORTS
  -> Rapporter un bug avec reproduction minimale
  -> Confirmer qu'un bug rapporté par quelqu'un d'autre se reproduit
  -> Identifier la cause du bug dans le code

*** Niveau 3 : BUG FIXES
  -> Corriger un bug simple (une ligne, quelques lignes)
  -> Ajouter des tests pour un bug non couvert
  -> Améliorer les messages d'erreur

**** Niveau 4 : NOUVELLES FONCTIONNALITÉS PETITES
  -> Proposer via une "Feature Request" (issue)
  -> Attendre l'approbation des mainteneurs
  -> Implémenter la fonctionnalité avec tests
  -> Documenter la fonctionnalité

***** Niveau 5 : ARCHITECTURE
  -> Proposer des changements architecturaux significatifs
  -> Nécessite beaucoup d'expérience et de discussion
  -> Reserved pour les contributeurs réguliers
*/


// ----------------------------------------------------------------------------
// [WORLD_MAP] NAVIGUER DANS LE CODE SOURCE BLAZOR
// ----------------------------------------------------------------------------

/*
REPO PRINCIPAL : https://github.com/dotnet/aspnetcore

STRUCTURE SIMPLIFIÉE DU CODE BLAZOR :

aspnetcore/
├── src/
│   ├── Components/                    <- Tout ce qui concerne Blazor
│   │   ├── Components/                <- Composants de base (@Component, etc.)
│   │   │   └── src/
│   │   │       ├── ComponentBase.cs   <- La classe de base de TOUS les composants
│   │   │       ├── RenderTree/        <- Moteur de rendu (algorithme de diffing)
│   │   │       └── Routing/           <- Router, NavigationManager...
│   │   │
│   │   ├── Server/                    <- Blazor Server (SignalR, circuit)
│   │   │
│   │   ├── WebAssembly/               <- Blazor WASM
│   │   │   ├── Authentication/        <- AuthenticationStateProvider WASM
│   │   │   ├── Hosting/               <- WebAssemblyHost, Service Worker
│   │   │   └── Services/
│   │   │
│   │   └── Web/                       <- Blazor Web App (nouveau .NET 8)
│   │
│   └── Mvc/                           <- Razor Pages et MVC (partagés avec Blazor)
│
└── test/
    └── Components/                    <- Tests de Blazor
*/

/*
COMMANDES POUR EXPLORER ET BUILDER ASPNETCORE :

# Cloner le repo
git clone https://github.com/dotnet/aspnetcore.git
cd aspnetcore

# Builder Blazor spécifiquement
./build.cmd --projects src/Components/Components/src

# Lancer les tests Blazor
dotnet test src/Components/Components/test/ -v n

# Voir les issues "good first issue" (parfaites pour débuter)
# https://github.com/dotnet/aspnetcore/issues?q=is:open+label:"good+first+issue"+label:area-blazor
*/


// ----------------------------------------------------------------------------
// [BUG] REPORTER UN BUG DE MANIÈRE PROFESSIONNELLE
// ----------------------------------------------------------------------------

/*
UN BON BUG REPORT = La moitié du travail de correction

ÉLÉMENTS ESSENTIELS :

1. TITRE CLAIR ET PRÉCIS
   [X] "Blazor ne fonctionne pas"
   [OK] "NavigationManager.NavigateTo() ne met pas à jour le titre de page dans Blazor WASM 8.0.1"

2. VERSION DES OUTILS
   -> .NET SDK version (dotnet --version)
   -> OS (Windows 11, macOS 14, Ubuntu 22.04)
   -> Navigateur si Blazor WASM (Chrome 121, Firefox 123...)
   -> Version du package concerné

3. COMPORTEMENT ACTUEL VS ATTENDU
   -> Actuel : "La page s'affiche sans titre (onglet vide)"
   -> Attendu : "L'onglet du navigateur devrait afficher le titre de la page"

4. REPRODUCTION MINIMALE (la partie la plus importante !)
   -> Le code le plus PETIT qui reproduit le bug
   -> Idéalement : un lien vers un repo GitHub ou Blazor REPL
   -> NE PAS envoyer 5000 lignes de code !

5. LOGS ET SCREENSHOTS
   -> Console errors (F12)
   -> Stack trace complet
   -> Screenshot si bug visuel

EXEMPLE DE BON RAPPORT :

Titre : [Blazor WASM] PageTitle component doesn't update browser tab after NavigateTo()

Environment:
- .NET SDK 8.0.101
- Microsoft.AspNetCore.Components.WebAssembly 8.0.1
- Chrome 121.0.6167.140, Windows 11

Actual behavior:
After calling NavigationManager.NavigateTo("/other-page"), the browser tab
title remains the title of the previous page until a full page reload.

Expected behavior:
The browser tab title should update immediately to match the <PageTitle>
of the new page.

Minimal reproduction:
https://github.com/user/blazor-pagetitle-bug

Steps to reproduce:
1. Clone the repo above
2. Run dotnet run in the Client project
3. Click the "Go to Products" button
4. Observe: Browser tab still shows "Home" instead of "Products"

Additional context:
This worked correctly in .NET 7. The regression appears to be in
the NavigationManager refactoring from #47523.
*/


// ----------------------------------------------------------------------------
// [OUTIL] CRÉER UNE PULL REQUEST PROFESSIONNELLE
// ----------------------------------------------------------------------------

/*
PROCESSUS COMPLET D'UNE CONTRIBUTION :

─────────────────────────────────────────────────────────────
ÉTAPE 1 : SETUP (une seule fois)
─────────────────────────────────────────────────────────────

# 1. Forker le repo sur GitHub (bouton "Fork")
# 2. Cloner VOTRE fork
git clone https://github.com/VOTRE-LOGIN/aspnetcore.git
cd aspnetcore

# 3. Ajouter le repo original comme "upstream"
git remote add upstream https://github.com/dotnet/aspnetcore.git

# 4. Configurer git
git config user.name "Votre Nom"
git config user.email "votre@email.com"

─────────────────────────────────────────────────────────────
ÉTAPE 2 : CRÉER UNE BRANCHE POUR VOTRE FIX
─────────────────────────────────────────────────────────────

# Toujours partir d'un main à jour
git checkout main
git fetch upstream
git merge upstream/main

# Créer une branche descriptive
git checkout -b fix/pagetitle-not-updating-after-navigation
# ou
git checkout -b feature/add-oncompleted-callback-to-editform
# ou
git checkout -b docs/clarify-cascading-parameter-usage

─────────────────────────────────────────────────────────────
ÉTAPE 3 : IMPLÉMENTER LE FIX
─────────────────────────────────────────────────────────────

# Votre travail de développement...
# ...corriger le bug, ajouter la fonctionnalité, améliorer la doc

─────────────────────────────────────────────────────────────
ÉTAPE 4 : VÉRIFIER LES TESTS
─────────────────────────────────────────────────────────────

# TOUJOURS ajouter un test qui prouve que le bug est corrigé !
# Un fix sans test = risque de régression future

# Lancer les tests existants (pour vérifier qu'on n'a rien cassé)
dotnet test src/Components/ -v n --no-build

# Vérifier le style de code
dotnet format --verify-no-changes

─────────────────────────────────────────────────────────────
ÉTAPE 5 : COMMITER ET POUSSER
─────────────────────────────────────────────────────────────

# Convention de commit ASP.NET Core :
git commit -m "Fix PageTitle not updating after NavigateTo() in WASM (#12345)"
# Format : "Type Description (#numéro-issue)"
# Types : Fix, Feature, Refactor, Test, Docs

git push origin fix/pagetitle-not-updating-after-navigation

─────────────────────────────────────────────────────────────
ÉTAPE 6 : CRÉER LA PULL REQUEST
─────────────────────────────────────────────────────────────

Sur GitHub, créer une PR avec :

TITRE : Fix PageTitle not updating after NavigateTo() in Blazor WASM

DESCRIPTION (template) :
---
## Description
<!-- Expliquer le problème et la solution -->
When calling NavigationManager.NavigateTo() in Blazor WASM,
the PageTitle component was not updating the browser tab title.

This was caused by [EXPLICATION TECHNIQUE].

Fixes #12345

## Changes Made
- [x] Fixed the `NavigationManager` to notify `PageTitle` after navigation
- [x] Added unit test for the regression
- [x] Updated CHANGELOG.md

## Testing
All existing tests pass. Added a new test in
`test/Microsoft.AspNetCore.Components.WebAssembly.Tests/`
that reproduces the bug and verifies the fix.

## Breaking Changes
None. This is a bug fix.
---

─────────────────────────────────────────────────────────────
ÉTAPE 7 : RÉPONDRE AU REVIEW
─────────────────────────────────────────────────────────────

-> Les mainteneurs vont commenter votre code -> C'est NORMAL et PRÉCIEUX
-> Répondre à chaque commentaire
-> Faire les modifications demandées
-> Ne pas prendre les critiques personnellement
-> Apprendre de leurs retours -> Vous progresserez énormément !

DÉLAI TYPIQUE :
-> Documentation : quelques jours
-> Bug fix simple : 1-2 semaines
-> Nouvelle fonctionnalité : 2-6 mois (beaucoup de discussion)
-> Changement architectural : 6+ mois

CONSEILS DES CONTRIBUTEURS EXPÉRIMENTÉS :
-> "Commencez par la documentation - ça vous apprend le codebase"
-> "Lisez les PRs précédentes similaires pour comprendre le style attendu"
-> "Ouvrez une issue AVANT de coder une fonctionnalité (éviter de travailler pour rien)"
-> "Soyez patients - les mainteneurs sont très occupés"
-> "Petit PR = PR accepté. Grand PR = Grand risque de rejet"
*/


// ----------------------------------------------------------------------------
// [IDEE] PROPOSER UNE FONCTIONNALITÉ (Feature Proposal)
// ----------------------------------------------------------------------------

/*
COMMENT PROPOSER UNE FONCTIONNALITÉ QUI SERA ACCEPTÉE :

1. VÉRIFIER QU'ELLE N'EXISTE PAS DÉJÀ
   -> Chercher dans les issues (ouvertes ET fermées)
   -> Chercher dans la documentation
   -> Chercher dans les discussions GitHub

2. VÉRIFIER QU'ELLE EST DANS LE SCOPE
   -> Blazor est un framework web -> Pas de fonctionnalités desktop
   -> Suivre les principes directeurs (simple, composable, performant)

3. STRUCTURER LA PROPOSITION

Titre : [Feature] Add cancellation token support to OnInitializedAsync

Problem:
When the user navigates away from a page while OnInitializedAsync() is
still running, the async operation continues unnecessarily, potentially
causing memory leaks or errors when the component is disposed.

Current workaround:
Developers must manually implement cancellation using CancellationToken
from IDisposable, which is verbose and error-prone.

Proposed solution:
Add a CancellationToken parameter to the OnInitializedAsync lifecycle method:

  protected override async Task OnInitializedAsync(CancellationToken ct)
  {
      _data = await _service.GetDataAsync(ct);  // Automatically cancelled on dispose
  }

The framework would:
1. Create a CancellationTokenSource when the component initializes
2. Pass the token to OnInitializedAsync
3. Cancel the token when the component is disposed

Alternatives considered:
1. Status quo (current workaround) - Too verbose
2. Extension method - Doesn't integrate naturally with the lifecycle

Impact:
- No breaking change (old signature still works)
- Reduces boilerplate code significantly
- Prevents common async disposal bugs

4. ATTENDRE LE FEEDBACK AVANT DE CODER
   -> Attendre que les mainteneurs disent "ok, on accepterait une PR pour ça"
   -> Ne pas coder 5 jours pour se faire dire "non, ce n'est pas dans notre vision"
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE — PARTIE 10 COMPLÈTE
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
EXERCICE FINAL : PROJET INTÉGRATEUR EXPERT
═══════════════════════════════════════════════════════════════

OBJECTIF : Construire une plateforme e-commerce complète en architecture
micro-frontend, avec bibliothèque de composants maison et contribution
à la communauté.

PARTIE A : MICRO-FRONTENDS (4h)
  a) Créer la solution avec Shell + 3 modules (Catalogue, Commandes, Compte)
  b) Implémenter le lazy loading par module
  c) Implémenter l'IntegrationEventBus
  d) Quand "AjouterAuPanier" est cliqué dans Catalogue,
     Module.Commandes reçoit l'événement et met à jour le badge panier
  e) Vérifier : Charger /catalogue -> Seul Module.Catalogue.dll téléchargé

PARTIE B : BIBLIOTHÈQUE DE COMPOSANTS (4h)
  a) Créer MonUI.Core (RCL)
  b) Implémenter MonButton avec toutes les variantes et l'état de chargement
  c) Implémenter MonInput avec validation
  d) Implémenter MonCard
  e) Écrire 5 tests bUnit pour chaque composant
  f) Créer un site de démo docs/MonUI.Docs
  g) Publier sur NuGet (ou GHCR)

PARTIE C : BLAZOR HYBRID (2h)
  a) Créer un projet MAUI Blazor
  b) Réutiliser les pages du Module.Catalogue
  c) Implémenter l'accès à la caméra pour scanner des produits
  d) Mode offline avec SQLite local

PARTIE D : CONTRIBUTION OPEN-SOURCE (2h)
  a) Trouver une issue "good first issue" sur aspnetcore
  b) Reproduire le bug en local
  c) Corriger le bug
  d) Écrire un test
  e) Ouvrir une Pull Request
  (Même si la PR n'est pas acceptée, vous avez appris !)

COMPÉTENCES VALIDÉES :
[OK] Micro-frontends avec RCL et lazy loading
[OK] IntegrationEventBus inter-modules
[OK] Bibliothèque de composants NuGet
[OK] Tests bUnit
[OK] Blazor Hybrid (MAUI)
[OK] Contribution open-source
═══════════════════════════════════════════════════════════════
*/


/*
═══════════════════════════════════════════════════════════════
[DOCS] RÉSUMÉ DE LA PARTIE 10 COMPLÈTE

[OK] CHAPITRE 29 - BLAZOR HYBRID (.NET MAUI) :
- BlazorWebView = Héberge Blazor dans une app MAUI native
- Exécution .NET locale (pas WASM) -> Accès complet au système
- Interfaces partagées (ICameraService, ILocationService, etc.)
- Implémentations séparées pour MAUI et Web
- Lazy SQLite pour mode offline
- Partage de COMPOSANTS entre Web et MAUI (réutilisation maximale)
- MauiProgram.cs = L'équivalent de Program.cs pour les apps MAUI

[OK] CHAPITRE 30 - PWA AVEC BLAZOR :
- manifest.webmanifest : Définir l'app installable (icônes, couleurs, raccourcis)
- Service Worker : Intercepter les requêtes réseau
- 5 stratégies de cache : Cache First, Network First, Stale While Revalidate...
- Offline fallback : Page dédiée quand réseau indisponible
- Notifications Push : Service Worker + showNotification()
- ServiceWorkerUpdateManager : Proposer les mises à jour à l'utilisateur
- Blazor WASM = Idéal pour les PWA (tout en local, pas de serveur requis)

[OK] CHAPITRE 31 - WEBSOCKETS & SIGNALR AVANCÉ :
- Hub = Contrôleur pour les connexions WebSocket
- OnConnectedAsync / OnDisconnectedAsync : Gestion du cycle de vie
- Groups : Envoyer à des sous-groupes d'utilisateurs
- Clients.Group() vs Clients.GroupExcept() vs Clients.Client()
- Reconnexion automatique avec WithAutomaticReconnect()
- IHubContext<T> : Envoyer des messages depuis les services (en dehors du Hub)
- Indicateur "est en train de taper" avec Timer
- Conversations privées via groupes dynamiques

[OK] CHAPITRE 32 - MICRO-FRONTENDS :
- Razor Class Libraries (RCL) = Approche recommandée pour Blazor
- LazyAssemblyLoader : Charger les DLLs à la demande
- Table de routage : "/catalogue" -> Module.Catalogue.dll
- IntegrationEventBus : Communication découplée entre modules
- IModuleRegistration : Pattern plugin pour l'auto-configuration
- Chaque module enregistre ses propres services dans DI

[OK] CHAPITRE 33 - BIBLIOTHÈQUE DE COMPOSANTS :
- Progressive Disclosure : Simple par défaut, avancé possible
- CssBuilder : Construire les classes CSS dynamiquement
- CaptureUnmatchedValues : Transmettre les attributs HTML additionnels
- bUnit : Tests des composants Blazor
- [Theory] + [InlineData] : Tests paramétriques (une seule méthode, N cas)
- SemVer : Major.Minor.Patch pour la gestion des versions
- MonUIOptions + AddMonUI() : Pattern d'extensibilité standard

[OK] CHAPITRE 34 - CONTRIBUTION OPEN-SOURCE :
- "good first issue" : Label GitHub pour les débutants
- Bug report = Titre + Version + Actuel vs Attendu + Reproduction minimale
- git remote add upstream : Synchronisation avec le repo original
- Branche descriptive : fix/description ou feature/description
- PR = Titre + Description + Changes + Testing + Breaking Changes?
- Feature Proposal : Ouvrir une issue AVANT de coder
- Petit PR = Meilleure chance d'acceptation

[OBJECTIF] PROGRAMME COMPLET TERMINÉ !
Vous avez maintenant les compétences pour :
-> Construire n'importe quelle application Blazor
-> Architecturer des systèmes pour grandes équipes
-> Créer et vendre des composants Blazor
-> Déployer en production avec Docker + CI/CD
-> Monitorer et maintenir vos applications
-> Contribuer à la communauté Blazor
═══════════════════════════════════════════════════════════════
*/

