// ============================================================================
// [LIVRE] ASP.NET CORE — PARTIES 1 & 2
// FONDATIONS .NET + WEB API
// Pour étudiants en génie logiciel — Niveau débutant -> intermédiaire
// ============================================================================

/*
COMMENT UTILISER CE GUIDE :
  - Chaque concept est expliqué avec COMMENT / POURQUOI / QUAND
  - Exécutez chaque exemple dans votre IDE (Visual Studio / Rider / VSCode)
  - Faites les exercices AVANT de regarder les corrigés
  - Les [ROUGE] marquent les pièges fréquents à éviter
*/


// ============================================================================
// [GUIDE] CHAPITRE 1 : C# AVANCÉ — FONDATIONS INDISPENSABLES
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser les Records pour modéliser des données immuables
[OK] Maîtriser LINQ pour manipuler des collections
[OK] Écrire du code asynchrone avec async/await
[OK] Utiliser les types nullables et le pattern matching
[OK] Créer des méthodes d'extension
*/

// ─── 1.1 RECORDS ─────────────────────────────────────────────────────────────

/*
COMMENT : Mot-clé 'record' crée une classe immuable avec égalité structurelle
POURQUOI : Idéal pour DTOs, Value Objects, résultats de requêtes
QUAND    : Données qui ne changent pas après création, comparaison par valeur
*/

// Record simple (immuable par défaut)
public record UtilisateurDto(int Id, string Nom, string Email);

// Record avec validation
public record ProduitDto
{
    public int Id { get; init; }
    public string Nom { get; init; }
    public decimal Prix { get; init; }

    public ProduitDto(int id, string nom, decimal prix)
    {
        if (string.IsNullOrWhiteSpace(nom))
            throw new ArgumentException("Nom requis", nameof(nom));
        if (prix < 0)
            throw new ArgumentException("Prix doit être positif", nameof(prix));
        Id = id; Nom = nom; Prix = prix;
    }
}

// Démonstration des records
public static class DemoRecords
{
    public static void Executer()
    {
        var u1 = new UtilisateurDto(1, "Alice", "alice@test.com");
        var u2 = new UtilisateurDto(1, "Alice", "alice@test.com");
        var u3 = new UtilisateurDto(2, "Bob", "bob@test.com");

        Console.WriteLine(u1 == u2);       // true  (égalité structurelle)
        Console.WriteLine(u1 == u3);       // false
        Console.WriteLine(u1.Equals(u2));  // true

        // Création basée sur un existant avec modifications ("with expression")
        var u4 = u1 with { Email = "alice.pro@test.com" };
        Console.WriteLine(u4); // UtilisateurDto { Id = 1, Nom = Alice, Email = alice.pro@test.com }

        // Déconstruction
        var (id, nom, email) = u1;
        Console.WriteLine($"Id={id}, Nom={nom}");
    }
}

// [ROUGE] PIÈGE : les records sont immuables par défaut (init), mais on peut
// créer des mutable records avec 'set' — à éviter en général

// ─── 1.2 LINQ ─────────────────────────────────────────────────────────────────

/*
COMMENT : Language Integrated Query — requêtes sur collections en C#
POURQUOI : Remplace les boucles foreach imbriquées, code plus lisible
QUAND    : Filtrage, transformation, agrégation de collections
*/

public static class DemoLinq
{
    public static void Executer()
    {
        var produits = new List<ProduitDto>
        {
            new(1, "Laptop", 1200m),
            new(2, "Souris", 25m),
            new(3, "Clavier", 75m),
            new(4, "Écran", 450m),
            new(5, "Casque", 150m),
        };

        // ─── FILTRER ───────────────────────────────────────────────────────────
        var produitsChers = produits
            .Where(p => p.Prix > 100)
            .ToList();
        // -> Laptop, Écran, Casque

        // ─── TRIER ────────────────────────────────────────────────────────────
        var parPrix = produits
            .OrderByDescending(p => p.Prix)
            .ToList();

        // ─── TRANSFORMER ──────────────────────────────────────────────────────
        var noms = produits
            .Select(p => p.Nom.ToUpper())
            .ToList();
        // -> ["LAPTOP", "SOURIS", ...]

        // ─── CHAÎNER ──────────────────────────────────────────────────────────
        var resumePrixEleves = produits
            .Where(p => p.Prix > 100)
            .OrderBy(p => p.Nom)
            .Select(p => new { p.Nom, PrixFormate = $"{p.Prix:C}" })
            .ToList();

        // ─── AGRÉGER ──────────────────────────────────────────────────────────
        var total = produits.Sum(p => p.Prix);          // 1900
        var moyenne = produits.Average(p => p.Prix);    // 380
        var max = produits.Max(p => p.Prix);            // 1200
        var count = produits.Count(p => p.Prix > 100);  // 3

        // ─── TROUVER ──────────────────────────────────────────────────────────
        var laptop = produits.FirstOrDefault(p => p.Nom == "Laptop");
        var existe = produits.Any(p => p.Prix < 10);     // false
        var tousChers = produits.All(p => p.Prix > 10);  // true

        // ─── GROUPER ──────────────────────────────────────────────────────────
        var parCategorie = produits
            .GroupBy(p => p.Prix > 100 ? "Cher" : "Abordable")
            .ToDictionary(g => g.Key, g => g.ToList());

        // ─── PAGINATION CLASSIQUE ─────────────────────────────────────────────
        int page = 1, taille = 2;
        var paginees = produits
            .Skip((page - 1) * taille)
            .Take(taille)
            .ToList();
        // -> [Laptop, Souris]

        // ─── JOINTURE ─────────────────────────────────────────────────────────
        var commandes = new List<(int ProduitId, int Quantite)>
        {
            (1, 2), (2, 5), (1, 1)
        };
        var details = produits
            .Join(commandes,
                p => p.Id,
                c => c.ProduitId,
                (p, c) => new { p.Nom, c.Quantite, Total = p.Prix * c.Quantite })
            .ToList();

        Console.WriteLine($"Total stock: {total:C}");
    }
}

// [ROUGE] PIÈGE : ToList() exécute la requête IMMÉDIATEMENT
// Sans ToList(), la requête est "lazy" (exécutée à l'itération)
// Toujours appeler ToList()/ToArray() pour matérialiser le résultat


// ─── 1.3 ASYNC / AWAIT ───────────────────────────────────────────────────────

/*
COMMENT : async/await permet d'attendre des opérations I/O sans bloquer le thread
POURQUOI : Serveur peut traiter d'autres requêtes pendant l'attente
QUAND    : Opérations réseau, lecture/écriture fichiers, accès BDD

RÈGLE D'OR : async tout le chemin (async "contamine" vers le haut)
*/

public class ServiceAsyncDemo
{
    // [OK] CORRECT : async/await propagé correctement
    public async Task<string> ObtenirDonneesAsync(CancellationToken ct = default)
    {
        // Simule une requête HTTP ou BDD
        await Task.Delay(100, ct);
        return "données";
    }

    // [OK] Plusieurs opérations séquentielles
    public async Task<(string A, string B)> SequentielAsync()
    {
        var a = await ObtenirDonneesAsync();   // Attend A
        var b = await ObtenirDonneesAsync();   // PUIS attend B
        return (a, b);                          // Total: ~200ms
    }

    // [OK] Plusieurs opérations EN PARALLÈLE
    public async Task<(string A, string B)> ParallelAsync()
    {
        var taskA = ObtenirDonneesAsync();  // Démarre A
        var taskB = ObtenirDonneesAsync();  // Démarre B en même temps
        return (await taskA, await taskB);  // Attend les deux -> ~100ms
    }

    // [OK] WhenAll pour plusieurs tasks
    public async Task<string[]> ToutEnParalleleAsync(IEnumerable<int> ids)
    {
        var tasks = ids.Select(id => ObtenirDonneesAsync());
        return await Task.WhenAll(tasks);
    }

    // [OK] CancellationToken = permettre l'annulation
    public async Task<List<string>> AvecAnnulationAsync(CancellationToken ct)
    {
        var resultats = new List<string>();
        for (int i = 0; i < 10; i++)
        {
            ct.ThrowIfCancellationRequested(); // Vérifier annulation manuellement
            await Task.Delay(50, ct);          // ct automatiquement vérifié
            resultats.Add($"Item {i}");
        }
        return resultats;
    }
}

// [ROUGE] PIÈGES ASYNC COURANTS :
// [X] .Result / .Wait()  -> deadlock possible !
// [X] async void         -> exceptions non gérables (sauf event handlers)
// [X] Pas de ct          -> opération non annulable


// ─── 1.4 TYPES NULLABLES & PATTERN MATCHING ──────────────────────────────────

public class DemoNullables
{
    // Nullable reference types (C# 8+, activé dans .NET 6+)
    public string? NomOptional { get; set; }    // Peut être null
    public string NomRequis { get; set; } = ""; // Ne peut PAS être null

    public void Exemples()
    {
        string? valeur = null;

        // ─── NULL COALESCING ───────────────────────────────────────────────────
        var nom = valeur ?? "Inconnu";           // "Inconnu" si null
        var longueur = valeur?.Length ?? 0;       // Null-safe member access

        // ─── NULL ASSIGNMENT ───────────────────────────────────────────────────
        string? texte = null;
        texte ??= "valeur par défaut"; // Assigner seulement si null

        // ─── PATTERN MATCHING ──────────────────────────────────────────────────
        object obj = 42;

        // is pattern
        if (obj is int nombre && nombre > 10)
            Console.WriteLine($"Grand nombre: {nombre}");

        // switch expression
        var description = obj switch
        {
            int n when n < 0 => "négatif",
            int n when n == 0 => "zéro",
            int n => $"positif: {n}",
            string s => $"texte: {s}",
            null => "null",
            _ => "autre"
        };

        // ─── RECORD DECONSTRUCTION PATTERN ────────────────────────────────────
        var user = new UtilisateurDto(1, "Alice", "alice@test.com");
        if (user is { Id: > 0, Nom: { Length: > 2 } })
            Console.WriteLine("Utilisateur valide");
    }
}


// ─── 1.5 MÉTHODES D'EXTENSION ────────────────────────────────────────────────

/*
COMMENT : Ajouter des méthodes à une classe existante sans la modifier
POURQUOI : Étendre les types du framework ou des bibliothèques tierces
QUAND    : Utilitaires sur string, IEnumerable, DateTime, etc.
*/

public static class Extensions
{
    // Extension sur string
    public static bool EstEmail(this string? valeur)
    {
        if (string.IsNullOrWhiteSpace(valeur)) return false;
        return valeur.Contains('@') && valeur.Contains('.');
    }

    // Extension sur IEnumerable
    public static IEnumerable<T> PaginationSimple<T>(
        this IEnumerable<T> source, int page, int taille)
    {
        return source.Skip((page - 1) * taille).Take(taille);
    }

    // Extension sur DateTime
    public static bool EstAujourdhui(this DateTime date)
        => date.Date == DateTime.Today;

    public static string VersAffichage(this DateTime date)
        => date.ToString("dd/MM/yyyy à HH:mm");

    // Extension qui retourne une nouvelle valeur (éviter mutation)
    public static string Tronquer(this string texte, int maxLongueur, string suffixe = "...")
    {
        if (texte.Length <= maxLongueur) return texte;
        return texte[..(maxLongueur - suffixe.Length)] + suffixe;
    }
}

// Utilisation
public static class DemoExtensions
{
    public static void Executer()
    {
        Console.WriteLine("alice@test.com".EstEmail()); // true
        Console.WriteLine("pas-un-email".EstEmail());   // false

        var produits = new[] { "A", "B", "C", "D", "E" };
        var page1 = produits.PaginationSimple(1, 2).ToList(); // ["A", "B"]
        var page2 = produits.PaginationSimple(2, 2).ToList(); // ["C", "D"]

        var date = DateTime.Now;
        Console.WriteLine(date.VersAffichage()); // "06/03/2026 à 14:30"

        var long_text = "Ceci est un texte très long qui sera tronqué";
        Console.WriteLine(long_text.Tronquer(20)); // "Ceci est un texte..."
    }
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE 1 — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ :

1. Créez un record 'CommandeDto' avec : Id, ClientNom, MontantTotal, DateCommande, Statut (enum)

2. Créez une méthode d'extension 'EstRecente()' sur CommandeDto
   (retourne true si commandée dans les 7 derniers jours)

3. Écrivez une méthode async 'ObtenirCommandesRecentesAsync(List<CommandeDto> commandes)'
   qui retourne les commandes récentes triées par montant décroissant

4. Utilisez LINQ pour calculer le chiffre d'affaires total des commandes récentes
*/

// ─── CORRIGÉ ─────────────────────────────────────────────────────────────────

public enum StatutCommande { EnAttente, Confirmee, Livree, Annulee }

public record CommandeDto(
    int Id,
    string ClientNom,
    decimal MontantTotal,
    DateTime DateCommande,
    StatutCommande Statut);

public static class CommandeExtensions
{
    public static bool EstRecente(this CommandeDto cmd)
        => cmd.DateCommande >= DateTime.UtcNow.AddDays(-7);
}

public class ServiceCommandesExercice
{
    public async Task<List<CommandeDto>> ObtenirCommandesRecentesAsync(
        List<CommandeDto> commandes, CancellationToken ct = default)
    {
        await Task.Delay(10, ct); // Simule I/O

        return commandes
            .Where(c => c.EstRecente() && c.Statut != StatutCommande.Annulee)
            .OrderByDescending(c => c.MontantTotal)
            .ToList();
    }

    public async Task AfficherStatistiquesAsync(List<CommandeDto> commandes)
    {
        var recentes = await ObtenirCommandesRecentesAsync(commandes);
        var chiffreAffaires = recentes.Sum(c => c.MontantTotal);
        var moyenne = recentes.Any() ? recentes.Average(c => c.MontantTotal) : 0m;
        Console.WriteLine($"CA récent: {chiffreAffaires:C} | Panier moyen: {moyenne:C}");
    }
}


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

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Maîtriser la CLI dotnet
[OK] Structurer un projet multi-couches
[OK] Gérer les packages NuGet
[OK] Configurer appsettings.json et User Secrets
*/

// ─── 2.1 CLI DOTNET ESSENTIELLE ──────────────────────────────────────────────

/*
COMMANDES INDISPENSABLES :

# Créer des projets
dotnet new webapi -n MonApi --use-controllers       # API avec controllers
dotnet new webapi -n MonApi                         # Minimal API
dotnet new classlib -n MonApp.Domain                # Bibliothèque de classe
dotnet new xunit -n MonApp.Tests                    # Projet de tests

# Solution (regroupe plusieurs projets)
dotnet new sln -n MonApp
dotnet sln add MonApp.API/MonApp.API.csproj
dotnet sln add MonApp.Domain/MonApp.Domain.csproj

# Références entre projets
dotnet add MonApp.API reference MonApp.Domain

# Packages NuGet
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Serilog.AspNetCore --version 8.0.0
dotnet remove package NomDuPackage
dotnet restore

# Build, Test, Run
dotnet build                    # Compiler
dotnet test                     # Lancer les tests
dotnet run                      # Lancer l'application
dotnet watch run                # Hot reload (développement)
dotnet publish -c Release       # Publier pour production

# Entity Framework Core
dotnet ef migrations add NomMigration
dotnet ef database update
dotnet ef migrations remove     # Annuler dernière migration
dotnet ef database drop         # Supprimer la BDD (dev)

# User Secrets (secrets de développement, ne jamais committer!)
dotnet user-secrets init
dotnet user-secrets set "JwtSettings:SecretKey" "ma-cle-super-secrete"
dotnet user-secrets list
*/

// ─── 2.2 STRUCTURE DE PROJET RECOMMANDÉE ─────────────────────────────────────

/*
MonApp/
├── MonApp.sln
├── src/
│   ├── MonApp.API/                    <- Point d'entrée, controllers, middleware
│   │   ├── Controllers/
│   │   ├── Middleware/
│   │   ├── Program.cs
│   │   └── appsettings.json
│   │
│   ├── MonApp.Application/            <- Logique métier, CQRS, interfaces
│   │   ├── Commands/
│   │   ├── Queries/
│   │   ├── DTOs/
│   │   └── Interfaces/
│   │
│   ├── MonApp.Domain/                 <- Entités, Value Objects, règles métier
│   │   ├── Entities/
│   │   ├── Enums/
│   │   └── Exceptions/
│   │
│   └── MonApp.Infrastructure/         <- BDD, emails, stockage
│       ├── Data/
│       │   ├── AppDbContext.cs
│       │   └── Migrations/
│       ├── Repositories/
│       └── Services/
│
└── tests/
    ├── MonApp.Tests.Unit/
    └── MonApp.Tests.Integration/

RÈGLE DE DÉPENDANCE (Clean Architecture) :
  API -> Application -> Domain
  Infrastructure -> Application + Domain
  (jamais le contraire !)
*/

// ─── 2.3 CONFIGURATION ASPNETCORE ────────────────────────────────────────────

/*
FICHIERS DE CONFIGURATION (par ordre de priorité) :
  1. appsettings.json                 (base)
  2. appsettings.{Environment}.json  (par environnement)
  3. User Secrets                    (développement)
  4. Variables d'environnement       (production)
  5. Arguments de ligne de commande

ENVIRONNEMENTS :
  ASPNETCORE_ENVIRONMENT = Development | Staging | Production
*/

// Modèle de configuration fortement typée
public class JwtOptions
{
    public const string SectionName = "JwtSettings";
    public string SecretKey { get; set; } = string.Empty;
    public string Issuer { get; set; } = string.Empty;
    public string Audience { get; set; } = string.Empty;
    public int ExpirationMinutes { get; set; } = 60;
}

public class DatabaseOptions
{
    public const string SectionName = "Database";
    public string ConnectionString { get; set; } = string.Empty;
    public int MaxRetries { get; set; } = 3;
    public bool EnableSensitiveLogging { get; set; } = false;
}

/*
appsettings.json :
{
  "JwtSettings": {
    "SecretKey": "Défini dans User Secrets en dev, Env Var en prod",
    "Issuer": "monapp.com",
    "Audience": "monapp.com",
    "ExpirationMinutes": 60
  },
  "Database": {
    "MaxRetries": 3,
    "EnableSensitiveLogging": false
  },
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MonApp;Trusted_Connection=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

appsettings.Development.json :
{
  "Database": {
    "EnableSensitiveLogging": true
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug"
    }
  }
}
*/

// Enregistrement dans Program.cs
/*
// Enregistrement avec validation
builder.Services.AddOptions<JwtOptions>()
    .BindConfiguration(JwtOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();  // Valide au démarrage

builder.Services.AddOptions<DatabaseOptions>()
    .BindConfiguration(DatabaseOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();

// Utilisation dans un service
public class MonService
{
    private readonly JwtOptions _jwt;
    public MonService(IOptions<JwtOptions> jwtOptions)
    {
        _jwt = jwtOptions.Value;
    }
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 3 : ARCHITECTURE ASPNETCORE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le pipeline de requête/réponse
[OK] Créer et ordonner les middlewares
[OK] Maîtriser l'injection de dépendances
[OK] Structurer Program.cs proprement
*/

// ─── 3.1 PIPELINE DE REQUÊTE ──────────────────────────────────────────────────

/*
UNE REQUÊTE HTTP PASSE PAR :

Client -> [HTTPS] -> [CORS] -> [Authentication] -> [Authorization]
       -> [Exception Handler] -> [Rate Limiting] -> [Routing]
       -> [Controller/Endpoint] -> [Réponse]

MIDDLEWARE = composant qui intercepte la requête ET/OU la réponse
           = "next" transmet au middleware suivant
*/

// Middleware personnalisé simple
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var methode = context.Request.Method;
        var chemin = context.Request.Path;

        try
        {
            await _next(context); // Passe au middleware suivant
        }
        finally
        {
            sw.Stop();
            var statut = context.Response.StatusCode;
            _logger.LogInformation("{Methode} {Chemin} -> {Statut} ({Ms}ms)",
                methode, chemin, statut, sw.ElapsedMilliseconds);
        }
    }
}

// Middleware qui modifie la réponse (ajouter un header)
public class SecurityHeadersMiddleware
{
    private readonly RequestDelegate _next;

    public SecurityHeadersMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        // AVANT : traitement de la requête
        context.Response.OnStarting(() =>
        {
            // Ajouter des headers de sécurité à CHAQUE réponse
            context.Response.Headers.TryAdd("X-Content-Type-Options", "nosniff");
            context.Response.Headers.TryAdd("X-Frame-Options", "DENY");
            context.Response.Headers.TryAdd("Referrer-Policy", "no-referrer");
            return Task.CompletedTask;
        });

        await _next(context);
        // APRÈS : réponse déjà envoyée
    }
}

// ─── 3.2 INJECTION DE DÉPENDANCES ───────────────────────────────────────────

/*
DURÉE DE VIE DES SERVICES :

  Singleton  : Une seule instance pour toute l'application
               -> Caches, configurations, services stateless

  Scoped     : Une instance par requête HTTP
               -> DbContext, repository, services avec état par requête

  Transient  : Nouvelle instance à chaque injection
               -> Services légers et stateless

[ROUGE] PIÈGE : Injecter un Scoped dans un Singleton = "captive dependency"
           L'instance Scoped sera gardée en vie trop longtemps !
*/

// Interfaces
public interface IServiceMeteo
{
    Task<string> ObtenirMeteoAsync(string ville, CancellationToken ct = default);
}

public interface ICacheService
{
    T? Get<T>(string cle);
    void Set<T>(string cle, T valeur, TimeSpan duree);
}

// Implémentations
public class ServiceMeteo : IServiceMeteo
{
    private readonly HttpClient _http;
    private readonly ILogger<ServiceMeteo> _logger;

    public ServiceMeteo(HttpClient http, ILogger<ServiceMeteo> logger)
    {
        _http = http;
        _logger = logger;
    }

    public async Task<string> ObtenirMeteoAsync(string ville, CancellationToken ct = default)
    {
        // Simulation (en vrai : appel API météo)
        _logger.LogDebug("Récupération météo pour {Ville}", ville);
        await Task.Delay(50, ct);
        return $"Ensoleillé à {ville}, 22°C";
    }
}

public class CacheMemoire : ICacheService
{
    private readonly Microsoft.Extensions.Caching.Memory.IMemoryCache _cache;

    public CacheMemoire(Microsoft.Extensions.Caching.Memory.IMemoryCache cache)
        => _cache = cache;

    public T? Get<T>(string cle)
        => _cache.TryGetValue(cle, out T? valeur) ? valeur : default;

    public void Set<T>(string cle, T valeur, TimeSpan duree)
        => _cache.Set(cle, valeur, duree);
}

// ─── 3.3 PROGRAM.CS COMPLET ──────────────────────────────────────────────────

/*
STRUCTURE DE PROGRAM.CS (ordre des middlewares = CRITIQUE) :

var builder = WebApplication.CreateBuilder(args);

// ─── SERVICES ──────────────────────────────────────────────────────────────
// 1. Configuration
builder.Services.AddOptions<JwtOptions>()
    .BindConfiguration(JwtOptions.SectionName)
    .ValidateOnStart();

// 2. Infrastructure
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddStackExchangeRedisCache(options =>
    options.Configuration = builder.Configuration.GetConnectionString("Redis"));

// 3. Services métier
builder.Services.AddScoped<IServiceProduits, ServiceProduits>();
builder.Services.AddScoped<IServiceCommandes, ServiceCommandes>();
builder.Services.AddSingleton<ICacheService, CacheMemoire>();

// 4. HTTP Clients
builder.Services.AddHttpClient<IServiceMeteo, ServiceMeteo>(client =>
{
    client.BaseAddress = new Uri("https://api.meteo.com");
    client.Timeout = TimeSpan.FromSeconds(10);
});

// 5. Authentication & Authorization
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(...);
builder.Services.AddAuthorization();

// 6. API
builder.Services.AddControllers()
    .AddJsonOptions(o => o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// 7. Santé & Monitoring
builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>()
    .AddRedis(builder.Configuration.GetConnectionString("Redis")!);

var app = builder.Build();

// ─── PIPELINE MIDDLEWARES (L'ORDRE COMPTE !) ────────────────────────────────
// 1. Exception handler en PREMIER (attrape tout)
if (app.Environment.IsDevelopment())
    app.UseDeveloperExceptionPage();
else
    app.UseExceptionHandler("/error");

// 2. HTTPS
app.UseHttpsRedirection();
app.UseHsts();

// 3. Sécurité (headers, CORS)
app.UseMiddleware<SecurityHeadersMiddleware>();
app.UseCors("politique-cors");

// 4. Timing / logging des requêtes
app.UseMiddleware<RequestTimingMiddleware>();

// 5. Authentification AVANT autorisation
app.UseAuthentication();
app.UseAuthorization();

// 6. Rate limiting
app.UseRateLimiter();

// 7. Endpoints
app.MapControllers();
app.MapHealthChecks("/health");
app.MapSwagger();

app.Run();
*/


// ============================================================================
// [GUIDE] CHAPITRE 4 : CONTROLLERS ET REST
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer des controllers avec attributs
[OK] Maîtriser le routing (conventionnel et attribut)
[OK] Utiliser le model binding (Body, Route, Query, Header)
[OK] Retourner les bons codes HTTP
[OK] Documenter avec Swagger
*/

using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;

// ─── 4.1 CONTROLLER DE BASE ──────────────────────────────────────────────────

// Entité exemple
public class Produit
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public string? Description { get; set; }
    public decimal Prix { get; set; }
    public int Stock { get; set; }
    public bool EstActif { get; set; } = true;
    public DateTime DateCreation { get; set; } = DateTime.UtcNow;
}

// DTOs (différents de l'entité pour sécurité)
public record CreerProduitDto(
    [Required][StringLength(200)] string Nom,
    string? Description,
    [Range(0.01, double.MaxValue)] decimal Prix,
    [Range(0, int.MaxValue)] int Stock);

public record MettreAJourProduitDto(
    [Required][StringLength(200)] string Nom,
    string? Description,
    [Range(0.01, double.MaxValue)] decimal Prix);

public record ProduitReponseDto(int Id, string Nom, string? Description, decimal Prix, int Stock, bool EstActif);

// Paramètres de pagination
public record PaginationParams
{
    [Range(1, int.MaxValue)] public int Page { get; init; } = 1;
    [Range(1, 100)] public int Taille { get; init; } = 20;
    public string? Recherche { get; init; }
    public string? TriPar { get; init; } = "id";
    public bool TriDesc { get; init; } = false;
}

public record PageResultat<T>(List<T> Items, int Total, int Page, int TotalPages)
{
    public bool APageSuivante => Page < TotalPages;
    public bool APagePrecedente => Page > 1;
}

// Service (simulation en mémoire)
public interface IServiceProduits
{
    Task<PageResultat<ProduitReponseDto>> ObtenirTousAsync(PaginationParams p, CancellationToken ct);
    Task<ProduitReponseDto?> ObtenirParIdAsync(int id, CancellationToken ct);
    Task<ProduitReponseDto> CreerAsync(CreerProduitDto dto, CancellationToken ct);
    Task<ProduitReponseDto?> MettreAJourAsync(int id, MettreAJourProduitDto dto, CancellationToken ct);
    Task<bool> SupprimerAsync(int id, CancellationToken ct);
}

// Controller CRUD complet
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ProduitsController : ControllerBase
{
    private readonly IServiceProduits _service;
    private readonly ILogger<ProduitsController> _logger;

    public ProduitsController(IServiceProduits service, ILogger<ProduitsController> logger)
    {
        _service = service;
        _logger = logger;
    }

    // ─── GET LISTE ────────────────────────────────────────────────────────────

    /// <summary>Obtenir la liste paginée des produits</summary>
    /// <param name="params">Paramètres de pagination et filtres</param>
    [HttpGet]
    [ProducesResponseType(typeof(PageResultat<ProduitReponseDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<PageResultat<ProduitReponseDto>>> ObtenirTous(
        [FromQuery] PaginationParams @params, CancellationToken ct)
    {
        var resultats = await _service.ObtenirTousAsync(@params, ct);
        return Ok(resultats);
    }

    // ─── GET UNIQUE ───────────────────────────────────────────────────────────

    /// <summary>Obtenir un produit par son identifiant</summary>
    /// <param name="id">Identifiant du produit</param>
    [HttpGet("{id:int}")]
    [ProducesResponseType(typeof(ProduitReponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<ProduitReponseDto>> ObtenirParId(int id, CancellationToken ct)
    {
        var produit = await _service.ObtenirParIdAsync(id, ct);
        if (produit is null)
        {
            _logger.LogWarning("Produit {Id} non trouvé", id);
            return NotFound(new { Message = $"Produit {id} non trouvé." });
        }
        return Ok(produit);
    }

    // ─── POST CREATE ──────────────────────────────────────────────────────────

    /// <summary>Créer un nouveau produit</summary>
    [HttpPost]
    [ProducesResponseType(typeof(ProduitReponseDto), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<ProduitReponseDto>> Creer(
        [FromBody] CreerProduitDto dto, CancellationToken ct)
    {
        // ModelState est validé automatiquement par [ApiController]
        var produit = await _service.CreerAsync(dto, ct);
        _logger.LogInformation("Produit créé: {Id} - {Nom}", produit.Id, produit.Nom);

        // 201 Created avec Location header pointant vers le nouveau produit
        return CreatedAtAction(nameof(ObtenirParId), new { id = produit.Id }, produit);
    }

    // ─── PUT UPDATE COMPLET ───────────────────────────────────────────────────

    /// <summary>Mettre à jour un produit (remplacement complet)</summary>
    [HttpPut("{id:int}")]
    [ProducesResponseType(typeof(ProduitReponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<ProduitReponseDto>> MettreAJour(
        int id, [FromBody] MettreAJourProduitDto dto, CancellationToken ct)
    {
        var produit = await _service.MettreAJourAsync(id, dto, ct);
        if (produit is null)
            return NotFound(new { Message = $"Produit {id} non trouvé." });

        return Ok(produit);
    }

    // ─── DELETE ───────────────────────────────────────────────────────────────

    /// <summary>Supprimer un produit</summary>
    [HttpDelete("{id:int}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Supprimer(int id, CancellationToken ct)
    {
        var supprime = await _service.SupprimerAsync(id, ct);
        if (!supprime)
            return NotFound(new { Message = $"Produit {id} non trouvé." });

        return NoContent(); // 204 = succès sans contenu
    }

    // ─── ACTIONS SPÉCIALES ────────────────────────────────────────────────────

    // Recherche par critères complexes (POST pour critères complexes)
    [HttpPost("recherche")]
    public async Task<ActionResult<PageResultat<ProduitReponseDto>>> Rechercher(
        [FromBody] PaginationParams criteres, CancellationToken ct)
    {
        return Ok(await _service.ObtenirTousAsync(criteres, ct));
    }

    // Différentes sources de paramètres
    [HttpGet("demo-binding/{idRoute:int}")]
    public IActionResult DemoBinding(
        int idRoute,                                     // [FromRoute] implicite
        [FromQuery] string? recherche,                   // ?recherche=...
        [FromHeader(Name = "X-API-Version")] string? version, // Header
        [FromBody] object? body = null)                  // Body JSON
    {
        return Ok(new { idRoute, recherche, version, body });
    }
}


// ─── 4.2 GESTION DES ERREURS GLOBALE ─────────────────────────────────────────

/*
COMMENT : Middleware qui capture toutes les exceptions non gérées
POURQUOI : Réponses d'erreur cohérentes (RFC 7807 ProblemDetails)
QUAND    : Toujours — en développement ET production
*/

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Exception non gérée pour {Method} {Path}",
                context.Request.Method, context.Request.Path);

            await EcrireReponseErreurAsync(context, ex);
        }
    }

    private static async Task EcrireReponseErreurAsync(HttpContext context, Exception ex)
    {
        if (context.Response.HasStarted) return;

        var (statusCode, titre, detail) = ex switch
        {
            ValidationException ve => (400, "Données invalides", ve.Message),
            UnauthorizedAccessException => (401, "Non authentifié", "Authentification requise."),
            KeyNotFoundException knfe => (404, "Introuvable", knfe.Message),
            InvalidOperationException ioe => (422, "Opération invalide", ioe.Message),
            _ => (500, "Erreur serveur", "Une erreur inattendue s'est produite.")
        };

        context.Response.StatusCode = statusCode;
        context.Response.ContentType = "application/problem+json";

        var problem = new
        {
            type = $"https://tools.ietf.org/html/rfc7231#section-6.{statusCode / 100}.{statusCode % 100}",
            title = titre,
            status = statusCode,
            detail = detail,
            instance = context.Request.Path.Value,
            traceId = context.TraceIdentifier,
            timestamp = DateTime.UtcNow
        };

        await context.Response.WriteAsJsonAsync(problem);
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 5 : MINIMAL APIS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer des endpoints avec MapGet/Post/Put/Delete
[OK] Organiser avec RouteGroupBuilder
[OK] Utiliser TypedResults pour la documentation
[OK] Choisir entre Controller et Minimal API
*/

/*
CONTROLLER vs MINIMAL API :

Controllers :
  [OK] Idéal pour APIs complexes (beaucoup d'endpoints, filtres, policies)
  [OK] Support natif de model binding complexe
  [OK] Meilleure organisation pour grandes équipes
  [OK] Plus de conventions (héritage, attributs)

Minimal APIs :
  [OK] Code plus concis pour petites APIs
  [OK] Performances légèrement meilleures
  [OK] Idéal pour microservices simples
  [OK] Plus flexible (pas de convention imposée)
*/

// Minimal API complète - organisation en module
public static class ProduitsMinimalApi
{
    public static void MapRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/produits-v2")
            .WithTags("Produits v2")
            .RequireAuthorization()
            .WithOpenApi();

        group.MapGet("/", ObtenirTous)
            .WithName("ObtenirTousProduits")
            .WithSummary("Liste des produits")
            .AllowAnonymous(); // Override RequireAuthorization

        group.MapGet("/{id:int}", ObtenirParId)
            .WithName("ObtenirProduitParId")
            .Produces<ProduitReponseDto>()
            .Produces(404);

        group.MapPost("/", Creer)
            .WithName("CreerProduit")
            .Produces<ProduitReponseDto>(201)
            .ProducesValidationProblem();

        group.MapPut("/{id:int}", MettreAJour)
            .WithName("MettreAJourProduit");

        group.MapDelete("/{id:int}", Supprimer)
            .WithName("SupprimerProduit")
            .RequireAuthorization("Admin"); // Policy spécifique
    }

    // Handlers (fonctions statiques ou lambdas)
    static async Task<IResult> ObtenirTous(
        [AsParameters] PaginationParams @params,
        IServiceProduits service,
        CancellationToken ct)
    {
        var resultats = await service.ObtenirTousAsync(@params, ct);
        return TypedResults.Ok(resultats);
    }

    static async Task<Results<Ok<ProduitReponseDto>, NotFound>> ObtenirParId(
        int id, IServiceProduits service, CancellationToken ct)
    {
        var produit = await service.ObtenirParIdAsync(id, ct);
        return produit is not null
            ? TypedResults.Ok(produit)
            : TypedResults.NotFound();
    }

    static async Task<Results<Created<ProduitReponseDto>, ValidationProblem>> Creer(
        CreerProduitDto dto, IServiceProduits service, CancellationToken ct)
    {
        // Validation manuelle (pas de [ApiController] auto-validation)
        var erreurs = new Dictionary<string, string[]>();
        if (string.IsNullOrWhiteSpace(dto.Nom))
            erreurs.Add("nom", new[] { "Le nom est requis." });
        if (dto.Prix <= 0)
            erreurs.Add("prix", new[] { "Le prix doit être positif." });

        if (erreurs.Any())
            return TypedResults.ValidationProblem(erreurs);

        var produit = await service.CreerAsync(dto, ct);
        return TypedResults.Created($"/api/produits-v2/{produit.Id}", produit);
    }

    static async Task<Results<Ok<ProduitReponseDto>, NotFound>> MettreAJour(
        int id, MettreAJourProduitDto dto, IServiceProduits service, CancellationToken ct)
    {
        var produit = await service.MettreAJourAsync(id, dto, ct);
        return produit is not null ? TypedResults.Ok(produit) : TypedResults.NotFound();
    }

    static async Task<Results<NoContent, NotFound>> Supprimer(
        int id, IServiceProduits service, CancellationToken ct)
    {
        var supprime = await _service.SupprimerAsync(id, ct);
        return supprime ? TypedResults.NoContent() : TypedResults.NotFound();
    }

    private static IServiceProduits _service = null!; // En vrai: injection via DI
}


// ============================================================================
// [GUIDE] CHAPITRE 6 : VALIDATION & FILTRES
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] DataAnnotations pour la validation
[OK] FluentValidation pour règles complexes
[OK] Créer des filtres d'action et d'exception
*/

// ─── FLUENT VALIDATION ───────────────────────────────────────────────────────

/*
PACKAGE : dotnet add package FluentValidation.AspNetCore
*/

using FluentValidation;

public class CreerProduitValidator : AbstractValidator<CreerProduitDto>
{
    public CreerProduitValidator()
    {
        RuleFor(x => x.Nom)
            .NotEmpty().WithMessage("Le nom est requis.")
            .MinimumLength(3).WithMessage("Le nom doit avoir au moins 3 caractères.")
            .MaximumLength(200).WithMessage("Le nom ne peut dépasser 200 caractères.")
            .Matches(@"^[a-zA-ZÀ-ÿ0-9\s\-_]+$").WithMessage("Caractères non autorisés.");

        RuleFor(x => x.Prix)
            .GreaterThan(0).WithMessage("Le prix doit être positif.")
            .LessThanOrEqualTo(100000).WithMessage("Prix trop élevé (max 100 000).");

        RuleFor(x => x.Stock)
            .GreaterThanOrEqualTo(0).WithMessage("Le stock ne peut pas être négatif.");

        // Validation conditionnelle
        RuleFor(x => x.Description)
            .MaximumLength(2000).WithMessage("Description trop longue.")
            .When(x => x.Description != null);
    }
}

// Filtre d'action personnalisé
public class LogActionFilter : IActionFilter
{
    private readonly ILogger<LogActionFilter> _logger;

    public LogActionFilter(ILogger<LogActionFilter> logger) => _logger = logger;

    public void OnActionExecuting(ActionExecutingContext context)
    {
        _logger.LogDebug("Début action: {Controller}.{Action} | Params: {@Params}",
            context.Controller.GetType().Name,
            context.ActionDescriptor.DisplayName,
            context.ActionArguments);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Exception != null)
        {
            _logger.LogError(context.Exception, "Exception dans {Action}",
                context.ActionDescriptor.DisplayName);
        }
        else
        {
            _logger.LogDebug("Fin action: {Action} -> {StatusCode}",
                context.ActionDescriptor.DisplayName,
                (context.Result as ObjectResult)?.StatusCode ?? 200);
        }
    }
}

// Filtre d'exception (alternative au middleware global pour les controllers)
public class ApiExceptionFilter : IExceptionFilter
{
    private readonly ILogger<ApiExceptionFilter> _logger;

    public ApiExceptionFilter(ILogger<ApiExceptionFilter> logger) => _logger = logger;

    public void OnException(ExceptionContext context)
    {
        _logger.LogError(context.Exception, "Exception non gérée");

        var problem = new ProblemDetails
        {
            Title = "Erreur serveur",
            Status = 500,
            Detail = context.Exception.Message,
            Instance = context.HttpContext.Request.Path
        };

        context.Result = new ObjectResult(problem) { StatusCode = 500 };
        context.ExceptionHandled = true;
    }
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE 2 — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ — API de gestion d'étudiants :

1. Créez un record 'EtudiantDto' avec : Id, Prenom, Nom, Email, DateNaissance, Filiere

2. Créez les DTOs : CreerEtudiantDto, MettreAJourEtudiantDto

3. Créez un FluentValidator pour CreerEtudiantDto qui valide :
   - Prenom/Nom : requis, 2-50 chars
   - Email : requis, format valide, domaine "@universite.fr" uniquement
   - DateNaissance : requis, âge entre 16 et 65 ans
   - Filiere : doit être dans ["Informatique", "Mathématiques", "Physique"]

4. Créez EtudiantsController avec CRUD complet (liste paginée, détail, créer, modifier, supprimer)
   Utilisez une List<EtudiantDto> en mémoire comme "base de données"

5. Ajoutez une action : GET /api/etudiants/par-filiere/{filiere}
*/

// ─── CORRIGÉ ─────────────────────────────────────────────────────────────────

public record EtudiantDto(int Id, string Prenom, string Nom, string Email,
    DateTime DateNaissance, string Filiere);

public record CreerEtudiantDto(string Prenom, string Nom, string Email,
    DateTime DateNaissance, string Filiere);

public record MettreAJourEtudiantDto(string Prenom, string Nom, string Filiere);

public class CreerEtudiantValidator : AbstractValidator<CreerEtudiantDto>
{
    private static readonly string[] FilieresValides = { "Informatique", "Mathématiques", "Physique" };

    public CreerEtudiantValidator()
    {
        RuleFor(x => x.Prenom)
            .NotEmpty().MinimumLength(2).MaximumLength(50);

        RuleFor(x => x.Nom)
            .NotEmpty().MinimumLength(2).MaximumLength(50);

        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress().WithMessage("Format email invalide.")
            .Must(e => e.EndsWith("@universite.fr"))
            .WithMessage("L'email doit appartenir au domaine @universite.fr");

        RuleFor(x => x.DateNaissance)
            .NotEmpty()
            .Must(d => DateTime.Today.Year - d.Year >= 16 && DateTime.Today.Year - d.Year <= 65)
            .WithMessage("L'étudiant doit avoir entre 16 et 65 ans.");

        RuleFor(x => x.Filiere)
            .NotEmpty()
            .Must(f => FilieresValides.Contains(f))
            .WithMessage($"Filière invalide. Valeurs autorisées : {string.Join(", ", FilieresValides)}");
    }
}

[ApiController]
[Route("api/etudiants")]
public class EtudiantsController : ControllerBase
{
    // Simulation BDD en mémoire (en vrai: injecter un service/repository)
    private static readonly List<EtudiantDto> _etudiants = new()
    {
        new(1, "Alice", "Martin", "alice.martin@universite.fr", new DateTime(2000, 5, 15), "Informatique"),
        new(2, "Bob", "Dupont", "bob.dupont@universite.fr", new DateTime(1999, 3, 20), "Mathématiques"),
        new(3, "Clara", "Durand", "clara.durand@universite.fr", new DateTime(2001, 9, 1), "Physique"),
    };
    private static int _nextId = 4;
    private readonly ILogger<EtudiantsController> _logger;

    public EtudiantsController(ILogger<EtudiantsController> logger) => _logger = logger;

    [HttpGet]
    public ActionResult<PageResultat<EtudiantDto>> ObtenirTous(
        [FromQuery] int page = 1, [FromQuery] int taille = 10,
        [FromQuery] string? recherche = null)
    {
        var query = _etudiants.AsEnumerable();
        if (!string.IsNullOrEmpty(recherche))
            query = query.Where(e => e.Nom.Contains(recherche, StringComparison.OrdinalIgnoreCase)
                                  || e.Prenom.Contains(recherche, StringComparison.OrdinalIgnoreCase));

        var total = query.Count();
        var items = query.Skip((page - 1) * taille).Take(taille).ToList();
        return Ok(new PageResultat<EtudiantDto>(items, total, page, (int)Math.Ceiling((double)total / taille)));
    }

    [HttpGet("{id:int}")]
    public ActionResult<EtudiantDto> ObtenirParId(int id)
    {
        var etudiant = _etudiants.FirstOrDefault(e => e.Id == id);
        return etudiant is null ? NotFound() : Ok(etudiant);
    }

    [HttpGet("par-filiere/{filiere}")]
    public ActionResult<List<EtudiantDto>> ParFiliere(string filiere)
    {
        var resultats = _etudiants
            .Where(e => e.Filiere.Equals(filiere, StringComparison.OrdinalIgnoreCase))
            .ToList();
        return Ok(resultats);
    }

    [HttpPost]
    public ActionResult<EtudiantDto> Creer([FromBody] CreerEtudiantDto dto)
    {
        var nouvel = new EtudiantDto(_nextId++, dto.Prenom, dto.Nom, dto.Email, dto.DateNaissance, dto.Filiere);
        _etudiants.Add(nouvel);
        _logger.LogInformation("Étudiant créé: {Prenom} {Nom}", dto.Prenom, dto.Nom);
        return CreatedAtAction(nameof(ObtenirParId), new { id = nouvel.Id }, nouvel);
    }

    [HttpPut("{id:int}")]
    public ActionResult<EtudiantDto> MettreAJour(int id, [FromBody] MettreAJourEtudiantDto dto)
    {
        var idx = _etudiants.FindIndex(e => e.Id == id);
        if (idx < 0) return NotFound();

        var existant = _etudiants[idx];
        var mis = existant with { Prenom = dto.Prenom, Nom = dto.Nom, Filiere = dto.Filiere };
        _etudiants[idx] = mis;
        return Ok(mis);
    }

    [HttpDelete("{id:int}")]
    public IActionResult Supprimer(int id)
    {
        var etudiant = _etudiants.FirstOrDefault(e => e.Id == id);
        if (etudiant is null) return NotFound();
        _etudiants.Remove(etudiant);
        return NoContent();
    }
}


// ============================================================================
// [DOCS] RÉCAPITULATIF PARTIES 1 & 2
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
CONCEPTS CLÉS À RETENIR :

C# AVANCÉ :
  [OK] Records = immuabilité + égalité structurelle + with expression
  [OK] LINQ   = Where/Select/OrderBy/GroupBy/Sum/Any/All/First
  [OK] Async  = async Task, await, CancellationToken, Task.WhenAll
  [OK] Nullable = ?., ??, ??=, is, switch expression

ARCHITECTURE :
  [OK] Controllers = [ApiController] + [Route] + IActionResult
  [OK] Model Binding = [FromBody] / [FromRoute] / [FromQuery] / [FromHeader]
  [OK] Codes HTTP = 200/201/204 (succès) + 400/401/403/404/422 (erreurs) + 500 (serveur)
  [OK] Middleware = pipeline ordonné, RequestDelegate, InvokeAsync

DI LIFETIME :
  [OK] Singleton  = une instance totale
  [OK] Scoped     = une instance par requête  <- DbContext
  [OK] Transient  = nouvelle instance à chaque injection

VALIDATION :
  [OK] DataAnnotations = [Required] [StringLength] [Range] [EmailAddress]
  [OK] FluentValidation = règles complexes, messages custom, conditions

BEST PRACTICES :
  [OK] Toujours des DTOs (jamais exposer les entités directement)
  [OK] CancellationToken dans tous les méthodes async
  [OK] Logging structuré avec placeholders (pas de string interpolation)
  [OK] ProblemDetails pour les erreurs (RFC 7807)
═══════════════════════════════════════════════════════════════
*/

// ============================================================================
// [LIVRE] ASP.NET CORE — PARTIE 3
// ACCÈS AUX DONNÉES AVEC ENTITY FRAMEWORK CORE
// Pour étudiants en génie logiciel — Niveau intermédiaire
// ============================================================================

/*
[OBJECTIF] OBJECTIFS DE CETTE PARTIE

À la fin de cette partie, vous saurez :
[OK] Configurer Entity Framework Core avec PostgreSQL/SQL Server
[OK] Créer des entités et des configurations Fluent API
[OK] Gérer les migrations
[OK] Implémenter le pattern Repository + Unit of Work
[OK] Optimiser les requêtes (N+1, AsNoTracking, projections)
[OK] Implémenter la pagination et le tri
*/


// ============================================================================
// [GUIDE] CHAPITRE 7 : ENTITY FRAMEWORK CORE — FONDATIONS
// ============================================================================

/*
COMMENT : ORM (Object-Relational Mapping) — traduit objets C# <-> tables SQL
POURQUOI : Évite d'écrire du SQL brut, migrations automatiques, LINQ queries
QUAND    : Applications avec base de données relationnelle

PACKAGES :
  dotnet add package Microsoft.EntityFrameworkCore
  dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL  (PostgreSQL)
  dotnet add package Microsoft.EntityFrameworkCore.SqlServer (SQL Server)
  dotnet add package Microsoft.EntityFrameworkCore.Tools
  dotnet add package Microsoft.EntityFrameworkCore.Design
*/

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

// ─── 7.1 ENTITÉS ──────────────────────────────────────────────────────────────

/*
ENTITÉ = Classe C# qui correspond à une table en base de données
PROPRIÉTÉ = Colonne de la table
CLÉ PRIMAIRE = Par convention : propriété nommée "Id" ou "{ClassName}Id"
*/

// Classe de base avec audit (timestamps)
public abstract class BaseEntity
{
    public int Id { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime? UpdatedAt { get; set; }
    public bool IsDeleted { get; set; } = false; // Soft delete
}

// Entité Catégorie
public class Categorie : BaseEntity
{
    [Required]
    [StringLength(100)]
    public string Nom { get; set; } = string.Empty;

    public string? Description { get; set; }

    // Navigation property : une catégorie -> plusieurs produits
    public ICollection<Article> Articles { get; set; } = new List<Article>();
}

// Entité Article (utilise Produit comme nom de classe mais Article pour la table)
public class Article : BaseEntity
{
    [Required]
    [StringLength(200)]
    public string Nom { get; set; } = string.Empty;

    public string? Description { get; set; }

    [Column(TypeName = "decimal(18,2)")]
    public decimal Prix { get; set; }

    public int Stock { get; set; }

    // Clé étrangère
    public int CategorieId { get; set; }

    // Navigation property
    public Categorie Categorie { get; set; } = null!;

    // Navigation vers les lignes de commande
    public ICollection<LigneCommande> LignesCommande { get; set; } = new List<LigneCommande>();
}

// Entité Commande
public class Commande : BaseEntity
{
    public string NumeroCommande { get; set; } = Guid.NewGuid().ToString()[..8].ToUpper();

    public string ClientEmail { get; set; } = string.Empty;
    public string ClientNom { get; set; } = string.Empty;

    [Column(TypeName = "decimal(18,2)")]
    public decimal MontantTotal { get; set; }

    public StatutCommandeEf Statut { get; set; } = StatutCommandeEf.EnAttente;

    public DateTime? DateLivraison { get; set; }

    // Navigation : une commande -> plusieurs lignes
    public ICollection<LigneCommande> Lignes { get; set; } = new List<LigneCommande>();
}

public enum StatutCommandeEf
{
    EnAttente = 0,
    Confirmee = 1,
    EnPreparation = 2,
    Expediee = 3,
    Livree = 4,
    Annulee = 5
}

// Table de jointure (Commande <-> Article)
public class LigneCommande
{
    public int Id { get; set; }

    // FK vers Commande
    public int CommandeId { get; set; }
    public Commande Commande { get; set; } = null!;

    // FK vers Article
    public int ArticleId { get; set; }
    public Article Article { get; set; } = null!;

    public int Quantite { get; set; }

    [Column(TypeName = "decimal(18,2)")]
    public decimal PrixUnitaire { get; set; } // Prix au moment de la commande

    public decimal SousTotal => Quantite * PrixUnitaire;
}


// ─── 7.2 DBCONTEXT ───────────────────────────────────────────────────────────

/*
COMMENT : Classe centrale qui représente la session avec la BDD
POURQUOI : Gère la connexion, les transactions, le tracking des changements
QUAND    : Une instance par requête HTTP (Scoped lifetime)
*/

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    // DbSet = accès à chaque table
    public DbSet<Categorie> Categories { get; set; }
    public DbSet<Article> Articles { get; set; }
    public DbSet<Commande> Commandes { get; set; }
    public DbSet<LigneCommande> LignesCommande { get; set; }

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

        // Appliquer toutes les configurations depuis l'assembly courant
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);

        // ─── FILTRE GLOBAL (Soft Delete) ─────────────────────────────────────
        // Automatiquement appliqué à TOUTES les requêtes
        modelBuilder.Entity<Article>().HasQueryFilter(a => !a.IsDeleted);
        modelBuilder.Entity<Categorie>().HasQueryFilter(c => !c.IsDeleted);
        modelBuilder.Entity<Commande>().HasQueryFilter(c => !c.IsDeleted);
    }

    // Gérer les timestamps automatiquement
    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        var maintenant = DateTime.UtcNow;

        foreach (var entry in ChangeTracker.Entries<BaseEntity>())
        {
            switch (entry.State)
            {
                case EntityState.Added:
                    entry.Entity.CreatedAt = maintenant;
                    break;
                case EntityState.Modified:
                    entry.Entity.UpdatedAt = maintenant;
                    break;
                case EntityState.Deleted:
                    // Soft delete : ne pas vraiment supprimer
                    entry.State = EntityState.Modified;
                    entry.Entity.IsDeleted = true;
                    entry.Entity.UpdatedAt = maintenant;
                    break;
            }
        }

        return await base.SaveChangesAsync(cancellationToken);
    }
}

// ─── CONFIGURATION FLUENT API ─────────────────────────────────────────────────

/*
Fluent API > DataAnnotations pour :
  - Configurations complexes
  - Garder les entités propres (pas de couplage avec EF)
  - Index composés, clés composites
*/

public class ArticleConfiguration : IEntityTypeConfiguration<Article>
{
    public void Configure(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder<Article> builder)
    {
        builder.ToTable("Articles");  // Nom de table explicite

        builder.HasKey(a => a.Id);

        builder.Property(a => a.Nom)
            .IsRequired()
            .HasMaxLength(200)
            .HasColumnName("nom");

        builder.Property(a => a.Prix)
            .HasColumnType("decimal(18,2)")
            .HasDefaultValue(0.00m);

        builder.Property(a => a.Stock)
            .HasDefaultValue(0);

        // ─── INDEX ───────────────────────────────────────────────────────────
        builder.HasIndex(a => a.Nom);                    // Index simple
        builder.HasIndex(a => a.CategorieId);            // Index sur FK
        builder.HasIndex(a => new { a.Nom, a.CategorieId }) // Index composé
            .IsUnique();                                 // Contrainte unicité

        // ─── RELATIONS ───────────────────────────────────────────────────────
        builder.HasOne(a => a.Categorie)               // Article -> Categorie (Many-to-One)
            .WithMany(c => c.Articles)                 // Categorie -> Articles (One-to-Many)
            .HasForeignKey(a => a.CategorieId)
            .OnDelete(DeleteBehavior.Restrict);        // Empêcher suppression si articles liés
    }
}

public class CommandeConfiguration : IEntityTypeConfiguration<Commande>
{
    public void Configure(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder<Commande> builder)
    {
        builder.ToTable("Commandes");
        builder.HasKey(c => c.Id);

        builder.Property(c => c.NumeroCommande)
            .IsRequired()
            .HasMaxLength(50);

        builder.HasIndex(c => c.NumeroCommande).IsUnique();

        builder.Property(c => c.Statut)
            .HasConversion<string>()  // Stocker l'enum comme string (lisible)
            .HasMaxLength(50);

        builder.HasMany(c => c.Lignes)
            .WithOne(l => l.Commande)
            .HasForeignKey(l => l.CommandeId)
            .OnDelete(DeleteBehavior.Cascade); // Supprimer les lignes avec la commande
    }
}


// ─── 7.3 MIGRATIONS ──────────────────────────────────────────────────────────

/*
COMMANDES MIGRATIONS :

# Créer une migration (après modification des entités)
dotnet ef migrations add InitialCreate -p MonApp.Infrastructure -s MonApp.API

# Appliquer à la BDD
dotnet ef database update -p MonApp.Infrastructure -s MonApp.API

# Voir le SQL généré (sans appliquer)
dotnet ef migrations script

# Annuler la dernière migration (si pas encore appliquée)
dotnet ef migrations remove

# Retourner à une version spécifique
dotnet ef database update NomDeLaMigration

CONFIGURATION DANS PROGRAM.CS :
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        npgsqlOptions => npgsqlOptions.EnableRetryOnFailure(maxRetryCount: 3)
    )
    .EnableSensitiveDataLogging(builder.Environment.IsDevelopment())
    .EnableDetailedErrors(builder.Environment.IsDevelopment())
);
*/


// ============================================================================
// [GUIDE] CHAPITRE 8 : CRUD AVEC EF CORE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer (Add/AddAsync)
[OK] Lire (Find, First, Where, Include, Select)
[OK] Mettre à jour (Update, tracking automatique)
[OK] Supprimer (Remove, soft delete)
[OK] Comprendre le Change Tracking
*/

// Service CRUD direct avec DbContext (avant pattern Repository)
public class ServiceArticlesDirectDb
{
    private readonly AppDbContext _ctx;
    private readonly ILogger<ServiceArticlesDirectDb> _logger;

    public ServiceArticlesDirectDb(AppDbContext ctx, ILogger<ServiceArticlesDirectDb> logger)
    {
        _ctx = ctx;
        _logger = logger;
    }

    // ─── CREATE ───────────────────────────────────────────────────────────────

    public async Task<Article> CreerArticleAsync(
        string nom, decimal prix, int stock, int categorieId, CancellationToken ct = default)
    {
        // Vérifier que la catégorie existe
        var categorie = await _ctx.Categories.FindAsync(new object[] { categorieId }, ct)
            ?? throw new KeyNotFoundException($"Catégorie {categorieId} non trouvée.");

        var article = new Article
        {
            Nom = nom,
            Prix = prix,
            Stock = stock,
            CategorieId = categorieId
        };

        _ctx.Articles.Add(article); // Marque comme EntityState.Added
        await _ctx.SaveChangesAsync(ct); // Exécute INSERT SQL

        _logger.LogInformation("Article créé: {Id} - {Nom}", article.Id, article.Nom);
        return article;
    }

    // ─── READ — Différentes méthodes ──────────────────────────────────────────

    // Find = cherche par clé primaire (utilise le cache interne en premier)
    public async Task<Article?> ObtenirParIdAsync(int id, CancellationToken ct = default)
        => await _ctx.Articles.FindAsync(new object[] { id }, ct);

    // Avec relations chargées (Eager Loading)
    public async Task<Article?> ObtenirAvecCategorieAsync(int id, CancellationToken ct = default)
        => await _ctx.Articles
            .Include(a => a.Categorie)          // JOIN avec Categories
            .Include(a => a.LignesCommande)     // JOIN avec LignesCommande
                .ThenInclude(l => l.Commande)   // JOIN nested
            .FirstOrDefaultAsync(a => a.Id == id, ct);

    // Lecture sans tracking (pour affichage uniquement, meilleure perf)
    public async Task<List<Article>> ObtenirTousAsync(CancellationToken ct = default)
        => await _ctx.Articles
            .AsNoTracking()                // Pas de Change Tracking = +15-30% perf
            .Include(a => a.Categorie)
            .OrderBy(a => a.Nom)
            .ToListAsync(ct);

    // Projection (ne récupère que les colonnes nécessaires)
    public async Task<List<object>> ObtenirResumesAsync(CancellationToken ct = default)
        => await _ctx.Articles
            .AsNoTracking()
            .Select(a => new { a.Id, a.Nom, a.Prix, CategorieNom = a.Categorie.Nom })
            .Cast<object>()
            .ToListAsync(ct);

    // Pagination
    public async Task<(List<Article> Items, int Total)> ObtenirPageAsync(
        int page, int taille, string? recherche = null, CancellationToken ct = default)
    {
        var query = _ctx.Articles
            .AsNoTracking()
            .Include(a => a.Categorie)
            .Where(a => !a.IsDeleted);

        if (!string.IsNullOrEmpty(recherche))
            query = query.Where(a =>
                EF.Functions.Like(a.Nom, $"%{recherche}%") ||        // LIKE SQL
                EF.Functions.Like(a.Description ?? "", $"%{recherche}%"));

        var total = await query.CountAsync(ct);
        var items = await query
            .OrderBy(a => a.Nom)
            .Skip((page - 1) * taille)
            .Take(taille)
            .ToListAsync(ct);

        return (items, total);
    }

    // ─── UPDATE ───────────────────────────────────────────────────────────────

    // Méthode 1 : Récupérer puis modifier (tracked)
    public async Task<Article?> MettreAJourPrixAsync(
        int id, decimal nouveauPrix, CancellationToken ct = default)
    {
        var article = await _ctx.Articles.FindAsync(new object[] { id }, ct);
        if (article is null) return null;

        article.Prix = nouveauPrix; // EF détecte automatiquement la modification
        // État devient EntityState.Modified
        await _ctx.SaveChangesAsync(ct); // Exécute UPDATE SQL (uniquement les colonnes modifiées)
        return article;
    }

    // Méthode 2 : Mise à jour sans récupération (ExecuteUpdateAsync - EF 7+)
    public async Task<int> MettreAJourStockAsync(int id, int quantiteAjoutee, CancellationToken ct = default)
    {
        // Exécute UPDATE sans charger l'entité -> Meilleure performance
        return await _ctx.Articles
            .Where(a => a.Id == id)
            .ExecuteUpdateAsync(
                setters => setters
                    .SetProperty(a => a.Stock, a => a.Stock + quantiteAjoutee)
                    .SetProperty(a => a.UpdatedAt, DateTime.UtcNow),
                ct);
    }

    // ─── DELETE ───────────────────────────────────────────────────────────────

    // Soft delete (recommandé — préserve l'historique)
    public async Task<bool> SupprimerAsync(int id, CancellationToken ct = default)
    {
        var article = await _ctx.Articles.FindAsync(new object[] { id }, ct);
        if (article is null) return false;

        _ctx.Articles.Remove(article);
        // Le SaveChangesAsync override intercepte et fait un soft delete
        await _ctx.SaveChangesAsync(ct);
        return true;
    }

    // Hard delete (ExecuteDeleteAsync - EF 7+)
    public async Task<int> SupprimerDefinitivementAsync(int id, CancellationToken ct = default)
    {
        return await _ctx.Articles
            .Where(a => a.Id == id)
            .ExecuteDeleteAsync(ct); // Exécute DELETE SQL directement
    }

    // Récupérer les soft-deleted (IgnoreQueryFilters)
    public async Task<List<Article>> ObtenirSupprimeesAsync(CancellationToken ct = default)
        => await _ctx.Articles
            .IgnoreQueryFilters()        // Ignore le filtre WHERE IsDeleted = false
            .Where(a => a.IsDeleted)
            .ToListAsync(ct);
}


// ─── 7.4 CHANGE TRACKING ─────────────────────────────────────────────────────

/*
COMMENT : EF Core surveille les modifications des entités chargées
POURQUOI : Génère automatiquement l'UPDATE SQL uniquement pour les colonnes modifiées
QUAND utiliser AsNoTracking :
  -> Lecture seule (affichage) : TOUJOURS
  -> Même transaction de lecture/écriture : ne pas utiliser
*/

public class DemoChangeTracking
{
    public static async Task DemonstrationAsync(AppDbContext ctx)
    {
        // Chargé = Tracked
        var article = await ctx.Articles.FindAsync(1);
        Console.WriteLine(ctx.Entry(article!).State); // Unchanged

        article!.Prix = 99.99m;
        Console.WriteLine(ctx.Entry(article).State); // Modified

        // Voir quelles propriétés ont changé
        var propModifiees = ctx.Entry(article).Properties
            .Where(p => p.IsModified)
            .Select(p => $"{p.Metadata.Name}: {p.OriginalValue} -> {p.CurrentValue}");

        foreach (var prop in propModifiees)
            Console.WriteLine(prop); // Prix: 75 -> 99.99

        // SaveChanges génère : UPDATE Articles SET prix = 99.99 WHERE Id = 1
        await ctx.SaveChangesAsync();
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 9 : REPOSITORY & UNIT OF WORK
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Implémenter le pattern Repository
[OK] Implémenter Unit of Work
[OK] Gérer les transactions
[OK] Tester facilement avec des mocks
*/

// ─── 9.1 INTERFACE REPOSITORY GÉNÉRIQUE ──────────────────────────────────────

/*
COMMENT : Abstraction de la couche d'accès aux données
POURQUOI : Découple la logique métier de l'infrastructure (EF Core)
AVANTAGES :
  - Testabilité (mock facile)
  - Changement de BDD sans modifier la logique métier
  - Centralisation des requêtes communes
*/

public interface IRepositoryBase<T> where T : BaseEntity
{
    // Lecture
    Task<T?> ObtenirParIdAsync(int id, CancellationToken ct = default);
    Task<IReadOnlyList<T>> ObtenirTousAsync(CancellationToken ct = default);
    Task<bool> ExisteAsync(int id, CancellationToken ct = default);
    Task<int> CompterAsync(CancellationToken ct = default);

    // Écriture (ne pas appeler SaveChanges directement)
    Task AjouterAsync(T entite, CancellationToken ct = default);
    Task AjouterPlusieursAsync(IEnumerable<T> entites, CancellationToken ct = default);
    void Modifier(T entite);
    void Supprimer(T entite);
    void SupprimerPlusieurs(IEnumerable<T> entites);
}

// Interface spécialisée pour Articles
public interface IArticleRepository : IRepositoryBase<Article>
{
    Task<(List<Article> Items, int Total)> ObtenirPageAvecCategorieAsync(
        int page, int taille, string? recherche = null,
        int? categorieId = null, CancellationToken ct = default);

    Task<List<Article>> ObtenirParCategorieAsync(int categorieId, CancellationToken ct = default);

    Task<Article?> ObtenirAvecCategorieAsync(int id, CancellationToken ct = default);

    Task<bool> NomExisteAsync(string nom, int? exclureId = null, CancellationToken ct = default);
}

// Interface spécialisée pour Commandes
public interface ICommandeRepository : IRepositoryBase<Commande>
{
    Task<Commande?> ObtenirAvecLignesAsync(int id, CancellationToken ct = default);
    Task<List<Commande>> ObtenirParClientAsync(string email, CancellationToken ct = default);
    Task<Commande?> ObtenirParNumeroAsync(string numero, CancellationToken ct = default);
}

// ─── 9.2 IMPLÉMENTATION REPOSITORY ───────────────────────────────────────────

// Repository générique de base
public abstract class RepositoryBase<T> : IRepositoryBase<T> where T : BaseEntity
{
    protected readonly AppDbContext _ctx;
    protected readonly DbSet<T> _dbSet;

    protected RepositoryBase(AppDbContext ctx)
    {
        _ctx = ctx;
        _dbSet = ctx.Set<T>();
    }

    public async Task<T?> ObtenirParIdAsync(int id, CancellationToken ct = default)
        => await _dbSet.FindAsync(new object[] { id }, ct);

    public async Task<IReadOnlyList<T>> ObtenirTousAsync(CancellationToken ct = default)
        => await _dbSet.AsNoTracking().ToListAsync(ct);

    public async Task<bool> ExisteAsync(int id, CancellationToken ct = default)
        => await _dbSet.AnyAsync(e => e.Id == id, ct);

    public async Task<int> CompterAsync(CancellationToken ct = default)
        => await _dbSet.CountAsync(ct);

    public async Task AjouterAsync(T entite, CancellationToken ct = default)
        => await _dbSet.AddAsync(entite, ct);

    public async Task AjouterPlusieursAsync(IEnumerable<T> entites, CancellationToken ct = default)
        => await _dbSet.AddRangeAsync(entites, ct);

    public void Modifier(T entite) => _dbSet.Update(entite);

    public void Supprimer(T entite) => _dbSet.Remove(entite);

    public void SupprimerPlusieurs(IEnumerable<T> entites) => _dbSet.RemoveRange(entites);
}

// Repository spécialisé Articles
public class ArticleRepository : RepositoryBase<Article>, IArticleRepository
{
    public ArticleRepository(AppDbContext ctx) : base(ctx) { }

    public async Task<(List<Article> Items, int Total)> ObtenirPageAvecCategorieAsync(
        int page, int taille, string? recherche = null,
        int? categorieId = null, CancellationToken ct = default)
    {
        var query = _dbSet
            .AsNoTracking()
            .Include(a => a.Categorie)
            .AsQueryable();

        // Filtres dynamiques
        if (!string.IsNullOrEmpty(recherche))
            query = query.Where(a =>
                a.Nom.Contains(recherche) ||
                (a.Description != null && a.Description.Contains(recherche)));

        if (categorieId.HasValue)
            query = query.Where(a => a.CategorieId == categorieId.Value);

        var total = await query.CountAsync(ct);
        var items = await query
            .OrderBy(a => a.Nom)
            .Skip((page - 1) * taille)
            .Take(taille)
            .ToListAsync(ct);

        return (items, total);
    }

    public async Task<List<Article>> ObtenirParCategorieAsync(
        int categorieId, CancellationToken ct = default)
        => await _dbSet
            .AsNoTracking()
            .Where(a => a.CategorieId == categorieId)
            .OrderBy(a => a.Nom)
            .ToListAsync(ct);

    public async Task<Article?> ObtenirAvecCategorieAsync(int id, CancellationToken ct = default)
        => await _dbSet
            .Include(a => a.Categorie)
            .FirstOrDefaultAsync(a => a.Id == id, ct);

    public async Task<bool> NomExisteAsync(
        string nom, int? exclureId = null, CancellationToken ct = default)
    {
        var query = _dbSet.Where(a => a.Nom == nom);
        if (exclureId.HasValue)
            query = query.Where(a => a.Id != exclureId.Value);
        return await query.AnyAsync(ct);
    }
}

// Repository Commandes
public class CommandeRepository : RepositoryBase<Commande>, ICommandeRepository
{
    public CommandeRepository(AppDbContext ctx) : base(ctx) { }

    public async Task<Commande?> ObtenirAvecLignesAsync(int id, CancellationToken ct = default)
        => await _dbSet
            .Include(c => c.Lignes)
                .ThenInclude(l => l.Article)
                    .ThenInclude(a => a.Categorie)
            .FirstOrDefaultAsync(c => c.Id == id, ct);

    public async Task<List<Commande>> ObtenirParClientAsync(
        string email, CancellationToken ct = default)
        => await _dbSet
            .AsNoTracking()
            .Include(c => c.Lignes)
            .Where(c => c.ClientEmail == email)
            .OrderByDescending(c => c.CreatedAt)
            .ToListAsync(ct);

    public async Task<Commande?> ObtenirParNumeroAsync(
        string numero, CancellationToken ct = default)
        => await _dbSet
            .Include(c => c.Lignes)
                .ThenInclude(l => l.Article)
            .FirstOrDefaultAsync(c => c.NumeroCommande == numero, ct);
}

// ─── 9.3 UNIT OF WORK ─────────────────────────────────────────────────────────

/*
COMMENT : Regroupe tous les repositories et partage le même DbContext
POURQUOI : Transactions cohérentes entre plusieurs repositories
QUAND    : Operations multi-entités qui doivent réussir ou échouer ensemble
*/

public interface IUnitOfWork : IAsyncDisposable
{
    IArticleRepository Articles { get; }
    ICommandeRepository Commandes { get; }
    // Ajouter d'autres repositories ici

    Task<int> SauvegarderAsync(CancellationToken ct = default);
    Task ExecuterEnTransactionAsync(Func<Task> operation, CancellationToken ct = default);
}

public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _ctx;

    // Lazy initialization des repositories
    private IArticleRepository? _articles;
    private ICommandeRepository? _commandes;

    public UnitOfWork(AppDbContext ctx) => _ctx = ctx;

    public IArticleRepository Articles
        => _articles ??= new ArticleRepository(_ctx);

    public ICommandeRepository Commandes
        => _commandes ??= new CommandeRepository(_ctx);

    public async Task<int> SauvegarderAsync(CancellationToken ct = default)
        => await _ctx.SaveChangesAsync(ct);

    // Transaction explicite pour opérations critiques
    public async Task ExecuterEnTransactionAsync(Func<Task> operation, CancellationToken ct = default)
    {
        await using var transaction = await _ctx.Database.BeginTransactionAsync(ct);
        try
        {
            await operation();
            await _ctx.SaveChangesAsync(ct);
            await transaction.CommitAsync(ct);
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }

    public async ValueTask DisposeAsync()
    {
        await _ctx.DisposeAsync();
        GC.SuppressFinalize(this);
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 10 : OPTIMISATION DES REQUÊTES EF CORE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Identifier et résoudre le problème N+1
[OK] Utiliser les projections pour minimiser les données
[OK] Implémenter la pagination performante
[OK] Comprendre AsNoTracking et ses impacts
*/

// ─── 10.1 PROBLÈME N+1 ET SOLUTIONS ─────────────────────────────────────────

/*
PROBLÈME N+1 :
  1 requête pour obtenir N articles
  + N requêtes pour obtenir la catégorie de chaque article
  = N+1 requêtes totales -> TRÈS lent !
*/

public class DemoN1Problem
{
    private readonly AppDbContext _ctx;

    public DemoN1Problem(AppDbContext ctx) => _ctx = ctx;

    // [X] MAUVAIS : N+1 queries
    public async Task<List<string>> MauvaisAsync()
    {
        var articles = await _ctx.Articles.ToListAsync(); // 1 requête
        var resultats = new List<string>();

        foreach (var article in articles)
        {
            // EF charge la catégorie paresseusement : N requêtes !
            // (Lazy Loading doit être activé, sinon NullReference)
            resultats.Add($"{article.Nom} - {article.Categorie?.Nom}");
        }

        return resultats; // Total : N+1 requêtes SQL
    }

    // [OK] BON : Include (Eager Loading) = 1 seule requête avec JOIN
    public async Task<List<string>> BonAsync()
    {
        var articles = await _ctx.Articles
            .AsNoTracking()
            .Include(a => a.Categorie)  // JOIN automatique
            .ToListAsync();

        return articles.Select(a => $"{a.Nom} - {a.Categorie.Nom}").ToList();
        // Total : 1 requête SQL
    }

    // [OK] ENCORE MIEUX : Projection directe (seulement les colonnes nécessaires)
    public async Task<List<string>> OptimalAsync()
    {
        return await _ctx.Articles
            .AsNoTracking()
            .Select(a => $"{a.Nom} - {a.Categorie.Nom}") // EF génère le JOIN automatiquement
            .ToListAsync();
        // Total : 1 requête SQL avec seulement les colonnes Nom
    }
}

// ─── 10.2 PROJECTIONS AVANCÉES ───────────────────────────────────────────────

public class DemoProjections
{
    private readonly AppDbContext _ctx;

    public DemoProjections(AppDbContext ctx) => _ctx = ctx;

    // Projection vers un DTO anonyme
    public async Task<List<object>> ObtenirResumeAsync(CancellationToken ct = default)
        => await _ctx.Articles
            .AsNoTracking()
            .Where(a => a.Stock > 0)
            .Select(a => (object)new
            {
                a.Id,
                a.Nom,
                a.Prix,
                a.Stock,
                Categorie = a.Categorie.Nom,
                // Calcul en SQL
                ValeurStock = a.Prix * a.Stock
            })
            .OrderByDescending(a => (decimal)((dynamic)a).ValeurStock)
            .ToListAsync(ct);

    // Projection vers un DTO typé (recommandé pour les APIs)
    public record ArticleResumeDto(int Id, string Nom, decimal Prix, string CategorieNom, int Stock);

    public async Task<List<ArticleResumeDto>> ObtenirResumesTypesAsync(CancellationToken ct = default)
        => await _ctx.Articles
            .AsNoTracking()
            .Select(a => new ArticleResumeDto(
                a.Id, a.Nom, a.Prix, a.Categorie.Nom, a.Stock))
            .ToListAsync(ct);

    // Statistiques groupées
    public async Task<List<object>> StatistiquesParCategorieAsync(CancellationToken ct = default)
        => await _ctx.Categories
            .AsNoTracking()
            .Select(c => (object)new
            {
                c.Nom,
                NombreArticles = c.Articles.Count,
                PrixMoyen = c.Articles.Average(a => (decimal?)a.Prix) ?? 0,
                ValeurTotaleStock = c.Articles.Sum(a => a.Prix * a.Stock)
            })
            .ToListAsync(ct);
}

// ─── 10.3 PAGINATION PERFORMANTE ─────────────────────────────────────────────

/*
PAGINATION :
  Méthode Skip/Take = basique mais parfois lente sur grands datasets
  Keyset Pagination = plus performante sur grandes tables (utilise un curseur)
*/

public class PaginationService
{
    private readonly AppDbContext _ctx;

    public PaginationService(AppDbContext ctx) => _ctx = ctx;

    // Pagination offset classique
    public record PageDto<T>(List<T> Items, int Total, int Page, int TotalPages, bool HasNext, bool HasPrev);

    public async Task<PageDto<ArticleResumeDto>> ObtenirPageOffsetAsync(
        int page, int taille, string? tri = "nom", bool desc = false,
        CancellationToken ct = default)
    {
        var query = _ctx.Articles
            .AsNoTracking()
            .Select(a => new ArticleResumeDto(a.Id, a.Nom, a.Prix, a.Categorie.Nom, a.Stock));

        // Tri dynamique
        query = (tri?.ToLower(), desc) switch
        {
            ("prix", false) => query.OrderBy(a => a.Prix),
            ("prix", true) => query.OrderByDescending(a => a.Prix),
            ("stock", false) => query.OrderBy(a => a.Stock),
            ("stock", true) => query.OrderByDescending(a => a.Stock),
            (_, false) => query.OrderBy(a => a.Nom),
            (_, true) => query.OrderByDescending(a => a.Nom)
        };

        var total = await query.CountAsync(ct);
        var totalPages = (int)Math.Ceiling((double)total / taille);
        var items = await query
            .Skip((page - 1) * taille)
            .Take(taille)
            .ToListAsync(ct);

        return new PageDto<ArticleResumeDto>(
            items, total, page, totalPages,
            page < totalPages, page > 1);
    }

    record ArticleResumeDto(int Id, string Nom, decimal Prix, string CategorieNom, int Stock);

    // Keyset Pagination (curseur) — beaucoup plus performant sur grandes tables
    public async Task<(List<Article> Items, int? ProchainCurseur)> ObtenirPageKeysetAsync(
        int? curseurDernierId, int taille, CancellationToken ct = default)
    {
        var query = _ctx.Articles.AsNoTracking().OrderBy(a => a.Id);

        if (curseurDernierId.HasValue)
            query = (IOrderedQueryable<Article>)query.Where(a => a.Id > curseurDernierId.Value);

        var items = await query.Take(taille + 1).ToListAsync(ct); // +1 pour savoir s'il y a une suite

        int? prochain = null;
        if (items.Count > taille)
        {
            prochain = items[taille - 1].Id; // Curseur = dernier Id de la page
            items.RemoveAt(taille);          // Retirer l'élément en trop
        }

        return (items, prochain);
    }
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE 3 — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ — Système de bibliothèque :

1. Créez les entités :
   - Livre (Id, Titre, Auteur, ISBN, AnneePub, NombrePages, GenreId)
   - Genre (Id, Nom, Description)
   - Emprunt (Id, LivreId, MembreId, DateEmprunt, DateRetourPrevue, DateRetourReelle?)
   - Membre (Id, Nom, Email, DateInscription)

2. Créez AppDbContext avec :
   - Fluent API pour les relations
   - Soft delete
   - Index sur ISBN (unique), sur Email (unique)

3. Créez les interfaces :
   - ILivreRepository avec ObtenirDisponiblesAsync, RechercherAsync(terme)
   - IEmpruntRepository avec ObtenirEnCoursAsync(membreId), EstEnRetardAsync(empruntId)

4. Créez le UnitOfWork correspondant

5. Créez le service EmpruntService avec :
   - EmprunterAsync(livreId, membreId) -> valider disponibilité + créer emprunt
   - RetournerAsync(empruntId) -> mettre à jour DateRetourReelle
*/

// ─── CORRIGÉ ─────────────────────────────────────────────────────────────────

// Entités
public class Genre : BaseEntity
{
    [Required][StringLength(100)] public string Nom { get; set; } = string.Empty;
    public string? Description { get; set; }
    public ICollection<LivreBiblio> Livres { get; set; } = new List<LivreBiblio>();
}

public class LivreBiblio : BaseEntity
{
    [Required][StringLength(300)] public string Titre { get; set; } = string.Empty;
    [Required][StringLength(200)] public string Auteur { get; set; } = string.Empty;
    [Required][StringLength(20)] public string ISBN { get; set; } = string.Empty;
    public int AnneePub { get; set; }
    public int NombrePages { get; set; }
    public int GenreId { get; set; }
    public Genre Genre { get; set; } = null!;
    public ICollection<Emprunt> Emprunts { get; set; } = new List<Emprunt>();

    public bool EstDisponible => !Emprunts.Any(e => e.DateRetourReelle == null && !e.IsDeleted);
}

public class Membre : BaseEntity
{
    [Required][StringLength(200)] public string Nom { get; set; } = string.Empty;
    [Required][StringLength(200)][EmailAddress] public string Email { get; set; } = string.Empty;
    public DateTime DateInscription { get; set; } = DateTime.UtcNow;
    public ICollection<Emprunt> Emprunts { get; set; } = new List<Emprunt>();
}

public class Emprunt : BaseEntity
{
    public int LivreId { get; set; }
    public LivreBiblio Livre { get; set; } = null!;
    public int MembreId { get; set; }
    public Membre Membre { get; set; } = null!;
    public DateTime DateEmprunt { get; set; } = DateTime.UtcNow;
    public DateTime DateRetourPrevue { get; set; }
    public DateTime? DateRetourReelle { get; set; }

    public bool EstEnRetard => DateRetourReelle == null && DateTime.UtcNow > DateRetourPrevue;
    public bool EstRendu => DateRetourReelle != null;
}

// DbContext
public class BiblioDbContext : DbContext
{
    public BiblioDbContext(DbContextOptions<BiblioDbContext> options) : base(options) { }

    public DbSet<Genre> Genres { get; set; }
    public DbSet<LivreBiblio> Livres { get; set; }
    public DbSet<Membre> Membres { get; set; }
    public DbSet<Emprunt> Emprunts { get; set; }

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

        // Soft delete filters
        modelBuilder.Entity<LivreBiblio>().HasQueryFilter(l => !l.IsDeleted);
        modelBuilder.Entity<Emprunt>().HasQueryFilter(e => !e.IsDeleted);

        // Index
        modelBuilder.Entity<LivreBiblio>()
            .HasIndex(l => l.ISBN).IsUnique();
        modelBuilder.Entity<Membre>()
            .HasIndex(m => m.Email).IsUnique();

        // Relations
        modelBuilder.Entity<LivreBiblio>()
            .HasOne(l => l.Genre)
            .WithMany(g => g.Livres)
            .HasForeignKey(l => l.GenreId)
            .OnDelete(DeleteBehavior.Restrict);

        modelBuilder.Entity<Emprunt>()
            .HasOne(e => e.Livre)
            .WithMany(l => l.Emprunts)
            .HasForeignKey(e => e.LivreId)
            .OnDelete(DeleteBehavior.Restrict);

        modelBuilder.Entity<Emprunt>()
            .HasOne(e => e.Membre)
            .WithMany(m => m.Emprunts)
            .HasForeignKey(e => e.MembreId)
            .OnDelete(DeleteBehavior.Restrict);
    }

    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        var now = DateTime.UtcNow;
        foreach (var entry in ChangeTracker.Entries<BaseEntity>())
        {
            if (entry.State == EntityState.Added) entry.Entity.CreatedAt = now;
            else if (entry.State == EntityState.Modified) entry.Entity.UpdatedAt = now;
            else if (entry.State == EntityState.Deleted)
            {
                entry.State = EntityState.Modified;
                entry.Entity.IsDeleted = true;
                entry.Entity.UpdatedAt = now;
            }
        }
        return await base.SaveChangesAsync(ct);
    }
}

// Repositories
public interface ILivreRepository : IRepositoryBase<LivreBiblio>
{
    Task<List<LivreBiblio>> ObtenirDisponiblesAsync(CancellationToken ct = default);
    Task<List<LivreBiblio>> RechercherAsync(string terme, CancellationToken ct = default);
}

public interface IEmpruntRepository : IRepositoryBase<Emprunt>
{
    Task<List<Emprunt>> ObtenirEnCoursAsync(int membreId, CancellationToken ct = default);
    Task<bool> EstEnRetardAsync(int empruntId, CancellationToken ct = default);
    Task<bool> LivreEstDisponibleAsync(int livreId, CancellationToken ct = default);
}

public class LivreRepository : RepositoryBase<LivreBiblio>, ILivreRepository
{
    public LivreRepository(AppDbContext ctx) : base(ctx) { }

    public async Task<List<LivreBiblio>> ObtenirDisponiblesAsync(CancellationToken ct = default)
        => await _dbSet.AsNoTracking()
            .Include(l => l.Genre)
            .Where(l => !l.Emprunts.Any(e => e.DateRetourReelle == null))
            .OrderBy(l => l.Titre)
            .ToListAsync(ct);

    public async Task<List<LivreBiblio>> RechercherAsync(string terme, CancellationToken ct = default)
        => await _dbSet.AsNoTracking()
            .Include(l => l.Genre)
            .Where(l => l.Titre.Contains(terme) || l.Auteur.Contains(terme) || l.ISBN == terme)
            .ToListAsync(ct);
}

public class EmpruntRepository : RepositoryBase<Emprunt>, IEmpruntRepository
{
    public EmpruntRepository(AppDbContext ctx) : base(ctx) { }

    public async Task<List<Emprunt>> ObtenirEnCoursAsync(int membreId, CancellationToken ct = default)
        => await _dbSet.AsNoTracking()
            .Include(e => e.Livre).ThenInclude(l => l.Genre)
            .Where(e => e.MembreId == membreId && e.DateRetourReelle == null)
            .ToListAsync(ct);

    public async Task<bool> EstEnRetardAsync(int empruntId, CancellationToken ct = default)
        => await _dbSet.AnyAsync(
            e => e.Id == empruntId && e.DateRetourReelle == null && DateTime.UtcNow > e.DateRetourPrevue,
            ct);

    public async Task<bool> LivreEstDisponibleAsync(int livreId, CancellationToken ct = default)
        => !await _dbSet.AnyAsync(
            e => e.LivreId == livreId && e.DateRetourReelle == null, ct);
}

// UnitOfWork
public interface IBiblioUnitOfWork : IAsyncDisposable
{
    ILivreRepository Livres { get; }
    IEmpruntRepository Emprunts { get; }
    Task<int> SauvegarderAsync(CancellationToken ct = default);
    Task ExecuterEnTransactionAsync(Func<Task> op, CancellationToken ct = default);
}

public class BiblioUnitOfWork : IBiblioUnitOfWork
{
    private readonly AppDbContext _ctx;
    private ILivreRepository? _livres;
    private IEmpruntRepository? _emprunts;

    public BiblioUnitOfWork(AppDbContext ctx) => _ctx = ctx;

    public ILivreRepository Livres => _livres ??= new LivreRepository(_ctx);
    public IEmpruntRepository Emprunts => _emprunts ??= new EmpruntRepository(_ctx);

    public async Task<int> SauvegarderAsync(CancellationToken ct = default)
        => await _ctx.SaveChangesAsync(ct);

    public async Task ExecuterEnTransactionAsync(Func<Task> op, CancellationToken ct = default)
    {
        await using var t = await _ctx.Database.BeginTransactionAsync(ct);
        try { await op(); await _ctx.SaveChangesAsync(ct); await t.CommitAsync(ct); }
        catch { await t.RollbackAsync(ct); throw; }
    }

    public async ValueTask DisposeAsync() { await _ctx.DisposeAsync(); GC.SuppressFinalize(this); }
}

// Service Emprunt
public class EmpruntService
{
    private readonly IBiblioUnitOfWork _uow;
    private readonly ILogger<EmpruntService> _logger;

    public EmpruntService(IBiblioUnitOfWork uow, ILogger<EmpruntService> logger)
    {
        _uow = uow;
        _logger = logger;
    }

    public async Task<Emprunt> EmprunterAsync(
        int livreId, int membreId, int dureeJours = 14, CancellationToken ct = default)
    {
        Emprunt emprunt = null!;

        await _uow.ExecuterEnTransactionAsync(async () =>
        {
            var disponible = await _uow.Emprunts.LivreEstDisponibleAsync(livreId, ct);
            if (!disponible)
                throw new InvalidOperationException("Ce livre n'est pas disponible.");

            emprunt = new Emprunt
            {
                LivreId = livreId,
                MembreId = membreId,
                DateEmprunt = DateTime.UtcNow,
                DateRetourPrevue = DateTime.UtcNow.AddDays(dureeJours)
            };

            await _uow.Emprunts.AjouterAsync(emprunt, ct);
        }, ct);

        _logger.LogInformation("Emprunt créé: Livre {LivreId} par Membre {MembreId}", livreId, membreId);
        return emprunt;
    }

    public async Task<Emprunt> RetournerAsync(int empruntId, CancellationToken ct = default)
    {
        var emprunt = await _uow.Emprunts.ObtenirParIdAsync(empruntId, ct)
            ?? throw new KeyNotFoundException($"Emprunt {empruntId} non trouvé.");

        if (emprunt.EstRendu)
            throw new InvalidOperationException("Ce livre a déjà été retourné.");

        emprunt.DateRetourReelle = DateTime.UtcNow;
        _uow.Emprunts.Modifier(emprunt);
        await _uow.SauvegarderAsync(ct);

        _logger.LogInformation("Livre retourné: Emprunt {Id}", empruntId);
        return emprunt;
    }
}


// ============================================================================
// [DOCS] RÉCAPITULATIF PARTIE 3
// ============================================================================

/*
═══════════════════════════════════════════════════════════════
CONCEPTS CLÉS À RETENIR :

ENTITY FRAMEWORK CORE :
  [OK] DbContext = session BDD (Scoped lifetime !)
  [OK] DbSet<T> = accès à une table
  [OK] Fluent API = configuration avancée (IEntityTypeConfiguration<T>)
  [OK] Migrations = versionnage du schéma BDD

CRUD :
  [OK] Add/AddAsync -> INSERT
  [OK] FindAsync -> SELECT par PK (cache)
  [OK] FirstOrDefaultAsync -> SELECT avec condition
  [OK] Update / modification tracking -> UPDATE
  [OK] Remove / SaveChanges override -> soft DELETE
  [OK] ExecuteUpdateAsync / ExecuteDeleteAsync -> bulk operations sans chargement

OPTIMISATIONS :
  [OK] AsNoTracking() -> TOUJOURS pour lectures seules (+15-30% perf)
  [OK] Include() -> Évite N+1 queries
  [OK] Select() -> Projection = moins de données transférées
  [OK] HasQueryFilter() -> Filtre global automatique (soft delete, tenant)

PATTERNS :
  [OK] Repository = abstraction de l'accès données
  [OK] UnitOfWork = transaction cohérente multi-repository
  [OK] Séparation des interfaces permet le mocking pour les tests

BONNES PRATIQUES :
  [OK] Toujours passer CancellationToken
  [OK] Index sur FK et colonnes de recherche fréquentes
  [OK] Jamais .Result ou .Wait() sur les méthodes async EF
  [OK] ExecuteUpdate/Delete pour les bulk operations
  [OK] Transactions pour les opérations multi-entités critiques
═══════════════════════════════════════════════════════════════
*/

// ============================================================================
// [LIVRE] ASP.NET CORE - PARTIE 4 : AUTHENTIFICATION & SÉCURITÉ
// ============================================================================
//
// [OBJECTIF] CETTE PARTIE COUVRE :
// - Chapitre 11 : ASP.NET Core Identity
// - Chapitre 12 : JWT Authentication
// - Chapitre 13 : OAuth & Social Login
// - Chapitre 14 : Sécurité Web (HTTPS, CORS, CSRF, XSS, Rate Limiting)
//
// [TEMPS] TEMPS : ~10-12 heures
// [DOCS] PRÉREQUIS : Parties 1, 2 et 3 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 11 : ASP.NET CORE IDENTITY
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Configurer ASP.NET Core Identity
[OK] Gérer les utilisateurs avec UserManager<T>
[OK] Gérer les sessions avec SignInManager<T>
[OK] Créer et gérer des rôles
[OK] Implémenter des politiques (Policies) d'autorisation
[OK] Personnaliser la table utilisateurs
*/


// ----------------------------------------------------------------------------
// [REFLEXION] QU'EST-CE QU'ASP.NET CORE IDENTITY ?
// ----------------------------------------------------------------------------

/*
[IDEE] ASP.NET CORE IDENTITY = Système complet de gestion d'identité

COMMENT :
  Bibliothèque Microsoft intégrée qui fournit :
  - Stockage des utilisateurs (table AspNetUsers)
  - Hachage des mots de passe (PBKDF2 par défaut)
  - Gestion des rôles (AspNetRoles)
  - Claims (informations utilisateur)
  - Tokens (confirmation email, reset password...)
  - Lockout (verrouillage après tentatives échouées)

POURQUOI :
  - Ne pas réinventer la roue
  - Sécurité éprouvée et maintenue par Microsoft
  - Intégration native avec EF Core

QUAND :
  - Toujours pour de nouvelles applications nécessitant auth
  - Remplace les systèmes auth maison

PACKAGES :
  dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
  dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] CONFIGURATION D'IDENTITY
// ----------------------------------------------------------------------------

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;

// ─── ENTITÉ UTILISATEUR PERSONNALISÉE ──────────────────────────────────────
/*
[IDEE] POURQUOI PERSONNALISER ?
IdentityUser de base contient : Email, UserName, PasswordHash, PhoneNumber...
On étend pour ajouter des champs métier.
*/

public class ApplicationUser : IdentityUser
{
    // Champs supplémentaires
    [MaxLength(100)]
    public string Prenom { get; set; } = string.Empty;

    [MaxLength(100)]
    public string Nom { get; set; } = string.Empty;

    public DateTime DateInscription { get; set; } = DateTime.UtcNow;

    public DateTime? DerniereConnexion { get; set; }

    [MaxLength(500)]
    public string? AvatarUrl { get; set; }

    public bool EstActif { get; set; } = true;

    public string? TenantId { get; set; } // Pour multi-tenant

    // Navigation properties
    public ICollection<CommandeUser> Commandes { get; set; } = new List<CommandeUser>();
}

public class CommandeUser
{
    public int Id { get; set; }
    public decimal Total { get; set; }
    public string UserId { get; set; } = string.Empty;
    public ApplicationUser User { get; set; } = null!;
}

// ─── DBCONTEXT AVEC IDENTITY ────────────────────────────────────────────────
public class AppIdentityDbContext : IdentityDbContext<ApplicationUser>
{
    public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options)
        : base(options) { }

    // Vos DbSet supplémentaires
    public DbSet<CommandeUser> CommandesUser { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder); // <- OBLIGATOIRE pour Identity

        // Renommer les tables Identity (optionnel)
        builder.Entity<ApplicationUser>().ToTable("utilisateurs");
        builder.Entity<IdentityRole>().ToTable("roles");
        builder.Entity<IdentityUserRole<string>>().ToTable("utilisateurs_roles");
        builder.Entity<IdentityUserClaim<string>>().ToTable("utilisateurs_claims");
        builder.Entity<IdentityUserLogin<string>>().ToTable("utilisateurs_logins");
        builder.Entity<IdentityRoleClaim<string>>().ToTable("roles_claims");
        builder.Entity<IdentityUserToken<string>>().ToTable("utilisateurs_tokens");

        // Index personnalisé
        builder.Entity<ApplicationUser>()
            .HasIndex(u => u.Email)
            .IsUnique();
    }
}

// ─── CONFIGURATION DANS PROGRAM.CS ─────────────────────────────────────────
/*
var builder = WebApplication.CreateBuilder(args);

// 1. Configurer le DbContext
builder.Services.AddDbContext<AppIdentityDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

// 2. Configurer Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    // ── Options du mot de passe ──
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequiredLength = 8;
    options.Password.RequiredUniqueChars = 4;

    // ── Options de verrouillage ──
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;

    // ── Options utilisateur ──
    options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = true;

    // ── Options de connexion ──
    options.SignIn.RequireConfirmedEmail = false; // true en production !
    options.SignIn.RequireConfirmedPhoneNumber = false;
})
.AddEntityFrameworkStores<AppIdentityDbContext>()  // Stockage EF Core
.AddDefaultTokenProviders();                         // Tokens email, reset password...

// 3. Configurer les cookies (pour apps web avec sessions)
builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict;
    options.ExpireTimeSpan = TimeSpan.FromHours(8);
    options.SlidingExpiration = true;
    options.LoginPath = "/api/auth/login";
    options.LogoutPath = "/api/auth/logout";
    options.AccessDeniedPath = "/api/auth/access-denied";
});

// ...

app.UseAuthentication(); // <- AVANT UseAuthorization
app.UseAuthorization();
*/


// ----------------------------------------------------------------------------
// [UTILISATEUR] USERMANAGER - GESTION DES UTILISATEURS
// ----------------------------------------------------------------------------

/*
[IDEE] UserManager<TUser>

COMMENT : Service injecté qui fournit toutes les opérations sur les utilisateurs
POURQUOI : Abstraction qui gère le hachage, les validations, les tokens...
QUAND : Partout où on manipule des utilisateurs

DURÉE DE VIE : Scoped (injecté automatiquement par Identity)
*/

// DTOs pour l'authentification
public record InscriptionDto(
    string Prenom,
    string Nom,
    string Email,
    string MotDePasse,
    string ConfirmationMotDePasse
);

public record ConnexionDto(string Email, string MotDePasse, bool SeRemembrer = false);

public record ChangerMotDePasseDto(string AncienMotDePasse, string NouveauMotDePasse, string Confirmation);

public record ResultatAuth(bool Succes, string? Message = null, string? Token = null, ApplicationUserDto? Utilisateur = null);

public record ApplicationUserDto(string Id, string Prenom, string Nom, string Email, IList<string> Roles);

// Service d'authentification
public interface IServiceAuth
{
    Task<ResultatAuth> InscrireAsync(InscriptionDto dto, CancellationToken ct = default);
    Task<ResultatAuth> ConnecterAsync(ConnexionDto dto, CancellationToken ct = default);
    Task<bool> DeconnecterAsync(CancellationToken ct = default);
    Task<ResultatAuth> ChangerMotDePasseAsync(string userId, ChangerMotDePasseDto dto, CancellationToken ct = default);
    Task<string?> GenererTokenResetMotDePasseAsync(string email, CancellationToken ct = default);
    Task<ResultatAuth> ResetMotDePasseAsync(string email, string token, string nouveauMotDePasse, CancellationToken ct = default);
    Task<ApplicationUserDto?> ObtenirProfilAsync(string userId, CancellationToken ct = default);
}

public class ServiceAuth : IServiceAuth
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly ILogger<ServiceAuth> _logger;

    public ServiceAuth(
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        ILogger<ServiceAuth> logger)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _logger = logger;
    }

    // ─── INSCRIPTION ─────────────────────────────────────────────────────────
    public async Task<ResultatAuth> InscrireAsync(InscriptionDto dto, CancellationToken ct = default)
    {
        // Vérifier que les mots de passe correspondent
        if (dto.MotDePasse != dto.ConfirmationMotDePasse)
            return new ResultatAuth(false, "Les mots de passe ne correspondent pas.");

        // Vérifier si email déjà utilisé
        var existant = await _userManager.FindByEmailAsync(dto.Email);
        if (existant != null)
            return new ResultatAuth(false, "Cet email est déjà utilisé.");

        // Créer l'entité utilisateur
        var utilisateur = new ApplicationUser
        {
            UserName = dto.Email,       // UserName = Email par convention
            Email = dto.Email,
            Prenom = dto.Prenom,
            Nom = dto.Nom,
            DateInscription = DateTime.UtcNow,
            EstActif = true
        };

        // Créer l'utilisateur (hachage automatique du mot de passe)
        var resultat = await _userManager.CreateAsync(utilisateur, dto.MotDePasse);

        if (!resultat.Succeeded)
        {
            var erreurs = string.Join(", ", resultat.Errors.Select(e => e.Description));
            _logger.LogWarning("Inscription échouée pour {Email}: {Erreurs}", dto.Email, erreurs);
            return new ResultatAuth(false, erreurs);
        }

        // Assigner rôle par défaut
        await _userManager.AddToRoleAsync(utilisateur, "Utilisateur");

        // Ajouter des claims personnalisés
        await _userManager.AddClaimsAsync(utilisateur, new[]
        {
            new System.Security.Claims.Claim("prenom", dto.Prenom),
            new System.Security.Claims.Claim("nom", dto.Nom)
        });

        _logger.LogInformation("Nouvel utilisateur inscrit: {Email}", dto.Email);

        var userDto = await ToDto(utilisateur);
        return new ResultatAuth(true, "Inscription réussie.", Utilisateur: userDto);
    }

    // ─── CONNEXION ───────────────────────────────────────────────────────────
    public async Task<ResultatAuth> ConnecterAsync(ConnexionDto dto, CancellationToken ct = default)
    {
        var utilisateur = await _userManager.FindByEmailAsync(dto.Email);
        if (utilisateur == null || !utilisateur.EstActif)
            return new ResultatAuth(false, "Email ou mot de passe incorrect.");

        // Vérifier si compte verrouillé
        if (await _userManager.IsLockedOutAsync(utilisateur))
        {
            var lockoutEnd = await _userManager.GetLockoutEndDateAsync(utilisateur);
            return new ResultatAuth(false, $"Compte verrouillé jusqu'à {lockoutEnd?.LocalDateTime:HH:mm}.");
        }

        // Vérifier le mot de passe
        var motDePasseOk = await _userManager.CheckPasswordAsync(utilisateur, dto.MotDePasse);
        if (!motDePasseOk)
        {
            // Incrémenter les tentatives échouées
            await _userManager.AccessFailedAsync(utilisateur);
            _logger.LogWarning("Tentative connexion échouée pour {Email}", dto.Email);
            return new ResultatAuth(false, "Email ou mot de passe incorrect.");
        }

        // Réinitialiser le compteur de tentatives
        await _userManager.ResetAccessFailedCountAsync(utilisateur);

        // Mettre à jour la date de dernière connexion
        utilisateur.DerniereConnexion = DateTime.UtcNow;
        await _userManager.UpdateAsync(utilisateur);

        // Connexion (crée un cookie de session)
        await _signInManager.SignInAsync(utilisateur, dto.SeRemembrer);

        _logger.LogInformation("Connexion réussie: {Email}", dto.Email);

        var userDto = await ToDto(utilisateur);
        return new ResultatAuth(true, "Connexion réussie.", Utilisateur: userDto);
    }

    // ─── DÉCONNEXION ─────────────────────────────────────────────────────────
    public async Task<bool> DeconnecterAsync(CancellationToken ct = default)
    {
        await _signInManager.SignOutAsync();
        return true;
    }

    // ─── CHANGER MOT DE PASSE ────────────────────────────────────────────────
    public async Task<ResultatAuth> ChangerMotDePasseAsync(string userId, ChangerMotDePasseDto dto, CancellationToken ct = default)
    {
        var utilisateur = await _userManager.FindByIdAsync(userId);
        if (utilisateur == null) return new ResultatAuth(false, "Utilisateur introuvable.");

        if (dto.NouveauMotDePasse != dto.Confirmation)
            return new ResultatAuth(false, "Les mots de passe ne correspondent pas.");

        var resultat = await _userManager.ChangePasswordAsync(utilisateur, dto.AncienMotDePasse, dto.NouveauMotDePasse);

        if (!resultat.Succeeded)
        {
            var erreurs = string.Join(", ", resultat.Errors.Select(e => e.Description));
            return new ResultatAuth(false, erreurs);
        }

        return new ResultatAuth(true, "Mot de passe changé avec succès.");
    }

    // ─── RESET MOT DE PASSE ──────────────────────────────────────────────────
    public async Task<string?> GenererTokenResetMotDePasseAsync(string email, CancellationToken ct = default)
    {
        var utilisateur = await _userManager.FindByEmailAsync(email);
        if (utilisateur == null) return null;

        // Générer token sécurisé (à envoyer par email)
        return await _userManager.GeneratePasswordResetTokenAsync(utilisateur);
    }

    public async Task<ResultatAuth> ResetMotDePasseAsync(string email, string token, string nouveauMotDePasse, CancellationToken ct = default)
    {
        var utilisateur = await _userManager.FindByEmailAsync(email);
        if (utilisateur == null) return new ResultatAuth(false, "Utilisateur introuvable.");

        var resultat = await _userManager.ResetPasswordAsync(utilisateur, token, nouveauMotDePasse);
        if (!resultat.Succeeded)
        {
            var erreurs = string.Join(", ", resultat.Errors.Select(e => e.Description));
            return new ResultatAuth(false, erreurs);
        }

        return new ResultatAuth(true, "Mot de passe réinitialisé.");
    }

    // ─── OBTENIR PROFIL ──────────────────────────────────────────────────────
    public async Task<ApplicationUserDto?> ObtenirProfilAsync(string userId, CancellationToken ct = default)
    {
        var utilisateur = await _userManager.FindByIdAsync(userId);
        if (utilisateur == null) return null;
        return await ToDto(utilisateur);
    }

    private async Task<ApplicationUserDto> ToDto(ApplicationUser user)
    {
        var roles = await _userManager.GetRolesAsync(user);
        return new ApplicationUserDto(user.Id, user.Prenom, user.Nom, user.Email!, roles);
    }
}


// ----------------------------------------------------------------------------
// [SCENARIO] GESTION DES RÔLES
// ----------------------------------------------------------------------------

/*
[IDEE] RÔLES vs CLAIMS vs POLICIES

Rôle :    "Admin", "Manager", "Utilisateur"
          -> Groupe d'utilisateurs avec mêmes permissions
          -> Simple, binaire (dans le rôle ou pas)

Claim :   "age=25", "departement=Finance", "abonnement=Premium"
          -> Attribut spécifique de l'utilisateur
          -> Plus flexible que les rôles

Policy :  Règle basée sur rôles ET/OU claims
          -> La plus flexible des trois
          -> Recommandée pour les nouvelles apps

EXEMPLES :
  [Authorize(Roles = "Admin")]                    -> Rôle simple
  [Authorize(Policy = "PeutModifierProduits")]     -> Policy complexe
*/

// Service de gestion des rôles
public class ServiceRole
{
    private readonly RoleManager<IdentityRole> _roleManager;
    private readonly UserManager<ApplicationUser> _userManager;

    public ServiceRole(RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)
    {
        _roleManager = roleManager;
        _userManager = userManager;
    }

    // Créer un rôle
    public async Task<bool> CreerRoleAsync(string nomRole)
    {
        if (await _roleManager.RoleExistsAsync(nomRole)) return false;
        var resultat = await _roleManager.CreateAsync(new IdentityRole(nomRole));
        return resultat.Succeeded;
    }

    // Assigner rôle à utilisateur
    public async Task<bool> AssignerRoleAsync(string userId, string nomRole)
    {
        var utilisateur = await _userManager.FindByIdAsync(userId);
        if (utilisateur == null) return false;

        if (!await _roleManager.RoleExistsAsync(nomRole)) return false;

        var resultat = await _userManager.AddToRoleAsync(utilisateur, nomRole);
        return resultat.Succeeded;
    }

    // Retirer rôle
    public async Task<bool> RetirerRoleAsync(string userId, string nomRole)
    {
        var utilisateur = await _userManager.FindByIdAsync(userId);
        if (utilisateur == null) return false;

        var resultat = await _userManager.RemoveFromRoleAsync(utilisateur, nomRole);
        return resultat.Succeeded;
    }

    // Obtenir tous les rôles d'un utilisateur
    public async Task<IList<string>> ObtenirRolesAsync(string userId)
    {
        var utilisateur = await _userManager.FindByIdAsync(userId);
        if (utilisateur == null) return new List<string>();
        return await _userManager.GetRolesAsync(utilisateur);
    }

    // Obtenir tous les utilisateurs d'un rôle
    public async Task<IList<ApplicationUser>> ObtenirUtilisateursParRoleAsync(string nomRole)
        => await _userManager.GetUsersInRoleAsync(nomRole);

    // Initialiser les rôles par défaut
    public async Task InitialiserRolesAsync()
    {
        var roles = new[] { "SuperAdmin", "Admin", "Manager", "Utilisateur" };
        foreach (var role in roles)
        {
            if (!await _roleManager.RoleExistsAsync(role))
                await _roleManager.CreateAsync(new IdentityRole(role));
        }
    }
}


// ----------------------------------------------------------------------------
// [SECURITE] AUTORISATION AVEC POLICIES
// ----------------------------------------------------------------------------

/*
[IDEE] POLICIES = Règles d'autorisation composables

COMMENT : Définir des requirements + handlers, enregistrer dans DI
POURQUOI : Plus flexible et testable que les rôles simples
QUAND : Logique d'autorisation complexe (multi-conditions)
*/

using Microsoft.AspNetCore.Authorization;

// ─── REQUIREMENT (ce qui est requis) ───────────────────────────────────────
public class AgeMinimumRequirement : IAuthorizationRequirement
{
    public int AgeMinimum { get; }
    public AgeMinimumRequirement(int ageMin) => AgeMinimum = ageMin;
}

public class AbonnementPremiumRequirement : IAuthorizationRequirement { }

public class MemeOrganisationRequirement : IAuthorizationRequirement { }

// ─── HANDLERS (logique de vérification) ────────────────────────────────────
public class AgeMinimumHandler : AuthorizationHandler<AgeMinimumRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AgeMinimumRequirement requirement)
    {
        var ageClaim = context.User.FindFirst("age");
        if (ageClaim != null && int.TryParse(ageClaim.Value, out var age))
        {
            if (age >= requirement.AgeMinimum)
                context.Succeed(requirement); // [OK] Autorisé
        }
        // Ne pas appeler context.Fail() -> laisse les autres handlers décider
        return Task.CompletedTask;
    }
}

public class AbonnementPremiumHandler : AuthorizationHandler<AbonnementPremiumRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AbonnementPremiumRequirement requirement)
    {
        var abonnement = context.User.FindFirst("abonnement")?.Value;
        if (abonnement == "Premium" || abonnement == "Enterprise")
            context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

// Handler basé sur ressource (vérifier propriété)
public class DocumentOwnerHandler : AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement,
        Document resource)
    {
        var userId = context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;

        if (resource.ProprietaireId == userId)
            context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

public class Document
{
    public int Id { get; set; }
    public string Titre { get; set; } = string.Empty;
    public string ProprietaireId { get; set; } = string.Empty;
    public string Contenu { get; set; } = string.Empty;
}

// ─── ENREGISTREMENT DES POLICIES ────────────────────────────────────────────
/*
Dans Program.cs :

builder.Services.AddAuthorization(options =>
{
    // Policy basée sur rôle
    options.AddPolicy("AdminSeulement", policy =>
        policy.RequireRole("Admin", "SuperAdmin"));

    // Policy basée sur claim
    options.AddPolicy("EmailConfirme", policy =>
        policy.RequireClaim("email_confirmed", "true"));

    // Policy avec requirement personnalisé
    options.AddPolicy("AgeMajeur", policy =>
        policy.Requirements.Add(new AgeMinimumRequirement(18)));

    // Policy combinée
    options.AddPolicy("ManagerOuAdmin", policy =>
        policy.RequireRole("Manager", "Admin")
              .RequireAuthenticatedUser());

    // Policy avec assertion lambda
    options.AddPolicy("France", policy =>
        policy.RequireAssertion(ctx =>
            ctx.User.HasClaim(c => c.Type == "pays" && c.Value == "FR")));

    // Policy par défaut (toute requête doit être authentifiée)
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

builder.Services.AddScoped<IAuthorizationHandler, AgeMinimumHandler>();
builder.Services.AddScoped<IAuthorizationHandler, AbonnementPremiumHandler>();
builder.Services.AddScoped<IAuthorizationHandler, DocumentOwnerHandler>();
*/

// ─── UTILISATION DANS LES CONTROLLERS ──────────────────────────────────────
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/[controller]")]
[Authorize]                                          // Authentifié
public class DocumentsController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly IAuthorizationService _authService;

    public DocumentsController(IAuthorizationService authService)
        => _authService = authService;

    [HttpGet]
    [Authorize(Roles = "Admin,Manager")]             // Rôle spécifique
    public IActionResult ObtenirTous() => Ok("Liste des documents");

    [HttpGet("premium")]
    [Authorize(Policy = "AbonnementPremium")]         // Policy personnalisée
    public IActionResult ContenuPremium() => Ok("Contenu premium");

    // Autorisation basée sur ressource (dans la méthode)
    [HttpPut("{id}")]
    public async Task<IActionResult> Modifier(int id, [Microsoft.AspNetCore.Mvc.FromBody] string contenu)
    {
        var document = new Document { Id = id, ProprietaireId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value! };

        var authResult = await _authService.AuthorizeAsync(User, document, Operations.Modifier);
        if (!authResult.Succeeded)
            return Forbid();

        return Ok("Document modifié");
    }

    [HttpDelete("{id}")]
    [Authorize(Policy = "AdminSeulement")]
    public IActionResult Supprimer(int id) => NoContent();
}

// Opérations pour authorization basée sur ressource
public static class Operations
{
    public static OperationAuthorizationRequirement Lire =
        new() { Name = "Lire" };
    public static OperationAuthorizationRequirement Modifier =
        new() { Name = "Modifier" };
    public static OperationAuthorizationRequirement Supprimer =
        new() { Name = "Supprimer" };
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE 8 — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ :

Construisez un système d'authentification pour une API scolaire.

1. Créez ApplicationUser avec champs supplémentaires :
   - Prenom, Nom, Filiere, NumeroEtudiant, DateInscription

2. Configurez Identity avec règles de mot de passe strictes :
   - Min 8 caractères, majuscule, chiffre requis
   - Verrouillage après 3 tentatives (10 min)

3. Créez AuthController avec :
   a) POST /api/auth/inscrire
   b) POST /api/auth/connecter
   c) POST /api/auth/deconnecter
   d) GET  /api/auth/profil (authentifié requis)
   e) PUT  /api/auth/changer-mot-de-passe (authentifié requis)

4. Créez une Policy "EtudiantInfoActif" :
   - Utilisateur doit être authentifié
   - Doit avoir le claim "filiere"
   - Doit avoir le rôle "Etudiant"

5. Créez RolesController (Admin seulement) pour gérer les rôles
*/

// ─── CORRIGÉ ────────────────────────────────────────────────────────────────

// 1. Entité utilisateur
public class EtudiantUser : IdentityUser
{
    [MaxLength(100)] public string Prenom { get; set; } = string.Empty;
    [MaxLength(100)] public string Nom { get; set; } = string.Empty;
    [MaxLength(100)] public string? Filiere { get; set; }
    [MaxLength(20)]  public string? NumeroEtudiant { get; set; }
    public DateTime DateInscription { get; set; } = DateTime.UtcNow;
    public bool EstActif { get; set; } = true;
}

// DTOs
public record InscrireEtudiantDto(
    string Prenom, string Nom, string Email,
    string Filiere, string MotDePasse, string Confirmation);

public record ConnecterDto(string Email, string MotDePasse);

public record ProfilDto(string Id, string NomComplet, string Email, string? Filiere, IList<string> Roles);

public record ChangerMdpDto(string AncienMdp, string NouveauMdp, string ConfirmationMdp);

// Requirement personnalisé
public class EtudiantActifRequirement : IAuthorizationRequirement { }

public class EtudiantActifHandler : AuthorizationHandler<EtudiantActifRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext ctx, EtudiantActifRequirement req)
    {
        var filiereClaim = ctx.User.FindFirst("filiere");
        var estEtudiant  = ctx.User.IsInRole("Etudiant");

        if (filiereClaim != null && estEtudiant)
            ctx.Succeed(req);

        return Task.CompletedTask;
    }
}

// Controller Auth
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/auth")]
public class AuthController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly UserManager<EtudiantUser> _userManager;
    private readonly SignInManager<EtudiantUser> _signInManager;

    public AuthController(UserManager<EtudiantUser> um, SignInManager<EtudiantUser> sm)
    {
        _userManager = um;
        _signInManager = sm;
    }

    // a) Inscription
    [HttpPost("inscrire")]
    public async Task<IActionResult> Inscrire([Microsoft.AspNetCore.Mvc.FromBody] InscrireEtudiantDto dto)
    {
        if (dto.MotDePasse != dto.Confirmation)
            return BadRequest(new { Message = "Les mots de passe ne correspondent pas." });

        var user = new EtudiantUser
        {
            UserName = dto.Email,
            Email = dto.Email,
            Prenom = dto.Prenom,
            Nom = dto.Nom,
            Filiere = dto.Filiere
        };

        var result = await _userManager.CreateAsync(user, dto.MotDePasse);
        if (!result.Succeeded)
            return BadRequest(new { Erreurs = result.Errors.Select(e => e.Description) });

        await _userManager.AddToRoleAsync(user, "Etudiant");
        await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("filiere", dto.Filiere));

        return Ok(new { Message = "Inscription réussie.", UserId = user.Id });
    }

    // b) Connexion
    [HttpPost("connecter")]
    public async Task<IActionResult> Connecter([Microsoft.AspNetCore.Mvc.FromBody] ConnecterDto dto)
    {
        var user = await _userManager.FindByEmailAsync(dto.Email);
        if (user == null || !user.EstActif)
            return Unauthorized(new { Message = "Identifiants invalides." });

        var result = await _signInManager.PasswordSignInAsync(user, dto.MotDePasse, false, lockoutOnFailure: true);

        if (result.IsLockedOut)
            return Unauthorized(new { Message = "Compte verrouillé. Réessayez dans 10 minutes." });

        if (!result.Succeeded)
            return Unauthorized(new { Message = "Identifiants invalides." });

        return Ok(new { Message = "Connexion réussie." });
    }

    // c) Déconnexion
    [HttpPost("deconnecter")]
    [Authorize]
    public async Task<IActionResult> Deconnecter()
    {
        await _signInManager.SignOutAsync();
        return Ok(new { Message = "Déconnexion réussie." });
    }

    // d) Profil
    [HttpGet("profil")]
    [Authorize]
    public async Task<IActionResult> Profil()
    {
        var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value!;
        var user = await _userManager.FindByIdAsync(userId);
        if (user == null) return NotFound();

        var roles = await _userManager.GetRolesAsync(user);
        return Ok(new ProfilDto(user.Id, $"{user.Prenom} {user.Nom}", user.Email!, user.Filiere, roles));
    }

    // e) Changer mot de passe
    [HttpPut("changer-mot-de-passe")]
    [Authorize]
    public async Task<IActionResult> ChangerMotDePasse([Microsoft.AspNetCore.Mvc.FromBody] ChangerMdpDto dto)
    {
        if (dto.NouveauMdp != dto.ConfirmationMdp)
            return BadRequest(new { Message = "Confirmation incorrecte." });

        var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value!;
        var user = await _userManager.FindByIdAsync(userId);
        if (user == null) return NotFound();

        var result = await _userManager.ChangePasswordAsync(user, dto.AncienMdp, dto.NouveauMdp);
        if (!result.Succeeded)
            return BadRequest(new { Erreurs = result.Errors.Select(e => e.Description) });

        return Ok(new { Message = "Mot de passe changé avec succès." });
    }
}

// Controller Rôles (Admin seulement)
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/roles")]
[Authorize(Roles = "Admin")]
public class RolesController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly RoleManager<IdentityRole> _roleManager;
    private readonly UserManager<EtudiantUser> _userManager;

    public RolesController(RoleManager<IdentityRole> rm, UserManager<EtudiantUser> um)
    {
        _roleManager = rm;
        _userManager = um;
    }

    [HttpGet]
    public IActionResult ObtenirRoles()
        => Ok(_roleManager.Roles.Select(r => r.Name).ToList());

    [HttpPost]
    public async Task<IActionResult> CreerRole([Microsoft.AspNetCore.Mvc.FromBody] string nomRole)
    {
        if (await _roleManager.RoleExistsAsync(nomRole))
            return Conflict(new { Message = "Rôle déjà existant." });

        var result = await _roleManager.CreateAsync(new IdentityRole(nomRole));
        return result.Succeeded ? Ok() : BadRequest(result.Errors);
    }

    [HttpPost("{userId}/assigner/{nomRole}")]
    public async Task<IActionResult> AssignerRole(string userId, string nomRole)
    {
        var user = await _userManager.FindByIdAsync(userId);
        if (user == null) return NotFound();

        var result = await _userManager.AddToRoleAsync(user, nomRole);
        return result.Succeeded ? Ok() : BadRequest(result.Errors);
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 12 : JWT AUTHENTICATION
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le fonctionnement des JWT
[OK] Générer des access tokens et refresh tokens
[OK] Protéger les endpoints API avec JWT
[OK] Implémenter le renouvellement de token
[OK] Sécuriser les tokens côté serveur
*/


// ----------------------------------------------------------------------------
// [CLE] QU'EST-CE QU'UN JWT ?
// ----------------------------------------------------------------------------

/*
JWT = JSON Web Token

STRUCTURE : header.payload.signature

Header :  { "alg": "HS256", "typ": "JWT" }
Payload : {
  "sub": "user-id-123",          <- Subject (userId)
  "email": "alice@ex.com",
  "role": "Admin",
  "exp": 1699999999,             <- Expiration (Unix timestamp)
  "iat": 1699996399,             <- Issued at
  "iss": "MonApi",               <- Issuer
  "aud": "MonApiClients"         <- Audience
}
Signature : HMACSHA256(base64(header) + "." + base64(payload), secretKey)

FLUX D'AUTHENTIFICATION JWT :
1. Client -> POST /api/auth/login (email + password)
2. Serveur -> Vérifie credentials -> Génère JWT
3. Client -> Stocke JWT (localStorage ou cookie HttpOnly)
4. Client -> Chaque requête : Authorization: Bearer <JWT>
5. Serveur -> Valide signature + expiration -> Autorise

AVANTAGES vs SESSION :
[OK] Stateless (serveur ne stocke rien)
[OK] Scalable (multiple serveurs, pas de session partagée)
[OK] Mobile-friendly
[X] Ne peut pas être révoqué facilement (résolu avec refresh tokens)

PACKAGES :
  dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
  dotnet add package System.IdentityModel.Tokens.Jwt
*/


// ----------------------------------------------------------------------------
// [CONFIG] CONFIGURATION JWT
// ----------------------------------------------------------------------------

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

// Options de configuration JWT
public class JwtSettings
{
    public const string SectionName = "JwtSettings";
    public string SecretKey { get; set; } = string.Empty;       // Min 32 chars
    public string Issuer { get; set; } = string.Empty;
    public string Audience { get; set; } = string.Empty;
    public int AccessTokenExpiresMinutes { get; set; } = 15;    // Court!
    public int RefreshTokenExpiresDays { get; set; } = 7;
}

// appsettings.json :
/*
{
  "JwtSettings": {
    "Issuer": "MonApi",
    "Audience": "MonApiClients",
    "AccessTokenExpiresMinutes": 15,
    "RefreshTokenExpiresDays": 7
  }
}
appsettings.Development.json :
{
  "JwtSettings": {
    "SecretKey": "ma-super-cle-secrete-de-dev-min-32-chars!!"
  }
}
PRODUCTION -> User Secrets ou Azure Key Vault (jamais dans appsettings.json!)
*/

// Dans Program.cs :
/*
var jwtSettings = builder.Configuration.GetSection(JwtSettings.SectionName).Get<JwtSettings>()!;
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection(JwtSettings.SectionName));

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,             // Vérifier expiration
        ValidateIssuerSigningKey = true,     // Vérifier signature
        ValidIssuer = jwtSettings.Issuer,
        ValidAudience = jwtSettings.Audience,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)),
        ClockSkew = TimeSpan.Zero            // Pas de tolérance de clock
    };

    // Support JWT dans les WebSockets (SignalR)
    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];
            var path = context.HttpContext.Request.Path;
            if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
                context.Token = accessToken;
            return Task.CompletedTask;
        }
    };
});

builder.Services.AddAuthorization();
*/


// ----------------------------------------------------------------------------
// [USINE] SERVICE DE GÉNÉRATION DE TOKENS
// ----------------------------------------------------------------------------

// Entité pour les refresh tokens
public class RefreshToken
{
    public int Id { get; set; }
    public string Token { get; set; } = string.Empty;
    public DateTime Expiration { get; set; }
    public bool EstUtilise { get; set; } = false;
    public bool EstRevoquer { get; set; } = false;
    public string UserId { get; set; } = string.Empty;
    public ApplicationUser User { get; set; } = null!;
    public DateTime DateCreation { get; set; } = DateTime.UtcNow;
    public string? RemplacePar { get; set; } // Token suivant (rotation)
}

public record TokenResponse(
    string AccessToken,
    string RefreshToken,
    DateTime AccessTokenExpiration,
    ApplicationUserDto Utilisateur
);

public interface IServiceToken
{
    Task<TokenResponse> GenererTokensAsync(ApplicationUser utilisateur);
    Task<TokenResponse?> RafraichirTokenAsync(string accessToken, string refreshToken, CancellationToken ct = default);
    Task<bool> RevoquerRefreshTokenAsync(string refreshToken, CancellationToken ct = default);
    ClaimsPrincipal? ValiderTokenExpire(string token);
}

public class ServiceToken : IServiceToken
{
    private readonly JwtSettings _settings;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly AppIdentityDbContext _ctx;

    public ServiceToken(
        IOptions<JwtSettings> settings,
        UserManager<ApplicationUser> userManager,
        AppIdentityDbContext ctx)
    {
        _settings = settings.Value;
        _userManager = userManager;
        _ctx = ctx;
    }

    // ─── GÉNÉRER ACCESS TOKEN ────────────────────────────────────────────────
    private async Task<string> GenererAccessTokenAsync(ApplicationUser utilisateur)
    {
        var roles = await _userManager.GetRolesAsync(utilisateur);
        var claims = await _userManager.GetClaimsAsync(utilisateur);

        // Construction des claims du token
        var tokenClaims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, utilisateur.Id),
            new(JwtRegisteredClaimNames.Email, utilisateur.Email!),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID unique
            new(JwtRegisteredClaimNames.Iat,
                DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
                ClaimValueTypes.Integer64),
            new("prenom", utilisateur.Prenom),
            new("nom", utilisateur.Nom),
        };

        // Ajouter les rôles comme claims
        tokenClaims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));

        // Ajouter les claims personnalisés de l'utilisateur
        tokenClaims.AddRange(claims);

        // Création du token
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.SecretKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _settings.Issuer,
            audience: _settings.Audience,
            claims: tokenClaims,
            notBefore: DateTime.UtcNow,
            expires: DateTime.UtcNow.AddMinutes(_settings.AccessTokenExpiresMinutes),
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    // ─── GÉNÉRER REFRESH TOKEN ────────────────────────────────────────────────
    private static string GenererRefreshToken()
    {
        // Token aléatoire cryptographiquement sécurisé
        var bytes = new byte[64];
        using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
        rng.GetBytes(bytes);
        return Convert.ToBase64String(bytes);
    }

    // ─── GÉNÉRER LES DEUX TOKENS ──────────────────────────────────────────────
    public async Task<TokenResponse> GenererTokensAsync(ApplicationUser utilisateur)
    {
        var accessToken = await GenererAccessTokenAsync(utilisateur);
        var refreshTokenStr = GenererRefreshToken();

        // Sauvegarder le refresh token en BDD
        var refreshToken = new RefreshToken
        {
            Token = refreshTokenStr,
            UserId = utilisateur.Id,
            Expiration = DateTime.UtcNow.AddDays(_settings.RefreshTokenExpiresDays),
        };
        _ctx.Set<RefreshToken>().Add(refreshToken);
        await _ctx.SaveChangesAsync();

        var roles = await _userManager.GetRolesAsync(utilisateur);
        var userDto = new ApplicationUserDto(utilisateur.Id, utilisateur.Prenom, utilisateur.Nom, utilisateur.Email!, roles);

        return new TokenResponse(
            accessToken,
            refreshTokenStr,
            DateTime.UtcNow.AddMinutes(_settings.AccessTokenExpiresMinutes),
            userDto
        );
    }

    // ─── RAFRAÎCHIR LES TOKENS ────────────────────────────────────────────────
    public async Task<TokenResponse?> RafraichirTokenAsync(
        string accessToken, string refreshToken, CancellationToken ct = default)
    {
        // Valider le token expiré (vérifier signature mais pas l'expiration)
        var principal = ValiderTokenExpire(accessToken);
        if (principal == null) return null;

        var userId = principal.FindFirstValue(JwtRegisteredClaimNames.Sub);
        if (string.IsNullOrEmpty(userId)) return null;

        // Vérifier le refresh token en BDD
        var storedRefreshToken = await _ctx.Set<RefreshToken>()
            .FirstOrDefaultAsync(rt => rt.Token == refreshToken && rt.UserId == userId, ct);

        if (storedRefreshToken == null
            || storedRefreshToken.EstUtilise
            || storedRefreshToken.EstRevoquer
            || storedRefreshToken.Expiration < DateTime.UtcNow)
            return null;

        // Marquer comme utilisé (rotation des tokens)
        storedRefreshToken.EstUtilise = true;

        var utilisateur = await _userManager.FindByIdAsync(userId);
        if (utilisateur == null || !utilisateur.EstActif) return null;

        // Générer nouveaux tokens
        var nouveauxTokens = await GenererTokensAsync(utilisateur);
        storedRefreshToken.RemplacePar = nouveauxTokens.RefreshToken;
        await _ctx.SaveChangesAsync(ct);

        return nouveauxTokens;
    }

    // ─── RÉVOQUER REFRESH TOKEN ───────────────────────────────────────────────
    public async Task<bool> RevoquerRefreshTokenAsync(string refreshToken, CancellationToken ct = default)
    {
        var token = await _ctx.Set<RefreshToken>()
            .FirstOrDefaultAsync(rt => rt.Token == refreshToken, ct);

        if (token == null) return false;

        token.EstRevoquer = true;
        await _ctx.SaveChangesAsync(ct);
        return true;
    }

    // ─── VALIDER TOKEN EXPIRÉ (pour refresh) ──────────────────────────────────
    public ClaimsPrincipal? ValiderTokenExpire(string token)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.SecretKey));
        try
        {
            var principal = new JwtSecurityTokenHandler().ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = false,       // NE PAS valider l'expiration !
                ValidateIssuerSigningKey = true,
                ValidIssuer = _settings.Issuer,
                ValidAudience = _settings.Audience,
                IssuerSigningKey = key
            }, out var securityToken);

            if (securityToken is not JwtSecurityToken jwtToken
                || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
                return null;

            return principal;
        }
        catch
        {
            return null;
        }
    }
}


// ----------------------------------------------------------------------------
// [VIDEO_GAME] CONTROLLER D'AUTHENTIFICATION JWT
// ----------------------------------------------------------------------------

[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/auth")]
public class AuthJwtController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly IServiceAuth _authService;
    private readonly IServiceToken _tokenService;
    private readonly UserManager<ApplicationUser> _userManager;

    public AuthJwtController(
        IServiceAuth authService,
        IServiceToken tokenService,
        UserManager<ApplicationUser> userManager)
    {
        _authService = authService;
        _tokenService = tokenService;
        _userManager = userManager;
    }

    // POST /api/auth/inscrire
    [HttpPost("inscrire")]
    [ProducesResponseType(typeof(TokenResponse), 200)]
    [ProducesResponseType(400)]
    public async Task<IActionResult> Inscrire([Microsoft.AspNetCore.Mvc.FromBody] InscriptionDto dto)
    {
        var result = await _authService.InscrireAsync(dto);
        if (!result.Succes) return BadRequest(new { result.Message });

        var utilisateur = await _userManager.FindByEmailAsync(dto.Email);
        var tokens = await _tokenService.GenererTokensAsync(utilisateur!);
        return Ok(tokens);
    }

    // POST /api/auth/connecter
    [HttpPost("connecter")]
    [ProducesResponseType(typeof(TokenResponse), 200)]
    [ProducesResponseType(401)]
    public async Task<IActionResult> Connecter([Microsoft.AspNetCore.Mvc.FromBody] ConnexionDto dto)
    {
        var utilisateur = await _userManager.FindByEmailAsync(dto.Email);
        if (utilisateur == null)
            return Unauthorized(new { Message = "Identifiants invalides." });

        var motDePasseOk = await _userManager.CheckPasswordAsync(utilisateur, dto.MotDePasse);
        if (!motDePasseOk)
        {
            await _userManager.AccessFailedAsync(utilisateur);
            return Unauthorized(new { Message = "Identifiants invalides." });
        }

        await _userManager.ResetAccessFailedCountAsync(utilisateur);
        var tokens = await _tokenService.GenererTokensAsync(utilisateur);
        return Ok(tokens);
    }

    // POST /api/auth/rafraichir
    [HttpPost("rafraichir")]
    public async Task<IActionResult> Rafraichir([Microsoft.AspNetCore.Mvc.FromBody] RafraichirTokenDto dto)
    {
        var tokens = await _tokenService.RafraichirTokenAsync(dto.AccessToken, dto.RefreshToken);
        if (tokens == null) return Unauthorized(new { Message = "Token invalide ou expiré." });
        return Ok(tokens);
    }

    // POST /api/auth/revoquer
    [HttpPost("revoquer")]
    [Authorize]
    public async Task<IActionResult> Revoquer([Microsoft.AspNetCore.Mvc.FromBody] RevoquerTokenDto dto)
    {
        var success = await _tokenService.RevoquerRefreshTokenAsync(dto.RefreshToken);
        return success ? Ok() : BadRequest(new { Message = "Token introuvable." });
    }

    // GET /api/auth/moi
    [HttpGet("moi")]
    [Authorize]
    public IActionResult Moi()
    {
        var claims = User.Claims.Select(c => new { c.Type, c.Value });
        return Ok(new
        {
            UserId = User.FindFirstValue(JwtRegisteredClaimNames.Sub),
            Email = User.FindFirstValue(ClaimTypes.Email),
            Roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value),
            Claims = claims
        });
    }
}

public record RafraichirTokenDto(string AccessToken, string RefreshToken);
public record RevoquerTokenDto(string RefreshToken);


// ============================================================================
// [COURS] EXERCICE PRATIQUE 9 — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ :

Implémentez l'authentification JWT complète pour une API de blog.

1. Entité BlogUser : Id, UserName, Email, Prenom, Nom, Bio, Role
2. JwtSettings dans appsettings.json (AccessToken: 10min, Refresh: 30jours)
3. BlogTokenService qui génère access + refresh tokens
4. AuthBlogController avec :
   - POST /api/auth/register
   - POST /api/auth/login -> retourne TokenResponse
   - POST /api/auth/refresh
   - POST /api/auth/logout (révoque refresh token)
5. ArticlesController :
   - GET  /api/articles       -> Public (pas d'auth)
   - POST /api/articles       -> Auteur seulement
   - DELETE /api/articles/{id}-> Admin seulement
*/

// ─── CORRIGÉ ────────────────────────────────────────────────────────────────

public class BlogUser : IdentityUser
{
    public string Prenom { get; set; } = string.Empty;
    public string Nom    { get; set; } = string.Empty;
    public string? Bio   { get; set; }
}

// Article simple
public class Article
{
    public int Id { get; set; }
    public string Titre { get; set; } = string.Empty;
    public string Contenu { get; set; } = string.Empty;
    public DateTime DatePublication { get; set; } = DateTime.UtcNow;
    public string AuteurId { get; set; } = string.Empty;
}

// TokenService simplifié pour le blog
public class BlogTokenService
{
    private readonly IConfiguration _config;

    public BlogTokenService(IConfiguration config) => _config = config;

    public string GenererToken(BlogUser user, IList<string> roles)
    {
        var secretKey = _config["JwtSettings:SecretKey"]!;
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, user.Id),
            new(JwtRegisteredClaimNames.Email, user.Email!),
            new("prenom", user.Prenom),
            new("nom", user.Nom),
        };
        claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));

        var token = new JwtSecurityToken(
            issuer: _config["JwtSettings:Issuer"],
            audience: _config["JwtSettings:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(
                int.Parse(_config["JwtSettings:AccessTokenExpiresMinutes"] ?? "10")),
            signingCredentials: creds);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

// Controller Blog Articles
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/articles")]
public class ArticlesBlogController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private static readonly List<Article> _articles = new()
    {
        new() { Id = 1, Titre = "Premier article", Contenu = "Contenu...", AuteurId = "user1" }
    };

    // GET /api/articles -> Public
    [HttpGet]
    [AllowAnonymous]
    public IActionResult ObtenirTous()
        => Ok(_articles.Select(a => new { a.Id, a.Titre, a.DatePublication }));

    // POST /api/articles -> Auteur seulement
    [HttpPost]
    [Authorize(Roles = "Auteur,Admin")]
    public IActionResult Creer([Microsoft.AspNetCore.Mvc.FromBody] CreerArticleDto dto)
    {
        var auteurId = User.FindFirstValue(JwtRegisteredClaimNames.Sub)!;
        var article = new Article
        {
            Id = _articles.Max(a => a.Id) + 1,
            Titre = dto.Titre,
            Contenu = dto.Contenu,
            AuteurId = auteurId
        };
        _articles.Add(article);
        return CreatedAtAction(nameof(ObtenirTous), new { id = article.Id }, article);
    }

    // DELETE /api/articles/{id} -> Admin seulement
    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")]
    public IActionResult Supprimer(int id)
    {
        var article = _articles.FirstOrDefault(a => a.Id == id);
        if (article == null) return NotFound();
        _articles.Remove(article);
        return NoContent();
    }
}

public record CreerArticleDto(string Titre, string Contenu);


// ============================================================================
// [GUIDE] CHAPITRE 13 : OAUTH & SOCIAL LOGIN
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le flux OAuth 2.0
[OK] Configurer Google Login
[OK] Configurer GitHub Login
[OK] Gérer le callback et créer le JWT après OAuth
[OK] Combiner OAuth avec Identity
*/


// ----------------------------------------------------------------------------
// [WEB] QU'EST-CE QU'OAUTH 2.0 ?
// ----------------------------------------------------------------------------

/*
OAuth 2.0 = Protocole d'AUTORISATION (délégation d'accès)
OpenID Connect (OIDC) = Couche d'IDENTITÉ au-dessus d'OAuth

FLUX AUTHORIZATION CODE (le plus sécurisé) :

1. Client -> Redirige vers Google : GET /oauth/google/login
2. App -> Redirige vers Google : https://accounts.google.com/oauth/authorize?...
3. Google -> Utilisateur se connecte et autorise
4. Google -> Redirige vers Callback : GET /signin-google?code=ABC
5. App (Backend) -> Échange code contre tokens (secret côté serveur)
6. Google -> Retourne access_token + id_token
7. App -> Crée/trouve l'utilisateur local -> Génère JWT interne

PROVIDERS SUPPORTÉS NATIVEMENT :
  dotnet add package Microsoft.AspNetCore.Authentication.Google
  dotnet add package Microsoft.AspNetCore.Authentication.GitHub (via Octokit)
  dotnet add package Microsoft.AspNetCore.Authentication.MicrosoftAccount
  dotnet add package AspNet.Security.OAuth.GitHub

CONFIGURATION (Google Developer Console) :
  - Créer projet -> APIs & Services -> Credentials -> OAuth 2.0
  - Authorized redirect URIs: https://localhost:5001/signin-google

CONFIGURATION (GitHub) :
  - Settings -> Developer settings -> OAuth Apps -> New OAuth App
  - Authorization callback URL: https://localhost:5001/signin-github
*/


// ----------------------------------------------------------------------------
// [CONFIG] CONFIGURATION OAUTH DANS PROGRAM.CS
// ----------------------------------------------------------------------------

/*
// appsettings.Development.json (via User Secrets en production !)
{
  "Authentication": {
    "Google": {
      "ClientId": "votre-google-client-id",
      "ClientSecret": "votre-google-client-secret"
    },
    "GitHub": {
      "ClientId": "votre-github-client-id",
      "ClientSecret": "votre-github-client-secret"
    }
  }
}

Dans Program.cs :

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => { ... }) // Votre config JWT existante
.AddGoogle(options =>
{
    options.ClientId = builder.Configuration["Authentication:Google:ClientId"]!;
    options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]!;
    options.CallbackPath = "/signin-google";

    // Demander des scopes supplémentaires
    options.Scope.Add("profile");
    options.Scope.Add("email");

    // Sauvegarder les tokens Google pour usage ultérieur
    options.SaveTokens = true;

    // Mapper les claims Google vers des claims standards
    options.ClaimActions.MapJsonKey("picture", "picture");
    options.ClaimActions.MapJsonKey("locale", "locale");
})
.AddGitHub(options =>
{
    options.ClientId = builder.Configuration["Authentication:GitHub:ClientId"]!;
    options.ClientSecret = builder.Configuration["Authentication:GitHub:ClientSecret"]!;
    options.CallbackPath = "/signin-github";
    options.Scope.Add("user:email");
    options.SaveTokens = true;
});
*/


// ----------------------------------------------------------------------------
// [VIDEO_GAME] CONTROLLER OAUTH
// ----------------------------------------------------------------------------

[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/oauth")]
public class OAuthController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IServiceToken _tokenService;
    private readonly ILogger<OAuthController> _logger;

    public OAuthController(
        UserManager<ApplicationUser> userManager,
        IServiceToken tokenService,
        ILogger<OAuthController> logger)
    {
        _userManager = userManager;
        _tokenService = tokenService;
        _logger = logger;
    }

    // GET /api/oauth/google -> Déclenche le flux OAuth
    [HttpGet("google")]
    public IActionResult Google([Microsoft.AspNetCore.Mvc.FromQuery] string? returnUrl = null)
    {
        var callbackUrl = Url.Action(nameof(GoogleCallback), "OAuth",
            new { returnUrl }, Request.Scheme)!;

        var properties = new Microsoft.AspNetCore.Authentication.AuthenticationProperties
        {
            RedirectUri = callbackUrl,
            Items = { { "returnUrl", returnUrl ?? "/" } }
        };

        return Challenge(properties, "Google");
    }

    // GET /signin-google -> Callback après authentification Google
    [HttpGet("/signin-google")]
    public async Task<IActionResult> GoogleCallback([Microsoft.AspNetCore.Mvc.FromQuery] string? returnUrl = null)
    {
        // Récupérer les infos de l'utilisateur Google authentifié
        var result = await HttpContext.AuthenticateAsync("Google");
        if (!result.Succeeded)
            return Redirect($"/auth/error?message=Google+authentication+failed");

        var googleId    = result.Principal?.FindFirstValue(ClaimTypes.NameIdentifier)!;
        var email       = result.Principal?.FindFirstValue(ClaimTypes.Email)!;
        var prenom      = result.Principal?.FindFirstValue(ClaimTypes.GivenName) ?? "";
        var nom         = result.Principal?.FindFirstValue(ClaimTypes.Surname) ?? "";
        var avatarUrl   = result.Principal?.FindFirstValue("picture");

        return await GererConnexionExterne("Google", googleId, email, prenom, nom, avatarUrl, returnUrl);
    }

    // GET /api/oauth/github
    [HttpGet("github")]
    public IActionResult GitHub([Microsoft.AspNetCore.Mvc.FromQuery] string? returnUrl = null)
    {
        var callbackUrl = Url.Action(nameof(GitHubCallback), "OAuth",
            new { returnUrl }, Request.Scheme)!;

        var properties = new Microsoft.AspNetCore.Authentication.AuthenticationProperties
        {
            RedirectUri = callbackUrl
        };

        return Challenge(properties, "GitHub");
    }

    // GET /signin-github
    [HttpGet("/signin-github")]
    public async Task<IActionResult> GitHubCallback([Microsoft.AspNetCore.Mvc.FromQuery] string? returnUrl = null)
    {
        var result = await HttpContext.AuthenticateAsync("GitHub");
        if (!result.Succeeded)
            return Redirect("/auth/error?message=GitHub+authentication+failed");

        var githubId  = result.Principal?.FindFirstValue(ClaimTypes.NameIdentifier)!;
        var email     = result.Principal?.FindFirstValue(ClaimTypes.Email)!;
        var username  = result.Principal?.FindFirstValue(ClaimTypes.Name) ?? "";

        return await GererConnexionExterne("GitHub", githubId, email, username, "", null, returnUrl);
    }

    // ─── LOGIQUE COMMUNE ─────────────────────────────────────────────────────
    private async Task<IActionResult> GererConnexionExterne(
        string provider, string providerId, string email,
        string prenom, string nom, string? avatarUrl, string? returnUrl)
    {
        // 1. Chercher l'utilisateur par login externe
        var utilisateur = await _userManager.FindByLoginAsync(provider, providerId);

        // 2. Si pas trouvé par login, chercher par email
        if (utilisateur == null && !string.IsNullOrEmpty(email))
        {
            utilisateur = await _userManager.FindByEmailAsync(email);

            if (utilisateur != null)
            {
                // Associer le login externe au compte existant
                await _userManager.AddLoginAsync(utilisateur, new UserLoginInfo(provider, providerId, provider));
            }
        }

        // 3. Si toujours pas trouvé -> Créer nouveau compte
        if (utilisateur == null)
        {
            utilisateur = new ApplicationUser
            {
                UserName = email,
                Email = email,
                EmailConfirmed = true,  // Email vérifié par le provider
                Prenom = prenom,
                Nom = nom,
                AvatarUrl = avatarUrl,
                EstActif = true
            };

            var createResult = await _userManager.CreateAsync(utilisateur);
            if (!createResult.Succeeded)
            {
                _logger.LogError("Erreur création utilisateur {Provider}: {Errors}",
                    provider, string.Join(", ", createResult.Errors.Select(e => e.Description)));
                return Redirect("/auth/error");
            }

            // Ajouter login externe
            await _userManager.AddLoginAsync(utilisateur,
                new UserLoginInfo(provider, providerId, provider));

            // Rôle par défaut
            await _userManager.AddToRoleAsync(utilisateur, "Utilisateur");
        }

        if (!utilisateur.EstActif)
            return Redirect("/auth/error?message=Account+disabled");

        // 4. Générer JWT interne
        var tokens = await _tokenService.GenererTokensAsync(utilisateur);

        // 5. Rediriger vers le frontend avec les tokens
        // Option A : Via fragment URL (pour SPA)
        var frontendUrl = returnUrl ?? "/";
        return Redirect($"{frontendUrl}?accessToken={tokens.AccessToken}&refreshToken={tokens.RefreshToken}");

        // Option B : Via cookie HttpOnly (plus sécurisé)
        // Response.Cookies.Append("accessToken", tokens.AccessToken, new CookieOptions { HttpOnly = true, Secure = true });
        // return Redirect(returnUrl ?? "/");
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 14 : SÉCURITÉ WEB
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Forcer HTTPS et configurer HSTS
[OK] Configurer CORS correctement
[OK] Protéger contre CSRF
[OK] Prévenir XSS
[OK] Implémenter le Rate Limiting
[OK] Sécuriser les headers HTTP
*/


// ----------------------------------------------------------------------------
// [VERROUILLE] HTTPS & HSTS
// ----------------------------------------------------------------------------

/*
[IDEE] HTTPS = Chiffrement des communications
HSTS = HTTP Strict Transport Security (forcer HTTPS côté client)

COMMENT :
Dans Program.cs :

// Forcer la redirection HTTP -> HTTPS
app.UseHttpsRedirection();

// HSTS (UNIQUEMENT en production)
if (!app.Environment.IsDevelopment())
{
    app.UseHsts(); // Ajoute header: Strict-Transport-Security: max-age=31536000

    // Configuration avancée HSTS
    builder.Services.AddHsts(options =>
    {
        options.Preload = true;
        options.IncludeSubDomains = true;
        options.MaxAge = TimeSpan.FromDays(365);
    });
}

// Configuration HTTPS dans Kestrel (développement)
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.UseHttps(); // Certificat de dev automatique
    });
});
*/


// ----------------------------------------------------------------------------
// [WEB] CORS (Cross-Origin Resource Sharing)
// ----------------------------------------------------------------------------

/*
[IDEE] QU'EST-CE QUE CORS ?

PROBLÈME : Le navigateur bloque les requêtes d'un domaine A vers un domaine B
SOLUTION : Le serveur B indique quels domaines peuvent le contacter

EXEMPLE :
  Frontend : https://monapp.com (port 3000 en dev)
  API      : https://api.monapp.com (port 5001 en dev)
  -> Sans CORS configuré, le navigateur bloque les requêtes !

RULE : Configurer le minimum nécessaire. Ne JAMAIS mettre AllowAnyOrigin + AllowCredentials !
*/

// Configuration CORS dans Program.cs
/*
builder.Services.AddCors(options =>
{
    // Policy de développement (permissive)
    options.AddPolicy("DevelopmentPolicy", policy =>
        policy.WithOrigins("http://localhost:3000", "http://localhost:5173")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials()); // Nécessaire pour cookies/auth

    // Policy de production (restrictive)
    options.AddPolicy("ProductionPolicy", policy =>
        policy.WithOrigins("https://monapp.com", "https://www.monapp.com")
              .WithMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
              .WithHeaders("Authorization", "Content-Type", "X-Requested-With")
              .AllowCredentials()
              .SetPreflightMaxAge(TimeSpan.FromMinutes(10))); // Cache preflight

    // Policy publique (API publique sans credentials)
    options.AddPolicy("PublicApiPolicy", policy =>
        policy.AllowAnyOrigin()     // [ATTENTION] Seulement si pas de credentials !
              .WithMethods("GET")
              .AllowAnyHeader());
});

// UTILISATION :
// Globale :
app.UseCors("ProductionPolicy");

// Par controller/action :
// [EnableCors("PublicApiPolicy")]
// [DisableCors]
*/

// Middleware CORS personnalisé pour logique dynamique
public class CorsMiddlewarePerso
{
    private readonly RequestDelegate _next;
    private readonly string[] _allowedOrigins;

    public CorsMiddlewarePerso(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var origin = context.Request.Headers.Origin.ToString();

        if (_allowedOrigins.Contains(origin))
        {
            context.Response.Headers.Append("Access-Control-Allow-Origin", origin);
            context.Response.Headers.Append("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
            context.Response.Headers.Append("Access-Control-Allow-Headers", "Authorization, Content-Type");
            context.Response.Headers.Append("Access-Control-Allow-Credentials", "true");
        }

        if (context.Request.Method == "OPTIONS")
        {
            context.Response.StatusCode = 200;
            return;
        }

        await _next(context);
    }
}


// ----------------------------------------------------------------------------
// [SECURITE] ANTI-CSRF
// ----------------------------------------------------------------------------

/*
[IDEE] CSRF = Cross-Site Request Forgery

ATTAQUE :
  Un site malveillant fait exécuter une requête à votre API
  avec les cookies d'un utilisateur authentifié.

PROTECTION :
  1. Utiliser JWT (pas de cookies) -> Immunisé naturellement
  2. Antiforgery token (pour apps avec cookies)
  3. SameSite=Strict sur les cookies

QUAND CSRF EST UN PROBLÈME :
  -> Authentification par cookies

QUAND CSRF N'EST PAS UN PROBLÈME :
  -> Authentification par JWT dans Authorization header
*/

/*
Configuration Antiforgery (pour apps avec cookies) :

builder.Services.AddAntiforgery(options =>
{
    options.HeaderName = "X-CSRF-TOKEN";    // Header pour API
    options.Cookie.Name = "CSRF-TOKEN";
    options.Cookie.HttpOnly = false;        // false = lisible par JS pour l'envoyer
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict;
});

// Générer le token CSRF pour le frontend
[HttpGet("csrf-token")]
public IActionResult GetCsrfToken([FromServices] IAntiforgery antiforgery)
{
    var tokens = antiforgery.GetAndStoreTokens(HttpContext);
    return Ok(new { Token = tokens.RequestToken });
}

// Valider le token CSRF sur les mutations
[HttpPost("data")]
[ValidateAntiForgeryToken]
public IActionResult PostData([FromBody] DataDto dto)
{
    return Ok();
}
*/

// Protection XSS via headers
public class SecurityHeadersMiddleware
{
    private readonly RequestDelegate _next;

    public SecurityHeadersMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        // Empêcher les navigateurs anciens de deviner le content-type
        context.Response.Headers.Append("X-Content-Type-Options", "nosniff");

        // Protection XSS dans les navigateurs anciens
        context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");

        // Empêcher le clickjacking
        context.Response.Headers.Append("X-Frame-Options", "DENY");

        // Content Security Policy (empêche XSS moderne)
        context.Response.Headers.Append("Content-Security-Policy",
            "default-src 'self'; " +
            "script-src 'self' 'unsafe-inline'; " +
            "style-src 'self' 'unsafe-inline'; " +
            "img-src 'self' data: https:; " +
            "font-src 'self'; " +
            "connect-src 'self'");

        // Referrer Policy
        context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");

        // Permissions Policy
        context.Response.Headers.Append("Permissions-Policy",
            "camera=(), microphone=(), geolocation=(), payment=()");

        // Supprimer le header qui révèle la technologie
        context.Response.Headers.Remove("Server");
        context.Response.Headers.Remove("X-Powered-By");

        await _next(context);
    }
}


// ----------------------------------------------------------------------------
// [TEMPS] RATE LIMITING (ASP.NET Core 7+)
// ----------------------------------------------------------------------------

/*
[IDEE] RATE LIMITING = Limiter le nombre de requêtes

POURQUOI :
  - Protection contre les attaques par force brute
  - Protection contre les abus d'API
  - Prévention des attaques DDoS
  - Contrôle des coûts

TYPES DE POLICIES :
  Fixed Window   = N requêtes par fenêtre fixe (ex: 100/minute)
  Sliding Window = N requêtes dans les dernières X secondes
  Token Bucket   = N tokens rechargés à taux fixe
  Concurrency    = N requêtes simultanées maximum

PACKAGE : Intégré dans ASP.NET Core 7+ (Microsoft.AspNetCore.RateLimiting)
*/

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

// Configuration dans Program.cs
/*
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // Policy globale fixe
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: ctx.User.Identity?.Name ?? ctx.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1)
            }));

    // Policy pour les endpoints publics
    options.AddFixedWindowLimiter("public", opt =>
    {
        opt.PermitLimit = 10;
        opt.Window = TimeSpan.FromSeconds(10);
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 2;
    });

    // Policy stricte pour l'auth (anti-brute force)
    options.AddFixedWindowLimiter("auth", opt =>
    {
        opt.PermitLimit = 5;
        opt.Window = TimeSpan.FromMinutes(1);
    });

    // Policy par IP pour l'API
    options.AddPolicy("api", ctx =>
    {
        var ip = ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        return RateLimitPartition.GetSlidingWindowLimiter(ip, _ => new SlidingWindowRateLimiterOptions
        {
            AutoReplenishment = true,
            PermitLimit = 60,
            Window = TimeSpan.FromMinutes(1),
            SegmentsPerWindow = 4
        });
    });
});

// Dans le pipeline :
app.UseRateLimiter();
*/

// Utilisation sur les endpoints
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/[controller]")]
[EnableRateLimiting("api")]   // Policy pour tout le controller
public class RateLimitedController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    // Endpoint avec sa propre policy
    [HttpPost("login")]
    [EnableRateLimiting("auth")]  // Plus stricte sur le login
    public IActionResult Login() => Ok();

    // Endpoint sans rate limiting
    [HttpGet("health")]
    [DisableRateLimiting]
    public IActionResult Health() => Ok("Healthy");

    // Endpoint avec policy publique
    [HttpGet("info")]
    [EnableRateLimiting("public")]
    public IActionResult Info() => Ok();
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE 10 (FINAL PARTIE 4) — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ — API Sécurisée Complète :

Créez une API de gestion de tickets de support avec sécurité complète.

Entités : Ticket (Id, Titre, Description, Priorité, Statut, AuteurId, TechnicienId)

1. Authentification JWT avec Identity (ApplicationUser)

2. Rôles : "Client", "Technicien", "Admin"

3. Endpoints avec contrôles d'accès :
   - GET  /api/tickets          -> Client voit ses tickets, Technicien/Admin voient tous
   - POST /api/tickets          -> Client seulement
   - PUT  /api/tickets/{id}/assigner -> Admin seulement (assigner technicien)
   - PUT  /api/tickets/{id}/resoudre -> Technicien ou Admin
   - DELETE /api/tickets/{id}   -> Admin seulement

4. Rate Limiting : 5 créations de tickets par heure par utilisateur

5. Headers de sécurité sur toutes les réponses

6. CORS configuré pour frontend sur localhost:3000
*/

// ─── CORRIGÉ ────────────────────────────────────────────────────────────────

// Entités
public enum PrioriteTicket { Basse, Normale, Haute, Critique }
public enum StatutTicket { Ouvert, EnCours, Resolu, Ferme }

public class Ticket
{
    public int Id { get; set; }
    public string Titre { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public PrioriteTicket Priorite { get; set; } = PrioriteTicket.Normale;
    public StatutTicket Statut { get; set; } = StatutTicket.Ouvert;
    public DateTime DateCreation { get; set; } = DateTime.UtcNow;
    public DateTime? DateResolution { get; set; }
    public string AuteurId { get; set; } = string.Empty;
    public string? TechnicienId { get; set; }
}

// DTOs
public record CreerTicketDto(string Titre, string Description, PrioriteTicket Priorite);
public record AssignerTicketDto(string TechnicienId);
public record ResoudreTicketDto(string CommentaireResolution);
public record TicketResponse(int Id, string Titre, StatutTicket Statut, PrioriteTicket Priorite,
    string AuteurId, string? TechnicienId, DateTime DateCreation);

// Service Ticket
public class ServiceTicket
{
    private static readonly List<Ticket> _tickets = new();
    private static int _nextId = 1;

    public IEnumerable<Ticket> ObtenirPourUtilisateur(string userId, IList<string> roles)
    {
        if (roles.Contains("Admin") || roles.Contains("Technicien"))
            return _tickets;
        return _tickets.Where(t => t.AuteurId == userId);
    }

    public Ticket? ObtenirParId(int id) => _tickets.FirstOrDefault(t => t.Id == id);

    public Ticket Creer(CreerTicketDto dto, string auteurId)
    {
        var ticket = new Ticket
        {
            Id = _nextId++,
            Titre = dto.Titre,
            Description = dto.Description,
            Priorite = dto.Priorite,
            AuteurId = auteurId
        };
        _tickets.Add(ticket);
        return ticket;
    }

    public bool Assigner(int id, string technicienId)
    {
        var ticket = ObtenirParId(id);
        if (ticket == null) return false;
        ticket.TechnicienId = technicienId;
        ticket.Statut = StatutTicket.EnCours;
        return true;
    }

    public bool Resoudre(int id, string userId, IList<string> roles)
    {
        var ticket = ObtenirParId(id);
        if (ticket == null) return false;
        if (!roles.Contains("Admin") && ticket.TechnicienId != userId) return false;
        ticket.Statut = StatutTicket.Resolu;
        ticket.DateResolution = DateTime.UtcNow;
        return true;
    }

    public bool Supprimer(int id)
    {
        var ticket = ObtenirParId(id);
        if (ticket == null) return false;
        _tickets.Remove(ticket);
        return true;
    }
}

// Controller Tickets
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/tickets")]
[Authorize]
public class TicketsController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly ServiceTicket _service;
    private readonly UserManager<ApplicationUser> _userManager;

    public TicketsController(ServiceTicket service, UserManager<ApplicationUser> um)
    {
        _service = service;
        _userManager = um;
    }

    private string UserId => User.FindFirstValue(JwtRegisteredClaimNames.Sub)!;

    private async Task<IList<string>> GetRoles()
    {
        var user = await _userManager.FindByIdAsync(UserId);
        return user == null ? new List<string>() : await _userManager.GetRolesAsync(user);
    }

    [HttpGet]
    public async Task<IActionResult> ObtenirTous()
    {
        var roles = await GetRoles();
        var tickets = _service.ObtenirPourUtilisateur(UserId, roles);
        return Ok(tickets.Select(t => new TicketResponse(
            t.Id, t.Titre, t.Statut, t.Priorite, t.AuteurId, t.TechnicienId, t.DateCreation)));
    }

    [HttpPost]
    [Authorize(Roles = "Client")]
    [EnableRateLimiting("ticketCreation")]
    public IActionResult Creer([Microsoft.AspNetCore.Mvc.FromBody] CreerTicketDto dto)
    {
        var ticket = _service.Creer(dto, UserId);
        return CreatedAtAction(nameof(ObtenirTous), new { id = ticket.Id },
            new TicketResponse(ticket.Id, ticket.Titre, ticket.Statut, ticket.Priorite,
                ticket.AuteurId, ticket.TechnicienId, ticket.DateCreation));
    }

    [HttpPut("{id}/assigner")]
    [Authorize(Roles = "Admin")]
    public IActionResult Assigner(int id, [Microsoft.AspNetCore.Mvc.FromBody] AssignerTicketDto dto)
    {
        var ok = _service.Assigner(id, dto.TechnicienId);
        return ok ? Ok() : NotFound();
    }

    [HttpPut("{id}/resoudre")]
    [Authorize(Roles = "Technicien,Admin")]
    public async Task<IActionResult> Resoudre(int id)
    {
        var roles = await GetRoles();
        var ok = _service.Resoudre(id, UserId, roles);
        return ok ? Ok() : Forbid();
    }

    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")]
    public IActionResult Supprimer(int id)
    {
        var ok = _service.Supprimer(id);
        return ok ? NoContent() : NotFound();
    }
}


// ============================================================================
// [DOCS] RÉCAPITULATIF PARTIE 4 COMPLÈTE
// ============================================================================

/*
[BRAVO] PARTIE 4 TERMINÉE — AUTHENTIFICATION & SÉCURITÉ

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 11 : ASP.NET Core Identity
[OK] Configuration Identity (UserManager, SignInManager, RoleManager)
[OK] Personnalisation de ApplicationUser
[OK] Inscription, connexion, déconnexion, reset de mot de passe
[OK] Gestion des rôles
[OK] Policies d'autorisation (Requirements + Handlers)
[OK] Autorisation basée sur ressource

Chapitre 12 : JWT Authentication
[OK] Structure et fonctionnement des JWT
[OK] Génération d'access tokens avec claims
[OK] Refresh tokens avec rotation et révocation
[OK] Rafraîchissement automatique des tokens
[OK] Configuration JwtBearer dans ASP.NET Core

Chapitre 13 : OAuth & Social Login
[OK] Flux OAuth 2.0 / OpenID Connect
[OK] Intégration Google Login
[OK] Intégration GitHub Login
[OK] Création/liaison de comptes via OAuth
[OK] Génération de JWT après OAuth

Chapitre 14 : Sécurité Web
[OK] HTTPS & HSTS
[OK] CORS (développement vs production)
[OK] Protection CSRF (antiforgery tokens)
[OK] Protection XSS (CSP, headers)
[OK] Rate Limiting (Fixed Window, Sliding Window, par IP)
[OK] Security Headers (X-Frame-Options, CSP, etc.)

-> PROCHAINE ÉTAPE : Partie 5 - Architecture Professionnelle [CONSTRUCTION]
   (Clean Architecture, CQRS, DDD, Microservices)
*/

// ============================================================================
// [LIVRE] ASP.NET CORE - PARTIE 5 : ARCHITECTURE PROFESSIONNELLE
// ============================================================================
//
// [OBJECTIF] CETTE PARTIE COUVRE :
// - Chapitre 15 : Clean Architecture
// - Chapitre 16 : CQRS avec MediatR
// - Chapitre 17 : Domain-Driven Design (DDD)
// - Chapitre 18 : Modular Monolith
// - Chapitre 19 : Introduction aux Microservices
//
// [TEMPS] TEMPS : ~12-15 heures
// [DOCS] PRÉREQUIS : Parties 1 à 4 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 15 : CLEAN ARCHITECTURE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les principes de la Clean Architecture
[OK] Organiser un projet en couches indépendantes
[OK] Respecter la règle de dépendance
[OK] Mettre en place les interfaces entre couches
[OK] Tester chaque couche indépendamment
*/


// ----------------------------------------------------------------------------
// [REFLEXION] QU'EST-CE QUE LA CLEAN ARCHITECTURE ?
// ----------------------------------------------------------------------------

/*
[IDEE] CLEAN ARCHITECTURE = Organisation du code pour maximiser la maintenabilité

INVENTÉE PAR : Robert C. Martin (Uncle Bob)

PROBLÈME QU'ELLE RÉSOUT :
  [X] Code spaghetti où tout dépend de tout
  [X] Impossible de changer la BDD sans tout réécrire
  [X] Impossible de tester sans démarrer le serveur
  [X] La logique métier mélangée avec le HTTP/SQL

PRINCIPE FONDAMENTAL : LA RÈGLE DE DÉPENDANCE
  Les cercles intérieurs NE CONNAISSENT PAS les cercles extérieurs.
  Les dépendances pointent TOUJOURS vers l'intérieur.

COUCHES (de l'intérieur vers l'extérieur) :

    ┌─────────────────────────────────────────┐
    │  4. INFRASTRUCTURE (EF Core, Email...)  │
    │  ┌──────────────────────────────────┐   │
    │  │  3. PRESENTATION (API, Controllers│   │
    │  │  ┌───────────────────────────┐   │   │
    │  │  │  2. APPLICATION (Use Cases│   │   │
    │  │  │  ┌────────────────────┐  │   │   │
    │  │  │  │  1. DOMAIN         │  │   │   │
    │  │  │  │  (Entities, Rules) │  │   │   │
    │  │  │  └────────────────────┘  │   │   │
    │  │  └───────────────────────────┘   │   │
    │  └──────────────────────────────────┘   │
    └─────────────────────────────────────────┘

STRUCTURE DE SOLUTION :
  MonApp.sln
  ├── src/
  │   ├── MonApp.Domain/          (Pas de dépendances externes)
  │   ├── MonApp.Application/     (Dépend de Domain)
  │   ├── MonApp.Infrastructure/  (Dépend de Application + Domain)
  │   └── MonApp.API/             (Dépend de tous)
  └── tests/
      ├── MonApp.Domain.Tests/
      ├── MonApp.Application.Tests/
      └── MonApp.Integration.Tests/
*/


// ============================================================================
// [DOSSIER] COUCHE 1 : DOMAIN
// ============================================================================

/*
CONTIENT :
  - Entités (Entity)
  - Value Objects
  - Domain Events
  - Interfaces des repositories (IRepository)
  - Exceptions métier
  - Règles métier

NE CONTIENT PAS :
  - EF Core
  - ASP.NET Core
  - NuGet packages (sauf très rarement)
  -> Zéro dépendance externe !
*/

namespace MonApp.Domain.Entities
{
    // ─── ENTITÉ DE BASE ────────────────────────────────────────────────────────
    public abstract class BaseEntity
    {
        public int Id { get; protected set; }
        public DateTime DateCreation { get; private set; } = DateTime.UtcNow;
        public DateTime? DateModification { get; private set; }

        // Domain Events (nous y reviendrons dans DDD)
        private readonly List<IDomainEvent> _domainEvents = new();
        public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

        protected void AjouterEvenement(IDomainEvent evenement) => _domainEvents.Add(evenement);
        public void EffacerEvenements() => _domainEvents.Clear();

        protected void MarquerModifie() => DateModification = DateTime.UtcNow;
    }

    // ─── ENTITÉ PRODUIT (DOMAIN) ───────────────────────────────────────────────
    public class ProduitDomain : BaseEntity
    {
        // Propriétés avec setters privés -> Immutabilité contrôlée
        public string Nom { get; private set; }
        public decimal Prix { get; private set; }
        public int Stock { get; private set; }
        public string Categorie { get; private set; }
        public bool EstActif { get; private set; }

        // Constructeur privé -> Utiliser factory method
        private ProduitDomain() { Nom = ""; Categorie = ""; }

        // ─── FACTORY METHOD ────────────────────────────────────────────────
        public static ProduitDomain Creer(string nom, decimal prix, int stock, string categorie)
        {
            // RÈGLES MÉTIER dans le domain !
            if (string.IsNullOrWhiteSpace(nom))
                throw new DomainException("Le nom du produit est obligatoire.");

            if (prix <= 0)
                throw new DomainException("Le prix doit être positif.");

            if (stock < 0)
                throw new DomainException("Le stock ne peut pas être négatif.");

            var produit = new ProduitDomain
            {
                Nom = nom.Trim(),
                Prix = prix,
                Stock = stock,
                Categorie = categorie,
                EstActif = true
            };

            // Publier un événement de domaine
            produit.AjouterEvenement(new ProduitCreéEvent(produit.Id, nom, prix));
            return produit;
        }

        // ─── COMPORTEMENTS MÉTIER ───────────────────────────────────────────
        public void ModifierPrix(decimal nouveauPrix)
        {
            if (nouveauPrix <= 0)
                throw new DomainException("Le prix doit être positif.");

            var ancienPrix = Prix;
            Prix = nouveauPrix;
            MarquerModifie();
            AjouterEvenement(new PrixModifiéEvent(Id, ancienPrix, nouveauPrix));
        }

        public void AjouterStock(int quantite)
        {
            if (quantite <= 0)
                throw new DomainException("La quantité à ajouter doit être positive.");
            Stock += quantite;
            MarquerModifie();
        }

        public void RetirerDuStock(int quantite)
        {
            if (quantite <= 0)
                throw new DomainException("La quantité à retirer doit être positive.");
            if (Stock < quantite)
                throw new StockInsuffisantException(Nom, quantite, Stock);
            Stock -= quantite;
            MarquerModifie();
        }

        public void Desactiver()
        {
            if (!EstActif) throw new DomainException("Le produit est déjà désactivé.");
            EstActif = false;
            MarquerModifie();
        }
    }
}

namespace MonApp.Domain.Exceptions
{
    // ─── EXCEPTIONS DE DOMAINE ─────────────────────────────────────────────────
    public class DomainException : Exception
    {
        public DomainException(string message) : base(message) { }
    }

    public class StockInsuffisantException : DomainException
    {
        public StockInsuffisantException(string produit, int demande, int disponible)
            : base($"Stock insuffisant pour '{produit}': demandé={demande}, disponible={disponible}.") { }
    }

    public class EntiteIntrouvableException : DomainException
    {
        public EntiteIntrouvableException(string entite, object id)
            : base($"{entite} avec l'identifiant '{id}' est introuvable.") { }
    }
}

namespace MonApp.Domain.Events
{
    // ─── INTERFACES D'ÉVÉNEMENTS ────────────────────────────────────────────────
    public interface IDomainEvent
    {
        DateTime OccurredOn { get; }
        string EventType { get; }
    }

    public abstract class DomainEventBase : IDomainEvent
    {
        public DateTime OccurredOn { get; } = DateTime.UtcNow;
        public abstract string EventType { get; }
    }

    public class ProduitCreéEvent : DomainEventBase
    {
        public int ProduitId { get; }
        public string Nom { get; }
        public decimal Prix { get; }
        public override string EventType => "produit.cree";

        public ProduitCreéEvent(int id, string nom, decimal prix)
        {
            ProduitId = id; Nom = nom; Prix = prix;
        }
    }

    public class PrixModifiéEvent : DomainEventBase
    {
        public int ProduitId { get; }
        public decimal AncienPrix { get; }
        public decimal NouveauPrix { get; }
        public override string EventType => "produit.prix_modifie";

        public PrixModifiéEvent(int id, decimal ancien, decimal nouveau)
        {
            ProduitId = id; AncienPrix = ancien; NouveauPrix = nouveau;
        }
    }
}

namespace MonApp.Domain.Interfaces
{
    // ─── INTERFACES REPOSITORIES (DOMAIN, pas Infrastructure!) ─────────────────
    public interface IRepository<T> where T : MonApp.Domain.Entities.BaseEntity
    {
        Task<T?> ObtenirParIdAsync(int id, CancellationToken ct = default);
        Task<IEnumerable<T>> ObtenirTousAsync(CancellationToken ct = default);
        Task AjouterAsync(T entite, CancellationToken ct = default);
        void Modifier(T entite);
        void Supprimer(T entite);
    }

    public interface IProduitRepository : IRepository<MonApp.Domain.Entities.ProduitDomain>
    {
        Task<IEnumerable<MonApp.Domain.Entities.ProduitDomain>> ObtenirParCategorieAsync(string categorie, CancellationToken ct = default);
        Task<bool> NomExisteAsync(string nom, CancellationToken ct = default);
    }

    public interface IUnitOfWork
    {
        IProduitRepository Produits { get; }
        Task<int> SauvegarderAsync(CancellationToken ct = default);
    }
}

// Aliases pour raccourcir dans les exemples suivants
using DomainException = MonApp.Domain.Exceptions.DomainException;
using EntiteIntrouvableException = MonApp.Domain.Exceptions.EntiteIntrouvableException;
using ProduitDomain = MonApp.Domain.Entities.ProduitDomain;
using IProduitRepository = MonApp.Domain.Interfaces.IProduitRepository;
using IDomainEvent = MonApp.Domain.Events.IDomainEvent;


// ============================================================================
// [DOSSIER] COUCHE 2 : APPLICATION
// ============================================================================

/*
CONTIENT :
  - Use Cases (commandes et queries)
  - DTOs (en entrée et sortie)
  - Interfaces des services externes (IEmailService, IStorageService)
  - Mappers (domain -> DTO)
  - Validators (règles d'entrée)
  - Event Handlers

NE CONTIENT PAS :
  - EF Core, SQL
  - ASP.NET Core (HttpContext, etc.)
  - Implémentations de services externes

DÉPEND DE : Domain uniquement
*/

namespace MonApp.Application.Produits.Commands
{
    // ─── COMMANDE : CRÉER PRODUIT ───────────────────────────────────────────────

    // Requête (entrée)
    public record CreerProduitCommand(string Nom, decimal Prix, int Stock, string Categorie);

    // Réponse (sortie)
    public record CreerProduitResult(int Id, string Nom, decimal Prix, int Stock, bool Succes);

    // Handler (use case)
    public class CreerProduitHandler
    {
        private readonly MonApp.Domain.Interfaces.IUnitOfWork _uow;
        private readonly ILogger<CreerProduitHandler> _logger;

        public CreerProduitHandler(MonApp.Domain.Interfaces.IUnitOfWork uow, ILogger<CreerProduitHandler> logger)
        {
            _uow = uow;
            _logger = logger;
        }

        public async Task<CreerProduitResult> HandleAsync(CreerProduitCommand command, CancellationToken ct = default)
        {
            // 1. Vérifier unicité
            if (await _uow.Produits.NomExisteAsync(command.Nom, ct))
                throw new DomainException($"Un produit avec le nom '{command.Nom}' existe déjà.");

            // 2. Créer l'entité via factory (règles dans le domain)
            var produit = ProduitDomain.Creer(command.Nom, command.Prix, command.Stock, command.Categorie);

            // 3. Persister
            await _uow.Produits.AjouterAsync(produit, ct);
            await _uow.SauvegarderAsync(ct);

            _logger.LogInformation("Produit créé: {Nom} (Id: {Id})", produit.Nom, produit.Id);

            // 4. Retourner DTO (pas l'entité domain !)
            return new CreerProduitResult(produit.Id, produit.Nom, produit.Prix, produit.Stock, true);
        }
    }

    // Commande : Modifier prix
    public record ModifierPrixCommand(int ProduitId, decimal NouveauPrix);

    public class ModifierPrixHandler
    {
        private readonly MonApp.Domain.Interfaces.IUnitOfWork _uow;

        public ModifierPrixHandler(MonApp.Domain.Interfaces.IUnitOfWork uow) => _uow = uow;

        public async Task HandleAsync(ModifierPrixCommand command, CancellationToken ct = default)
        {
            var produit = await _uow.Produits.ObtenirParIdAsync(command.ProduitId, ct)
                ?? throw new EntiteIntrouvableException("Produit", command.ProduitId);

            // Logique dans le domain
            produit.ModifierPrix(command.NouveauPrix);

            _uow.Produits.Modifier(produit);
            await _uow.SauvegarderAsync(ct);

            // Publier les domain events
            foreach (var evt in produit.DomainEvents)
                await PublierEvenementAsync(evt, ct);
            produit.EffacerEvenements();
        }

        private static Task PublierEvenementAsync(IDomainEvent evt, CancellationToken ct)
        {
            // En vrai : MediatR, MessageBus, etc.
            Console.WriteLine($"Event publié: {evt.EventType}");
            return Task.CompletedTask;
        }
    }
}

namespace MonApp.Application.Produits.Queries
{
    // ─── QUERY : OBTENIR PRODUIT ────────────────────────────────────────────────

    public record ObtenirProduitParIdQuery(int Id);

    public record ProduitDto(int Id, string Nom, decimal Prix, int Stock, string Categorie, bool EstActif);

    public class ObtenirProduitHandler
    {
        private readonly IProduitRepository _repo;

        public ObtenirProduitHandler(IProduitRepository repo) => _repo = repo;

        public async Task<ProduitDto?> HandleAsync(ObtenirProduitParIdQuery query, CancellationToken ct = default)
        {
            var produit = await _repo.ObtenirParIdAsync(query.Id, ct);
            if (produit == null) return null;

            // Mapping domain -> DTO (ici manuel, en vrai : AutoMapper)
            return new ProduitDto(produit.Id, produit.Nom, produit.Prix, produit.Stock, produit.Categorie, produit.EstActif);
        }
    }

    public record ObtenirProduitsQuery(string? Categorie = null, int Page = 1, int Taille = 20);
    public record ProduitsPageDto(IEnumerable<ProduitDto> Items, int Total);
}

namespace MonApp.Application.Interfaces
{
    // ─── INTERFACES SERVICES EXTERNES ──────────────────────────────────────────
    public interface IEmailService
    {
        Task EnvoyerAsync(string destinataire, string sujet, string corps, CancellationToken ct = default);
    }

    public interface IStorageService
    {
        Task<string> UploadAsync(Stream fichier, string nomFichier, string contentType, CancellationToken ct = default);
        Task<Stream?> DownloadAsync(string chemin, CancellationToken ct = default);
        Task SupprimerAsync(string chemin, CancellationToken ct = default);
    }

    public interface ICacheService
    {
        Task<T?> ObtenirAsync<T>(string cle, CancellationToken ct = default);
        Task DefinirAsync<T>(string cle, T valeur, TimeSpan? expiration = null, CancellationToken ct = default);
        Task SupprimerAsync(string cle, CancellationToken ct = default);
    }
}


// ============================================================================
// [DOSSIER] COUCHE 3 : INFRASTRUCTURE
// ============================================================================

/*
CONTIENT :
  - Implémentations EF Core (DbContext, Repositories)
  - Implémentations des services externes (EmailService, StorageService)
  - Migrations
  - Configurations Fluent API

DÉPEND DE : Application + Domain
*/

namespace MonApp.Infrastructure.Persistence
{
    // ─── DBCONTEXT ──────────────────────────────────────────────────────────────
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

        public DbSet<ProduitDomain> Produits { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Configuration de l'entité domain
            modelBuilder.Entity<ProduitDomain>(entity =>
            {
                entity.ToTable("produits");
                entity.HasKey(p => p.Id);

                // Mapper les propriétés privées
                entity.Property(p => p.Nom).IsRequired().HasMaxLength(200);
                entity.Property(p => p.Prix).HasColumnType("decimal(10,2)");
                entity.Property(p => p.Stock);
                entity.Property(p => p.Categorie).HasMaxLength(100);
                entity.Property(p => p.EstActif);
                entity.Property(p => p.DateCreation);
                entity.Property(p => p.DateModification);

                entity.HasIndex(p => p.Nom).IsUnique();

                // Ignorer les Domain Events (pas en BDD)
                entity.Ignore(p => p.DomainEvents);
            });
        }
    }

    // ─── REPOSITORY IMPLÉMENTATION ──────────────────────────────────────────────
    public class ProduitRepository : IProduitRepository
    {
        private readonly AppDbContext _ctx;
        public ProduitRepository(AppDbContext ctx) => _ctx = ctx;

        public async Task<ProduitDomain?> ObtenirParIdAsync(int id, CancellationToken ct = default)
            => await _ctx.Produits.FindAsync(new object[] { id }, ct);

        public async Task<IEnumerable<ProduitDomain>> ObtenirTousAsync(CancellationToken ct = default)
            => await _ctx.Produits.AsNoTracking().ToListAsync(ct);

        public async Task<IEnumerable<ProduitDomain>> ObtenirParCategorieAsync(string categorie, CancellationToken ct = default)
            => await _ctx.Produits.AsNoTracking()
                .Where(p => p.Categorie == categorie).ToListAsync(ct);

        public async Task<bool> NomExisteAsync(string nom, CancellationToken ct = default)
            => await _ctx.Produits.AnyAsync(p => p.Nom == nom, ct);

        public async Task AjouterAsync(ProduitDomain entite, CancellationToken ct = default)
            => await _ctx.Produits.AddAsync(entite, ct);

        public void Modifier(ProduitDomain entite)
            => _ctx.Entry(entite).State = EntityState.Modified;

        public void Supprimer(ProduitDomain entite)
            => _ctx.Produits.Remove(entite);
    }

    // ─── UNIT OF WORK IMPLÉMENTATION ────────────────────────────────────────────
    public class UnitOfWorkImpl : MonApp.Domain.Interfaces.IUnitOfWork
    {
        private readonly AppDbContext _ctx;
        private IProduitRepository? _produits;

        public UnitOfWorkImpl(AppDbContext ctx) => _ctx = ctx;

        public IProduitRepository Produits
            => _produits ??= new ProduitRepository(_ctx);

        public async Task<int> SauvegarderAsync(CancellationToken ct = default)
            => await _ctx.SaveChangesAsync(ct);
    }
}

namespace MonApp.Infrastructure.Services
{
    // ─── SERVICE EMAIL (IMPLÉMENTATION) ────────────────────────────────────────
    public class EmailServiceSmtp : MonApp.Application.Interfaces.IEmailService
    {
        private readonly ILogger<EmailServiceSmtp> _logger;

        public EmailServiceSmtp(ILogger<EmailServiceSmtp> logger) => _logger = logger;

        public async Task EnvoyerAsync(string destinataire, string sujet, string corps, CancellationToken ct = default)
        {
            _logger.LogInformation("Email envoyé à {Destinataire}: {Sujet}", destinataire, sujet);
            // Vraie implémentation avec SmtpClient ou SendGrid...
            await Task.CompletedTask;
        }
    }
}


// ============================================================================
// [DOSSIER] COUCHE 4 : PRÉSENTATION (API)
// ============================================================================

/*
CONTIENT :
  - Controllers
  - Middleware
  - Program.cs
  - Configuration DI

DÉPEND DE : Application (inject les handlers)
NE DÉPEND PAS DIRECTEMENT DE : Domain, Infrastructure (via DI)
*/

namespace MonApp.API.Controllers
{
    [Microsoft.AspNetCore.Mvc.ApiController]
    [Microsoft.AspNetCore.Mvc.Route("api/[controller]")]
    public class ProduitsCleanController : Microsoft.AspNetCore.Mvc.ControllerBase
    {
        private readonly MonApp.Application.Produits.Commands.CreerProduitHandler _creerHandler;
        private readonly MonApp.Application.Produits.Queries.ObtenirProduitHandler _obtenirHandler;
        private readonly MonApp.Application.Produits.Commands.ModifierPrixHandler _modifierPrixHandler;

        public ProduitsCleanController(
            MonApp.Application.Produits.Commands.CreerProduitHandler creerHandler,
            MonApp.Application.Produits.Queries.ObtenirProduitHandler obtenirHandler,
            MonApp.Application.Produits.Commands.ModifierPrixHandler modifierPrixHandler)
        {
            _creerHandler = creerHandler;
            _obtenirHandler = obtenirHandler;
            _modifierPrixHandler = modifierPrixHandler;
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> Get(int id, CancellationToken ct)
        {
            var produit = await _obtenirHandler.HandleAsync(
                new MonApp.Application.Produits.Queries.ObtenirProduitParIdQuery(id), ct);

            return produit is null ? NotFound() : Ok(produit);
        }

        [HttpPost]
        public async Task<IActionResult> Post(
            [Microsoft.AspNetCore.Mvc.FromBody] MonApp.Application.Produits.Commands.CreerProduitCommand command,
            CancellationToken ct)
        {
            var result = await _creerHandler.HandleAsync(command, ct);
            return CreatedAtAction(nameof(Get), new { id = result.Id }, result);
        }

        [HttpPut("{id}/prix")]
        public async Task<IActionResult> ModifierPrix(int id,
            [Microsoft.AspNetCore.Mvc.FromBody] decimal nouveauPrix, CancellationToken ct)
        {
            await _modifierPrixHandler.HandleAsync(
                new MonApp.Application.Produits.Commands.ModifierPrixCommand(id, nouveauPrix), ct);
            return Ok();
        }
    }
}

// Enregistrement DI dans Program.cs
/*
// Infrastructure
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddScoped<IProduitRepository, ProduitRepository>();
builder.Services.AddScoped<MonApp.Domain.Interfaces.IUnitOfWork, UnitOfWorkImpl>();
builder.Services.AddScoped<MonApp.Application.Interfaces.IEmailService, EmailServiceSmtp>();

// Application Handlers
builder.Services.AddScoped<CreerProduitHandler>();
builder.Services.AddScoped<ObtenirProduitHandler>();
builder.Services.AddScoped<ModifierPrixHandler>();
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE 11 — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ — Système de Gestion de Cours (Clean Architecture) :

Structurez un système de cours en ligne en Clean Architecture.

1. Domain : Entité "Cours" avec :
   - Propriétés : Id, Titre, Description, Prix, NombrePlaces, PlacesRestantes
   - Méthode Inscrire(etudiantId) -> vérifie les places, lance DomainEvent
   - DomainEvent : "EtudiantInscritEvent"
   - Interface : ICoursRepository

2. Application :
   - Command : InscrireEtudiantCommand(CoursId, EtudiantId) + Handler
   - Query   : ObtenirCoursQuery(Id) + Handler + CoursDto

3. Infrastructure :
   - CoursRepository (en mémoire pour simplifier)

4. API :
   - CoursController avec GET /{id} et POST /{id}/inscrire
*/

// ─── CORRIGÉ ────────────────────────────────────────────────────────────────

// --- DOMAIN ---
namespace Exercice11.Domain
{
    public abstract class Entity { public int Id { get; protected set; } }

    public interface IDomainEvt { DateTime Horodatage { get; } }

    public class EtudiantInscritEvent : IDomainEvt
    {
        public DateTime Horodatage { get; } = DateTime.UtcNow;
        public int CoursId { get; }
        public string EtudiantId { get; }
        public EtudiantInscritEvent(int coursId, string etudiantId)
        { CoursId = coursId; EtudiantId = etudiantId; }
    }

    public class CoursEntity : Entity
    {
        public string Titre { get; private set; }
        public decimal Prix { get; private set; }
        public int NombrePlaces { get; private set; }
        public int PlacesRestantes { get; private set; }

        private readonly List<IDomainEvt> _events = new();
        public IReadOnlyList<IDomainEvt> Events => _events;

        private CoursEntity() { Titre = ""; }

        public static CoursEntity Creer(string titre, decimal prix, int places)
        {
            if (string.IsNullOrWhiteSpace(titre)) throw new Exception("Titre requis.");
            if (prix < 0) throw new Exception("Prix invalide.");
            if (places <= 0) throw new Exception("Nombre de places invalide.");

            return new CoursEntity { Titre = titre, Prix = prix,
                NombrePlaces = places, PlacesRestantes = places };
        }

        public void Inscrire(string etudiantId)
        {
            if (PlacesRestantes <= 0) throw new Exception("Plus de places disponibles.");
            PlacesRestantes--;
            _events.Add(new EtudiantInscritEvent(Id, etudiantId));
        }
    }

    public interface ICoursRepository
    {
        Task<CoursEntity?> ObtenirAsync(int id, CancellationToken ct = default);
        Task AjouterAsync(CoursEntity cours, CancellationToken ct = default);
        void Modifier(CoursEntity cours);
        Task SauvegarderAsync(CancellationToken ct = default);
    }
}

// --- APPLICATION ---
namespace Exercice11.Application
{
    using Exercice11.Domain;

    public record InscrireEtudiantCommand(int CoursId, string EtudiantId);
    public record CoursDto(int Id, string Titre, decimal Prix, int PlacesRestantes);

    public class InscrireEtudiantHandler
    {
        private readonly ICoursRepository _repo;
        public InscrireEtudiantHandler(ICoursRepository repo) => _repo = repo;

        public async Task HandleAsync(InscrireEtudiantCommand cmd, CancellationToken ct = default)
        {
            var cours = await _repo.ObtenirAsync(cmd.CoursId, ct)
                ?? throw new Exception($"Cours {cmd.CoursId} introuvable.");
            cours.Inscrire(cmd.EtudiantId);
            _repo.Modifier(cours);
            await _repo.SauvegarderAsync(ct);
        }
    }

    public class ObtenirCoursHandler
    {
        private readonly ICoursRepository _repo;
        public ObtenirCoursHandler(ICoursRepository repo) => _repo = repo;

        public async Task<CoursDto?> HandleAsync(int id, CancellationToken ct = default)
        {
            var c = await _repo.ObtenirAsync(id, ct);
            return c == null ? null : new CoursDto(c.Id, c.Titre, c.Prix, c.PlacesRestantes);
        }
    }
}

// --- INFRASTRUCTURE ---
namespace Exercice11.Infrastructure
{
    using Exercice11.Domain;

    public class CoursRepositoryMemoire : ICoursRepository
    {
        private readonly List<CoursEntity> _cours = new();
        private int _nextId = 1;

        public CoursRepositoryMemoire()
        {
            var c1 = CoursEntity.Creer("ASP.NET Core", 49.99m, 30);
            c1.GetType().GetProperty("Id")!.SetValue(c1, _nextId++);
            _cours.Add(c1);
        }

        public Task<CoursEntity?> ObtenirAsync(int id, CancellationToken ct = default)
            => Task.FromResult(_cours.FirstOrDefault(c => c.Id == id));

        public Task AjouterAsync(CoursEntity cours, CancellationToken ct = default)
        {
            _cours.Add(cours); return Task.CompletedTask;
        }

        public void Modifier(CoursEntity cours) { /* already in-memory */ }
        public Task SauvegarderAsync(CancellationToken ct = default) => Task.CompletedTask;
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 16 : CQRS AVEC MEDIATR
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le pattern CQRS
[OK] Installer et configurer MediatR
[OK] Créer des Commands et Queries
[OK] Utiliser les Behaviors (Pipeline)
[OK] Gérer les notifications (Events)
*/


// ----------------------------------------------------------------------------
// [REFLEXION] QU'EST-CE QUE CQRS ?
// ----------------------------------------------------------------------------

/*
CQRS = Command Query Responsibility Segregation

PRINCIPE : Séparer les opérations d'ÉCRITURE (Commands) des LECTURES (Queries)

AVANT CQRS :
  IServiceProduit :
    GetById() / GetAll() / Create() / Update() / Delete()
    -> Un seul objet fait tout -> Couplage

AVEC CQRS :
  Commands (écriture) :  CreerProduitCommand / ModifierPrixCommand / SupprimerProduitCommand
  Queries (lecture) :    ObtenirProduitQuery / ObtenirProduitsQuery / RechercherProduitsQuery

AVANTAGES :
[OK] Séparation des responsabilités
[OK] Optimisation indépendante lecture/écriture
[OK] Scalabilité (BDD lecture séparée de BDD écriture)
[OK] Historizartion des commandes (Event Sourcing)

MEDIATR = Bibliothèque qui implémente le pattern Mediator
  -> Les Controllers ne connaissent pas les handlers
  -> Tout passe par IMediator (médiateur central)

INSTALLATION :
  dotnet add package MediatR
  dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
*/


// ----------------------------------------------------------------------------
// [CONFIG] CONFIGURATION MEDIATR
// ----------------------------------------------------------------------------

using MediatR;

// Dans Program.cs :
/*
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
    // ou : cfg.RegisterServicesFromAssemblyContaining<CreerProduitCommand>()
*/


// ----------------------------------------------------------------------------
// [MESSAGE] COMMANDS AVEC MEDIATR
// ----------------------------------------------------------------------------

// ─── COMMAND (REQUÊTE D'ÉCRITURE) ───────────────────────────────────────────

// IRequest<T> = Command qui retourne T
// IRequest    = Command qui retourne Unit (void)
public record CreerProduitCommandMediatR : IRequest<ProduitCreatedResult>
{
    public string Nom { get; init; } = string.Empty;
    public decimal Prix { get; init; }
    public int Stock { get; init; }
    public string Categorie { get; init; } = string.Empty;
}

public record ProduitCreatedResult(int Id, string Nom, decimal Prix);

// Handler de la command
public class CreerProduitCommandHandler : IRequestHandler<CreerProduitCommandMediatR, ProduitCreatedResult>
{
    private readonly MonApp.Domain.Interfaces.IUnitOfWork _uow;
    private readonly ILogger<CreerProduitCommandHandler> _logger;

    public CreerProduitCommandHandler(MonApp.Domain.Interfaces.IUnitOfWork uow,
        ILogger<CreerProduitCommandHandler> logger)
    {
        _uow = uow;
        _logger = logger;
    }

    public async Task<ProduitCreatedResult> Handle(
        CreerProduitCommandMediatR request, CancellationToken cancellationToken)
    {
        var produit = ProduitDomain.Creer(request.Nom, request.Prix, request.Stock, request.Categorie);

        await _uow.Produits.AjouterAsync(produit, cancellationToken);
        await _uow.SauvegarderAsync(cancellationToken);

        _logger.LogInformation("Produit créé via MediatR: {Nom}", produit.Nom);
        return new ProduitCreatedResult(produit.Id, produit.Nom, produit.Prix);
    }
}

// Command sans retour
public record SupprimerProduitCommand(int Id) : IRequest;

public class SupprimerProduitHandler : IRequestHandler<SupprimerProduitCommand>
{
    private readonly MonApp.Domain.Interfaces.IUnitOfWork _uow;

    public SupprimerProduitHandler(MonApp.Domain.Interfaces.IUnitOfWork uow) => _uow = uow;

    public async Task Handle(SupprimerProduitCommand request, CancellationToken cancellationToken)
    {
        var produit = await _uow.Produits.ObtenirParIdAsync(request.Id, cancellationToken)
            ?? throw new EntiteIntrouvableException("Produit", request.Id);

        _uow.Produits.Supprimer(produit);
        await _uow.SauvegarderAsync(cancellationToken);
    }
}


// ----------------------------------------------------------------------------
// [RECHERCHE] QUERIES AVEC MEDIATR
// ----------------------------------------------------------------------------

// ─── QUERY (REQUÊTE DE LECTURE) ─────────────────────────────────────────────

public record ObtenirProduitQuery(int Id) : IRequest<ProduitDetailDto?>;

public record ProduitDetailDto(int Id, string Nom, decimal Prix, int Stock, string Categorie, bool EstActif);

public class ObtenirProduitQueryHandler : IRequestHandler<ObtenirProduitQuery, ProduitDetailDto?>
{
    private readonly IProduitRepository _repo;

    public ObtenirProduitQueryHandler(IProduitRepository repo) => _repo = repo;

    public async Task<ProduitDetailDto?> Handle(ObtenirProduitQuery request, CancellationToken cancellationToken)
    {
        var produit = await _repo.ObtenirParIdAsync(request.Id, cancellationToken);
        return produit == null ? null :
            new ProduitDetailDto(produit.Id, produit.Nom, produit.Prix, produit.Stock, produit.Categorie, produit.EstActif);
    }
}

public record ObtenirProduitsQuery(string? Categorie, int Page, int PageSize) : IRequest<PagedProduitsDto>;

public record PagedProduitsDto(IEnumerable<ProduitDetailDto> Items, int Total, int Page, int TotalPages);

public class ObtenirProduitsQueryHandler : IRequestHandler<ObtenirProduitsQuery, PagedProduitsDto>
{
    private readonly IProduitRepository _repo;

    public ObtenirProduitsQueryHandler(IProduitRepository repo) => _repo = repo;

    public async Task<PagedProduitsDto> Handle(ObtenirProduitsQuery request, CancellationToken cancellationToken)
    {
        var tous = await _repo.ObtenirTousAsync(cancellationToken);
        var filtrés = string.IsNullOrEmpty(request.Categorie)
            ? tous
            : tous.Where(p => p.Categorie == request.Categorie);

        var total = filtrés.Count();
        var items = filtrés
            .Skip((request.Page - 1) * request.PageSize)
            .Take(request.PageSize)
            .Select(p => new ProduitDetailDto(p.Id, p.Nom, p.Prix, p.Stock, p.Categorie, p.EstActif));

        return new PagedProduitsDto(items, total, request.Page, (int)Math.Ceiling((double)total / request.PageSize));
    }
}


// ----------------------------------------------------------------------------
// [LIEN] PIPELINE BEHAVIORS (CROSS-CUTTING CONCERNS)
// ----------------------------------------------------------------------------

/*
[IDEE] BEHAVIORS = Middleware du pipeline MediatR

COMME le middleware ASP.NET Core, mais pour les commands/queries.
S'exécute avant ET après chaque handler.

USAGES TYPIQUES :
  - Logging automatique
  - Validation automatique (FluentValidation)
  - Gestion des exceptions
  - Caching
  - Performance monitoring
*/

// ─── BEHAVIOR DE LOGGING ────────────────────────────────────────────────────
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
        => _logger = logger;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var nomRequete = typeof(TRequest).Name;
        _logger.LogInformation("[BLACK_RIGHT-POINTING_TRIANGLE] Début {Requete}: {@Data}", nomRequete, request);

        var chrono = System.Diagnostics.Stopwatch.StartNew();
        try
        {
            var response = await next();
            chrono.Stop();
            _logger.LogInformation("[BLACK_LEFT-POINTING_TRIANGLE] Fin {Requete} ({Duree}ms)", nomRequete, chrono.ElapsedMilliseconds);
            return response;
        }
        catch (Exception ex)
        {
            chrono.Stop();
            _logger.LogError(ex, "[X] Erreur {Requete} ({Duree}ms)", nomRequete, chrono.ElapsedMilliseconds);
            throw;
        }
    }
}

// ─── BEHAVIOR DE VALIDATION ─────────────────────────────────────────────────
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<AbstractValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<AbstractValidator<TRequest>> validators)
        => _validators = validators;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any()) return await next();

        var context = new ValidationContext<TRequest>(request);
        var failures = _validators
            .Select(v => v.Validate(context))
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Any())
            throw new FluentValidation.ValidationException(failures);

        return await next();
    }
}

// ─── BEHAVIOR DE PERFORMANCE ────────────────────────────────────────────────
public class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _logger;

    public PerformanceBehavior(ILogger<PerformanceBehavior<TRequest, TResponse>> logger)
        => _logger = logger;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var chrono = System.Diagnostics.Stopwatch.StartNew();
        var response = await next();
        chrono.Stop();

        if (chrono.ElapsedMilliseconds > 500)
        {
            _logger.LogWarning("[ATTENTION] Requête lente: {Requete} ({Duree}ms) {@Data}",
                typeof(TRequest).Name, chrono.ElapsedMilliseconds, request);
        }

        return response;
    }
}

// Enregistrement behaviors dans Program.cs :
/*
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PerformanceBehavior<,>));
*/


// ----------------------------------------------------------------------------
// [ANNONCE] NOTIFICATIONS (EVENTS AVEC MEDIATR)
// ----------------------------------------------------------------------------

/*
INotification = Événement publié à plusieurs handlers
Un événement -> N handlers (fan-out)
*/

// Notification
public class ProduitCreeNotification : INotification
{
    public int ProduitId { get; }
    public string Nom { get; }
    public decimal Prix { get; }
    public ProduitCreeNotification(int id, string nom, decimal prix) { ProduitId = id; Nom = nom; Prix = prix; }
}

// Handler 1 : Envoyer email
public class EnvoyerEmailACreationHandler : INotificationHandler<ProduitCreeNotification>
{
    private readonly MonApp.Application.Interfaces.IEmailService _email;
    private readonly ILogger<EnvoyerEmailACreationHandler> _logger;

    public EnvoyerEmailACreationHandler(MonApp.Application.Interfaces.IEmailService email,
        ILogger<EnvoyerEmailACreationHandler> logger)
    {
        _email = email; _logger = logger;
    }

    public async Task Handle(ProduitCreeNotification notification, CancellationToken cancellationToken)
    {
        await _email.EnvoyerAsync("admin@shop.com", "Nouveau produit",
            $"Produit '{notification.Nom}' créé à {notification.Prix}€", cancellationToken);
        _logger.LogInformation("Email envoyé pour produit {Id}", notification.ProduitId);
    }
}

// Handler 2 : Logger audit
public class AuditProduitCreationHandler : INotificationHandler<ProduitCreeNotification>
{
    private readonly ILogger<AuditProduitCreationHandler> _logger;

    public AuditProduitCreationHandler(ILogger<AuditProduitCreationHandler> logger)
        => _logger = logger;

    public Task Handle(ProduitCreeNotification notification, CancellationToken cancellationToken)
    {
        _logger.LogInformation("AUDIT: Produit {Id} ({Nom}) créé à {Prix}€",
            notification.ProduitId, notification.Nom, notification.Prix);
        return Task.CompletedTask;
    }
}

// Controller utilisant MediatR
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/produits-cqrs")]
public class ProduitsCqrsController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly IMediator _mediator;

    public ProduitsCqrsController(IMediator mediator) => _mediator = mediator;

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id, CancellationToken ct)
    {
        var result = await _mediator.Send(new ObtenirProduitQuery(id), ct);
        return result is null ? NotFound() : Ok(result);
    }

    [HttpGet]
    public async Task<IActionResult> GetAll(
        [Microsoft.AspNetCore.Mvc.FromQuery] string? categorie,
        [Microsoft.AspNetCore.Mvc.FromQuery] int page = 1,
        [Microsoft.AspNetCore.Mvc.FromQuery] int taille = 20,
        CancellationToken ct = default)
    {
        var result = await _mediator.Send(new ObtenirProduitsQuery(categorie, page, taille), ct);
        return Ok(result);
    }

    [HttpPost]
    public async Task<IActionResult> Post(
        [Microsoft.AspNetCore.Mvc.FromBody] CreerProduitCommandMediatR command, CancellationToken ct)
    {
        var result = await _mediator.Send(command, ct);
        return CreatedAtAction(nameof(Get), new { id = result.Id }, result);
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id, CancellationToken ct)
    {
        await _mediator.Send(new SupprimerProduitCommand(id), ct);
        return NoContent();
    }
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE 12 — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ — CQRS avec MediatR pour une API de tâches (Todo) :

1. Commands :
   - CreerTacheCommand(Titre, Description, Priorite) -> TacheCreeeResult
   - CompleterTacheCommand(Id)
   - SupprimerTacheCommand(Id)

2. Queries :
   - ObtenirTachesQuery(filtre: Toutes|EnCours|Completees) -> List<TacheDto>
   - ObtenirTacheQuery(Id) -> TacheDto?

3. Validation Behavior :
   - CreerTacheCommand : Titre non vide, min 3 chars
   - CompleterTacheCommand : Id > 0

4. Notification :
   - TacheCompleteeNotification
   - Handler qui log l'heure de complétion
*/

// ─── CORRIGÉ ────────────────────────────────────────────────────────────────

// Modèle
public class TacheTodo
{
    public int Id { get; set; }
    public string Titre { get; set; } = string.Empty;
    public string? Description { get; set; }
    public int Priorite { get; set; } = 1;
    public bool EstComplete { get; set; }
    public DateTime DateCreation { get; set; } = DateTime.UtcNow;
    public DateTime? DateCompletion { get; set; }
}

// Stockage en mémoire
public static class TacheStore
{
    public static readonly List<TacheTodo> Taches = new()
    {
        new() { Id = 1, Titre = "Apprendre CQRS", Priorite = 2, EstComplete = false }
    };
    public static int NextId = 2;
}

// DTOs
public record TacheDto(int Id, string Titre, string? Description, int Priorite, bool EstComplete, DateTime DateCreation);
public record TacheCreeeResult(int Id, string Titre);

// Commands
public record CreerTacheCommand(string Titre, string? Description, int Priorite) : IRequest<TacheCreeeResult>;
public record CompleterTacheCommand(int Id) : IRequest;
public record SupprimerTacheCommand(int Id) : IRequest;

// Validators
public class CreerTacheValidator : AbstractValidator<CreerTacheCommand>
{
    public CreerTacheValidator()
    {
        RuleFor(c => c.Titre).NotEmpty().MinimumLength(3).MaximumLength(200);
        RuleFor(c => c.Priorite).InclusiveBetween(1, 5);
    }
}

// Queries
public record ObtenirTachesQuery(string Filtre = "Toutes") : IRequest<List<TacheDto>>;
public record ObtenirTacheQuery(int Id) : IRequest<TacheDto?>;

// Notification
public class TacheCompleteeNotification : INotification
{
    public int TacheId { get; init; }
    public string Titre { get; init; } = string.Empty;
    public DateTime HeureCompletion { get; init; } = DateTime.UtcNow;
}

// Handlers
public class CreerTacheHandler : IRequestHandler<CreerTacheCommand, TacheCreeeResult>
{
    public Task<TacheCreeeResult> Handle(CreerTacheCommand req, CancellationToken ct)
    {
        var tache = new TacheTodo
        {
            Id = TacheStore.NextId++,
            Titre = req.Titre,
            Description = req.Description,
            Priorite = req.Priorite
        };
        TacheStore.Taches.Add(tache);
        return Task.FromResult(new TacheCreeeResult(tache.Id, tache.Titre));
    }
}

public class CompleterTacheHandler : IRequestHandler<CompleterTacheCommand>
{
    private readonly IMediator _mediator;
    public CompleterTacheHandler(IMediator mediator) => _mediator = mediator;

    public async Task Handle(CompleterTacheCommand req, CancellationToken ct)
    {
        var tache = TacheStore.Taches.FirstOrDefault(t => t.Id == req.Id)
            ?? throw new Exception($"Tâche {req.Id} introuvable.");
        tache.EstComplete = true;
        tache.DateCompletion = DateTime.UtcNow;
        await _mediator.Publish(new TacheCompleteeNotification { TacheId = tache.Id, Titre = tache.Titre }, ct);
    }
}

public class SupprimerTacheHandler : IRequestHandler<SupprimerTacheCommand>
{
    public Task Handle(SupprimerTacheCommand req, CancellationToken ct)
    {
        var tache = TacheStore.Taches.FirstOrDefault(t => t.Id == req.Id)
            ?? throw new Exception($"Tâche {req.Id} introuvable.");
        TacheStore.Taches.Remove(tache);
        return Task.CompletedTask;
    }
}

public class ObtenirTachesHandler : IRequestHandler<ObtenirTachesQuery, List<TacheDto>>
{
    public Task<List<TacheDto>> Handle(ObtenirTachesQuery req, CancellationToken ct)
    {
        var taches = req.Filtre switch
        {
            "EnCours" => TacheStore.Taches.Where(t => !t.EstComplete),
            "Completees" => TacheStore.Taches.Where(t => t.EstComplete),
            _ => TacheStore.Taches.AsEnumerable()
        };
        return Task.FromResult(taches
            .Select(t => new TacheDto(t.Id, t.Titre, t.Description, t.Priorite, t.EstComplete, t.DateCreation))
            .ToList());
    }
}

public class ObtenirTacheHandler : IRequestHandler<ObtenirTacheQuery, TacheDto?>
{
    public Task<TacheDto?> Handle(ObtenirTacheQuery req, CancellationToken ct)
    {
        var t = TacheStore.Taches.FirstOrDefault(x => x.Id == req.Id);
        return Task.FromResult(t == null ? null :
            new TacheDto(t.Id, t.Titre, t.Description, t.Priorite, t.EstComplete, t.DateCreation));
    }
}

public class TacheCompleteeNotificationHandler : INotificationHandler<TacheCompleteeNotification>
{
    private readonly ILogger<TacheCompleteeNotificationHandler> _logger;
    public TacheCompleteeNotificationHandler(ILogger<TacheCompleteeNotificationHandler> l) => _logger = l;

    public Task Handle(TacheCompleteeNotification n, CancellationToken ct)
    {
        _logger.LogInformation("[OK] Tâche '{Titre}' (Id:{Id}) complétée à {Heure:HH:mm:ss}",
            n.Titre, n.TacheId, n.HeureCompletion);
        return Task.CompletedTask;
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 17 : DOMAIN-DRIVEN DESIGN (DDD)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les concepts clés du DDD
[OK] Créer des Value Objects
[OK] Définir des Aggregates et Aggregate Roots
[OK] Utiliser les Domain Events
[OK] Organiser les Bounded Contexts
*/


// ----------------------------------------------------------------------------
// [MODULE] CONCEPTS DDD ESSENTIELS
// ----------------------------------------------------------------------------

/*
DDD = Domain-Driven Design (conception orientée domaine)
Par Eric Evans (2003)

CONCEPTS CLÉS :

1. ENTITY : Objet avec identité propre (persiste dans le temps)
   -> Identifié par son Id, pas ses attributs
   -> Ex: Utilisateur, Commande, Produit

2. VALUE OBJECT : Objet défini par ses valeurs (pas d'identité)
   -> Immuable
   -> Égalité par valeur
   -> Ex: Adresse, Argent, Email, Période

3. AGGREGATE : Groupe d'entités traitées comme une unité
   -> Aggregate Root = porte d'entrée unique
   -> Transactions atomiques sur l'aggregate
   -> Ex: Commande (root) + LignesCommande

4. DOMAIN SERVICE : Logique qui n'appartient à aucune entité
   -> Ex: ServiceCalculTaxes, ServiceConversionDevise

5. REPOSITORY : Abstraction de la persistance (une par aggregate)

6. BOUNDED CONTEXT : Frontière d'un modèle cohérent
   -> "Produit" dans Catalogue ≠ "Produit" dans Commande

7. UBIQUITOUS LANGUAGE : Vocabulaire commun dev + métier
*/


// ----------------------------------------------------------------------------
// [GEM_STONE] VALUE OBJECTS
// ----------------------------------------------------------------------------

// Value Object de base
public abstract class ValueObject
{
    protected abstract IEnumerable<object?> GetEqualityComponents();

    public override bool Equals(object? obj)
    {
        if (obj == null || obj.GetType() != GetType()) return false;
        return GetEqualityComponents().SequenceEqual(((ValueObject)obj).GetEqualityComponents());
    }

    public override int GetHashCode()
        => GetEqualityComponents().Aggregate(1, (current, obj) =>
            HashCode.Combine(current, obj?.GetHashCode() ?? 0));

    public static bool operator ==(ValueObject? a, ValueObject? b)
        => a?.Equals(b) ?? b is null;

    public static bool operator !=(ValueObject? a, ValueObject? b) => !(a == b);
}

// ─── VALUE OBJECT : EMAIL ───────────────────────────────────────────────────
public class Email : ValueObject
{
    public string Valeur { get; private set; }

    private Email(string valeur) => Valeur = valeur;

    public static Email Creer(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            throw new DomainException("L'email est obligatoire.");

        email = email.Trim().ToLowerInvariant();

        if (!email.Contains('@') || !email.Contains('.'))
            throw new DomainException($"'{email}' n'est pas un email valide.");

        return new Email(email);
    }

    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Valeur;
    }

    public override string ToString() => Valeur;
}

// ─── VALUE OBJECT : ARGENT ───────────────────────────────────────────────────
public class Argent : ValueObject
{
    public decimal Montant { get; private set; }
    public string Devise { get; private set; }

    private Argent(decimal montant, string devise) { Montant = montant; Devise = devise; }

    public static Argent Creer(decimal montant, string devise = "EUR")
    {
        if (montant < 0) throw new DomainException("Le montant ne peut pas être négatif.");
        if (string.IsNullOrWhiteSpace(devise)) throw new DomainException("La devise est obligatoire.");
        return new Argent(montant, devise.ToUpperInvariant());
    }

    public Argent Ajouter(Argent autre)
    {
        if (Devise != autre.Devise)
            throw new DomainException($"Impossible d'additionner {Devise} et {autre.Devise}.");
        return new Argent(Montant + autre.Montant, Devise);
    }

    public Argent Multiplier(int facteur) => new(Montant * facteur, Devise);

    public static Argent Zero(string devise = "EUR") => new(0, devise);

    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Montant;
        yield return Devise;
    }

    public override string ToString() => $"{Montant:N2} {Devise}";
}

// ─── VALUE OBJECT : ADRESSE ──────────────────────────────────────────────────
public class Adresse : ValueObject
{
    public string Rue { get; private set; }
    public string Ville { get; private set; }
    public string CodePostal { get; private set; }
    public string Pays { get; private set; }

    private Adresse(string rue, string ville, string codePostal, string pays)
    {
        Rue = rue; Ville = ville; CodePostal = codePostal; Pays = pays;
    }

    public static Adresse Creer(string rue, string ville, string codePostal, string pays)
    {
        if (string.IsNullOrWhiteSpace(rue)) throw new DomainException("La rue est obligatoire.");
        if (string.IsNullOrWhiteSpace(ville)) throw new DomainException("La ville est obligatoire.");
        return new Adresse(rue.Trim(), ville.Trim(), codePostal.Trim(), pays.Trim());
    }

    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Rue; yield return Ville; yield return CodePostal; yield return Pays;
    }

    public override string ToString() => $"{Rue}, {CodePostal} {Ville}, {Pays}";
}


// ----------------------------------------------------------------------------
// [ARBRE] AGGREGATE ET AGGREGATE ROOT
// ----------------------------------------------------------------------------

/*
AGGREGATE ROOT = Entité principale qui contrôle l'accès à l'aggregate entier
  -> Toutes les modifications passent par l'Aggregate Root
  -> Une seule transaction par aggregate
  -> Les entités internes NE SONT PAS accédées directement
*/

// Entité interne (non accessible directement)
public class LigneCommandeDDD
{
    public int Id { get; private set; }
    public int ProduitId { get; private set; }
    public string NomProduit { get; private set; }
    public Argent PrixUnitaire { get; private set; }
    public int Quantite { get; private set; }
    public Argent SousTotal => PrixUnitaire.Multiplier(Quantite);

    private LigneCommandeDDD() { NomProduit = ""; PrixUnitaire = Argent.Zero(); }

    internal static LigneCommandeDDD Creer(int produitId, string nomProduit, Argent prixUnitaire, int quantite)
    {
        if (quantite <= 0) throw new DomainException("La quantité doit être positive.");
        return new LigneCommandeDDD
        {
            ProduitId = produitId,
            NomProduit = nomProduit,
            PrixUnitaire = prixUnitaire,
            Quantite = quantite
        };
    }
}

// Aggregate Root
public class CommandeDDD : MonApp.Domain.Entities.BaseEntity
{
    public string NumeroCommande { get; private set; }
    public Email EmailClient { get; private set; }
    public Adresse AdresseLivraison { get; private set; }
    public StatutCommande Statut { get; private set; }
    public Argent Total { get; private set; }
    public DateTime DateCommande { get; private set; }

    private readonly List<LigneCommandeDDD> _lignes = new();
    public IReadOnlyList<LigneCommandeDDD> Lignes => _lignes.AsReadOnly();

    private CommandeDDD()
    {
        NumeroCommande = ""; EmailClient = null!; AdresseLivraison = null!;
        Statut = StatutCommande.Brouillon; Total = Argent.Zero();
    }

    // Factory method
    public static CommandeDDD Creer(string emailClient, Adresse adresseLivraison)
    {
        var commande = new CommandeDDD
        {
            NumeroCommande = $"CMD-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString()[..8].ToUpper()}",
            EmailClient = Email.Creer(emailClient),
            AdresseLivraison = adresseLivraison,
            Statut = StatutCommande.Brouillon,
            DateCommande = DateTime.UtcNow,
            Total = Argent.Zero()
        };
        return commande;
    }

    // Comportements métier sur l'aggregate
    public void AjouterLigne(int produitId, string nomProduit, Argent prixUnitaire, int quantite)
    {
        if (Statut != StatutCommande.Brouillon)
            throw new DomainException("Impossible de modifier une commande non-brouillon.");

        var ligneExistante = _lignes.FirstOrDefault(l => l.ProduitId == produitId);
        if (ligneExistante != null)
        {
            // Logique de mise à jour (simplifiée)
            _lignes.Remove(ligneExistante);
        }

        var ligne = LigneCommandeDDD.Creer(produitId, nomProduit, prixUnitaire, quantite);
        _lignes.Add(ligne);
        RecalculerTotal();
    }

    public void SupprimerLigne(int produitId)
    {
        if (Statut != StatutCommande.Brouillon)
            throw new DomainException("Impossible de modifier une commande non-brouillon.");

        var ligne = _lignes.FirstOrDefault(l => l.ProduitId == produitId)
            ?? throw new DomainException($"Ligne produit {produitId} introuvable.");
        _lignes.Remove(ligne);
        RecalculerTotal();
    }

    public void Valider()
    {
        if (Statut != StatutCommande.Brouillon)
            throw new DomainException("La commande ne peut être validée que depuis l'état Brouillon.");
        if (!_lignes.Any())
            throw new DomainException("Impossible de valider une commande sans lignes.");

        Statut = StatutCommande.Validee;
        AjouterEvenement(new CommandeValideeEvent(Id, NumeroCommande, Total));
    }

    public void Expedition()
    {
        if (Statut != StatutCommande.Validee)
            throw new DomainException("La commande doit être validée avant expédition.");
        Statut = StatutCommande.Expediee;
        AjouterEvenement(new CommandeExpedieeEvent(Id, NumeroCommande));
    }

    public void Annuler(string raison)
    {
        if (Statut == StatutCommande.Livree)
            throw new DomainException("Impossible d'annuler une commande déjà livrée.");
        Statut = StatutCommande.Annulee;
        AjouterEvenement(new CommandeAnnuleeEvent(Id, NumeroCommande, raison));
    }

    private void RecalculerTotal()
    {
        Total = _lignes.Aggregate(Argent.Zero(), (acc, l) => acc.Ajouter(l.SousTotal));
        MarquerModifie();
    }
}

public enum StatutCommande { Brouillon, Validee, Expediee, Livree, Annulee }

// Events
public class CommandeValideeEvent : MonApp.Domain.Events.DomainEventBase
{
    public int CommandeId { get; }
    public string Numero { get; }
    public Argent Total { get; }
    public override string EventType => "commande.validee";
    public CommandeValideeEvent(int id, string numero, Argent total) { CommandeId = id; Numero = numero; Total = total; }
}

public class CommandeExpedieeEvent : MonApp.Domain.Events.DomainEventBase
{
    public int CommandeId { get; }
    public string Numero { get; }
    public override string EventType => "commande.expediee";
    public CommandeExpedieeEvent(int id, string numero) { CommandeId = id; Numero = numero; }
}

public class CommandeAnnuleeEvent : MonApp.Domain.Events.DomainEventBase
{
    public int CommandeId { get; }
    public string Numero { get; }
    public string Raison { get; }
    public override string EventType => "commande.annulee";
    public CommandeAnnuleeEvent(int id, string numero, string raison) { CommandeId = id; Numero = numero; Raison = raison; }
}


// ============================================================================
// [GUIDE] CHAPITRE 18 : MODULAR MONOLITH
// ============================================================================

/*
[IDEE] MODULAR MONOLITH = Application monolithique bien modulée

POURQUOI :
  - Simplicité de déploiement (1 application)
  - Découpage logique comme des microservices
  - Facilement décomposable en microservices ensuite
  - Évite la complexité des microservices prématurément

STRUCTURE :
  MonApp/
  ├── Program.cs
  ├── Modules/
  │   ├── Catalogue/          <- Module Catalogue
  │   │   ├── CatalogueModule.cs    (enregistrement DI)
  │   │   ├── Api/                   (endpoints)
  │   │   ├── Application/           (use cases)
  │   │   ├── Domain/                (entités)
  │   │   └── Infrastructure/        (repos, EF)
  │   ├── Commandes/          <- Module Commandes
  │   │   ├── CommandesModule.cs
  │   │   └── ...
  │   └── Utilisateurs/       <- Module Utilisateurs
  │       └── ...
  └── Shared/                 <- Code partagé entre modules
      ├── Events/              (événements inter-modules)
      └── Contracts/           (interfaces publiques)
*/

// Interface publique d'un module (contrat)
public interface ICatalogueModule
{
    Task<bool> ProduitExisteAsync(int produitId, CancellationToken ct = default);
    Task<decimal> ObtenirPrixAsync(int produitId, CancellationToken ct = default);
}

// Module Catalogue
public static class CatalogueModuleEnregistrement
{
    public static IServiceCollection AjouterModuleCatalogue(
        this IServiceCollection services, IConfiguration configuration)
    {
        // Enregistrement spécifique au module
        services.AddScoped<ICatalogueModule, CatalogueModuleImpl>();
        // services.AddDbContext<CatalogueDbContext>(...)
        // services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CatalogueModuleEnregistrement).Assembly))

        return services;
    }
}

public class CatalogueModuleImpl : ICatalogueModule
{
    public Task<bool> ProduitExisteAsync(int produitId, CancellationToken ct = default)
        => Task.FromResult(produitId > 0); // Simulation

    public Task<decimal> ObtenirPrixAsync(int produitId, CancellationToken ct = default)
        => Task.FromResult(99.99m); // Simulation
}

// Communication inter-modules via événements (pas d'appels directs)
public interface IEventBus
{
    Task PublierAsync<T>(T evt, CancellationToken ct = default) where T : class;
    void Souscrire<T>(Func<T, CancellationToken, Task> handler) where T : class;
}

public class InMemoryEventBus : IEventBus
{
    private readonly Dictionary<Type, List<object>> _handlers = new();

    public async Task PublierAsync<T>(T evt, CancellationToken ct = default) where T : class
    {
        if (!_handlers.TryGetValue(typeof(T), out var handlers)) return;
        foreach (var handler in handlers)
        {
            await ((Func<T, CancellationToken, Task>)handler)(evt, ct);
        }
    }

    public void Souscrire<T>(Func<T, CancellationToken, Task> handler) where T : class
    {
        if (!_handlers.ContainsKey(typeof(T)))
            _handlers[typeof(T)] = new List<object>();
        _handlers[typeof(T)].Add(handler);
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 19 : INTRODUCTION AUX MICROSERVICES
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre quand utiliser les microservices
[OK] Configurer un API Gateway simple
[OK] Faire de la communication HTTP entre services
[OK] Comprendre les bases du messaging (async communication)
[OK] Gérer les pannes (circuit breaker, retry)
*/


// ----------------------------------------------------------------------------
// [REFLEXION] QUAND UTILISER LES MICROSERVICES ?
// ----------------------------------------------------------------------------

/*
QUAND OUI :
  [OK] Équipes nombreuses (5+ équipes)
  [OK] Services qui scalent très différemment
  [OK] Technologies différentes nécessaires
  [OK] Déploiements indépendants requis

QUAND NON (commencer par Monolith) :
  [X] Startup / MVP (trop tôt)
  [X] Petite équipe (< 10 dev)
  [X] Domaine métier pas encore clair

SERVICES TYPIQUES D'UN E-COMMERCE :
  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐
  │  Catalogue  │   │  Commandes  │   │  Paiements  │
  └─────────────┘   └─────────────┘   └─────────────┘
         ^                 ^                 ^
  ┌──────────────────────────────────────────────────┐
  │                  API GATEWAY                      │
  └──────────────────────────────────────────────────┘
         ^
  ┌──────────────────────────────────────────────────┐
  │                   CLIENT                         │
  └──────────────────────────────────────────────────┘
*/


// ----------------------------------------------------------------------------
// [WEB] COMMUNICATION HTTP ENTRE SERVICES
// ----------------------------------------------------------------------------

// Utiliser HttpClient avec DI (Named ou Typed)

// ─── TYPED HTTP CLIENT ───────────────────────────────────────────────────────
public class CatalogueServiceClient
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<CatalogueServiceClient> _logger;

    public CatalogueServiceClient(HttpClient httpClient, ILogger<CatalogueServiceClient> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<ProduitMicroDto?> ObtenirProduitAsync(int id, CancellationToken ct = default)
    {
        try
        {
            var response = await _httpClient.GetAsync($"/api/produits/{id}", ct);

            if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
                return null;

            response.EnsureSuccessStatusCode();
            return await response.Content.ReadFromJsonAsync<ProduitMicroDto>(cancellationToken: ct);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Erreur communication avec le service Catalogue pour produit {Id}", id);
            throw;
        }
    }

    public async Task<bool> VerifierStockAsync(int produitId, int quantite, CancellationToken ct = default)
    {
        var response = await _httpClient.GetAsync($"/api/produits/{produitId}/stock?quantite={quantite}", ct);
        return response.IsSuccessStatusCode;
    }
}

public record ProduitMicroDto(int Id, string Nom, decimal Prix, int Stock);

// Enregistrement dans Program.cs avec Polly (retry + circuit breaker) :
/*
using Microsoft.Extensions.Http.Resilience;

builder.Services.AddHttpClient<CatalogueServiceClient>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["Services:Catalogue:BaseUrl"]!);
    client.DefaultRequestHeaders.Add("X-Service-Name", "CommandesService");
    client.Timeout = TimeSpan.FromSeconds(30);
})
.AddStandardResilienceHandler(options =>
{
    // Retry : 3 tentatives avec backoff exponentiel
    options.Retry.MaxRetryAttempts = 3;
    options.Retry.Delay = TimeSpan.FromMilliseconds(200);
    options.Retry.BackoffType = DelayBackoffType.Exponential;

    // Circuit Breaker : coupe si 50% d'erreurs
    options.CircuitBreaker.FailureRatio = 0.5;
    options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
    options.CircuitBreaker.MinimumThroughput = 3;

    // Timeout total
    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(10);
});
*/


// ----------------------------------------------------------------------------
// [ANNONCE] COMMUNICATION ASYNCHRONE (MESSAGING)
// ----------------------------------------------------------------------------

/*
[IDEE] POURQUOI LE MESSAGING ?

PROBLÈME AVEC HTTP SYNC :
  Service A -> appelle -> Service B
  Si Service B est tombé -> Service A échoue aussi !

AVEC MESSAGING (ASYNC) :
  Service A -> publie sur Bus de messages
  Service B -> lit depuis Bus quand il est disponible
  -> Découplage fort !

MESSAGERIES POPULAIRES :
  - RabbitMQ (open-source, AMQP)
  - Azure Service Bus (cloud Azure)
  - AWS SQS/SNS (cloud AWS)
  - Apache Kafka (streaming haute performance)

PACKAGES :
  dotnet add package MassTransit.RabbitMQ
  dotnet add package MassTransit.Azure.ServiceBus.Core
*/

// Interface générique pour l'event bus
public interface IMessageBus
{
    Task PublierAsync<T>(T message, CancellationToken ct = default) where T : class;
}

// Message d'événement inter-services
public record CommandePasseeMessage
{
    public Guid MessageId { get; init; } = Guid.NewGuid();
    public DateTime Timestamp { get; init; } = DateTime.UtcNow;
    public int CommandeId { get; init; }
    public string EmailClient { get; init; } = string.Empty;
    public decimal Total { get; init; }
    public List<LigneMessageDto> Lignes { get; init; } = new();
}

public record LigneMessageDto(int ProduitId, string NomProduit, int Quantite, decimal PrixUnitaire);

// Consommateur dans le service Paiements
public class CommandePasseeConsumer
{
    private readonly ILogger<CommandePasseeConsumer> _logger;

    public CommandePasseeConsumer(ILogger<CommandePasseeConsumer> logger) => _logger = logger;

    public async Task ConsommerAsync(CommandePasseeMessage message, CancellationToken ct = default)
    {
        _logger.LogInformation("Traitement paiement pour commande {Id}: {Total}€",
            message.CommandeId, message.Total);

        // Logique de paiement...
        await Task.Delay(100, ct); // Simulation

        _logger.LogInformation("Paiement traité pour commande {Id}", message.CommandeId);
    }
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE 13 (FINAL PARTIE 5) — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ — Architecture Complète avec CQRS + DDD :

Implémentez un système de gestion de panier d'achat.

1. Value Objects : Argent, Email
2. Entité Domain : Panier (Aggregate Root)
   - AjouterArticle(produitId, nom, prix, quantite)
   - ModifierQuantite(produitId, nouvelleQuantite)
   - SupprimerArticle(produitId)
   - Vider()
   - Domain Events : ArticleAjoutéEvent, PanierVideEvent

3. CQRS avec MediatR :
   - Command : AjouterAuPanierCommand + Handler
   - Command : ViderPanierCommand + Handler
   - Query : ObtenirPanierQuery + Handler

4. Controller : PanierController
*/

// ─── CORRIGÉ ────────────────────────────────────────────────────────────────

// Valeur objet Prix
public class PrixProduit : ValueObject
{
    public decimal Valeur { get; }
    public string Devise { get; }

    private PrixProduit(decimal valeur, string devise) { Valeur = valeur; Devise = devise; }

    public static PrixProduit Creer(decimal valeur, string devise = "EUR")
    {
        if (valeur < 0) throw new DomainException("Le prix ne peut pas être négatif.");
        return new PrixProduit(valeur, devise.ToUpper());
    }

    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Valeur; yield return Devise;
    }
}

// Entité ArticlePanier (interne à l'aggregate)
public class ArticlePanier
{
    public int ProduitId { get; private set; }
    public string NomProduit { get; private set; }
    public PrixProduit Prix { get; private set; }
    public int Quantite { get; private set; }
    public decimal SousTotal => Prix.Valeur * Quantite;

    private ArticlePanier() { NomProduit = ""; Prix = PrixProduit.Creer(0); }

    internal static ArticlePanier Creer(int produitId, string nom, PrixProduit prix, int quantite)
    {
        if (quantite <= 0) throw new DomainException("Quantité invalide.");
        return new ArticlePanier { ProduitId = produitId, NomProduit = nom, Prix = prix, Quantite = quantite };
    }

    internal void ModifierQuantite(int nouvelle)
    {
        if (nouvelle <= 0) throw new DomainException("Quantité invalide.");
        Quantite = nouvelle;
    }
}

// Events
public class ArticleAjouteEvent : MonApp.Domain.Events.DomainEventBase
{
    public override string EventType => "panier.article_ajoute";
    public int ProduitId { get; }
    public string NomProduit { get; }
    public ArticleAjouteEvent(int produitId, string nom) { ProduitId = produitId; NomProduit = nom; }
}

public class PanierVideEvent : MonApp.Domain.Events.DomainEventBase
{
    public override string EventType => "panier.vide";
    public string UtilisateurId { get; }
    public PanierVideEvent(string userId) => UtilisateurId = userId;
}

// Aggregate Root Panier
public class PanierAggregate : MonApp.Domain.Entities.BaseEntity
{
    public string UtilisateurId { get; private set; }
    private readonly List<ArticlePanier> _articles = new();
    public IReadOnlyList<ArticlePanier> Articles => _articles.AsReadOnly();
    public decimal Total => _articles.Sum(a => a.SousTotal);
    public int NombreArticles => _articles.Sum(a => a.Quantite);

    private PanierAggregate() { UtilisateurId = ""; }

    public static PanierAggregate CreerPourUtilisateur(string userId)
    {
        if (string.IsNullOrWhiteSpace(userId)) throw new DomainException("UserId requis.");
        return new PanierAggregate { UtilisateurId = userId };
    }

    public void AjouterArticle(int produitId, string nom, decimal prix, int quantite)
    {
        var existant = _articles.FirstOrDefault(a => a.ProduitId == produitId);
        if (existant != null)
            existant.ModifierQuantite(existant.Quantite + quantite);
        else
            _articles.Add(ArticlePanier.Creer(produitId, nom, PrixProduit.Creer(prix), quantite));

        AjouterEvenement(new ArticleAjouteEvent(produitId, nom));
        MarquerModifie();
    }

    public void ModifierQuantite(int produitId, int quantite)
    {
        var article = _articles.FirstOrDefault(a => a.ProduitId == produitId)
            ?? throw new DomainException($"Produit {produitId} pas dans le panier.");
        if (quantite <= 0)
            _articles.Remove(article);
        else
            article.ModifierQuantite(quantite);
        MarquerModifie();
    }

    public void SupprimerArticle(int produitId)
    {
        var article = _articles.FirstOrDefault(a => a.ProduitId == produitId)
            ?? throw new DomainException($"Produit {produitId} pas dans le panier.");
        _articles.Remove(article);
        MarquerModifie();
    }

    public void Vider()
    {
        _articles.Clear();
        AjouterEvenement(new PanierVideEvent(UtilisateurId));
        MarquerModifie();
    }
}

// CQRS
public record AjouterAuPanierCommand(string UtilisateurId, int ProduitId, string NomProduit, decimal Prix, int Quantite) : IRequest;
public record ViderPanierCommand(string UtilisateurId) : IRequest;
public record ObtenirPanierQuery(string UtilisateurId) : IRequest<PanierDto?>;
public record PanierDto(string UtilisateurId, List<ArticlePanierDto> Articles, decimal Total, int NombreArticles);
public record ArticlePanierDto(int ProduitId, string Nom, decimal Prix, int Quantite, decimal SousTotal);

// In-memory store
public static class PanierStore
{
    public static Dictionary<string, PanierAggregate> Paniers = new();
}

// Handlers
public class AjouterAuPanierHandler : IRequestHandler<AjouterAuPanierCommand>
{
    public Task Handle(AjouterAuPanierCommand req, CancellationToken ct)
    {
        if (!PanierStore.Paniers.TryGetValue(req.UtilisateurId, out var panier))
        {
            panier = PanierAggregate.CreerPourUtilisateur(req.UtilisateurId);
            PanierStore.Paniers[req.UtilisateurId] = panier;
        }
        panier.AjouterArticle(req.ProduitId, req.NomProduit, req.Prix, req.Quantite);
        return Task.CompletedTask;
    }
}

public class ViderPanierHandler : IRequestHandler<ViderPanierCommand>
{
    public Task Handle(ViderPanierCommand req, CancellationToken ct)
    {
        if (PanierStore.Paniers.TryGetValue(req.UtilisateurId, out var panier))
            panier.Vider();
        return Task.CompletedTask;
    }
}

public class ObtenirPanierHandler : IRequestHandler<ObtenirPanierQuery, PanierDto?>
{
    public Task<PanierDto?> Handle(ObtenirPanierQuery req, CancellationToken ct)
    {
        if (!PanierStore.Paniers.TryGetValue(req.UtilisateurId, out var panier))
            return Task.FromResult<PanierDto?>(null);

        var dto = new PanierDto(
            panier.UtilisateurId,
            panier.Articles.Select(a => new ArticlePanierDto(
                a.ProduitId, a.NomProduit, a.Prix.Valeur, a.Quantite, a.SousTotal)).ToList(),
            panier.Total,
            panier.NombreArticles);

        return Task.FromResult<PanierDto?>(dto);
    }
}

// Controller
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/panier")]
[Authorize]
public class PanierController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly IMediator _mediator;
    private string UserId => User.FindFirstValue(System.Security.Claims.ClaimTypes.NameIdentifier)!;

    public PanierController(IMediator mediator) => _mediator = mediator;

    [HttpGet]
    public async Task<IActionResult> Obtenir(CancellationToken ct)
    {
        var panier = await _mediator.Send(new ObtenirPanierQuery(UserId), ct);
        return panier is null ? Ok(new PanierDto(UserId, new(), 0, 0)) : Ok(panier);
    }

    [HttpPost("articles")]
    public async Task<IActionResult> Ajouter([Microsoft.AspNetCore.Mvc.FromBody] AjouterArticleDto dto, CancellationToken ct)
    {
        await _mediator.Send(new AjouterAuPanierCommand(UserId, dto.ProduitId, dto.NomProduit, dto.Prix, dto.Quantite), ct);
        return Ok();
    }

    [HttpDelete]
    public async Task<IActionResult> Vider(CancellationToken ct)
    {
        await _mediator.Send(new ViderPanierCommand(UserId), ct);
        return NoContent();
    }
}

public record AjouterArticleDto(int ProduitId, string NomProduit, decimal Prix, int Quantite);


// ============================================================================
// [DOCS] RÉCAPITULATIF PARTIE 5 COMPLÈTE
// ============================================================================

/*
[BRAVO] PARTIE 5 TERMINÉE — ARCHITECTURE PROFESSIONNELLE

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 15 : Clean Architecture
[OK] 4 couches : Domain, Application, Infrastructure, Présentation
[OK] Règle de dépendance (vers l'intérieur)
[OK] Entités avec comportements et factory methods
[OK] Interfaces dans le Domain, implémentations en Infrastructure
[OK] Handlers dans Application (use cases)

Chapitre 16 : CQRS avec MediatR
[OK] Séparation Commands (écriture) / Queries (lecture)
[OK] IRequest<T> et IRequestHandler<T, R>
[OK] Pipeline Behaviors (logging, validation, performance)
[OK] INotification et INotificationHandler (fan-out events)
[OK] Controller délégant tout au IMediator

Chapitre 17 : DDD
[OK] Value Objects (Email, Argent, Adresse) avec égalité valeur
[OK] Entities avec comportements métier encapsulés
[OK] Aggregate Root (CommandeDDD) contrôlant l'accès
[OK] Domain Events (CommandeValideeEvent, etc.)
[OK] Exceptions de domaine

Chapitre 18 : Modular Monolith
[OK] Organisation par modules
[OK] Communication inter-modules via interfaces ou event bus
[OK] InMemoryEventBus pour découplage

Chapitre 19 : Microservices
[OK] Quand utiliser les microservices vs monolith
[OK] TypedHttpClient avec Polly (retry, circuit breaker)
[OK] Communication asynchrone via messaging
[OK] Patterns de résilience

-> PROCHAINE ÉTAPE : Partie 6 - Performance & Scalabilité [RAPIDE]
   (Caching Redis, Compression, Logging Serilog, Health Checks)
*/

// ============================================================================
// [LIVRE] ASP.NET CORE - PARTIES 6, 7 & 8 (SUITE)
// DÉPLOIEMENT, TEMPS RÉEL & SAAS EXPERT
// ============================================================================

// (suite du Chapitre 28 : Déploiement)

/*
OPTIONS DE DÉPLOIEMENT :

1. Azure App Service (PaaS, le plus simple)
2. Linux VPS avec Nginx reverse proxy
3. Kubernetes (pour microservices à grande échelle)

═══════════════════════════════════════════════════════════
DÉPLOIEMENT SUR LINUX VPS (Ubuntu 22.04) + NGINX
═══════════════════════════════════════════════════════════

# 1. Sur le serveur : Installer .NET 8
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y dotnet-runtime-8.0

# 2. Publier l'application (sur votre machine de dev)
dotnet publish -c Release -o ./publish --runtime linux-x64 --self-contained false

# 3. Copier sur le serveur
scp -r ./publish user@monserveur.com:/var/www/monapp

# 4. Créer un service systemd (/etc/systemd/system/monapp.service)

[Unit]
Description=MonApp ASP.NET Core API
After=network.target

[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/var/www/monapp
ExecStart=/usr/bin/dotnet /var/www/monapp/MonApp.API.dll
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=monapp
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://localhost:5000
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

# Sécurité
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true

[Install]
WantedBy=multi-user.target

# 5. Activer et démarrer
sudo systemctl daemon-reload
sudo systemctl enable monapp
sudo systemctl start monapp
sudo systemctl status monapp

# 6. Nginx reverse proxy (/etc/nginx/sites-available/monapp)
server {
    listen 80;
    server_name api.monapp.com;

    # Rediriger vers HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.monapp.com;

    # SSL (Certbot)
    ssl_certificate /etc/letsencrypt/live/api.monapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.monapp.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # Headers sécurité
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header Strict-Transport-Security "max-age=63072000" always;

    # Proxy vers Kestrel
    location / {
        proxy_pass http://localhost:5000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection keep-alive;
        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;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 90s;
        proxy_connect_timeout 90s;

        # Limites
        client_max_body_size 10M;
    }

    # Logs
    access_log /var/log/nginx/monapp.access.log;
    error_log /var/log/nginx/monapp.error.log;
}

# 7. Obtenir certificat SSL gratuit (Let's Encrypt)
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.monapp.com

# 8. Activer Nginx
sudo nginx -t
sudo systemctl reload nginx
*/


// ============================================================================
// [GUIDE] CHAPITRE 29 : SIGNALR (TEMPS RÉEL)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre WebSockets et SignalR
[OK] Créer des Hubs SignalR
[OK] Implémenter un chat temps réel
[OK] Envoyer des notifications live
[OK] Gérer les connexions et groupes
*/

// ----------------------------------------------------------------------------
// [PLUGIN] QU'EST-CE QUE SIGNALR ?
// ----------------------------------------------------------------------------

/*
SignalR = Bibliothèque pour communication bidirectionnelle temps réel

COMMENT :
  - Utilise WebSockets (si disponible)
  - Fallback: Server-Sent Events, Long Polling
  - Abstraction transparente

POURQUOI :
  - Notifications push en temps réel
  - Chat, jeux multijoueurs, dashboards live
  - Mises à jour en direct (prix, stocks, alertes)

PACKAGES :
  dotnet add package Microsoft.AspNetCore.SignalR
  // Client JS : @microsoft/signalr
*/

using Microsoft.AspNetCore.SignalR;

// ----------------------------------------------------------------------------
// [VIDEO_GAME] HUB SIGNALR
// ----------------------------------------------------------------------------

/*
Hub = Classe centrale qui gère les connexions et messages
*/

// Hub de chat
public class ChatHub : Hub
{
    private readonly ILogger<ChatHub> _logger;
    private static readonly Dictionary<string, string> _utilisateurs = new();

    public ChatHub(ILogger<ChatHub> logger) => _logger = logger;

    // ─── ÉVÉNEMENTS DE CONNEXION ────────────────────────────────────────────

    public override async Task OnConnectedAsync()
    {
        _logger.LogInformation("Client connecté: {ConnectionId}", Context.ConnectionId);
        // Notifier tous les autres clients
        await Clients.Others.SendAsync("UtilisateurConnecte", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        if (_utilisateurs.TryGetValue(Context.ConnectionId, out var pseudo))
        {
            _utilisateurs.Remove(Context.ConnectionId);
            await Clients.All.SendAsync("UtilisateurDeconnecte", pseudo);
        }
        _logger.LogInformation("Client déconnecté: {ConnectionId}", Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }

    // ─── MÉTHODES APPELABLES PAR LES CLIENTS ─────────────────────────────────

    // Rejoindre le chat avec un pseudo
    public async Task Rejoindre(string pseudo)
    {
        _utilisateurs[Context.ConnectionId] = pseudo;

        // Rejoindre un groupe (room)
        await Groups.AddToGroupAsync(Context.ConnectionId, "general");

        // Envoyer à TOUS dans le groupe
        await Clients.Group("general").SendAsync("MessageSysteme",
            $"{pseudo} a rejoint le chat.");

        _logger.LogInformation("{Pseudo} a rejoint le chat", pseudo);
    }

    // Rejoindre une salle spécifique
    public async Task RejoindreRoom(string roomId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, $"room-{roomId}");
        await Clients.Group($"room-{roomId}").SendAsync("MessageSysteme",
            $"{ObtenirPseudo()} a rejoint la room {roomId}.");
    }

    // Envoyer un message à tous
    public async Task EnvoyerMessage(string message)
    {
        var pseudo = ObtenirPseudo();
        var timestamp = DateTime.UtcNow;

        _logger.LogInformation("{Pseudo}: {Message}", pseudo, message);

        // Envoyer à tous les clients connectés
        await Clients.All.SendAsync("NouveauMessage", new
        {
            Pseudo = pseudo,
            Message = message,
            Timestamp = timestamp,
            ConnectionId = Context.ConnectionId
        });
    }

    // Envoyer dans une room spécifique
    public async Task EnvoyerDansRoom(string roomId, string message)
    {
        var pseudo = ObtenirPseudo();
        await Clients.Group($"room-{roomId}").SendAsync("NouveauMessage", new
        {
            Pseudo = pseudo,
            Message = message,
            Room = roomId,
            Timestamp = DateTime.UtcNow
        });
    }

    // Message privé à un utilisateur spécifique
    public async Task MessagePrive(string connectionIdDestinataire, string message)
    {
        var pseudo = ObtenirPseudo();
        // Envoyer seulement au destinataire ET à l'expéditeur
        await Clients.Client(connectionIdDestinataire).SendAsync("MessagePrive", new
        {
            De = pseudo,
            Message = message,
            Timestamp = DateTime.UtcNow
        });
        await Clients.Caller.SendAsync("MessagePrive", new
        {
            De = pseudo,
            A = connectionIdDestinataire,
            Message = message,
            Timestamp = DateTime.UtcNow
        });
    }

    // Obtenir la liste des utilisateurs connectés
    public Task<IEnumerable<string>> ObtenirUtilisateurs()
    {
        return Task.FromResult(_utilisateurs.Values.AsEnumerable());
    }

    private string ObtenirPseudo()
    {
        return _utilisateurs.TryGetValue(Context.ConnectionId, out var pseudo)
            ? pseudo
            : Context.ConnectionId[..8];
    }
}

// Hub de notifications (avec authentification)
[Authorize]
public class NotificationsHub : Hub
{
    private static readonly Dictionary<string, HashSet<string>> _connexionsUtilisateur = new();

    public override async Task OnConnectedAsync()
    {
        var userId = Context.UserIdentifier; // = ClaimTypes.NameIdentifier par défaut
        if (!string.IsNullOrEmpty(userId))
        {
            if (!_connexionsUtilisateur.ContainsKey(userId))
                _connexionsUtilisateur[userId] = new HashSet<string>();
            _connexionsUtilisateur[userId].Add(Context.ConnectionId);
        }
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        var userId = Context.UserIdentifier;
        if (!string.IsNullOrEmpty(userId) && _connexionsUtilisateur.ContainsKey(userId))
        {
            _connexionsUtilisateur[userId].Remove(Context.ConnectionId);
            if (!_connexionsUtilisateur[userId].Any())
                _connexionsUtilisateur.Remove(userId);
        }
        await base.OnDisconnectedAsync(exception);
    }

    // Marquer notification comme lue
    public async Task MarquerLue(string notificationId)
    {
        var userId = Context.UserIdentifier!;
        // Logique BDD...
        await Clients.Caller.SendAsync("NotificationLue", notificationId);
    }
}

// Service pour envoyer des notifications depuis n'importe où dans l'app
public class ServiceNotifications
{
    private readonly IHubContext<NotificationsHub> _hubContext;
    private readonly ILogger<ServiceNotifications> _logger;

    public ServiceNotifications(
        IHubContext<NotificationsHub> hubContext,
        ILogger<ServiceNotifications> logger)
    {
        _hubContext = hubContext;
        _logger = logger;
    }

    // Envoyer à un utilisateur spécifique (par UserId)
    public async Task EnvoyerAUtilisateurAsync(
        string userId, string type, object payload, CancellationToken ct = default)
    {
        await _hubContext.Clients
            .User(userId)
            .SendAsync("Notification", new { Type = type, Payload = payload, Timestamp = DateTime.UtcNow }, ct);

        _logger.LogInformation("Notification {Type} envoyée à {UserId}", type, userId);
    }

    // Envoyer à tous
    public async Task EnvoyerATousAsync(string type, object payload, CancellationToken ct = default)
    {
        await _hubContext.Clients.All.SendAsync("Notification", new
        {
            Type = type,
            Payload = payload,
            Timestamp = DateTime.UtcNow
        }, ct);
    }

    // Envoyer à un groupe
    public async Task EnvoyerAGroupeAsync(
        string groupe, string type, object payload, CancellationToken ct = default)
    {
        await _hubContext.Clients.Group(groupe).SendAsync("Notification", new
        {
            Type = type,
            Payload = payload,
            Timestamp = DateTime.UtcNow
        }, ct);
    }
}

// Hub de tableau de bord en temps réel
public class DashboardHub : Hub
{
    // Les clients s'abonnent aux mises à jour de métriques
    public async Task AbonnerMetriques(string[] metriques)
    {
        foreach (var metrique in metriques)
            await Groups.AddToGroupAsync(Context.ConnectionId, $"metrique-{metrique}");

        await Clients.Caller.SendAsync("AbonnementConfirme", metriques);
    }

    public async Task SeDesabonner(string metrique)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"metrique-{metrique}");
    }
}

// Service qui pousse les métriques en temps réel
public class MetriquesPushService : BackgroundService
{
    private readonly IHubContext<DashboardHub> _hub;
    private readonly ILogger<MetriquesPushService> _logger;

    public MetriquesPushService(IHubContext<DashboardHub> hub, ILogger<MetriquesPushService> logger)
    {
        _hub = hub;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Simuler des métriques temps réel
            var metriques = new
            {
                Cpu = new Random().Next(10, 90),
                Memoire = new Random().Next(30, 80),
                RequetesParSeconde = new Random().Next(100, 5000),
                Timestamp = DateTime.UtcNow
            };

            // Envoyer aux clients abonnés aux métriques "cpu" et "memoire"
            await _hub.Clients.Group("metrique-cpu")
                .SendAsync("MiseAJourMetrique", new { Nom = "cpu", Valeur = metriques.Cpu }, stoppingToken);

            await _hub.Clients.Group("metrique-memoire")
                .SendAsync("MiseAJourMetrique", new { Nom = "memoire", Valeur = metriques.Memoire }, stoppingToken);

            await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
        }
    }
}

/*
═══════════════════════════════════════════════════════════
CONFIGURATION DANS PROGRAM.CS
═══════════════════════════════════════════════════════════

builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaximumReceiveMessageSize = 32 * 1024; // 32KB
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.HandshakeTimeout = TimeSpan.FromSeconds(15);
})
.AddJsonProtocol(options =>
{
    options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
})
.AddStackExchangeRedis(builder.Configuration.GetConnectionString("Redis")!); // Scale-out Redis

builder.Services.AddScoped<ServiceNotifications>();
builder.Services.AddHostedService<MetriquesPushService>();

// Dans le pipeline :
app.MapHub<ChatHub>("/hubs/chat");
app.MapHub<NotificationsHub>("/hubs/notifications");
app.MapHub<DashboardHub>("/hubs/dashboard");
*/

/*
═══════════════════════════════════════════════════════════
CLIENT JAVASCRIPT (TypeScript)
═══════════════════════════════════════════════════════════

import * as signalR from "@microsoft/signalr";

// Connexion au Hub Chat
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", {
        accessTokenFactory: () => localStorage.getItem("accessToken") || ""
    })
    .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Reconnexion auto
    .configureLogging(signalR.LogLevel.Information)
    .build();

// Écouter les messages
connection.on("NouveauMessage", (data) => {
    console.log(`${data.pseudo}: ${data.message}`);
    afficherMessage(data);
});

connection.on("UtilisateurConnecte", (connectionId) => {
    console.log(`Nouvel utilisateur: ${connectionId}`);
});

// Démarrer la connexion
await connection.start();
console.log("Connecté au chat !");

// Rejoindre le chat
await connection.invoke("Rejoindre", "Alice");

// Envoyer un message
await connection.invoke("EnvoyerMessage", "Bonjour tout le monde !");

// Gérer la déconnexion
connection.onclose(async () => {
    console.log("Déconnecté. Reconnexion...");
    await connection.start();
});
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE 16 — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ — Système de notifications en temps réel pour l'API de billets :

1. Créez CommandesNotificationHub avec :
   - Méthode : SuivreCommande(commandeId) -> rejoint groupe "commande-{id}"
   - Méthode : ArretSuivi(commandeId) -> quitte le groupe

2. Créez ServiceNotificationsCommandes qui expose :
   - NotifierStatutChangeAsync(commandeId, statut)
   - NotifierNouvelleCommandeAsync(commandeId, email)

3. Intégrez dans CommandesController.Valider() pour pousser une notif
   quand une commande est validée.

4. Client JS : Connecter et afficher les mises à jour en console.
*/

// ─── CORRIGÉ ────────────────────────────────────────────────────────────────

public class CommandesNotificationHub : Hub
{
    public async Task SuivreCommande(int commandeId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, $"commande-{commandeId}");
        await Clients.Caller.SendAsync("SuiviConfirme", commandeId);
    }

    public async Task ArretSuivi(int commandeId)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"commande-{commandeId}");
    }
}

public class ServiceNotificationsCommandes
{
    private readonly IHubContext<CommandesNotificationHub> _hub;

    public ServiceNotificationsCommandes(IHubContext<CommandesNotificationHub> hub)
        => _hub = hub;

    public async Task NotifierStatutChangeAsync(int commandeId, string statut, CancellationToken ct = default)
    {
        await _hub.Clients.Group($"commande-{commandeId}").SendAsync(
            "StatutMisAJour",
            new { CommandeId = commandeId, Statut = statut, Timestamp = DateTime.UtcNow },
            ct);
    }

    public async Task NotifierNouvelleCommandeAsync(
        int commandeId, string email, CancellationToken ct = default)
    {
        // Notifier les admins connectés
        await _hub.Clients.Group("admins").SendAsync(
            "NouvelleCommande",
            new { CommandeId = commandeId, Email = email, Timestamp = DateTime.UtcNow },
            ct);
    }
}

/*
// Client JS corrigé :
const conn = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/commandes")
    .withAutomaticReconnect()
    .build();

conn.on("StatutMisAJour", (data) => {
    console.log(`Commande ${data.commandeId}: nouveau statut -> ${data.statut}`);
});

conn.on("SuiviConfirme", (id) => console.log(`Suivi commande ${id} activé`));

await conn.start();
await conn.invoke("SuivreCommande", 42); // Suivre commande #42
*/


// ============================================================================
// [GUIDE] CHAPITRE 30 : BACKGROUND SERVICES
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer des services en arrière-plan (IHostedService)
[OK] Implémenter des workers avec BackgroundService
[OK] Planifier des tâches (jobs)
[OK] Gérer les queues de travail
[OK] Utiliser Hangfire pour les jobs planifiés
*/

// ----------------------------------------------------------------------------
// [CONFIG] IHOSTEDSERVICE ET BACKGROUNDSERVICE
// ----------------------------------------------------------------------------

/*
IHostedService     = Interface de base (StartAsync/StopAsync)
BackgroundService  = Classe abstraite qui simplifie IHostedService
                     -> Implémenter seulement ExecuteAsync
*/

// ─── WORKER SIMPLE ──────────────────────────────────────────────────────────
public class EmailWorker : BackgroundService
{
    private readonly ILogger<EmailWorker> _logger;
    private readonly IServiceScopeFactory _scopeFactory;

    public EmailWorker(ILogger<EmailWorker> logger, IServiceScopeFactory scopeFactory)
    {
        _logger = logger;
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("EmailWorker démarré");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // Important : créer un scope pour les services Scoped (DbContext, etc.)
                using var scope = _scopeFactory.CreateScope();
                var emailService = scope.ServiceProvider.GetRequiredService<MonApp.Application.Interfaces.IEmailService>();

                await TraiterEmailsEnAttenteAsync(emailService, stoppingToken);
            }
            catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogError(ex, "Erreur dans EmailWorker");
            }

            // Attendre 30 secondes avant la prochaine vérification
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }

        _logger.LogInformation("EmailWorker arrêté");
    }

    private async Task TraiterEmailsEnAttenteAsync(
        MonApp.Application.Interfaces.IEmailService emailService, CancellationToken ct)
    {
        // Simulation : récupérer les emails en attente depuis BDD
        _logger.LogDebug("Vérification des emails en attente...");
        await Task.Delay(100, ct); // Simulation traitement
    }
}

// ─── WORKER AVEC QUEUE ──────────────────────────────────────────────────────

// Channel = File d'attente thread-safe performante
using System.Threading.Channels;

public interface IEmailQueue
{
    void EnqueueEmail(EmailMessage email);
    IAsyncEnumerable<EmailMessage> DequeueAsync(CancellationToken ct);
}

public record EmailMessage(string Destinataire, string Sujet, string Corps);

public class EmailQueue : IEmailQueue
{
    private readonly Channel<EmailMessage> _channel;

    public EmailQueue(int capaciteMax = 100)
    {
        var options = new BoundedChannelOptions(capaciteMax)
        {
            FullMode = BoundedChannelFullMode.Wait,
            SingleReader = false,
            SingleWriter = false
        };
        _channel = Channel.CreateBounded<EmailMessage>(options);
    }

    public void EnqueueEmail(EmailMessage email)
    {
        if (!_channel.Writer.TryWrite(email))
        {
            // File pleine : logger et rejeter ou utiliser une autre stratégie
            throw new InvalidOperationException("La file d'emails est pleine.");
        }
    }

    public async IAsyncEnumerable<EmailMessage> DequeueAsync(
        [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
    {
        await foreach (var message in _channel.Reader.ReadAllAsync(ct))
        {
            yield return message;
        }
    }
}

public class EmailQueueWorker : BackgroundService
{
    private readonly IEmailQueue _queue;
    private readonly ILogger<EmailQueueWorker> _logger;
    private readonly IServiceScopeFactory _scopeFactory;

    public EmailQueueWorker(IEmailQueue queue, ILogger<EmailQueueWorker> logger,
        IServiceScopeFactory scopeFactory)
    {
        _queue = queue;
        _logger = logger;
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var email in _queue.DequeueAsync(stoppingToken))
        {
            try
            {
                using var scope = _scopeFactory.CreateScope();
                var service = scope.ServiceProvider.GetRequiredService<MonApp.Application.Interfaces.IEmailService>();
                await service.EnvoyerAsync(email.Destinataire, email.Sujet, email.Corps, stoppingToken);
                _logger.LogInformation("Email envoyé à {Dest}", email.Destinataire);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Erreur envoi email à {Dest}", email.Destinataire);
            }
        }
    }
}

// Utilisation dans un controller
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/email")]
public class EmailController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly IEmailQueue _queue;

    public EmailController(IEmailQueue queue) => _queue = queue;

    [HttpPost("envoyer")]
    public IActionResult EnvoyerEmail([Microsoft.AspNetCore.Mvc.FromBody] EmailMessage email)
    {
        _queue.EnqueueEmail(email);
        return Accepted(new { Message = "Email mis en file d'attente." });
    }
}

// ─── JOB PLANIFIÉ AVEC HANGFIRE ─────────────────────────────────────────────
/*
PACKAGES :
  dotnet add package Hangfire.AspNetCore
  dotnet add package Hangfire.SqlServer  (ou .InMemory pour dev)

CONFIGURATION :
  builder.Services.AddHangfire(config =>
      config.UseSimpleAssemblyNameTypeSerializer()
            .UseRecommendedSerializerSettings()
            .UseSqlServerStorage(builder.Configuration.GetConnectionString("DefaultConnection")));
  builder.Services.AddHangfireServer();

  app.UseHangfireDashboard("/jobs", new DashboardOptions
  {
      Authorization = new[] { new HangfireAuthFilter() }
  });
*/

// Service avec jobs Hangfire
public class ServiceJobsPlanifies
{
    private readonly IBackgroundJobClient _jobClient;
    private readonly IRecurringJobManager _recurringJobs;

    public ServiceJobsPlanifies(IBackgroundJobClient jobClient, IRecurringJobManager recurringJobs)
    {
        _jobClient = jobClient;
        _recurringJobs = recurringJobs;
    }

    public void InitialiserJobsRecurrents()
    {
        // Job quotidien à minuit : nettoyer les données
        _recurringJobs.AddOrUpdate(
            "nettoyage-quotidien",
            () => NettoyerDonneesAnciennesAsync(),
            "0 0 * * *");  // CRON: minuit chaque jour

        // Job hebdomadaire : rapport email
        _recurringJobs.AddOrUpdate(
            "rapport-hebdomadaire",
            () => EnvoyerRapportHebdomadaireAsync(),
            "0 8 * * MON"); // Lundi à 8h

        // Job toutes les 5 minutes : vérifier les stocks
        _recurringJobs.AddOrUpdate(
            "verification-stocks",
            () => VerifierStocksAsync(),
            "*/5 * * * *");
    }

    // Exécuter un job en arrière-plan immédiatement
    public string PlanifierEnvoi(string userId)
    {
        return _jobClient.Enqueue(
            () => EnvoyerEmailBienvenueAsync(userId));
    }

    // Exécuter avec délai
    public string PlanifierRappel(string userId, TimeSpan delai)
    {
        return _jobClient.Schedule(
            () => EnvoyerRappelAsync(userId),
            delai);
    }

    // Méthodes de jobs (doivent être publiques pour Hangfire)
    [Hangfire.AutomaticRetry(Attempts = 3)]
    public async Task EnvoyerEmailBienvenueAsync(string userId)
    {
        await Task.Delay(100);
        Console.WriteLine($"Email bienvenue envoyé à {userId}");
    }

    public async Task EnvoyerRappelAsync(string userId)
    {
        await Task.Delay(100);
        Console.WriteLine($"Rappel envoyé à {userId}");
    }

    public async Task NettoyerDonneesAnciennesAsync()
    {
        Console.WriteLine("Nettoyage des données anciennes...");
        await Task.Delay(500);
    }

    public async Task EnvoyerRapportHebdomadaireAsync()
    {
        Console.WriteLine("Envoi rapport hebdomadaire...");
        await Task.Delay(1000);
    }

    public async Task VerifierStocksAsync()
    {
        Console.WriteLine("Vérification des stocks...");
        await Task.Delay(200);
    }
}

// Attribut Hangfire
namespace Hangfire
{
    [AttributeUsage(AttributeTargets.Method)]
    public class AutomaticRetryAttribute : Attribute
    {
        public int Attempts { get; set; }
    }
}

interface IBackgroundJobClient
{
    string Enqueue(System.Linq.Expressions.Expression<Action> methodCall);
    string Schedule(System.Linq.Expressions.Expression<Action> methodCall, TimeSpan delay);
}

interface IRecurringJobManager
{
    void AddOrUpdate(string id, System.Linq.Expressions.Expression<Action> methodCall, string cronExpression);
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE 17 — AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ — Système de traitement asynchrone de commandes :

1. Créez une interface ICommandeQueue avec EnqueueCommande(commandeId)

2. Créez CommandeProcessingWorker (BackgroundService) qui :
   - Lit les commandes depuis la queue toutes les 5 secondes
   - Pour chaque commande : simule le traitement (délai aléatoire 1-3s)
   - Log le début, la fin et la durée

3. Créez un job Hangfire "nettoyage-commandes-annulees" qui :
   - S'exécute chaque nuit à 2h
   - Supprime les commandes annulées de plus de 30 jours

4. Intégrez dans CommandesController.PasserCommande() :
   - Enqueue immédiatement le traitement de la commande
*/

// ─── CORRIGÉ ────────────────────────────────────────────────────────────────

public interface ICommandeQueue
{
    void Enqueue(int commandeId);
    Task<int?> DequeueAsync(CancellationToken ct);
}

public class CommandeQueueImpl : ICommandeQueue
{
    private readonly Channel<int> _channel = Channel.CreateUnbounded<int>();

    public void Enqueue(int commandeId) => _channel.Writer.TryWrite(commandeId);

    public async Task<int?> DequeueAsync(CancellationToken ct)
    {
        try { return await _channel.Reader.ReadAsync(ct); }
        catch (OperationCanceledException) { return null; }
    }
}

public class CommandeProcessingWorker : BackgroundService
{
    private readonly ICommandeQueue _queue;
    private readonly ILogger<CommandeProcessingWorker> _logger;

    public CommandeProcessingWorker(ICommandeQueue queue, ILogger<CommandeProcessingWorker> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        _logger.LogInformation("CommandeProcessingWorker démarré");
        while (!ct.IsCancellationRequested)
        {
            var commandeId = await _queue.DequeueAsync(ct);
            if (commandeId == null) continue;

            var debut = DateTime.UtcNow;
            _logger.LogInformation("Traitement commande {Id}...", commandeId);
            try
            {
                var duree = new Random().Next(1000, 3000);
                await Task.Delay(duree, ct);
                _logger.LogInformation("Commande {Id} traitée en {Ms}ms", commandeId,
                    (DateTime.UtcNow - debut).TotalMilliseconds);
            }
            catch (Exception ex) when (!ct.IsCancellationRequested)
            {
                _logger.LogError(ex, "Erreur traitement commande {Id}", commandeId);
            }
        }
    }
}


// ============================================================================
// [GUIDE] PARTIE 10 : SAAS & NIVEAU EXPERT
// ============================================================================

/*
[OBJECTIF] CETTE SECTION COUVRE :
- Chapitre 33 : Multi-tenant Architecture
- Chapitre 34 : Paiement avec Stripe
- Chapitre 35 : Secrets Management
- Chapitre 36 : Production Hardening
*/


// ============================================================================
// [GUIDE] CHAPITRE 33 : MULTI-TENANT ARCHITECTURE
// ============================================================================

/*
[IDEE] MULTI-TENANT = Une seule application qui sert plusieurs clients (tenants)

STRATÉGIES :
  1. Base de données séparée par tenant (isolation maximale, coût élevé)
  2. Schéma séparé par tenant (PostgreSQL)
  3. Table partagée avec TenantId (moins isolé, plus économique)
  -> On implémente la stratégie 3 (la plus courante pour SaaS)

IDENTIFICATION DU TENANT :
  - Sous-domaine : client1.monapp.com
  - Header HTTP : X-Tenant-Id
  - JWT Claim    : "tenant_id"
  - URL path     : /api/tenant1/...
*/

// ─── SERVICE TENANT COURANT ──────────────────────────────────────────────────

public interface ICurrentTenantService
{
    string? TenantId { get; }
    bool EstMultiTenant { get; }
}

public class CurrentTenantService : ICurrentTenantService
{
    private readonly IHttpContextAccessor _httpContext;

    public CurrentTenantService(IHttpContextAccessor httpContext)
        => _httpContext = httpContext;

    public string? TenantId
    {
        get
        {
            var ctx = _httpContext.HttpContext;
            if (ctx == null) return null;

            // Priorité 1: JWT Claim
            var claimTenant = ctx.User.FindFirst("tenant_id")?.Value;
            if (!string.IsNullOrEmpty(claimTenant)) return claimTenant;

            // Priorité 2: Header HTTP
            if (ctx.Request.Headers.TryGetValue("X-Tenant-Id", out var headerTenant))
                return headerTenant.ToString();

            // Priorité 3: Sous-domaine
            var host = ctx.Request.Host.Host;
            if (host.Contains('.'))
            {
                var parts = host.Split('.');
                if (parts.Length >= 3) return parts[0]; // client1.monapp.com -> client1
            }

            return null;
        }
    }

    public bool EstMultiTenant => TenantId != null;
}

// ─── DBCONTEXT MULTI-TENANT ──────────────────────────────────────────────────

public class TenantDbContext : DbContext
{
    private readonly ICurrentTenantService _tenantService;

    public TenantDbContext(DbContextOptions<TenantDbContext> options,
        ICurrentTenantService tenantService) : base(options)
    {
        _tenantService = tenantService;
    }

    public DbSet<ArticleTenant> Articles { get; set; }

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

        // Filtre global par TenantId sur TOUTES les entités
        modelBuilder.Entity<ArticleTenant>()
            .HasQueryFilter(a => a.TenantId == _tenantService.TenantId);
    }

    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        var tenantId = _tenantService.TenantId
            ?? throw new InvalidOperationException("TenantId non défini.");

        // Assigner automatiquement le TenantId sur les nouvelles entités
        foreach (var entry in ChangeTracker.Entries<TenantEntity>())
        {
            if (entry.State == EntityState.Added)
                entry.Entity.TenantId = tenantId;
        }

        return await base.SaveChangesAsync(ct);
    }
}

// Classe de base pour entités multi-tenant
public abstract class TenantEntity
{
    public int Id { get; set; }
    public string TenantId { get; set; } = string.Empty;
}

public class ArticleTenant : TenantEntity
{
    public string Titre { get; set; } = string.Empty;
    public string Contenu { get; set; } = string.Empty;
    public DateTime DateCreation { get; set; } = DateTime.UtcNow;
}

// Middleware de résolution du tenant
public class TenantMiddleware
{
    private readonly RequestDelegate _next;

    public TenantMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context, ICurrentTenantService tenantService)
    {
        var tenantId = tenantService.TenantId;

        if (tenantId == null)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsJsonAsync(new { Message = "Tenant non identifié." });
            return;
        }

        await _next(context);
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 34 : PAIEMENT AVEC STRIPE
// ============================================================================

/*
[IDEE] STRIPE = Plateforme de paiement en ligne

PACKAGES :
  dotnet add package Stripe.net

CONFIGURATION :
  appsettings.json:
  {
    "Stripe": {
      "SecretKey": "sk_test_...",
      "WebhookSecret": "whsec_..."
    }
  }
  // En production: User Secrets ou Azure Key Vault
*/

// Options Stripe
public class StripeOptions
{
    public const string SectionName = "Stripe";
    public string SecretKey { get; set; } = string.Empty;
    public string WebhookSecret { get; set; } = string.Empty;
    public string PriceIdMensuel { get; set; } = string.Empty;
    public string PriceIdAnnuel { get; set; } = string.Empty;
}

// DTOs de paiement
public record CreerCheckoutSessionDto(string PriceId, string SuccessUrl, string CancelUrl);
public record CheckoutSessionResponse(string SessionId, string Url);
public record AbonnementInfo(string CustomerId, string SubscriptionId, string Statut, DateTime? ProchainePaiement);

// Service Stripe
public class StripeService
{
    private readonly StripeOptions _options;
    private readonly ILogger<StripeService> _logger;

    public StripeService(IOptions<StripeOptions> options, ILogger<StripeService> logger)
    {
        _options = options.Value;
        _logger = logger;
        // StripeConfiguration.ApiKey = _options.SecretKey; // Dans vrai code
    }

    // Créer une session de checkout (abonnement)
    public async Task<CheckoutSessionResponse> CreerCheckoutSessionAsync(
        string userId, CreerCheckoutSessionDto dto)
    {
        _logger.LogInformation("Création session checkout pour {UserId}", userId);

        // En vrai avec Stripe SDK :
        /*
        var options = new SessionCreateOptions
        {
            PaymentMethodTypes = new List<string> { "card" },
            Mode = "subscription",
            LineItems = new List<SessionLineItemOptions>
            {
                new() { Price = dto.PriceId, Quantity = 1 }
            },
            SuccessUrl = dto.SuccessUrl + "?session_id={CHECKOUT_SESSION_ID}",
            CancelUrl = dto.CancelUrl,
            ClientReferenceId = userId,
            CustomerEmail = "user@example.com",  // Récupérer depuis DB
            SubscriptionData = new SessionSubscriptionDataOptions
            {
                Metadata = new Dictionary<string, string> { ["userId"] = userId }
            }
        };
        var service = new SessionService();
        var session = await service.CreateAsync(options);
        return new CheckoutSessionResponse(session.Id, session.Url);
        */

        // Simulation pour l'exercice
        return new CheckoutSessionResponse(
            Guid.NewGuid().ToString(),
            $"https://checkout.stripe.com/pay/sim_{Guid.NewGuid()}");
    }

    // Gérer les webhooks Stripe (événements asynchrones)
    public async Task<bool> TraiterWebhookAsync(string payload, string stripeSignature)
    {
        try
        {
            // En vrai :
            // var stripeEvent = EventUtility.ConstructEvent(payload, stripeSignature, _options.WebhookSecret);

            // Simuler la gestion d'événements
            _logger.LogInformation("Webhook Stripe reçu");

            // switch (stripeEvent.Type)
            // {
            //     case "checkout.session.completed":
            //         var session = stripeEvent.Data.Object as Session;
            //         await ActiverAbonnementAsync(session!.ClientReferenceId!, session.Id);
            //         break;
            //     case "invoice.payment_failed":
            //         await SuspendreAbonnementAsync(customerId);
            //         break;
            //     case "customer.subscription.deleted":
            //         await AnnulerAbonnementAsync(customerId);
            //         break;
            // }

            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erreur traitement webhook Stripe");
            return false;
        }
    }

    private async Task ActiverAbonnementAsync(string userId, string sessionId)
    {
        _logger.LogInformation("Activation abonnement pour {UserId}", userId);
        await Task.CompletedTask;
    }
}

// Controller Stripe
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/paiement")]
public class PaiementController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly StripeService _stripe;
    private readonly ILogger<PaiementController> _logger;

    public PaiementController(StripeService stripe, ILogger<PaiementController> logger)
    {
        _stripe = stripe;
        _logger = logger;
    }

    // Créer une session Stripe Checkout
    [HttpPost("checkout")]
    [Authorize]
    public async Task<IActionResult> CreerCheckout([Microsoft.AspNetCore.Mvc.FromBody] CreerCheckoutSessionDto dto)
    {
        var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value!;
        var session = await _stripe.CreerCheckoutSessionAsync(userId, dto);
        return Ok(session);
    }

    // Webhook Stripe (SANS authentification JWT - appelé par Stripe)
    [HttpPost("webhook")]
    [AllowAnonymous]
    [Microsoft.AspNetCore.Mvc.Consumes("application/json")]
    public async Task<IActionResult> Webhook()
    {
        // Lire le corps brut (Stripe vérifie la signature)
        using var reader = new System.IO.StreamReader(HttpContext.Request.Body);
        var payload = await reader.ReadToEndAsync();
        var signature = Request.Headers["Stripe-Signature"].ToString();

        var ok = await _stripe.TraiterWebhookAsync(payload, signature);
        return ok ? Ok() : BadRequest();
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 35 : SECRETS MANAGEMENT
// ============================================================================

/*
HIÉRARCHIE DE SÉCURITÉ DES SECRETS :

NIVEAU 1 (développement) : User Secrets
  dotnet user-secrets set "Jwt:SecretKey" "dev-secret-key"

NIVEAU 2 (CI/CD) : Variables d'environnement
  export Jwt__SecretKey="ci-secret"
  (Note: __ = séparateur de section)

NIVEAU 3 (production cloud) : Azure Key Vault / AWS Secrets Manager
  -> Rotation automatique des clés
  -> Audit des accès
  -> Intégration native avec Identity

PACKAGES Azure Key Vault :
  dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
  dotnet add package Azure.Identity
*/

/*
Configuration Azure Key Vault dans Program.cs :

using Azure.Identity;

if (!builder.Environment.IsDevelopment())
{
    var keyVaultUri = new Uri(builder.Configuration["KeyVault:Uri"]!);

    builder.Configuration.AddAzureKeyVault(
        keyVaultUri,
        new DefaultAzureCredential()); // Utilise l'identité managée Azure

    // DefaultAzureCredential essaie dans cet ordre :
    // 1. Variables d'environnement (CI/CD)
    // 2. Managed Identity (Azure App Service, VM)
    // 3. Visual Studio (développement)
    // 4. Azure CLI
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 36 : PRODUCTION HARDENING
// ============================================================================

/*
CHECKLIST PRODUCTION :
[WHITE_SQUARE] HTTPS forcé (HSTS activé)
[WHITE_SQUARE] Logs structurés (Serilog -> ELK/Seq)
[WHITE_SQUARE] Health checks configurés
[WHITE_SQUARE] Rate limiting activé
[WHITE_SQUARE] Security headers (CSP, X-Frame-Options...)
[WHITE_SQUARE] Gestion d'erreurs globale (ProblemDetails)
[WHITE_SQUARE] Variables sensibles dans Key Vault / env vars
[WHITE_SQUARE] Backups BDD automatiques
[WHITE_SQUARE] Monitoring et alertes (Application Insights)
[WHITE_SQUARE] Circuit breakers sur les services externes
[WHITE_SQUARE] Connection pooling optimisé
[WHITE_SQUARE] Index BDD vérifiés
[WHITE_SQUARE] Migrations appliquées de manière sécurisée
*/

// Validation de la configuration au démarrage
public class ConfigurationValidator
{
    private readonly IConfiguration _config;
    private readonly ILogger<ConfigurationValidator> _logger;

    public ConfigurationValidator(IConfiguration config, ILogger<ConfigurationValidator> logger)
    {
        _config = config;
        _logger = logger;
    }

    public void Valider()
    {
        var erreurs = new List<string>();

        // Vérifier les paramètres critiques
        if (string.IsNullOrEmpty(_config["JwtSettings:SecretKey"]))
            erreurs.Add("JwtSettings:SecretKey manquant");

        if (_config["JwtSettings:SecretKey"]?.Length < 32)
            erreurs.Add("JwtSettings:SecretKey trop court (min 32 caractères)");

        if (string.IsNullOrEmpty(_config.GetConnectionString("DefaultConnection")))
            erreurs.Add("ConnectionStrings:DefaultConnection manquant");

        if (erreurs.Any())
        {
            foreach (var err in erreurs)
                _logger.LogCritical("Configuration invalide: {Erreur}", err);

            throw new InvalidOperationException(
                $"Configuration invalide : {string.Join(", ", erreurs)}");
        }

        _logger.LogInformation("[OK] Configuration validée avec succès");
    }
}

// Appliqur les migrations au démarrage de manière sécurisée
public static class MigrationExtensions
{
    public static async Task AppliquerMigrationsAsync(this WebApplication app)
    {
        using var scope = app.Services.CreateScope();
        var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();

        try
        {
            logger.LogInformation("Application des migrations...");
            var ctx = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var pending = await ctx.Database.GetPendingMigrationsAsync();

            if (!pending.Any())
            {
                logger.LogInformation("[OK] Aucune migration en attente");
                return;
            }

            logger.LogInformation("[PACKAGE] Migrations en attente : {Migrations}",
                string.Join(", ", pending));

            await ctx.Database.MigrateAsync();
            logger.LogInformation("[OK] Migrations appliquées avec succès");
        }
        catch (Exception ex)
        {
            logger.LogCritical(ex, "[X] Erreur lors de l'application des migrations");
            throw;
        }
    }
}


// ============================================================================
// [OBJECTIF] PROJET FIL ROUGE — PLATEFORME SAAS COMPLÈTE
// ============================================================================

/*
RÉCAPITULATIF ARCHITECTURE DU PROJET FIL ROUGE :

┌─────────────────────────────────────────────────────────────────────────────┐
│                         PLATEFORME SAAS                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│  Frontend (React/Vue)    ->   API Gateway (YARP/Nginx)                        │
├──────────────┬───────────────┬──────────────┬───────────────────────────────┤
│  Auth Module │ Catalog Module│ Orders Module│ Notifications Module           │
│  JWT + OAuth │ CRUD + Cache  │ CQRS + DDD   │ SignalR + Background           │
├──────────────┴───────────────┴──────────────┴───────────────────────────────┤
│  Infrastructure                                                               │
│  PostgreSQL + Redis + Hangfire + Stripe + Serilog + Health Checks            │
├─────────────────────────────────────────────────────────────────────────────┤
│  DevOps                                                                       │
│  Docker + GitHub Actions CI/CD + Azure App Service                           │
└─────────────────────────────────────────────────────────────────────────────┘

FONCTIONNALITÉS :
  [OK] Authentification JWT + Refresh Tokens + Google OAuth
  [OK] Gestion des utilisateurs (Admin, User, Manager)
  [OK] CRUD complet avec pagination et filtres
  [OK] Architecture Clean + CQRS + MediatR
  [OK] Cache Redis + Compression
  [OK] Notifications temps réel (SignalR)
  [OK] Background jobs (Hangfire)
  [OK] Paiements Stripe (abonnements)
  [OK] Multi-tenant (TenantId dans claims + query filters)
  [OK] Logging structuré Serilog
  [OK] Health checks + monitoring
  [OK] Tests unitaires + intégration (>80% couverture)
  [OK] Docker + CI/CD GitHub Actions
  [OK] Déploiement Azure App Service
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE FINAL 18 — PROJET FIL ROUGE
// ============================================================================

/*
ÉNONCÉ FINAL — Mini SaaS de Gestion de Tâches :

Construisez un mini-SaaS complet avec :

1. ARCHITECTURE :
   Clean Architecture (Domain, Application, Infrastructure, API)
   CQRS avec MediatR

2. AUTHENTIFICATION :
   - POST /api/auth/register (Identity + JWT)
   - POST /api/auth/login
   - POST /api/auth/refresh

3. GESTION DES TÂCHES (CRUD complet) :
   - Entité : Tache (Id, Titre, Description, Priorite, Statut, EcheanceDate, UtilisateurId, TenantId)
   - GET  /api/taches?statut=&page=&taille=
   - POST /api/taches
   - PUT  /api/taches/{id}
   - DELETE /api/taches/{id}

4. TEMPS RÉEL :
   - Hub : TachesHub -> notifier quand une tâche est créée/modifiée

5. PERFORMANCE :
   - Cache Redis (5 min) pour la liste des tâches par utilisateur
   - Compression Brotli activée

6. QUALITÉ :
   - Tests unitaires pour le domain (au moins 5 tests)
   - Tests d'intégration pour les endpoints

7. DEVOPS :
   - Dockerfile multi-stage
   - docker-compose.yml (API + PostgreSQL + Redis)
*/

// ─── CORRIGÉ — STRUCTURE COMPLÈTE ───────────────────────────────────────────

// DOMAIN
namespace FilRouge.Domain
{
    public enum PrioriteTache { Basse = 1, Normale = 2, Haute = 3, Urgente = 4 }
    public enum StatutTache { AAfaire, EnCours, EnRevue, Terminee, Annulee }

    public abstract class BaseEntityFR
    {
        public int Id { get; protected set; }
        public DateTime CreatedAt { get; protected set; } = DateTime.UtcNow;
        public DateTime? UpdatedAt { get; protected set; }
        protected void Touch() => UpdatedAt = DateTime.UtcNow;
    }

    public class TacheAggregate : BaseEntityFR
    {
        public string Titre { get; private set; }
        public string? Description { get; private set; }
        public PrioriteTache Priorite { get; private set; }
        public StatutTache Statut { get; private set; }
        public DateTime? DateEcheance { get; private set; }
        public string UtilisateurId { get; private set; }
        public string TenantId { get; private set; }

        private TacheAggregate() { Titre = ""; UtilisateurId = ""; TenantId = ""; }

        public static TacheAggregate Creer(string titre, string userId, string tenantId,
            string? desc = null, PrioriteTache priorite = PrioriteTache.Normale,
            DateTime? echeance = null)
        {
            if (string.IsNullOrWhiteSpace(titre)) throw new Exception("Titre requis.");
            if (echeance.HasValue && echeance.Value < DateTime.UtcNow)
                throw new Exception("La date d'échéance ne peut pas être dans le passé.");

            return new TacheAggregate
            {
                Titre = titre.Trim(), Description = desc, Priorite = priorite,
                Statut = StatutTache.AAfaire, DateEcheance = echeance,
                UtilisateurId = userId, TenantId = tenantId
            };
        }

        public void MettreAJour(string titre, string? desc, PrioriteTache priorite, DateTime? echeance)
        {
            if (Statut == StatutTache.Terminee || Statut == StatutTache.Annulee)
                throw new Exception("Impossible de modifier une tâche terminée ou annulée.");
            Titre = titre.Trim(); Description = desc;
            Priorite = priorite; DateEcheance = echeance;
            Touch();
        }

        public void Demarrer()
        {
            if (Statut != StatutTache.AAfaire)
                throw new Exception("La tâche doit être en statut 'À faire' pour démarrer.");
            Statut = StatutTache.EnCours;
            Touch();
        }

        public void Terminer()
        {
            if (Statut == StatutTache.Annulee) throw new Exception("Tâche annulée.");
            Statut = StatutTache.Terminee;
            Touch();
        }

        public void Annuler()
        {
            if (Statut == StatutTache.Terminee) throw new Exception("Tâche déjà terminée.");
            Statut = StatutTache.Annulee;
            Touch();
        }
    }

    public interface ITacheRepository
    {
        Task<TacheAggregate?> ObtenirAsync(int id, CancellationToken ct = default);
        Task<(List<TacheAggregate> Items, int Total)> ObtenirParUtilisateurAsync(
            string userId, StatutTache? statut, int page, int taille, CancellationToken ct = default);
        Task AjouterAsync(TacheAggregate tache, CancellationToken ct = default);
        void Modifier(TacheAggregate tache);
        void Supprimer(TacheAggregate tache);
        Task<int> SauvegarderAsync(CancellationToken ct = default);
    }
}

// APPLICATION
namespace FilRouge.Application
{
    using FilRouge.Domain;

    // DTOs
    public record TacheDto(int Id, string Titre, string? Description, PrioriteTache Priorite,
        StatutTache Statut, DateTime? DateEcheance, DateTime CreatedAt);
    public record TachesPageeesDto(List<TacheDto> Items, int Total, int Page, int TotalPages);
    public record CreerTacheDto(string Titre, string? Description, PrioriteTache Priorite, DateTime? DateEcheance);
    public record MettreAJourTacheDto(string Titre, string? Description, PrioriteTache Priorite, DateTime? DateEcheance);

    private static TacheDto ToDto(TacheAggregate t)
        => new(t.Id, t.Titre, t.Description, t.Priorite, t.Statut, t.DateEcheance, t.CreatedAt);

    // Commands
    public record CreerTacheCommand(CreerTacheDto Dto, string UserId, string TenantId) : IRequest<TacheDto>;
    public record MettreAJourTacheCommand(int Id, MettreAJourTacheDto Dto, string UserId) : IRequest<TacheDto?>;
    public record SupprimerTacheCommand(int Id, string UserId) : IRequest<bool>;
    public record ChangerStatutCommand(int Id, StatutTache NouveauStatut, string UserId) : IRequest<TacheDto?>;

    // Queries
    public record ObtenirTachesQuery(string UserId, StatutTache? Statut, int Page, int Taille) : IRequest<TachesPageeesDto>;
    public record ObtenirTacheQuery(int Id, string UserId) : IRequest<TacheDto?>;

    // Handlers
    public class CreerTacheHandler : IRequestHandler<CreerTacheCommand, TacheDto>
    {
        private readonly ITacheRepository _repo;
        private readonly IHubContext<TachesHub> _hub;

        public CreerTacheHandler(ITacheRepository repo, IHubContext<TachesHub> hub)
        { _repo = repo; _hub = hub; }

        public async Task<TacheDto> Handle(CreerTacheCommand req, CancellationToken ct)
        {
            var tache = TacheAggregate.Creer(req.Dto.Titre, req.UserId, req.TenantId,
                req.Dto.Description, req.Dto.Priorite, req.Dto.DateEcheance);
            await _repo.AjouterAsync(tache, ct);
            await _repo.SauvegarderAsync(ct);
            var dto = ToDto(tache);
            // Notification temps réel
            await _hub.Clients.User(req.UserId).SendAsync("TacheCree", dto, ct);
            return dto;
        }
    }

    public class ObtenirTachesHandler : IRequestHandler<ObtenirTachesQuery, TachesPageeesDto>
    {
        private readonly ITacheRepository _repo;
        private readonly IDistributedCache _cache;

        public ObtenirTachesHandler(ITacheRepository repo, IDistributedCache cache)
        { _repo = repo; _cache = cache; }

        public async Task<TachesPageeesDto> Handle(ObtenirTachesQuery req, CancellationToken ct)
        {
            var cle = $"taches:{req.UserId}:{req.Statut}:{req.Page}:{req.Taille}";
            var cached = await _cache.GetStringAsync(cle, ct);
            if (cached != null) return JsonSerializer.Deserialize<TachesPageeesDto>(cached)!;

            var (items, total) = await _repo.ObtenirParUtilisateurAsync(req.UserId, req.Statut, req.Page, req.Taille, ct);
            var result = new TachesPageeesDto(
                items.Select(ToDto).ToList(), total, req.Page,
                (int)Math.Ceiling((double)total / req.Taille));

            await _cache.SetStringAsync(cle, JsonSerializer.Serialize(result),
                new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }, ct);
            return result;
        }
    }
}

// HUB
public class TachesHub : Hub
{
    public async Task AbonnerTaches() => await Groups.AddToGroupAsync(Context.ConnectionId, $"user-{Context.UserIdentifier}");
}

// INFRASTRUCTURE (simplifié - en mémoire)
namespace FilRouge.Infrastructure
{
    using FilRouge.Domain;

    public class TacheRepositoryMem : ITacheRepository
    {
        private static readonly List<TacheAggregate> _store = new();
        private static int _nextId = 1;

        public Task<TacheAggregate?> ObtenirAsync(int id, CancellationToken ct = default)
            => Task.FromResult(_store.FirstOrDefault(t => t.Id == id));

        public Task<(List<TacheAggregate> Items, int Total)> ObtenirParUtilisateurAsync(
            string userId, StatutTache? statut, int page, int taille, CancellationToken ct = default)
        {
            var q = _store.Where(t => t.UtilisateurId == userId);
            if (statut.HasValue) q = q.Where(t => t.Statut == statut.Value);
            var total = q.Count();
            var items = q.Skip((page - 1) * taille).Take(taille).ToList();
            return Task.FromResult((items, total));
        }

        public Task AjouterAsync(TacheAggregate tache, CancellationToken ct = default)
        {
            // Définir l'ID via réflexion (simplification)
            typeof(FilRouge.Domain.BaseEntityFR).GetProperty("Id")!.SetValue(tache, _nextId++);
            _store.Add(tache);
            return Task.CompletedTask;
        }

        public void Modifier(TacheAggregate tache) { }
        public void Supprimer(TacheAggregate tache) => _store.Remove(tache);
        public Task<int> SauvegarderAsync(CancellationToken ct = default) => Task.FromResult(1);
    }
}

// API CONTROLLER
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/taches")]
[Authorize]
public class TachesFilRougeController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly IMediator _mediator;
    private string UserId => User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "";
    private string TenantId => User.FindFirst("tenant_id")?.Value ?? "default";

    public TachesFilRougeController(IMediator mediator) => _mediator = mediator;

    [HttpGet]
    public async Task<IActionResult> Get(
        [Microsoft.AspNetCore.Mvc.FromQuery] FilRouge.Domain.StatutTache? statut,
        [Microsoft.AspNetCore.Mvc.FromQuery] int page = 1,
        [Microsoft.AspNetCore.Mvc.FromQuery] int taille = 20,
        CancellationToken ct = default)
    {
        var result = await _mediator.Send(
            new FilRouge.Application.ObtenirTachesQuery(UserId, statut, page, taille), ct);
        return Ok(result);
    }

    [HttpPost]
    public async Task<IActionResult> Post(
        [Microsoft.AspNetCore.Mvc.FromBody] FilRouge.Application.CreerTacheDto dto, CancellationToken ct)
    {
        var result = await _mediator.Send(
            new FilRouge.Application.CreerTacheCommand(dto, UserId, TenantId), ct);
        return CreatedAtAction(nameof(Get), new { id = result.Id }, result);
    }
}


// ============================================================================
// [DOCS] RÉCAPITULATIF GLOBAL — TOUT ASP.NET CORE
// ============================================================================

/*
[BRAVO] FÉLICITATIONS ! VOUS AVEZ MAÎTRISÉ ASP.NET CORE DE A À Z

═══════════════════════════════════════════════════════════════════
PARTIE 1 — FONDATIONS .NET
[OK] C# avancé : Records, LINQ, async/await, Nullable, Pattern Matching
[OK] Écosystème .NET : CLI, Structure projet, NuGet, Configuration
[OK] Architecture ASP.NET Core : Kestrel, Middleware, DI

PARTIE 2 — WEB API
[OK] REST : Verbes HTTP, codes statut, conventions URL
[OK] Controllers : Routing, Model Binding, IActionResult, Swagger
[OK] Minimal APIs : MapGet/Post/Put/Delete, groupes, TypedResults
[OK] Validation : DataAnnotations, FluentValidation
[OK] Erreurs : ProblemDetails, middleware global, exceptions métier
[OK] Filtres : Action, Exception, Resource

PARTIE 3 — ACCÈS AUX DONNÉES
[OK] EF Core : DbContext, entités, migrations, CRUD
[OK] Tracking : AsNoTracking pour performances
[OK] Repository + Unit of Work
[OK] Optimisation : N+1, Include, projection, index, pagination

PARTIE 4 — AUTHENTIFICATION & SÉCURITÉ
[OK] Identity : UserManager, SignInManager, Rôles, Policies
[OK] JWT : Access tokens, Refresh tokens, rotation, révocation
[OK] OAuth : Google, GitHub, flux Authorization Code
[OK] Sécurité : HTTPS, CORS, CSRF, XSS, Rate Limiting, Security Headers

PARTIE 5 — ARCHITECTURE PROFESSIONNELLE
[OK] Clean Architecture : Domain, Application, Infrastructure, API
[OK] CQRS + MediatR : Commands, Queries, Behaviors, Notifications
[OK] DDD : Value Objects, Aggregates, Domain Events
[OK] Modular Monolith : Modules découplés, event bus interne
[OK] Microservices : HTTP client, Resilience, Messaging

PARTIE 6 — PERFORMANCE & SCALABILITÉ
[OK] MemoryCache : GetOrCreate, invalidation
[OK] Redis : Cache distribué, serialisation JSON
[OK] Compression : Brotli, Gzip
[OK] Response Caching : Cache-Control headers

PARTIE 7 — TESTS
[OK] Unit Testing : xUnit, Moq, FluentAssertions, AAA pattern
[OK] Integration Testing : WebApplicationFactory, TestServer
[OK] TDD : Red -> Green -> Refactor

PARTIE 8 — DEVOPS & PRODUCTION
[OK] Docker : Dockerfile multi-stage, docker-compose
[OK] CI/CD : GitHub Actions (test, build, push, deploy)
[OK] Déploiement : Linux VPS + Nginx + systemd

PARTIE 9 — TEMPS RÉEL & AVANCÉ
[OK] SignalR : Hubs, groupes, notifications, dashboard live
[OK] Background Services : BackgroundService, Channel<T>
[OK] Hangfire : Jobs planifiés, récurrents, retry

PARTIE 10 — SAAS & EXPERT
[OK] Multi-tenant : TenantId, Query Filters EF Core, Middleware
[OK] Stripe : Checkout Session, Webhooks, abonnements
[OK] Secrets Management : User Secrets -> Key Vault
[OK] Production Hardening : Checklist complète, validation config, migrations

═══════════════════════════════════════════════════════════════════
TEMPS DE MAÎTRISE ESTIMÉ :
  4 mois intensifs (40h/semaine)
  6-9 mois rythme normal (20h/semaine)
  1 an pour niveau enterprise (avec projets réels)

RESSOURCES COMPLÉMENTAIRES :
  [GUIDE] https://learn.microsoft.com/aspnet/core
  [GUIDE] https://docs.microsoft.com/dotnet
  [MOVIE_CAMERA] https://dotnet.microsoft.com/learn
  [SPEECH_BALLOON] https://discord.gg/csharp
  [DOCS] Clean Code — Robert C. Martin
  [DOCS] Domain-Driven Design — Eric Evans
  [DOCS] Building Microservices — Sam Newman
═══════════════════════════════════════════════════════════════════
*/