Voici un **programme complet, structuré et professionnel** pour maîtriser
**ASP.NET Core** de débutant à expert [RAPIDE]

Ce parcours est conçu pour atteindre un **niveau entreprise / SaaS-ready / cloud-ready**.

---

# [VERT] PARTIE 1 — Fondations .NET

## Chapitre 1 — Rappels C# avancés

* OOP avancée
* Records
* LINQ approfondi
* async / await
* Task, ValueTask
* Delegates & events
* Nullable reference types

## Chapitre 2 — Écosystème .NET

* .NET SDK
* CLI (`dotnet new`, `build`, `run`, `publish`)
* Structure d’un projet
* NuGet
* Environnements (Development, Production)
* Configuration (appsettings.json)

## Chapitre 3 — Architecture ASP.NET Core

* Kestrel
* Middleware pipeline
* Request/Response lifecycle
* Dependency Injection intégrée

---

# [VERT] PARTIE 2 — Web API (Fondamental)

## Chapitre 4 — Création d’API REST

* Controllers
* Routing
* Attributs `[HttpGet]`, `[HttpPost]`
* Model Binding
* IActionResult

## Chapitre 5 — Minimal APIs

* Différences avec Controllers
* Endpoints rapides
* Bonnes pratiques

## Chapitre 6 — Validation & Gestion d’erreurs

* DataAnnotations
* Validation automatique
* Middleware d’erreurs global
* ProblemDetails

## Chapitre 7 — Filtres & Middleware

* Action filters
* Exception filters
* Middleware personnalisé

---

# [VERT] PARTIE 3 — Accès aux données

## Chapitre 8 — Introduction à **Entity Framework Core**

* DbContext
* DbSet
* Migrations
* Tracking / NoTracking

## Chapitre 9 — Repositories & Unit of Work

* Pattern Repository
* Pattern UoW
* Tests avec InMemory DB

## Chapitre 10 — Optimisation Base de données

* Index
* Include / ThenInclude
* Pagination
* Requêtes optimisées

---

# [VERT] PARTIE 4 — Authentification & Sécurité

## Chapitre 11 — ASP.NET Core Identity

* UserManager
* SignInManager
* Rôles
* Policies

## Chapitre 12 — JWT Authentication

* Token generation
* Refresh tokens
* Protection API

## Chapitre 13 — OAuth & Social Login

* Google
* GitHub
* Microsoft

## Chapitre 14 — Sécurité Web

* HTTPS
* CORS
* CSRF
* XSS
* Rate limiting

---

# [VERT] PARTIE 5 — Architecture Professionnelle

## Chapitre 15 — Clean Architecture

* Layers :

  * Presentation
  * Application
  * Domain
  * Infrastructure

## Chapitre 16 — CQRS

* Commands
* Queries
* MediatR

## Chapitre 17 — DDD (Domain Driven Design)

* Entities
* Value Objects
* Aggregates
* Domain events

## Chapitre 18 — Modular Monolith

## Chapitre 19 — Microservices

* API Gateway
* Communication HTTP
* Messaging

---

# [VERT] PARTIE 6 — Performance & Scalabilité

## Chapitre 20 — Performance API

* Async everywhere
* Caching (MemoryCache, Redis)
* Compression
* Response caching

## Chapitre 21 — Scalabilité

* Load balancing
* Horizontal scaling
* Sticky sessions

## Chapitre 22 — Logging & Observabilité

* Logging structuré
* Serilog
* Health checks
* Monitoring

---

# [VERT] PARTIE 7 — Tests

## Chapitre 23 — Unit Testing

* xUnit
* Moq
* FluentAssertions

## Chapitre 24 — Integration Testing

* WebApplicationFactory
* TestServer

## Chapitre 25 — Test Driven Development (TDD)

---

# [VERT] PARTIE 8 — DevOps & Production

## Chapitre 26 — Docker

* Dockerfile
* Multi-stage build
* Docker Compose

## Chapitre 27 — CI/CD

* GitHub Actions
* Azure DevOps
* Pipeline automatique

## Chapitre 28 — Déploiement

* Azure App Service
* Linux VPS
* Nginx reverse proxy

---

# [VERT] PARTIE 9 — Temps Réel & Avancé

## Chapitre 29 — SignalR

* WebSockets
* Chat temps réel
* Notifications live

## Chapitre 30 — Background Services

* HostedService
* Workers
* Jobs planifiés

## Chapitre 31 — gRPC

## Chapitre 32 — GraphQL

---

# [VERT] PARTIE 10 — SaaS & Niveau Expert

## Chapitre 33 — Multi-tenant Architecture

* Base par client
* Shared DB avec TenantId
* Isolation sécurisée

## Chapitre 34 — Paiement & Abonnement

* Stripe integration
* Webhooks

## Chapitre 35 — Sécurité avancée

* Secrets management
* Key Vault
* Protection contre attaques avancées

## Chapitre 36 — Production hardening

* Logs audit
* Backup DB
* Plan de reprise

---

# [OBJECTIF] PROJET FIL ROUGE (OBLIGATOIRE)

Créer une plateforme SaaS complète :

* Authentification JWT
* Dashboard admin
* Gestion utilisateurs
* API REST complète
* Paiement abonnement
* Multi-tenant
* Docker
* Déploiement cloud
* Monitoring

---

# [HOURGLASS_WITH_FLOWING_SAND] Durée estimée

* 4 mois intensifs
* 6–9 mois rythme normal
* 1 an pour maîtrise entreprise

---

// ============================================================================
// [LIVRE] ASP.NET CORE - PARTIE 1 : FONDATIONS .NET
// ============================================================================
//
// [OBJECTIF] CETTE PARTIE COUVRE :
// - Chapitre 1 : Rappels C# Avancés
// - Chapitre 2 : Écosystème .NET
// - Chapitre 3 : Architecture ASP.NET Core
//
// [TEMPS] TEMPS : ~6-8 heures
// [DOCS] PRÉREQUIS : C# de base (variables, boucles, fonctions)
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 1 : RAPPELS C# AVANCÉS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Maîtriser l'OOP avancée en C#
[OK] Utiliser les Records
[OK] Écrire du LINQ fluide
[OK] Programmer de manière asynchrone (async/await)
[OK] Comprendre Task et ValueTask
[OK] Utiliser Delegates, Events et Lambdas
[OK] Gérer Nullable Reference Types
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] OOP AVANCÉE EN C#
// ----------------------------------------------------------------------------

/*
[IDEE] POURQUOI L'OOP AVANCÉE ?

ASP.NET Core est entièrement basé sur l'OOP.
Comprendre l'héritage, les interfaces, les classes abstraites
est INDISPENSABLE pour architecturer correctement.
*/

// ─── HÉRITAGE ET POLYMORPHISME ─────────────────────────────────────────────

// Classe de base (abstraite = ne peut pas être instanciée directement)
public abstract class Animal
{
    // Propriété automatique
    public string Nom { get; set; }
    public int Age { get; set; }

    // Constructeur
    protected Animal(string nom, int age)
    {
        Nom = nom;
        Age = age;
    }

    // Méthode abstraite (DOIT être implémentée par les enfants)
    public abstract string FaireUnBruit();

    // Méthode virtuelle (PEUT être redéfinie par les enfants)
    public virtual string SePresenter()
    {
        return $"Je m'appelle {Nom} et j'ai {Age} ans.";
    }

    // Méthode scellée dans une classe dérivée (empêche autre héritage)
    public override string ToString() => $"Animal: {Nom}";
}

public class Chien : Animal
{
    public string Race { get; set; }

    public Chien(string nom, int age, string race) : base(nom, age)
    {
        Race = race;
    }

    // IMPLÉMENTATION OBLIGATOIRE de la méthode abstraite
    public override string FaireUnBruit() => "Ouaf!";

    // REDÉFINITION de la méthode virtuelle
    public override string SePresenter()
    {
        return base.SePresenter() + $" Je suis un {Race}.";
    }
}

// ─── INTERFACES ────────────────────────────────────────────────────────────

/*
[IDEE] INTERFACE vs CLASSE ABSTRAITE

Interface :
- Contrat pur (pas d'état, pas d'implémentation par défaut*)
- Multiple interfaces possibles sur une classe
- Commence par "I" par convention (IAnimal, IRepository)
- Utiliser pour définir des comportements

Classe abstraite :
- Peut avoir des champs, constructeurs, implémentations par défaut
- Héritage simple uniquement
- Utiliser pour partager du code entre classes liées

* Depuis C# 8, les interfaces peuvent avoir des implémentations par défaut.
*/

// Interface = CONTRAT
public interface IPersistable
{
    int Id { get; set; }
    void Sauvegarder();
    Task<bool> SupprimerAsync();
}

public interface IValidatable
{
    bool EstValide();
    IEnumerable<string> ObtenirErreurs();
}

// Une classe peut implémenter PLUSIEURS interfaces
public class Utilisateur : IPersistable, IValidatable
{
    public int Id { get; set; }
    public string Email { get; set; } = string.Empty;
    public string MotDePasse { get; set; } = string.Empty;

    public void Sauvegarder()
    {
        // Logique de sauvegarde
        Console.WriteLine($"Utilisateur {Email} sauvegardé.");
    }

    public async Task<bool> SupprimerAsync()
    {
        await Task.Delay(100); // Simulation opération async
        return true;
    }

    public bool EstValide()
    {
        return !string.IsNullOrEmpty(Email) && Email.Contains('@')
               && MotDePasse.Length >= 8;
    }

    public IEnumerable<string> ObtenirErreurs()
    {
        var erreurs = new List<string>();
        if (string.IsNullOrEmpty(Email)) erreurs.Add("Email requis");
        if (!Email.Contains('@')) erreurs.Add("Email invalide");
        if (MotDePasse.Length < 8) erreurs.Add("Mot de passe trop court (min 8)");
        return erreurs;
    }
}

// ─── GENERICS (TYPES GÉNÉRIQUES) ───────────────────────────────────────────

/*
[IDEE] POURQUOI LES GÉNÉRIQUES ?

Évitent la duplication de code en créant des classes/méthodes
qui fonctionnent avec n'importe quel type.
*/

// Classe générique simple
public class BoiteGenerique<T>
{
    private T _valeur;

    public BoiteGenerique(T valeur)
    {
        _valeur = valeur;
    }

    public T ObtenirValeur() => _valeur;
    public void DefinirValeur(T valeur) => _valeur = valeur;
}

// Contrainte de type générique
public class Repository<T> where T : class, IPersistable, new()
{
    private readonly List<T> _elements = new();

    public void Ajouter(T element)
    {
        element.Sauvegarder();
        _elements.Add(element);
    }

    public T? TrouverParId(int id)
    {
        return _elements.FirstOrDefault(e => e.Id == id);
    }
}

// Méthode générique
public static class OutilsGeneriques
{
    public static T? TrouverPremier<T>(IEnumerable<T> collection, Func<T, bool> predicat)
    {
        foreach (var item in collection)
        {
            if (predicat(item)) return item;
        }
        return default;
    }
}

// ─── EXTENSION METHODS ─────────────────────────────────────────────────────

/*
[IDEE] MÉTHODES D'EXTENSION

Permettent d'ajouter des méthodes à des types existants
SANS modifier leur code source. Très utilisées dans ASP.NET Core !
*/

public static class StringExtensions
{
    // Méthode d'extension sur string
    public static bool EstEmailValide(this string email)
    {
        return !string.IsNullOrEmpty(email)
               && email.Contains('@')
               && email.Contains('.');
    }

    public static string Tronquer(this string texte, int longueurMax)
    {
        if (texte.Length <= longueurMax) return texte;
        return texte.Substring(0, longueurMax) + "...";
    }
}

// Utilisation :
// string email = "test@example.com";
// bool valide = email.EstEmailValide(); // [OK]


// ----------------------------------------------------------------------------
// [NOTE] RECORDS (C# 9+)
// ----------------------------------------------------------------------------

/*
[IDEE] QU'EST-CE QU'UN RECORD ?

COMMENT : Déclaré avec le mot-clé 'record' au lieu de 'class'
POURQUOI : Pour créer des types de données IMMUABLES avec égalité par valeur
QUAND :
  - DTOs (Data Transfer Objects) dans ASP.NET Core
  - Réponses d'API
  - Objets valeur en Domain-Driven Design
  - Configuration

DIFFÉRENCES AVEC UNE CLASSE :
[OK] Égalité par valeur (pas par référence)
[OK] Immuable par défaut
[OK] ToString() automatique et lisible
[OK] Déconstruction intégrée
[OK] Expression "with" pour créer des copies modifiées
*/

// ─── RECORD POSITIONNEL (syntaxe courte) ───────────────────────────────────
public record UtilisateurDto(int Id, string Prenom, string Nom, string Email);

// Utilisation :
// var user1 = new UtilisateurDto(1, "Alice", "Martin", "alice@ex.com");
// var user2 = new UtilisateurDto(1, "Alice", "Martin", "alice@ex.com");
// bool egal = user1 == user2; // [OK] TRUE (égalité par valeur !)
// Console.WriteLine(user1); // UtilisateurDto { Id = 1, Prenom = Alice, ... }

// Expression with (copie avec modification)
// var user3 = user1 with { Email = "newemail@ex.com" };

// ─── RECORD DE CLASSE (syntaxe longue) ─────────────────────────────────────
public record CommandeDto
{
    public int Id { get; init; }         // init = assignable SEULEMENT à la création
    public DateTime DateCreation { get; init; }
    public decimal Montant { get; init; }
    public string StatutCommande { get; init; } = "EnAttente";

    // Constructor peut exister
    public CommandeDto(int id, decimal montant)
    {
        Id = id;
        Montant = montant;
        DateCreation = DateTime.UtcNow;
    }

    // Méthodes autorisées dans un record
    public bool EstPaye() => StatutCommande == "Paye";
}

// ─── RECORD STRUCT (C# 10, pour performances) ──────────────────────────────
public record struct Point(double X, double Y)
{
    public double Distance() => Math.Sqrt(X * X + Y * Y);
}

// ─── RECORDS IMBRIQUÉS ET HÉRITAGE ─────────────────────────────────────────
public record Personne(string Prenom, string Nom);
public record Employe(string Prenom, string Nom, string Poste) : Personne(Prenom, Nom);


// ----------------------------------------------------------------------------
// [RECHERCHE] LINQ (Language Integrated Query)
// ----------------------------------------------------------------------------

/*
[IDEE] POURQUOI LINQ ?

LINQ permet de requêter des collections (listes, BDD via EF Core)
avec une syntaxe élégante et fortement typée.
Dans ASP.NET Core, LINQ est omniprésent avec Entity Framework Core.
*/

// Données de démonstration
var produits = new List<Produit>
{
    new(1, "Laptop", 999.99m, "Informatique", true),
    new(2, "Souris", 29.99m, "Informatique", true),
    new(3, "Bureau", 299.99m, "Mobilier", false),
    new(4, "Clavier", 79.99m, "Informatique", true),
    new(5, "Chaise", 199.99m, "Mobilier", true),
};

public record Produit(int Id, string Nom, decimal Prix, string Categorie, bool EnStock);

// ─── MÉTHODES LINQ ESSENTIELLES ────────────────────────────────────────────

// WHERE : Filtrer
var produitsDispo = produits.Where(p => p.EnStock);

// SELECT : Projeter (transformer)
var nomsSeuls = produits.Select(p => p.Nom);
var produitsDto = produits.Select(p => new { p.Nom, p.Prix });

// ORDERBY / THENBY : Trier
var parPrix = produits.OrderBy(p => p.Prix);
var parCategoriePuisPrix = produits
    .OrderBy(p => p.Categorie)
    .ThenByDescending(p => p.Prix);

// GROUPBY : Grouper
var parCategorie = produits.GroupBy(p => p.Categorie);
foreach (var groupe in parCategorie)
{
    Console.WriteLine($"Catégorie: {groupe.Key}");
    foreach (var p in groupe) Console.WriteLine($"  - {p.Nom}");
}

// AGGREGATE : Agréger
int count = produits.Count();
int countDispo = produits.Count(p => p.EnStock);
decimal prixMoyen = produits.Average(p => p.Prix);
decimal prixTotal = produits.Sum(p => p.Prix);
decimal prixMax = produits.Max(p => p.Prix);
decimal prixMin = produits.Min(p => p.Prix);

// FIRST / FIRSTORDEFAULT : Premier élément
var premier = produits.First(); // Exception si vide
var premierOuNull = produits.FirstOrDefault(); // null si vide
var laptop = produits.FirstOrDefault(p => p.Nom == "Laptop");

// ANY / ALL / NONE : Vérifications
bool aDesDisponibles = produits.Any(p => p.EnStock);
bool tousDispo = produits.All(p => p.EnStock);
bool aucunDispo = !produits.Any(p => p.EnStock);

// TAKE / SKIP : Pagination
var page1 = produits.Skip(0).Take(2); // 2 premiers
var page2 = produits.Skip(2).Take(2); // 2 suivants

// DISTINCT / UNION / INTERSECT / EXCEPT
var categories = produits.Select(p => p.Categorie).Distinct();

// SELECTMANY : Aplatir collections imbriquées
var commandes = new List<Commande>
{
    new(1, new[] { "Article1", "Article2" }),
    new(2, new[] { "Article3" })
};
var tousArticles = commandes.SelectMany(c => c.Articles);

public record Commande(int Id, string[] Articles);

// JOIN : Jointure
var categories2 = new List<Categorie>
{
    new(1, "Informatique", "IT"),
    new(2, "Mobilier", "MOB")
};
public record Categorie(int Id, string Nom, string Code);

var jointure = produits.Join(
    categories2,
    p => p.Categorie,    // Clé dans produits
    c => c.Nom,          // Clé dans categories2
    (p, c) => new { p.Nom, p.Prix, CodeCategorie = c.Code }
);

// ─── SYNTAX DE REQUÊTE (SQL-like) ──────────────────────────────────────────
// Alternative à la syntaxe de méthode, selon préférence
var requeteSql =
    from p in produits
    where p.EnStock && p.Prix < 500
    orderby p.Prix descending
    select new { p.Nom, p.Prix };

// ─── LINQ DIFFÉRÉ vs IMMÉDIAT ──────────────────────────────────────────────
/*
DIFFÉRÉ (lazy) : La requête n'est PAS exécutée jusqu'au besoin
  -> Where, Select, OrderBy, GroupBy, etc.

IMMÉDIAT (eager) : La requête est exécutée immédiatement
  -> ToList(), ToArray(), ToDictionary(), Count(), First(), etc.
*/

var requeteDifferee = produits.Where(p => p.EnStock); // Pas encore exécutée
var resultatImmediat = requeteDifferee.ToList(); // Exécutée maintenant !


// ----------------------------------------------------------------------------
// [RAPIDE] ASYNC / AWAIT
// ----------------------------------------------------------------------------

/*
[IDEE] POURQUOI ASYNC/AWAIT ?

COMMENT :
  - Méthode préfixée avec 'async'
  - 'await' avant opérations lentes (I/O)
  - Retourne Task, Task<T> ou ValueTask<T>

POURQUOI :
  - Évite de BLOQUER le thread pendant les opérations I/O
  - Permet au serveur de traiter d'AUTRES requêtes pendant l'attente
  - Performance considérablement améliorée (serveur gère + de requêtes)

QUAND :
  - TOUJOURS pour accès base de données
  - TOUJOURS pour appels HTTP externes
  - TOUJOURS pour lecture/écriture fichiers
  - Pas besoin pour calculs purs CPU
*/

// ─── MÉTHODE SYNCHRONE vs ASYNCHRONE ──────────────────────────────────────

// [X] SYNCHRONE - Bloque le thread !
public string LireFichierSync(string chemin)
{
    return File.ReadAllText(chemin); // <- Thread bloqué pendant lecture
}

// [OK] ASYNCHRONE - Thread libre pendant la lecture
public async Task<string> LireFichierAsync(string chemin)
{
    return await File.ReadAllTextAsync(chemin); // <- Thread libre pendant lecture
}

// ─── TYPES DE RETOUR ───────────────────────────────────────────────────────

// Task : Méthode async sans retour de valeur
public async Task EnvoyerEmailAsync(string destinataire, string message)
{
    // Simulation envoi email
    await Task.Delay(500);
    Console.WriteLine($"Email envoyé à {destinataire}");
}

// Task<T> : Méthode async qui retourne une valeur
public async Task<List<Utilisateur>> ObtenirUtilisateursAsync()
{
    await Task.Delay(100); // Simulation requête BDD
    return new List<Utilisateur>
    {
        new() { Id = 1, Email = "alice@example.com" }
    };
}

// ValueTask<T> : Pour méthodes qui sont SOUVENT synchrones
// Plus performant que Task<T> dans ce cas (pas d'allocation heap)
public async ValueTask<int> ObtenirCountAsync(bool depuisCache)
{
    if (depuisCache)
    {
        return 42; // Valeur directe, pas de Task créée -> économie mémoire
    }

    var users = await ObtenirUtilisateursAsync();
    return users.Count;
}

// ─── PATTERNS ASYNC IMPORTANTS ─────────────────────────────────────────────

// Exécuter PLUSIEURS tâches EN PARALLÈLE
public async Task ExemplesParallelisme()
{
    // [X] Séquentiel (lent - 200ms total)
    var users1 = await ObtenirUtilisateursAsync();  // 100ms
    var users2 = await ObtenirUtilisateursAsync();  // 100ms

    // [OK] Parallèle (rapide - 100ms total)
    var tache1 = ObtenirUtilisateursAsync();
    var tache2 = ObtenirUtilisateursAsync();
    await Task.WhenAll(tache1, tache2); // Les deux en même temps !

    // WhenAny : Continuer dès que l'UNE des tâches termine
    var tacheRapide = await Task.WhenAny(tache1, tache2);
}

// CancellationToken : Annuler des tâches
public async Task<List<Utilisateur>> ObtenirUsersAvecAnnulation(
    CancellationToken cancellationToken = default)
{
    // Si la requête HTTP est annulée (navigateur ferme la page), on arrête
    await Task.Delay(1000, cancellationToken);
    cancellationToken.ThrowIfCancellationRequested();
    return new List<Utilisateur>();
}

// Timeout
public async Task<string> ObtenirDonneesAvecTimeout()
{
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
    try
    {
        return await LireFichierAsync("fichier.txt"); // + passer cts.Token en vrai
    }
    catch (OperationCanceledException)
    {
        return "Timeout dépassé!";
    }
}

// ─── ERREURS COMMUNES ASYNC ────────────────────────────────────────────────

/*
[X] DEADLOCK - À éviter absolument !
Ne jamais faire .Result ou .Wait() sur un Task dans un contexte synchronisé
(comme Windows Forms, ASP.NET classique)

Dans ASP.NET Core c'est moins risqué, mais toujours éviter :
*/
// [X] MAUVAIS
// var result = ObtenirUtilisateursAsync().Result; // Risque deadlock
// [OK] BON
// var result = await ObtenirUtilisateursAsync();

/*
[ATTENTION] async void
N'utiliser JAMAIS async void sauf pour les event handlers !
Les exceptions dans async void ne peuvent pas être catchées proprement.
*/
// [X] MAUVAIS
// public async void FaireTruc() { await Task.Delay(100); }

// [OK] BON
// public async Task FaireTruc() { await Task.Delay(100); }


// ----------------------------------------------------------------------------
// [OBJECTIF] DELEGATES, EVENTS ET EXPRESSIONS LAMBDA
// ----------------------------------------------------------------------------

/*
[IDEE] POURQUOI DELEGATES ?

COMMENT : Pointeur vers une ou plusieurs méthodes
POURQUOI : Injecter du comportement, créer des callbacks
QUAND : Patterns d'événements, stratégies, middleware personnalisé

En ASP.NET Core, les delegates sont utilisés partout :
- Middleware pipeline (RequestDelegate)
- Injection de dépendances (factory delegates)
- Event sourcing
*/

// ─── DELEGATE NATIF ────────────────────────────────────────────────────────
public delegate bool FiltreProduit(Produit p);

// Utilisation
var filtreCher = new FiltreProduit(p => p.Prix > 100);
var produitsCher = produits.Where(p => filtreCher(p));

// ─── FUNC ET ACTION (delegates pré-définis) ────────────────────────────────
/*
Func<TInput, TOutput> : Delegate qui RETOURNE une valeur
Action<TInput> : Delegate qui ne retourne RIEN (void)
Predicate<T> : Delegate Func<T, bool>
*/

// Func<Produit, bool> = prend un Produit, retourne bool
Func<Produit, bool> estCher = p => p.Prix > 100;
Func<Produit, decimal> prixAvecTaxe = p => p.Prix * 1.20m;
Func<decimal, decimal, decimal> additionner = (a, b) => a + b;

// Action<string> = prend une string, ne retourne rien
Action<string> logger = message => Console.WriteLine($"LOG: {message}");
Action<Produit> afficherProduit = p => Console.WriteLine($"{p.Nom}: {p.Prix}€");

// Predicate<Produit> = Func<Produit, bool>
Predicate<Produit> enStock = p => p.EnStock;

// Utilisation
produits.ForEach(afficherProduit);
var produitsChers = produits.Where(estCher);

// ─── EXPRESSIONS LAMBDA ────────────────────────────────────────────────────
// Lambda = syntaxe courte pour créer des delegates inline

// Lambda simple (expression)
Func<int, int> doubler = x => x * 2;

// Lambda avec bloc
Func<int, int, int> maximum = (a, b) =>
{
    if (a > b) return a;
    return b;
};

// Lambda sans paramètre
Action direBonjour = () => Console.WriteLine("Bonjour!");

// ─── EVENTS ────────────────────────────────────────────────────────────────
/*
COMMENT : Basé sur delegates, utilise EventHandler<T> par convention
POURQUOI : Communication découplée entre objets
QUAND : Notifications, audit, Domain Events dans DDD
*/

// Définir les données de l'événement
public class ProduitCreéEventArgs : EventArgs
{
    public Produit Produit { get; }
    public DateTime DateCreation { get; }

    public ProduitCreéEventArgs(Produit produit)
    {
        Produit = produit;
        DateCreation = DateTime.UtcNow;
    }
}

// Classe qui publie l'événement
public class ServiceProduit
{
    // Déclaration de l'événement
    public event EventHandler<ProduitCreéEventArgs>? ProduitCree;

    public void CreerProduit(Produit produit)
    {
        // ... logique de création ...

        // Déclencher l'événement
        OnProduitCree(new ProduitCreéEventArgs(produit));
    }

    protected virtual void OnProduitCree(ProduitCreéEventArgs args)
    {
        ProduitCree?.Invoke(this, args); // <- ?. = null-safe
    }
}

// S'abonner à l'événement
var service = new ServiceProduit();
service.ProduitCree += (sender, args) =>
{
    Console.WriteLine($"Nouveau produit créé: {args.Produit.Nom}");
};


// ----------------------------------------------------------------------------
// [?] NULLABLE REFERENCE TYPES (C# 8+)
// ----------------------------------------------------------------------------

/*
[IDEE] POURQUOI NULLABLE REFERENCE TYPES ?

COMMENT : Activé dans le projet avec <Nullable>enable</Nullable>
POURQUOI : Éliminer les NullReferenceException en production
QUAND : Tous les nouveaux projets ASP.NET Core l'ont activé par défaut

CONVENTION :
string   -> Non-nullable (ne peut PAS être null)
string?  -> Nullable (PEUT être null)
*/

// ─── DÉCLARATIONS ──────────────────────────────────────────────────────────
string nomRequis = "Alice";   // Ne peut pas être null
string? nomOptional = null;   // Peut être null

int ageRequis = 25;
int? ageOptional = null;      // int? = Nullable<int>

// ─── OPÉRATEURS NULL-SAFE ──────────────────────────────────────────────────

// ?. (null-conditional) : Accès sans risque
string? email = null;
int? longueur = email?.Length; // null (pas d'exception!)
string? upper = email?.ToUpper(); // null

// ?? (null-coalescing) : Valeur par défaut si null
string affichage = email ?? "Email non renseigné";

// ??= (null-coalescing assignment)
email ??= "default@example.com"; // Assigne SEULEMENT si email est null

// ! (null-forgiving) : Dire au compilateur "je sais qu'il n'est pas null"
// À utiliser avec PRÉCAUTION
string emailConfirme = email!; // On "promet" qu'email n'est pas null

// ─── PATTERN MATCHING ──────────────────────────────────────────────────────
/*
Pattern matching = vérification de types/valeurs élégante
Très utilisé dans les APIs pour le routage, la validation, etc.
*/

object objet = "Bonjour";

// is pattern
if (objet is string texte)
{
    Console.WriteLine($"C'est une string: {texte.ToUpper()}");
}

// switch expression (C# 8+)
string Description(object obj) => obj switch
{
    int n when n > 0 => $"Entier positif: {n}",
    int n when n < 0 => "Entier négatif",
    int => "Zéro",
    string s => $"String: {s}",
    null => "Null",
    _ => "Autre type"
};

// Positional pattern matching avec Records
static string ClassifierProduit(Produit p) => p switch
{
    { EnStock: false } => "Indisponible",
    { Prix: > 500 } => "Premium",
    { Prix: > 100 } => "Standard",
    _ => "Économique"
};

/*
[DOCS] RÉCAPITULATIF CHAPITRE 1

[OK] OOP : Héritage, Interfaces, Génériques, Extension Methods
[OK] Records : Immuabilité, égalité par valeur, init properties
[OK] LINQ : Where, Select, OrderBy, GroupBy, Join, etc.
[OK] Async/Await : Task, Task<T>, ValueTask, WhenAll, CancellationToken
[OK] Delegates : Func, Action, Predicate, Events, Lambdas
[OK] Nullable : string?, ?., ??, ??=, pattern matching
*/


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

/*
ÉNONCÉ :

Créez un système de gestion d'une bibliothèque.

1. Créez un record "Livre" avec : Id, Titre, Auteur, Annee, Genre, EstDisponible

2. Créez une interface "IBibliotheque" avec les méthodes :
   - Task<IEnumerable<Livre>> ObtenirTousAsync()
   - Task<Livre?> TrouverParIdAsync(int id)
   - Task AjouterAsync(Livre livre)

3. Créez une classe "Bibliotheque" qui implémente IBibliotheque
   avec une liste interne de livres.

4. Écrivez des requêtes LINQ pour :
   a) Trouver tous les livres disponibles, triés par année (décroissant)
   b) Grouper par genre et afficher le nombre de livres par genre
   c) Obtenir le titre du livre le plus récent par genre

5. Créez un event "LivreAjoute" qui se déclenche à chaque ajout
*/

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

// 1. Record Livre
public record Livre(
    int Id,
    string Titre,
    string Auteur,
    int Annee,
    string Genre,
    bool EstDisponible = true
);

// 2. Interface
public interface IBibliotheque
{
    Task<IEnumerable<Livre>> ObtenirTousAsync();
    Task<Livre?> TrouverParIdAsync(int id);
    Task AjouterAsync(Livre livre);
}

// Événement
public class LivreAjouteEventArgs : EventArgs
{
    public Livre Livre { get; }
    public LivreAjouteEventArgs(Livre livre) => Livre = livre;
}

// 3. Implémentation
public class Bibliotheque : IBibliotheque
{
    private readonly List<Livre> _livres = new()
    {
        new(1, "Clean Code", "Robert Martin", 2008, "Technique"),
        new(2, "The Pragmatic Programmer", "David Thomas", 1999, "Technique"),
        new(3, "Dune", "Frank Herbert", 1965, "Science-Fiction"),
        new(4, "Foundation", "Isaac Asimov", 1951, "Science-Fiction", false),
        new(5, "Domain-Driven Design", "Eric Evans", 2003, "Technique"),
        new(6, "Neuromancer", "William Gibson", 1984, "Science-Fiction"),
    };

    // 5. Event
    public event EventHandler<LivreAjouteEventArgs>? LivreAjoute;

    public async Task<IEnumerable<Livre>> ObtenirTousAsync()
    {
        await Task.Delay(10); // Simulation I/O
        return _livres.AsEnumerable();
    }

    public async Task<Livre?> TrouverParIdAsync(int id)
    {
        await Task.Delay(10);
        return _livres.FirstOrDefault(l => l.Id == id);
    }

    public async Task AjouterAsync(Livre livre)
    {
        await Task.Delay(10);
        _livres.Add(livre);
        LivreAjoute?.Invoke(this, new LivreAjouteEventArgs(livre));
    }

    // 4a. Livres disponibles triés par année décroissante
    public async Task<IEnumerable<Livre>> ObtenirDisponiblesAsync()
    {
        var tous = await ObtenirTousAsync();
        return tous
            .Where(l => l.EstDisponible)
            .OrderByDescending(l => l.Annee);
    }

    // 4b. Grouper par genre
    public async Task<Dictionary<string, int>> CompterParGenreAsync()
    {
        var tous = await ObtenirTousAsync();
        return tous
            .GroupBy(l => l.Genre)
            .ToDictionary(g => g.Key, g => g.Count());
    }

    // 4c. Livre le plus récent par genre
    public async Task<Dictionary<string, string>> PlusRecentParGenreAsync()
    {
        var tous = await ObtenirTousAsync();
        return tous
            .GroupBy(l => l.Genre)
            .ToDictionary(
                g => g.Key,
                g => g.OrderByDescending(l => l.Annee).First().Titre
            );
    }
}

// Programme de test
public class ProgrammeTest
{
    public static async Task ExecuterAsync()
    {
        var biblio = new Bibliotheque();

        // S'abonner à l'event
        biblio.LivreAjoute += (sender, args) =>
            Console.WriteLine($"[OK] Nouveau livre ajouté: '{args.Livre.Titre}'");

        // Ajouter un livre
        await biblio.AjouterAsync(new Livre(7, "C# in Depth", "Jon Skeet", 2019, "Technique"));

        // Disponibles triés
        Console.WriteLine("\n[DOCS] Livres disponibles (du plus récent):");
        var dispo = await biblio.ObtenirDisponiblesAsync();
        foreach (var l in dispo)
            Console.WriteLine($"  [{l.Annee}] {l.Titre} - {l.Auteur}");

        // Par genre
        Console.WriteLine("\n[GRAPHIQUE] Livres par genre:");
        var parGenre = await biblio.CompterParGenreAsync();
        foreach (var (genre, count) in parGenre)
            Console.WriteLine($"  {genre}: {count} livre(s)");

        // Plus récent par genre
        Console.WriteLine("\n[TROPHEE] Livre le plus récent par genre:");
        var plusRecent = await biblio.PlusRecentParGenreAsync();
        foreach (var (genre, titre) in plusRecent)
            Console.WriteLine($"  {genre}: '{titre}'");
    }
}


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

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser le CLI .NET en profondeur
[OK] Comprendre la structure d'un projet
[OK] Gérer les packages NuGet
[OK] Configurer les environnements
[OK] Utiliser appsettings.json
*/


// ----------------------------------------------------------------------------
// [OUTIL] .NET CLI - LIGNE DE COMMANDE
// ----------------------------------------------------------------------------

/*
[IDEE] LE CLI .NET EST VOTRE MEILLEUR AMI

COMMENT : Commandes 'dotnet <commande>'
POURQUOI : Créer, compiler, exécuter, publier sans avoir besoin d'IDE
QUAND : Toujours! CI/CD, containers, développement quotidien
*/

// ─── COMMANDES ESSENTIELLES ────────────────────────────────────────────────

/*
═══ VÉRIFIER L'INSTALLATION ═══
dotnet --version           -> Affiche la version (ex: 8.0.100)
dotnet --info              -> Infos complètes (runtimes, SDKs installés)

═══ CRÉER UN PROJET ═══
dotnet new list            -> Lister tous les templates disponibles
dotnet new webapi -n MonApi                    -> API REST minimale
dotnet new webapi -n MonApi --use-controllers  -> Avec Controllers
dotnet new mvc -n MonApp                       -> Application MVC
dotnet new console -n MaConsole                -> Application console
dotnet new classlib -n MaBibliotheque          -> Bibliothèque de classes
dotnet new xunit -n MesTests                   -> Projet de tests xUnit
dotnet new sln -n MaSolution                   -> Fichier solution
dotnet new gitignore                           -> .gitignore pour .NET

═══ GESTION DE SOLUTION ═══
dotnet sln add MonApi/MonApi.csproj            -> Ajouter projet à solution
dotnet sln add MesTests/MesTests.csproj
dotnet sln list                                -> Lister projets de la solution

═══ COMPILER ET EXÉCUTER ═══
dotnet build                    -> Compiler le projet
dotnet build --configuration Release  -> Mode release (optimisé)
dotnet run                      -> Compiler ET exécuter
dotnet run --project MonApi/    -> Exécuter un projet spécifique
dotnet run --launch-profile "https"  -> Avec profil spécifique

═══ TESTS ═══
dotnet test                     -> Exécuter tous les tests
dotnet test --filter "Category=Unit"  -> Filtrer tests
dotnet test --logger trx        -> Rapport XML (pour CI/CD)
dotnet test --collect:"XPlat Code Coverage"  -> Couverture de code

═══ NUGET ═══
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 8.0.0
dotnet remove package NomDuPackage
dotnet restore                  -> Restaurer tous les packages
dotnet list package             -> Lister packages installés
dotnet list package --outdated  -> Packages avec mise à jour disponible

═══ PUBLIER / DÉPLOYER ═══
dotnet publish -c Release -o ./dist
dotnet publish -c Release --runtime linux-x64 --self-contained
dotnet publish -c Release -p:PublishSingleFile=true

═══ OUTILS ═══
dotnet tool install --global dotnet-ef    -> Installer EF Core tools
dotnet tool update --global dotnet-ef
dotnet ef migrations add NomMigration     -> Créer migration EF
dotnet ef database update                 -> Appliquer migrations
dotnet format                             -> Formater le code
*/


// ----------------------------------------------------------------------------
// [DOSSIER] STRUCTURE D'UN PROJET ASP.NET CORE
// ----------------------------------------------------------------------------

/*
STRUCTURE D'UN PROJET WebAPI TYPIQUE :
──────────────────────────────────────

MonApi/
├── MonApi.sln                    <- Fichier solution (gère plusieurs projets)
├── src/
│   └── MonApi/
│       ├── MonApi.csproj         <- Fichier projet (packages, configs)
│       ├── Program.cs            <- Point d'entrée
│       ├── appsettings.json      <- Configuration de base
│       ├── appsettings.Development.json  <- Config développement
│       ├── appsettings.Production.json   <- Config production
│       ├── Controllers/          <- Contrôleurs API
│       │   └── ProduitsController.cs
│       ├── Models/               <- Entités / DTOs
│       │   ├── Produit.cs
│       │   └── DTOs/
│       │       └── ProduitDto.cs
│       ├── Services/             <- Logique métier
│       │   ├── IProduitService.cs
│       │   └── ProduitService.cs
│       ├── Data/                 <- Accès données
│       │   ├── AppDbContext.cs
│       │   └── Migrations/
│       ├── Middleware/           <- Middleware personnalisé
│       └── Extensions/           <- Méthodes d'extension pour Program.cs
└── tests/
    └── MonApi.Tests/
        ├── MonApi.Tests.csproj
        ├── Controllers/
        └── Services/


FICHIER .csproj (descripteur du projet) :
─────────────────────────────────────────
*/

// MonApi.csproj - Exemple complet commenté
/*
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>    <!-- Version .NET ciblée -->
    <Nullable>enable</Nullable>                  <!-- Nullable Reference Types -->
    <ImplicitUsings>enable</ImplicitUsings>      <!-- Using implicites -->
    <RootNamespace>MonApi</RootNamespace>        <!-- Namespace racine -->
    <AssemblyName>MonApi</AssemblyName>          <!-- Nom de l'assembly -->
    <UserSecretsId>guid-unique-pour-secrets</UserSecretsId>  <!-- Pour secrets locaux -->
  </PropertyGroup>

  <ItemGroup>
    <!-- Packages NuGet -->
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
    <PackageReference Include="AutoMapper" Version="13.0.1" />
    <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
    <PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
  </ItemGroup>

  <ItemGroup>
    <!-- Référence à un autre projet dans la solution -->
    <ProjectReference Include="../MonApi.Core/MonApi.Core.csproj" />
  </ItemGroup>

</Project>
*/


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

/*
[IDEE] LE SYSTÈME DE CONFIGURATION ASP.NET CORE

COMMENT : Hiérarchie de sources qui se fusionnent
POURQUOI : Différentes configs selon l'environnement (dev, prod)
QUAND : TOUJOURS. Jamais de valeurs en dur dans le code.

SOURCES (ordre de priorité, la dernière écrase) :
1. appsettings.json (base)
2. appsettings.{Environment}.json (selon ASPNETCORE_ENVIRONMENT)
3. User Secrets (développement local, jamais commité)
4. Variables d'environnement
5. Arguments de ligne de commande (priorité maximale)
*/

// ─── appsettings.json ──────────────────────────────────────────────────────
/*
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MaDB;Trusted_Connection=true;",
    "ReadOnlyConnection": "Server=replica.localhost;..."
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Issuer": "MonApi",
    "Audience": "MonApiClients",
    "ExpiresInMinutes": 60
  },
  "Email": {
    "SmtpHost": "smtp.example.com",
    "SmtpPort": 587,
    "SenderEmail": "noreply@example.com"
  },
  "FeatureFlags": {
    "NouvelleUI": false,
    "ExportPDF": true
  }
}
*/

// ─── appsettings.Development.json ─────────────────────────────────────────
/*
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=dev.db"  // SQLite en dev
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug",  // Plus verbeux en développement
      "MonApi": "Debug"
    }
  }
}
*/

// ─── LIRE LA CONFIGURATION DANS LE CODE ────────────────────────────────────

// Méthode 1 : Options Pattern (RECOMMANDÉ)
// Définir une classe de configuration fortement typée
public class JwtOptions
{
    public const string SectionName = "Jwt"; // Nom de la section dans JSON

    public string Issuer { get; set; } = string.Empty;
    public string Audience { get; set; } = string.Empty;
    public string SecretKey { get; set; } = string.Empty;
    public int ExpiresInMinutes { get; set; } = 60;
}

public class EmailOptions
{
    public const string SectionName = "Email";

    public string SmtpHost { get; set; } = string.Empty;
    public int SmtpPort { get; set; } = 587;
    public string SenderEmail { get; set; } = string.Empty;
    public bool EnableSsl { get; set; } = true;
}

// Dans Program.cs :
// builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection(JwtOptions.SectionName));
// builder.Services.Configure<EmailOptions>(builder.Configuration.GetSection(EmailOptions.SectionName));

// Dans un service :
public class ServiceEmail
{
    private readonly EmailOptions _options;

    // IOptions<T> = Injecté par DI, lit la config une fois
    public ServiceEmail(IOptions<EmailOptions> options)
    {
        _options = options.Value;
    }

    public Task EnvoyerAsync(string destinataire, string sujet, string corps)
    {
        Console.WriteLine($"Envoi via {_options.SmtpHost}:{_options.SmtpPort}");
        return Task.CompletedTask;
    }
}

/*
VARIANTES IOptions :
IOptions<T>        -> Singleton, lu au démarrage, ne change pas
IOptionsSnapshot<T> -> Scoped, re-lu à chaque requête (si fichier change)
IOptionsMonitor<T>  -> Singleton, notifié dès que la config change (hot reload)
*/


// ----------------------------------------------------------------------------
// [MONDE] ENVIRONNEMENTS
// ----------------------------------------------------------------------------

/*
VARIABLE ASPNETCORE_ENVIRONMENT :

Development  -> Développement local, stack traces détaillées
Staging      -> Pré-production, test final
Production   -> Production, optimisations maximales

COMMENT DÉFINIR :

Windows :
  $env:ASPNETCORE_ENVIRONMENT="Development"

Linux/Mac :
  export ASPNETCORE_ENVIRONMENT="Development"

launchSettings.json (pour Visual Studio / Rider) :
  "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }

Docker :
  ENV ASPNETCORE_ENVIRONMENT=Production

UTILISATION DANS LE CODE :
*/

// Dans Program.cs ou dans du code applicatif
public class ConfigurationService
{
    private readonly IWebHostEnvironment _env;

    public ConfigurationService(IWebHostEnvironment env)
    {
        _env = env;
    }

    public void AfficherInfo()
    {
        Console.WriteLine($"Environnement: {_env.EnvironmentName}");

        if (_env.IsDevelopment())
        {
            Console.WriteLine("Mode développement: détails supplémentaires activés");
        }
        else if (_env.IsProduction())
        {
            Console.WriteLine("Mode production: optimisations actives");
        }
    }
}

// Dans Program.cs :
/*
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
    app.UseDeveloperExceptionPage(); // Stack trace détaillée
}
else
{
    app.UseExceptionHandler("/erreur");
    app.UseHsts(); // Forcer HTTPS
}
*/


// ----------------------------------------------------------------------------
// [SECURISE] USER SECRETS (Secrets de développement)
// ----------------------------------------------------------------------------

/*
[IDEE] POURQUOI USER SECRETS ?

PROBLÈME : Ne JAMAIS commiter des clés secrètes, connexions BDD, etc.
SOLUTION : User Secrets stocke les secrets hors du projet

COMMENT :
1. dotnet user-secrets init  -> Initialise (crée GUID dans .csproj)
2. dotnet user-secrets set "Jwt:SecretKey" "ma-cle-ultra-secrete"
3. dotnet user-secrets set "ConnectionStrings:DefaultConnection" "..."

STOCKAGE :
Windows: %APPDATA%\Microsoft\UserSecrets\{guid}\secrets.json
Linux/Mac: ~/.microsoft/usersecrets/{guid}/secrets.json

[ATTENTION] User Secrets = développement SEULEMENT
En production -> Variables d'environnement, Azure Key Vault, AWS Secrets Manager
*/

/*
[DOCS] RÉCAPITULATIF CHAPITRE 2

[OK] CLI .NET : new, build, run, test, publish, ef
[OK] Structure de projet : Organisation recommandée
[OK] .csproj : PackageReference, PropertyGroup
[OK] Configuration : appsettings.json, Options Pattern
[OK] Environnements : Development, Staging, Production
[OK] User Secrets : Pour ne pas commiter les secrets
*/


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

/*
ÉNONCÉ :

1. Créez un nouveau projet WebAPI :
   dotnet new webapi -n BiblioAPI --use-controllers -o BiblioAPI

2. Créez une classe de configuration fortement typée "BiblioOptions" avec :
   - NomBibliotheque : string
   - MaxLivresParPage : int (default: 20)
   - AutoriserInscriptions : bool
   - BaseUrlImages : string

3. Ajoutez la section "Bibliotheque" dans appsettings.json
   et appsettings.Development.json (avec des valeurs différentes)

4. Créez un service "BiblioInfoService" qui utilise IOptions<BiblioOptions>
   et expose une méthode ObtenirInfo() retournant les informations configurées.

5. Créez un endpoint /api/info qui retourne ces informations.
*/

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

// 2. Classe de configuration
// Options/BiblioOptions.cs
public class BiblioOptions
{
    public const string SectionName = "Bibliotheque";

    public string NomBibliotheque { get; set; } = "Bibliothèque Municipale";
    public int MaxLivresParPage { get; set; } = 20;
    public bool AutoriserInscriptions { get; set; } = true;
    public string BaseUrlImages { get; set; } = "https://localhost:5001/images/";
}

// 3. appsettings.json
/*
{
  "Bibliotheque": {
    "NomBibliotheque": "Bibliothèque Principale",
    "MaxLivresParPage": 20,
    "AutoriserInscriptions": true,
    "BaseUrlImages": "https://api.monsite.com/images/"
  }
}
*/

// 3. appsettings.Development.json
/*
{
  "Bibliotheque": {
    "NomBibliotheque": "Bibliothèque DEV",
    "MaxLivresParPage": 5,
    "AutoriserInscriptions": true,
    "BaseUrlImages": "https://localhost:5001/images/"
  }
}
*/

// 4. Service
// Services/BiblioInfoService.cs
public class BiblioInfoDto
{
    public string NomBibliotheque { get; set; } = string.Empty;
    public int MaxLivresParPage { get; set; }
    public bool InscriptionsOuvertes { get; set; }
    public string BaseUrlImages { get; set; } = string.Empty;
    public string Version { get; set; } = "1.0.0";
    public DateTime DateHeure { get; set; } = DateTime.UtcNow;
}

public interface IBiblioInfoService
{
    BiblioInfoDto ObtenirInfo();
}

public class BiblioInfoService : IBiblioInfoService
{
    private readonly BiblioOptions _options;

    public BiblioInfoService(IOptions<BiblioOptions> options)
    {
        _options = options.Value;
    }

    public BiblioInfoDto ObtenirInfo()
    {
        return new BiblioInfoDto
        {
            NomBibliotheque = _options.NomBibliotheque,
            MaxLivresParPage = _options.MaxLivresParPage,
            InscriptionsOuvertes = _options.AutoriserInscriptions,
            BaseUrlImages = _options.BaseUrlImages,
            DateHeure = DateTime.UtcNow
        };
    }
}

// 5. Controller
// Controllers/InfoController.cs
/*
[ApiController]
[Route("api/[controller]")]
public class InfoController : ControllerBase
{
    private readonly IBiblioInfoService _infoService;

    public InfoController(IBiblioInfoService infoService)
    {
        _infoService = infoService;
    }

    [HttpGet]
    [ProducesResponseType(typeof(BiblioInfoDto), StatusCodes.Status200OK)]
    public IActionResult ObtenirInfo()
    {
        var info = _infoService.ObtenirInfo();
        return Ok(info);
    }
}
*/

// Program.cs - Enregistrement
/*
var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<BiblioOptions>(
    builder.Configuration.GetSection(BiblioOptions.SectionName));
builder.Services.AddScoped<IBiblioInfoService, BiblioInfoService>();

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapControllers();
app.Run();
*/


// ============================================================================
// [GUIDE] CHAPITRE 3 : ARCHITECTURE ASP.NET CORE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le rôle de Kestrel
[OK] Maîtriser le pipeline de middleware
[OK] Comprendre le cycle requête/réponse
[OK] Utiliser l'injection de dépendances (DI) native
*/


// ----------------------------------------------------------------------------
// [RAPIDE] KESTREL : LE SERVEUR WEB
// ----------------------------------------------------------------------------

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

COMMENT : Serveur HTTP intégré, cross-platform
POURQUOI : Performance (l'un des serveurs HTTP les plus rapides du monde)
QUAND : Toujours utilisé en interne, même derrière Nginx/IIS

ARCHITECTURE :

Internet -> [Nginx/IIS (reverse proxy)] -> Kestrel -> App ASP.NET Core

POURQUOI UN REVERSE PROXY ?
- SSL/TLS termination
- Load balancing
- Fichiers statiques
- Rate limiting
- Sécurité additionnelle
*/

// Configuration Kestrel dans Program.cs
/*
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    // Port HTTP
    options.ListenLocalhost(5000);

    // Port HTTPS
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.UseHttps("certificat.pfx", "motdepasse");
    });

    // Toutes interfaces réseau
    options.Listen(System.Net.IPAddress.Any, 8080);

    // Limites
    options.Limits.MaxConcurrentConnections = 1000;
    options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
    options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});
*/


// ----------------------------------------------------------------------------
// [SYNC] PIPELINE DE MIDDLEWARE
// ----------------------------------------------------------------------------

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

COMMENT :
  Une chaîne de composants qui traitent la requête HTTP
  Chaque middleware peut : traiter, transformer, court-circuiter

POURQUOI :
  Séparation des responsabilités (cross-cutting concerns)
  Ajout facile de fonctionnalités sans toucher au code métier

QUAND :
  Logging, Authentification, CORS, Compression, Erreurs, etc.

VISUALISATION :

Request ──[BLACK_RIGHT-POINTING_POINTER]  [Auth] -> [Logging] -> [CORS] -> [Routing] -> [Controller]
Response [BLACK_LEFT-POINTING_POINTER]──  [Auth] <- [Logging] <- [CORS] <- [Routing] <- [Controller]

ORDRE DES MIDDLEWARE (CRITIQUE !) :
1. Exception Handler
2. HTTPS Redirection
3. HSTS
4. Static Files
5. Routing
6. CORS
7. Authentication
8. Authorization
9. Custom middleware
10. Endpoints (Controllers)
*/

// ─── MIDDLEWARE INTÉGRÉS ───────────────────────────────────────────────────
/*
Dans Program.cs, l'ORDRE est crucial :

var app = builder.Build();

// 1. Gestion des exceptions (TOUJOURS EN PREMIER)
if (app.Environment.IsDevelopment())
    app.UseDeveloperExceptionPage();
else
    app.UseExceptionHandler("/erreur");

// 2. HTTPS
app.UseHttpsRedirection();

// 3. Fichiers statiques
app.UseStaticFiles();

// 4. Routing
app.UseRouting();

// 5. CORS (avant Auth !)
app.UseCors("MaPolicy");

// 6. Authentification (AVANT Autorisation)
app.UseAuthentication();

// 7. Autorisation
app.UseAuthorization();

// 8. Endpoints
app.MapControllers();
app.Run();
*/

// ─── CRÉER UN MIDDLEWARE PERSONNALISÉ ─────────────────────────────────────

// Middleware de journalisation des requêtes
public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<LoggingMiddleware> _logger;

    // RequestDelegate = le middleware SUIVANT dans la chaîne
    public LoggingMiddleware(RequestDelegate next, ILogger<LoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    // MÉTHODE OBLIGATOIRE : InvokeAsync (ou Invoke)
    public async Task InvokeAsync(HttpContext context)
    {
        var debut = DateTime.UtcNow;
        var methode = context.Request.Method;
        var path = context.Request.Path;

        _logger.LogInformation("-> {Methode} {Path}", methode, path);

        // APPELER LE MIDDLEWARE SUIVANT
        await _next(context);

        // CODE EXÉCUTÉ APRÈS les middlewares suivants (réponse)
        var duree = (DateTime.UtcNow - debut).TotalMilliseconds;
        var status = context.Response.StatusCode;
        _logger.LogInformation("<- {Status} {Path} ({Duree}ms)", status, path, duree);
    }
}

// Extension method pour enregistrement propre
public static class LoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseLoggingPersonnalise(this IApplicationBuilder app)
    {
        return app.UseMiddleware<LoggingMiddleware>();
    }
}

// Middleware de gestion des erreurs global
public class GestionErreurMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GestionErreurMiddleware> _logger;

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

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

    private static async Task GererErreurAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = exception switch
        {
            NotFoundException => StatusCodes.Status404NotFound,
            ValidationException => StatusCodes.Status400BadRequest,
            UnauthorizedException => StatusCodes.Status401Unauthorized,
            _ => StatusCodes.Status500InternalServerError
        };

        var reponse = new
        {
            Status = context.Response.StatusCode,
            Message = exception.Message,
            Timestamp = DateTime.UtcNow
        };

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

// Exceptions personnalisées
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class ValidationException : Exception
{
    public ValidationException(string message) : base(message) { }
}

public class UnauthorizedException : Exception
{
    public UnauthorizedException(string message) : base(message) { }
}

// Middleware inline (pour cas simples)
/*
// Dans Program.cs :
app.Use(async (context, next) =>
{
    // Avant le prochain middleware
    context.Response.Headers.Add("X-Custom-Header", "MonApi");
    await next();
    // Après le prochain middleware
});

// Middleware qui court-circuite (ne passe PAS au suivant)
app.Use(async (context, next) =>
{
    if (context.Request.Headers["X-Api-Key"] != "cle-valide")
    {
        context.Response.StatusCode = 401;
        await context.Response.WriteAsync("Clé API invalide");
        return; // Court-circuit - pas d'appel à next()
    }
    await next();
});

// Map : Middleware pour un chemin spécifique
app.Map("/health", healthApp =>
{
    healthApp.Run(async context =>
    {
        await context.Response.WriteAsync("Healthy!");
    });
});
*/


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

/*
[IDEE] QU'EST-CE QUE LA DÉPENDANCE INJECTION ?

PROBLÈME SANS DI :
*/
// [X] COUPLAGE FORT - Difficile à tester, difficile à maintenir
public class CommandesControllerSansDI
{
    private readonly ServiceCommandes _service; // Dépendance concrète

    public CommandesControllerSansDI()
    {
        // [X] Crée lui-même sa dépendance
        var repo = new RepositoireCommandes();
        _service = new ServiceCommandes(repo);
    }
}

/*
AVEC DI :
- Les dépendances sont DÉCLARÉES (pas créées)
- Le conteneur DI les FOURNIT automatiquement
- Couplage faible = testable, maintenable

DURÉES DE VIE (LIFETIMES) :

Singleton  = Une SEULE instance pour toute l'application
Scoped     = Une instance par requête HTTP <- PLUS COURANT pour services
Transient  = Nouvelle instance à chaque injection <- Pour services légers

RÈGLE : Ne jamais injecter Scoped dans Singleton (Captive Dependency Problem)
*/

// ─── INTERFACES ET IMPLÉMENTATIONS ─────────────────────────────────────────

public interface IServiceProduit
{
    Task<IEnumerable<ProduitDto2>> ObtenirTousAsync();
    Task<ProduitDto2?> ObtenirParIdAsync(int id);
    Task<ProduitDto2> CreerAsync(CreerProduitRequest request);
    Task<bool> SupprimerAsync(int id);
}

public record ProduitDto2(int Id, string Nom, decimal Prix, string Categorie);
public record CreerProduitRequest(string Nom, decimal Prix, string Categorie);

public class ServiceProduitImpl : IServiceProduit
{
    private readonly IRepositoireProduit _repo;
    private readonly ILogger<ServiceProduitImpl> _logger;

    // [OK] DI : Dépendances déclarées, pas créées
    public ServiceProduitImpl(IRepositoireProduit repo, ILogger<ServiceProduitImpl> logger)
    {
        _repo = repo;
        _logger = logger;
    }

    public async Task<IEnumerable<ProduitDto2>> ObtenirTousAsync()
    {
        _logger.LogInformation("Récupération de tous les produits");
        var produits = await _repo.ObtenirTousAsync();
        return produits.Select(p => new ProduitDto2(p.Id, p.Nom, p.Prix, p.Categorie));
    }

    public async Task<ProduitDto2?> ObtenirParIdAsync(int id)
    {
        var produit = await _repo.ObtenirParIdAsync(id);
        if (produit is null) return null;
        return new ProduitDto2(produit.Id, produit.Nom, produit.Prix, produit.Categorie);
    }

    public async Task<ProduitDto2> CreerAsync(CreerProduitRequest request)
    {
        var entite = new EntiteProduit(0, request.Nom, request.Prix, request.Categorie);
        var cree = await _repo.AjouterAsync(entite);
        return new ProduitDto2(cree.Id, cree.Nom, cree.Prix, cree.Categorie);
    }

    public async Task<bool> SupprimerAsync(int id)
    {
        return await _repo.SupprimerAsync(id);
    }
}

public interface IRepositoireProduit
{
    Task<IEnumerable<EntiteProduit>> ObtenirTousAsync();
    Task<EntiteProduit?> ObtenirParIdAsync(int id);
    Task<EntiteProduit> AjouterAsync(EntiteProduit produit);
    Task<bool> SupprimerAsync(int id);
}

public record EntiteProduit(int Id, string Nom, decimal Prix, string Categorie);

public class RepositoireProduitMemoire : IRepositoireProduit
{
    private readonly List<EntiteProduit> _produits = new()
    {
        new(1, "Laptop", 999.99m, "Informatique"),
        new(2, "Souris", 29.99m, "Informatique"),
    };
    private int _nextId = 3;

    public Task<IEnumerable<EntiteProduit>> ObtenirTousAsync()
        => Task.FromResult(_produits.AsEnumerable());

    public Task<EntiteProduit?> ObtenirParIdAsync(int id)
        => Task.FromResult(_produits.FirstOrDefault(p => p.Id == id));

    public Task<EntiteProduit> AjouterAsync(EntiteProduit produit)
    {
        var nouveauProduit = produit with { Id = _nextId++ };
        _produits.Add(nouveauProduit);
        return Task.FromResult(nouveauProduit);
    }

    public Task<bool> SupprimerAsync(int id)
    {
        var produit = _produits.FirstOrDefault(p => p.Id == id);
        if (produit is null) return Task.FromResult(false);
        _produits.Remove(produit);
        return Task.FromResult(true);
    }
}

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

builder.Services.AddScoped<IServiceProduit, ServiceProduitImpl>();
builder.Services.AddScoped<IRepositoireProduit, RepositoireProduitMemoire>();

// Transient (nouvelle instance à chaque injection)
builder.Services.AddTransient<IEmailService, EmailService>();

// Singleton (une seule instance partagée)
builder.Services.AddSingleton<ICacheService, CacheMemoire>();

// Factory (quand la création est complexe)
builder.Services.AddScoped<IMonService>(provider =>
{
    var config = provider.GetRequiredService<IOptions<MonOptions>>().Value;
    var logger = provider.GetRequiredService<ILogger<MonService>>();
    return new MonService(config, logger);
});
*/


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

/*
ÉNONCÉ :

Créez un mini-système de middleware et DI pour une API de gestion d'étudiants :

1. Créez un record "Etudiant" (Id, Nom, Prenom, Email, DateInscription, Note)

2. Créez l'interface IEtudiantRepository avec :
   - ObtenirTousAsync()
   - ObtenirParIdAsync(int id)
   - AjouterAsync(Etudiant e)
   - MettreAJourNoteAsync(int id, double note)

3. Implémentez EtudiantRepositoryMemoire

4. Créez un middleware "TempsReponseMiddleware" qui ajoute
   un header "X-Response-Time" à chaque réponse avec la durée en ms

5. Créez un service IEtudiantService avec méthode
   ObtenirMoyenneAsync() qui retourne la moyenne des notes
*/

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

// 1. Record Etudiant
public record Etudiant(
    int Id,
    string Nom,
    string Prenom,
    string Email,
    DateTime DateInscription,
    double? Note = null
);

// 2 & 3. Repository
public interface IEtudiantRepository
{
    Task<IEnumerable<Etudiant>> ObtenirTousAsync();
    Task<Etudiant?> ObtenirParIdAsync(int id);
    Task<Etudiant> AjouterAsync(Etudiant etudiant);
    Task<Etudiant?> MettreAJourNoteAsync(int id, double note);
}

public class EtudiantRepositoryMemoire : IEtudiantRepository
{
    private readonly List<Etudiant> _etudiants = new()
    {
        new(1, "Martin", "Alice", "alice@univ.fr", DateTime.Now.AddMonths(-6), 15.5),
        new(2, "Dupont", "Bob", "bob@univ.fr", DateTime.Now.AddMonths(-8), 12.0),
        new(3, "Leblanc", "Clara", "clara@univ.fr", DateTime.Now.AddMonths(-3)),
    };
    private int _nextId = 4;

    public Task<IEnumerable<Etudiant>> ObtenirTousAsync()
        => Task.FromResult(_etudiants.AsEnumerable());

    public Task<Etudiant?> ObtenirParIdAsync(int id)
        => Task.FromResult(_etudiants.FirstOrDefault(e => e.Id == id));

    public Task<Etudiant> AjouterAsync(Etudiant etudiant)
    {
        var nouvel = etudiant with { Id = _nextId++ };
        _etudiants.Add(nouvel);
        return Task.FromResult(nouvel);
    }

    public Task<Etudiant?> MettreAJourNoteAsync(int id, double note)
    {
        var index = _etudiants.FindIndex(e => e.Id == id);
        if (index < 0) return Task.FromResult<Etudiant?>(null);

        var mise_a_jour = _etudiants[index] with { Note = note };
        _etudiants[index] = mise_a_jour;
        return Task.FromResult<Etudiant?>(mise_a_jour);
    }
}

// 4. Middleware
public class TempsReponseMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
        var chrono = System.Diagnostics.Stopwatch.StartNew();
        await _next(context);
        chrono.Stop();
        context.Response.Headers.TryAdd("X-Response-Time", $"{chrono.ElapsedMilliseconds}ms");
    }
}

// 5. Service
public interface IEtudiantService
{
    Task<double?> ObtenirMoyenneAsync();
    Task<IEnumerable<Etudiant>> ObtenirTop3Async();
}

public class EtudiantService : IEtudiantService
{
    private readonly IEtudiantRepository _repo;

    public EtudiantService(IEtudiantRepository repo) => _repo = repo;

    public async Task<double?> ObtenirMoyenneAsync()
    {
        var tous = await _repo.ObtenirTousAsync();
        var notes = tous.Where(e => e.Note.HasValue).Select(e => e.Note!.Value).ToList();
        return notes.Any() ? notes.Average() : null;
    }

    public async Task<IEnumerable<Etudiant>> ObtenirTop3Async()
    {
        var tous = await _repo.ObtenirTousAsync();
        return tous
            .Where(e => e.Note.HasValue)
            .OrderByDescending(e => e.Note)
            .Take(3);
    }
}

/*
Program.cs complet :

var builder = WebApplication.CreateBuilder(args);

// DI
builder.Services.AddScoped<IEtudiantRepository, EtudiantRepositoryMemoire>();
builder.Services.AddScoped<IEtudiantService, EtudiantService>();
builder.Services.AddControllers();

var app = builder.Build();

// Middleware (ordre important!)
app.UseMiddleware<TempsReponseMiddleware>();
app.UseHttpsRedirection();
app.UseRouting();
app.MapControllers();

app.Run();
*/


// ============================================================================
// [DOCS] RÉCAPITULATIF PARTIE 1 COMPLÈTE
// ============================================================================

/*
[BRAVO] PARTIE 1 TERMINÉE !

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 1 : C# Avancé
[OK] OOP : Héritage, interfaces, génériques, extension methods
[OK] Records : Immuabilité, égalité valeur, with expression
[OK] LINQ : Toutes les opérations de requête
[OK] Async/Await : Task, Task<T>, WhenAll, CancellationToken
[OK] Delegates : Func, Action, Events, Lambdas
[OK] Nullable : string?, ?., ??, pattern matching

Chapitre 2 : Écosystème
[OK] CLI .NET : Toutes commandes importantes
[OK] Structure projet : Organisation recommandée
[OK] Configuration : Options Pattern, environnements
[OK] Secrets : User Secrets pour développement local

Chapitre 3 : Architecture
[OK] Kestrel : Serveur HTTP performant
[OK] Middleware Pipeline : Ordre, création de middleware
[OK] Injection de Dépendances : Lifetimes, enregistrement
[OK] Cycle requête/réponse

-> PROCHAINE ÉTAPE : Partie 2 - Web API
Vous apprendrez à créer des API REST complètes avec Controllers
et Minimal APIs. [RAPIDE]
*/

// ============================================================================
// [LIVRE] ASP.NET CORE - PARTIE 2 : WEB API (FONDAMENTAL)
// ============================================================================
//
// [OBJECTIF] CETTE PARTIE COUVRE :
// - Chapitre 4 : Création d'API REST avec Controllers
// - Chapitre 5 : Minimal APIs
// - Chapitre 6 : Validation & Gestion d'Erreurs
// - Chapitre 7 : Filtres & Middleware Avancé
//
// [TEMPS] TEMPS : ~8-10 heures
// [DOCS] PRÉREQUIS : Partie 1 complétée
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 4 : CRÉATION D'API REST AVEC CONTROLLERS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre l'architecture REST
[OK] Créer des Controllers API complets
[OK] Maîtriser le routing (attributs, conventions)
[OK] Gérer le Model Binding et la sérialisation
[OK] Retourner les bons types de réponses (IActionResult)
[OK] Documenter avec Swagger/OpenAPI
*/


// ----------------------------------------------------------------------------
// [WEB] ARCHITECTURE REST - LES RÈGLES DU JEU
// ----------------------------------------------------------------------------

/*
REST = Representational State Transfer

[IDEE] PRINCIPES CLÉS :

1. RESSOURCES = Noms (pas verbes !)
   [OK] /api/produits           -> Collection de produits
   [OK] /api/produits/5         -> Produit avec Id=5
   [OK] /api/produits/5/photos  -> Photos du produit 5
   [X] /api/obtenirProduit     -> Verbe interdit en REST

2. VERBES HTTP = Actions
   GET    -> Lire (Read)      -> Idempotent
   POST   -> Créer (Create)   -> Non-idempotent
   PUT    -> Remplacer (Full Update) -> Idempotent
   PATCH  -> Modifier partiellement -> Non-idempotent
   DELETE -> Supprimer        -> Idempotent

3. CODES DE STATUT HTTP = Résultats
   200 OK              -> Succès général (GET, PUT, PATCH)
   201 Created         -> Création réussie (POST)
   204 No Content      -> Succès sans corps (DELETE, PUT)
   400 Bad Request     -> Requête invalide (validation)
   401 Unauthorized    -> Non authentifié
   403 Forbidden       -> Authentifié mais non autorisé
   404 Not Found       -> Ressource introuvable
   409 Conflict        -> Conflit (doublon, état invalide)
   422 Unprocessable   -> Données valides mais non traitables
   500 Internal Error  -> Erreur serveur

4. CONVENTION DES URLS REST :
   GET    /api/produits           -> Liste tous les produits
   POST   /api/produits           -> Crée un nouveau produit
   GET    /api/produits/{id}      -> Retourne le produit {id}
   PUT    /api/produits/{id}      -> Remplace le produit {id}
   PATCH  /api/produits/{id}      -> Modifie partiellement le produit {id}
   DELETE /api/produits/{id}      -> Supprime le produit {id}
   GET    /api/produits/{id}/avis -> Avis du produit {id}
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] ANATOMY D'UN CONTROLLER API
// ----------------------------------------------------------------------------

using Microsoft.AspNetCore.Mvc;
using System.Net.Mime;

/*
STRUCTURE MINIMALE D'UN CONTROLLER :
*/

[ApiController]                  // <- ATTRIBUT CLÉ #1 : Active comportements API
[Route("api/[controller]")]      // <- ATTRIBUT CLÉ #2 : URL de base (/api/produits)
[Produces(MediaTypeNames.Application.Json)] // Produit du JSON
[Consumes(MediaTypeNames.Application.Json)] // Consomme du JSON
public class ProduitsController : ControllerBase  // ControllerBase (pas Controller!)
{
    /*
    ControllerBase = Classe de base pour API
    Controller     = Hérite de ControllerBase + support Vues (MVC)
    -> Pour une API pure : TOUJOURS ControllerBase
    */

    private readonly IServiceProduitApi _service;
    private readonly ILogger<ProduitsController> _logger;

    // DI : Injection par constructeur
    public ProduitsController(IServiceProduitApi service, ILogger<ProduitsController> logger)
    {
        _service = service;
        _logger = logger;
    }

    // ─── GET COLLECTION ────────────────────────────────────────────────────
    [HttpGet]                                    // GET /api/produits
    [ProducesResponseType(typeof(IEnumerable<ProduitResponse>), StatusCodes.Status200OK)]
    public async Task<ActionResult<IEnumerable<ProduitResponse>>> ObtenirTous(
        [FromQuery] string? categorie = null,    // ?categorie=Informatique
        [FromQuery] int page = 1,                // ?page=2
        [FromQuery] int taille = 20)             // ?taille=10
    {
        _logger.LogInformation("Récupération produits - Page {Page}, Taille {Taille}", page, taille);
        var produits = await _service.ObtenirTousAsync(categorie, page, taille);
        return Ok(produits);
    }

    // ─── GET PAR ID ─────────────────────────────────────────────────────────
    [HttpGet("{id:int}")]                        // GET /api/produits/5
    [ProducesResponseType(typeof(ProduitResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<ProduitResponse>> ObtenirParId(int id)
    {
        var produit = await _service.ObtenirParIdAsync(id);
        if (produit is null)
        {
            return NotFound(new { Message = $"Produit avec l'id {id} introuvable." });
        }
        return Ok(produit);
    }

    // ─── POST (CRÉER) ───────────────────────────────────────────────────────
    [HttpPost]                                   // POST /api/produits
    [ProducesResponseType(typeof(ProduitResponse), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<ProduitResponse>> Creer(
        [FromBody] CreerProduitDto dto)          // Corps JSON
    {
        var cree = await _service.CreerAsync(dto);

        // 201 Created avec header Location: /api/produits/5
        return CreatedAtAction(
            nameof(ObtenirParId),
            new { id = cree.Id },
            cree
        );
    }

    // ─── PUT (REMPLACER ENTIÈREMENT) ────────────────────────────────────────
    [HttpPut("{id:int}")]                        // PUT /api/produits/5
    [ProducesResponseType(typeof(ProduitResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<ProduitResponse>> Remplacer(int id, [FromBody] ModifierProduitDto dto)
    {
        var mis_a_jour = await _service.RemplacerAsync(id, dto);
        if (mis_a_jour is null) return NotFound();
        return Ok(mis_a_jour);
    }

    // ─── PATCH (MODIFIER PARTIELLEMENT) ────────────────────────────────────
    [HttpPatch("{id:int}/prix")]                 // PATCH /api/produits/5/prix
    [ProducesResponseType(typeof(ProduitResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<ProduitResponse>> ModifierPrix(int id, [FromBody] ModifierPrixDto dto)
    {
        var mis_a_jour = await _service.ModifierPrixAsync(id, dto.NouveauPrix);
        if (mis_a_jour is null) return NotFound();
        return Ok(mis_a_jour);
    }

    // ─── DELETE ─────────────────────────────────────────────────────────────
    [HttpDelete("{id:int}")]                     // DELETE /api/produits/5
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Supprimer(int id)
    {
        var supprime = await _service.SupprimerAsync(id);
        if (!supprime) return NotFound();
        return NoContent(); // 204 - Succès sans corps
    }

    // ─── ROUTE IMBRIQUÉE ────────────────────────────────────────────────────
    [HttpGet("{id:int}/avis")]                   // GET /api/produits/5/avis
    [ProducesResponseType(typeof(IEnumerable<AvisDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<IEnumerable<AvisDto>>> ObtenirAvis(int id)
    {
        var avis = await _service.ObtenirAvisAsync(id);
        return Ok(avis);
    }
}

// ─── DTOs (Data Transfer Objects) ─────────────────────────────────────────

public record ProduitResponse(
    int Id,
    string Nom,
    decimal Prix,
    string Categorie,
    bool EnStock,
    DateTime DateCreation
);

public record CreerProduitDto(
    string Nom,
    decimal Prix,
    string Categorie
);

public record ModifierProduitDto(
    string Nom,
    decimal Prix,
    string Categorie,
    bool EnStock
);

public record ModifierPrixDto(decimal NouveauPrix);

public record AvisDto(int Id, string Auteur, string Texte, int Note);

// Interface service
public interface IServiceProduitApi
{
    Task<IEnumerable<ProduitResponse>> ObtenirTousAsync(string? categorie, int page, int taille);
    Task<ProduitResponse?> ObtenirParIdAsync(int id);
    Task<ProduitResponse> CreerAsync(CreerProduitDto dto);
    Task<ProduitResponse?> RemplacerAsync(int id, ModifierProduitDto dto);
    Task<ProduitResponse?> ModifierPrixAsync(int id, decimal nouveauPrix);
    Task<bool> SupprimerAsync(int id);
    Task<IEnumerable<AvisDto>> ObtenirAvisAsync(int id);
}


// ----------------------------------------------------------------------------
// [LIEN] ROUTING AVANCÉ
// ----------------------------------------------------------------------------

/*
[IDEE] TYPES DE ROUTING

1. ATTRIBUTE ROUTING (recommandé pour API)
   Chaque action a sa route explicite en attribut

2. CONVENTION ROUTING (pour MVC avec vues)
   Routes définies centralement

ROUTE TEMPLATES :
[Route("api/v{version:apiVersion}/[controller]")]  -> Versioning
[Route("api/[controller]")]                         -> [controller] = nom de la classe sans "Controller"
*/

// Exemples de contraintes de route
/*
{id:int}        -> Entier uniquement
{id:long}       -> Long uniquement
{id:guid}       -> GUID uniquement
{id:min(1)}     -> Entier >= 1
{id:max(100)}   -> Entier <= 100
{id:range(1,100)} -> Entre 1 et 100
{nom:alpha}     -> Lettres uniquement
{nom:length(3)} -> Exactement 3 caractères
{nom:minlength(3):maxlength(50)} -> Entre 3 et 50 chars
{nom:regex(^[a-z]+$)} -> Correspond à la regex
*/

[ApiController]
[Route("api/v{version:int}/[controller]")]
public class CategoriesController : ControllerBase
{
    // GET api/v1/categories
    [HttpGet]
    public IActionResult ObtenirV1(int version)
    {
        return Ok(new { Version = version, Categories = new[] { "A", "B" } });
    }

    // GET api/v1/categories/nom-produit -> slug-like
    [HttpGet("{nom:alpha:minlength(2)}")]
    public IActionResult ObtenirParNom(string nom)
    {
        return Ok(new { Nom = nom });
    }
}


// ----------------------------------------------------------------------------
// [ENTREE] MODEL BINDING - RÉCUPÉRER LES DONNÉES
// ----------------------------------------------------------------------------

/*
[IDEE] MODEL BINDING = Transformation automatique de la requête HTTP en objets C#

SOURCES DE DONNÉES :
[FromRoute]      -> /api/produits/{id}         -> Dans l'URL
[FromQuery]      -> ?page=1&taille=20          -> Query string
[FromBody]       -> Corps JSON/XML             -> Corps de la requête
[FromHeader]     -> Authorization: Bearer ...  -> En-têtes HTTP
[FromForm]       -> multipart/form-data        -> Formulaire
[FromServices]   -> Injecter depuis DI         -> Conteneur DI
*/

[HttpPost("recherche")]
public async Task<IActionResult> Rechercher(
    [FromQuery] string? terme,               // ?terme=laptop
    [FromQuery] decimal? prixMin,            // ?prixMin=100
    [FromQuery] decimal? prixMax,            // ?prixMax=500
    [FromHeader(Name = "X-Lang")] string? langue, // Header X-Lang: fr
    [FromBody] FiltresAvances filtres)       // Corps JSON
{
    // ...
    return Ok();
}

public record FiltresAvances(
    string[] Categories,
    bool? EnStock,
    string? Tri
);

// ─── BINDING DE REQUÊTE COMPLEXE ───────────────────────────────────────────

// Classe de requête avec binding automatique
public class RequeteProduits
{
    [FromQuery(Name = "q")]
    public string? Terme { get; set; }

    [FromQuery]
    public decimal? PrixMin { get; set; }

    [FromQuery]
    public decimal? PrixMax { get; set; }

    [FromQuery]
    public string? Categorie { get; set; }

    [FromQuery]
    public string? Tri { get; set; } = "nom";

    [FromQuery]
    public int Page { get; set; } = 1;

    [FromQuery(Name = "ps")]
    public int PageSize { get; set; } = 20;
}

[HttpGet("v2")]
public IActionResult RechercherV2([FromQuery] RequeteProduits requete)
{
    return Ok(requete);
}


// ----------------------------------------------------------------------------
// [SORTIE] TYPES DE RÉPONSES - IActionResult vs ActionResult<T>
// ----------------------------------------------------------------------------

/*
[IDEE] QUAND UTILISER QUOI ?

IActionResult :
  - Flexible, peut retourner n'importe quoi
  - Swagger ne sait pas le type exact
  - Utiliser avec [ProducesResponseType]

ActionResult<T> :
  - Fortement typé (Swagger génère la doc automatiquement)
  - RECOMMANDÉ pour la plupart des cas
*/

// ActionResult<T> : Swagger connaît le type de retour !
[HttpGet("{id:int}")]
public async Task<ActionResult<ProduitResponse>> Get(int id)
{
    var produit = await _service.ObtenirParIdAsync(id);

    // Les deux syntaxes sont équivalentes :
    if (produit is null) return NotFound();              // Implicit conversion
    return Ok(produit);                                   // Explicit Ok(T)
    // return produit;                                    // Aussi valide (retour direct)
}

// ─── MÉTHODES HELPER DE CONTROLLERBASE ─────────────────────────────────────
/*
// 2xx Succès
Ok(object value)              -> 200 OK avec corps
Created(uri, value)           -> 201 Created
CreatedAtAction(action, value)-> 201 Created avec Location header
Accepted()                    -> 202 Accepted
NoContent()                   -> 204 No Content

// 3xx Redirections
Redirect(url)                 -> 302 Redirect
RedirectPermanent(url)        -> 301 Permanent Redirect

// 4xx Erreur client
BadRequest()                  -> 400 Bad Request
BadRequest(modelState)        -> 400 avec erreurs
Unauthorized()                -> 401 Unauthorized
Forbid()                      -> 403 Forbidden
NotFound()                    -> 404 Not Found
NotFound(value)               -> 404 avec corps
Conflict()                    -> 409 Conflict
UnprocessableEntity()         -> 422 Unprocessable Entity

// 5xx Erreur serveur
StatusCode(500)               -> Code personnalisé
Problem(detail, ...)          -> RFC 7807 ProblemDetails
*/


// ----------------------------------------------------------------------------
// [FICHIER] PAGINATION (ESSENTIEL POUR LA PRODUCTION)
// ----------------------------------------------------------------------------

// Classe générique de réponse paginée
public class PagedResult<T>
{
    public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
    public int Page { get; set; }
    public int PageSize { get; set; }
    public int TotalItems { get; set; }
    public int TotalPages => (int)Math.Ceiling((double)TotalItems / PageSize);
    public bool HasNext => Page < TotalPages;
    public bool HasPrevious => Page > 1;
}

// Paramètres de pagination réutilisables
public class PaginationParams
{
    private const int MaxPageSize = 100;
    private int _pageSize = 20;

    public int Page { get; set; } = 1;
    public int PageSize
    {
        get => _pageSize;
        set => _pageSize = value > MaxPageSize ? MaxPageSize : value;
    }
}

[HttpGet("paged")]
[ProducesResponseType(typeof(PagedResult<ProduitResponse>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResult<ProduitResponse>>> ObtenirPagine(
    [FromQuery] PaginationParams pagination,
    [FromQuery] string? categorie = null)
{
    var result = await _service.ObtenirPagineAsync(pagination, categorie);
    return Ok(result);
}


// ----------------------------------------------------------------------------
// [OUTIL] CONFIGURATION SWAGGER / OPENAPI
// ----------------------------------------------------------------------------

/*
SWAGGER = Outil de documentation et test interactif des APIs
OpenAPI = Standard qui décrit l'API (format YAML/JSON)
*/

// Dans Program.cs :
/*
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "Mon API",
        Version = "v1",
        Description = "API de gestion des produits",
        Contact = new OpenApiContact
        {
            Name = "Mon Équipe",
            Email = "api@example.com"
        },
        License = new OpenApiLicense { Name = "MIT" }
    });

    // Inclure les commentaires XML pour la documentation
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    options.IncludeXmlComments(xmlPath);

    // Support d'authentification JWT dans Swagger
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "Entrez: Bearer {votre-jwt-token}"
    });
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } },
            Array.Empty<string>()
        }
    });
});
*/

/// <summary>
/// Récupère un produit par son identifiant.
/// </summary>
/// <param name="id">Identifiant unique du produit</param>
/// <returns>Le produit correspondant</returns>
/// <response code="200">Retourne le produit</response>
/// <response code="404">Produit non trouvé</response>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ProduitResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<ActionResult<ProduitResponse>> GetWithDocs(int id) => throw new NotImplementedException();


// ============================================================================
// [COURS] EXERCICE PRATIQUE 4 - AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ :

Créez un Controller REST complet "EtudiantsController" pour une API scolaire.

Ressource : Etudiant (Id, Nom, Prenom, Email, Filiere, Moyenne)

Endpoints à implémenter :
1. GET /api/etudiants                  -> Liste paginée avec filtre optionnel ?filiere=
2. GET /api/etudiants/{id}             -> Étudiant par Id (404 si absent)
3. POST /api/etudiants                 -> Créer un étudiant (201 + location)
4. PUT /api/etudiants/{id}             -> Remplacer un étudiant
5. PATCH /api/etudiants/{id}/moyenne   -> Mettre à jour la moyenne seule
6. DELETE /api/etudiants/{id}          -> Supprimer (204 ou 404)
7. GET /api/etudiants/{id}/bulletin    -> Obtenir un résumé de l'étudiant
*/

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

// Models
public record EtudiantEntity(int Id, string Nom, string Prenom, string Email, string Filiere, double? Moyenne);
public record EtudiantResponse(int Id, string NomComplet, string Email, string Filiere, double? Moyenne, string Mention);
public record CreerEtudiantDto(string Nom, string Prenom, string Email, string Filiere);
public record ModifierEtudiantDto(string Nom, string Prenom, string Email, string Filiere);
public record ModifierMoyenneDto(double Moyenne);
public record BulletinDto(string NomComplet, string Filiere, double? Moyenne, string Mention, string Statut);

// Service
public interface IServiceEtudiant
{
    Task<PagedResult<EtudiantResponse>> ObtenirTousAsync(PaginationParams pagination, string? filiere);
    Task<EtudiantResponse?> ObtenirParIdAsync(int id);
    Task<EtudiantResponse> CreerAsync(CreerEtudiantDto dto);
    Task<EtudiantResponse?> RemplacerAsync(int id, ModifierEtudiantDto dto);
    Task<EtudiantResponse?> ModifierMoyenneAsync(int id, double moyenne);
    Task<bool> SupprimerAsync(int id);
    Task<BulletinDto?> ObtenirBulletinAsync(int id);
}

public class ServiceEtudiant : IServiceEtudiant
{
    private readonly List<EtudiantEntity> _etudiants = new()
    {
        new(1, "Martin", "Alice", "alice@univ.fr", "Informatique", 15.5),
        new(2, "Dupont", "Bob", "bob@univ.fr", "Maths", 12.0),
        new(3, "Leblanc", "Clara", "clara@univ.fr", "Informatique", 18.5),
    };
    private int _nextId = 4;

    private static string CalculerMention(double? moy) => moy switch
    {
        null => "Non évalué",
        >= 16 => "Très Bien",
        >= 14 => "Bien",
        >= 12 => "Assez Bien",
        >= 10 => "Passable",
        _ => "Insuffisant"
    };

    private static EtudiantResponse ToResponse(EtudiantEntity e) =>
        new(e.Id, $"{e.Prenom} {e.Nom}", e.Email, e.Filiere, e.Moyenne, CalculerMention(e.Moyenne));

    public Task<PagedResult<EtudiantResponse>> ObtenirTousAsync(PaginationParams p, string? filiere)
    {
        var query = _etudiants.AsEnumerable();
        if (!string.IsNullOrEmpty(filiere))
            query = query.Where(e => e.Filiere.Equals(filiere, StringComparison.OrdinalIgnoreCase));

        var total = query.Count();
        var items = query.Skip((p.Page - 1) * p.PageSize).Take(p.PageSize).Select(ToResponse);

        return Task.FromResult(new PagedResult<EtudiantResponse>
        {
            Items = items,
            Page = p.Page,
            PageSize = p.PageSize,
            TotalItems = total
        });
    }

    public Task<EtudiantResponse?> ObtenirParIdAsync(int id)
    {
        var e = _etudiants.FirstOrDefault(x => x.Id == id);
        return Task.FromResult(e is null ? null : ToResponse(e));
    }

    public Task<EtudiantResponse> CreerAsync(CreerEtudiantDto dto)
    {
        var e = new EtudiantEntity(_nextId++, dto.Nom, dto.Prenom, dto.Email, dto.Filiere, null);
        _etudiants.Add(e);
        return Task.FromResult(ToResponse(e));
    }

    public Task<EtudiantResponse?> RemplacerAsync(int id, ModifierEtudiantDto dto)
    {
        var idx = _etudiants.FindIndex(x => x.Id == id);
        if (idx < 0) return Task.FromResult<EtudiantResponse?>(null);
        var ancien = _etudiants[idx];
        var nouveau = ancien with { Nom = dto.Nom, Prenom = dto.Prenom, Email = dto.Email, Filiere = dto.Filiere };
        _etudiants[idx] = nouveau;
        return Task.FromResult<EtudiantResponse?>(ToResponse(nouveau));
    }

    public Task<EtudiantResponse?> ModifierMoyenneAsync(int id, double moyenne)
    {
        var idx = _etudiants.FindIndex(x => x.Id == id);
        if (idx < 0) return Task.FromResult<EtudiantResponse?>(null);
        var mis_a_jour = _etudiants[idx] with { Moyenne = moyenne };
        _etudiants[idx] = mis_a_jour;
        return Task.FromResult<EtudiantResponse?>(ToResponse(mis_a_jour));
    }

    public Task<bool> SupprimerAsync(int id)
    {
        var e = _etudiants.FirstOrDefault(x => x.Id == id);
        if (e is null) return Task.FromResult(false);
        _etudiants.Remove(e);
        return Task.FromResult(true);
    }

    public Task<BulletinDto?> ObtenirBulletinAsync(int id)
    {
        var e = _etudiants.FirstOrDefault(x => x.Id == id);
        if (e is null) return Task.FromResult<BulletinDto?>(null);
        var mention = CalculerMention(e.Moyenne);
        var statut = e.Moyenne >= 10 ? "Admis" : (e.Moyenne is null ? "En cours" : "Ajourné");
        return Task.FromResult<BulletinDto?>(new BulletinDto(
            $"{e.Prenom} {e.Nom}", e.Filiere, e.Moyenne, mention, statut));
    }
}

// Controller
[ApiController]
[Route("api/[controller]")]
public class EtudiantsController : ControllerBase
{
    private readonly IServiceEtudiant _service;

    public EtudiantsController(IServiceEtudiant service) => _service = service;

    // 1. GET /api/etudiants
    [HttpGet]
    [ProducesResponseType(typeof(PagedResult<EtudiantResponse>), 200)]
    public async Task<ActionResult<PagedResult<EtudiantResponse>>> ObtenirTous(
        [FromQuery] PaginationParams pagination, [FromQuery] string? filiere)
    {
        var result = await _service.ObtenirTousAsync(pagination, filiere);
        return Ok(result);
    }

    // 2. GET /api/etudiants/{id}
    [HttpGet("{id:int}")]
    [ProducesResponseType(typeof(EtudiantResponse), 200)]
    [ProducesResponseType(404)]
    public async Task<ActionResult<EtudiantResponse>> ObtenirParId(int id)
    {
        var etudiant = await _service.ObtenirParIdAsync(id);
        return etudiant is null ? NotFound() : Ok(etudiant);
    }

    // 3. POST /api/etudiants
    [HttpPost]
    [ProducesResponseType(typeof(EtudiantResponse), 201)]
    [ProducesResponseType(400)]
    public async Task<ActionResult<EtudiantResponse>> Creer([FromBody] CreerEtudiantDto dto)
    {
        var cree = await _service.CreerAsync(dto);
        return CreatedAtAction(nameof(ObtenirParId), new { id = cree.Id }, cree);
    }

    // 4. PUT /api/etudiants/{id}
    [HttpPut("{id:int}")]
    [ProducesResponseType(typeof(EtudiantResponse), 200)]
    [ProducesResponseType(404)]
    public async Task<ActionResult<EtudiantResponse>> Remplacer(int id, [FromBody] ModifierEtudiantDto dto)
    {
        var result = await _service.RemplacerAsync(id, dto);
        return result is null ? NotFound() : Ok(result);
    }

    // 5. PATCH /api/etudiants/{id}/moyenne
    [HttpPatch("{id:int}/moyenne")]
    [ProducesResponseType(typeof(EtudiantResponse), 200)]
    [ProducesResponseType(404)]
    public async Task<ActionResult<EtudiantResponse>> ModifierMoyenne(int id, [FromBody] ModifierMoyenneDto dto)
    {
        var result = await _service.ModifierMoyenneAsync(id, dto.Moyenne);
        return result is null ? NotFound() : Ok(result);
    }

    // 6. DELETE /api/etudiants/{id}
    [HttpDelete("{id:int}")]
    [ProducesResponseType(204)]
    [ProducesResponseType(404)]
    public async Task<IActionResult> Supprimer(int id)
    {
        var supprime = await _service.SupprimerAsync(id);
        return supprime ? NoContent() : NotFound();
    }

    // 7. GET /api/etudiants/{id}/bulletin
    [HttpGet("{id:int}/bulletin")]
    [ProducesResponseType(typeof(BulletinDto), 200)]
    [ProducesResponseType(404)]
    public async Task<ActionResult<BulletinDto>> ObtenirBulletin(int id)
    {
        var bulletin = await _service.ObtenirBulletinAsync(id);
        return bulletin is null ? NotFound() : Ok(bulletin);
    }
}


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

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre quand utiliser Minimal APIs vs Controllers
[OK] Créer des endpoints avec la syntaxe Minimal
[OK] Organiser et factoriser les Minimal APIs
[OK] Grouper les routes
[OK] Utiliser les Minimal API avec DI
*/


// ----------------------------------------------------------------------------
// [REFLEXION] MINIMAL APIs VS CONTROLLERS
// ----------------------------------------------------------------------------

/*
CONTROLLERS :
[OK] Organisation claire pour grandes APIs
[OK] Séparation logique (un controller par ressource)
[OK] Support de filtres d'action
[OK] Familier avec MVC classique
[X] Plus de code boilerplate
[X] Moins performant (légèrement)

MINIMAL APIs :
[OK] Démarrage ultra-rapide (quelques lignes)
[OK] Plus performantes (moins de middleware)
[OK] Idéales pour microservices
[OK] Lisibles pour petites APIs
[X] Moins structurées pour grandes APIs
[X] Pas de filtres d'action natifs

RÈGLE :
-> Petite API / Microservice -> Minimal API
-> Grande API / Équipe -> Controllers
*/


// ----------------------------------------------------------------------------
// [RAPIDE] SYNTAXE DE BASE DES MINIMAL APIs
// ----------------------------------------------------------------------------

/*
Dans Program.cs :

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IServiceProduitApi, ServiceProduitImpl>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// ─── ENDPOINTS SIMPLES ────────────────────────────────────────────────────

// GET - Retourner des données
app.MapGet("/api/produits", async (IServiceProduitApi service) =>
{
    var produits = await service.ObtenirTousAsync(null, 1, 20);
    return Results.Ok(produits);
});

// GET avec paramètre
app.MapGet("/api/produits/{id:int}", async (int id, IServiceProduitApi service) =>
{
    var produit = await service.ObtenirParIdAsync(id);
    return produit is null
        ? Results.NotFound(new { Message = $"Produit {id} introuvable" })
        : Results.Ok(produit);
});

// POST
app.MapPost("/api/produits", async (CreerProduitDto dto, IServiceProduitApi service) =>
{
    var cree = await service.CreerAsync(dto);
    return Results.CreatedAtRoute("obtenirProduit", new { id = cree.Id }, cree);
}).WithName("creerProduit");

// PUT
app.MapPut("/api/produits/{id:int}", async (int id, ModifierProduitDto dto, IServiceProduitApi service) =>
{
    var mis_a_jour = await service.RemplacerAsync(id, dto);
    return mis_a_jour is null ? Results.NotFound() : Results.Ok(mis_a_jour);
});

// DELETE
app.MapDelete("/api/produits/{id:int}", async (int id, IServiceProduitApi service) =>
{
    var supprime = await service.SupprimerAsync(id);
    return supprime ? Results.NoContent() : Results.NotFound();
});

// ─── TYPAGE FORT : TypedResults ────────────────────────────────────────────
// Préféré à Results pour un meilleur support Swagger

app.MapGet("/api/produits/{id:int}", async (int id, IServiceProduitApi service) =>
{
    var produit = await service.ObtenirParIdAsync(id);
    return produit is null
        ? TypedResults.NotFound()
        : TypedResults.Ok(produit);
});

// ─── QUERY STRING ──────────────────────────────────────────────────────────
app.MapGet("/api/produits", async (
    IServiceProduitApi service,
    string? categorie = null,
    int page = 1,
    int taille = 20) =>
{
    var produits = await service.ObtenirTousAsync(categorie, page, taille);
    return TypedResults.Ok(produits);
});

app.Run();
*/


// ----------------------------------------------------------------------------
// [DOSSIER] ORGANISATION : ENDPOINT GROUPS
// ----------------------------------------------------------------------------

/*
Pour garder Program.cs propre, extraire les endpoints dans des classes
*/

// IEndpointRouteBuilderExtensions.cs
public static class ProduitEndpoints
{
    public static void MapProduitEndpoints(this IEndpointRouteBuilder app)
    {
        // Groupe avec préfixe commun
        var group = app.MapGroup("/api/produits")
            .WithTags("Produits")       // Pour Swagger
            .WithOpenApi();             // Documentation OpenAPI

        group.MapGet("/", ObtenirTous);
        group.MapGet("/{id:int}", ObtenirParId).WithName("ObtenirProduitParId");
        group.MapPost("/", Creer);
        group.MapPut("/{id:int}", Modifier);
        group.MapDelete("/{id:int}", Supprimer);
    }

    // Handlers séparés (meilleure lisibilité)
    private static async Task<IResult> ObtenirTous(
        IServiceProduitApi service,
        int page = 1,
        int taille = 20,
        string? categorie = null)
    {
        var produits = await service.ObtenirTousAsync(categorie, page, taille);
        return TypedResults.Ok(produits);
    }

    private static async Task<Results<Ok<ProduitResponse>, NotFound>> ObtenirParId(
        int id, IServiceProduitApi service)
    {
        var produit = await service.ObtenirParIdAsync(id);
        return produit is null ? TypedResults.NotFound() : TypedResults.Ok(produit);
    }

    private static async Task<Results<Created<ProduitResponse>, BadRequest>> Creer(
        CreerProduitDto dto, IServiceProduitApi service)
    {
        if (string.IsNullOrEmpty(dto.Nom))
            return TypedResults.BadRequest();

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

    private static async Task<Results<Ok<ProduitResponse>, NotFound>> Modifier(
        int id, ModifierProduitDto dto, IServiceProduitApi service)
    {
        var result = await service.RemplacerAsync(id, dto);
        return result is null ? TypedResults.NotFound() : TypedResults.Ok(result);
    }

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

// Dans Program.cs :
// app.MapProduitEndpoints();


// ============================================================================
// [GUIDE] CHAPITRE 6 : VALIDATION & GESTION D'ERREURS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Valider avec Data Annotations
[OK] Utiliser FluentValidation
[OK] Implémenter la gestion globale des erreurs
[OK] Utiliser le format ProblemDetails
[OK] Créer des réponses d'erreur standardisées
*/


// ----------------------------------------------------------------------------
// [OK] DATA ANNOTATIONS
// ----------------------------------------------------------------------------

/*
[IDEE] VALIDATION AUTOMATIQUE AVEC [ApiController]

Quand [ApiController] est présent :
- ModelState est vérifié AVANT d'entrer dans l'action
- Si invalide -> 400 Bad Request automatique avec les erreurs
*/

using System.ComponentModel.DataAnnotations;

public class CreerEtudiantRequest
{
    [Required(ErrorMessage = "Le nom est requis")]
    [StringLength(50, MinimumLength = 2, ErrorMessage = "Nom entre 2 et 50 caractères")]
    public string Nom { get; set; } = string.Empty;

    [Required(ErrorMessage = "Le prénom est requis")]
    [StringLength(50, MinimumLength = 2)]
    public string Prenom { get; set; } = string.Empty;

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

    [Required]
    [StringLength(100)]
    public string Filiere { get; set; } = string.Empty;

    [Range(0, 20, ErrorMessage = "La note doit être entre 0 et 20")]
    public double? Moyenne { get; set; }

    [Phone(ErrorMessage = "Numéro de téléphone invalide")]
    public string? Telephone { get; set; }

    [Url(ErrorMessage = "URL invalide")]
    public string? SiteWeb { get; set; }

    // Validation personnalisée
    [CustomValidation(typeof(CreerEtudiantRequest), nameof(ValiderEmail))]
    public string? EmailPro { get; set; }

    public static ValidationResult? ValiderEmail(string? email, ValidationContext ctx)
    {
        if (email != null && !email.EndsWith(".edu"))
            return new ValidationResult("L'email professionnel doit se terminer par .edu");
        return ValidationResult.Success;
    }
}

// Annotations sur une classe entière
public class ProduitComplet : IValidatableObject
{
    [Required]
    public string Nom { get; set; } = string.Empty;

    [Range(0.01, double.MaxValue)]
    public decimal Prix { get; set; }

    [Range(0, double.MaxValue)]
    public decimal? PrixPromo { get; set; }

    // Validation inter-champs
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (PrixPromo.HasValue && PrixPromo >= Prix)
        {
            yield return new ValidationResult(
                "Le prix promo doit être inférieur au prix normal",
                new[] { nameof(PrixPromo) }
            );
        }
    }
}


// ----------------------------------------------------------------------------
// [HOT] FLUENTVALIDATION (RECOMMANDÉ EN PRODUCTION)
// ----------------------------------------------------------------------------

/*
INSTALLATION : dotnet add package FluentValidation.AspNetCore

AVANTAGES :
[OK] Validation séparée du modèle (SRP)
[OK] Syntaxe fluide et lisible
[OK] Règles réutilisables
[OK] Tests unitaires faciles
*/

using FluentValidation;

public class CreerEtudiantValidator : AbstractValidator<CreerEtudiantDto>
{
    public CreerEtudiantValidator()
    {
        RuleFor(e => e.Nom)
            .NotEmpty().WithMessage("Le nom est obligatoire")
            .Length(2, 50).WithMessage("Nom entre 2 et 50 caractères")
            .Matches(@"^[a-zA-ZÀ-ÿ\s\-]+$").WithMessage("Nom invalide (lettres et tirets seulement)");

        RuleFor(e => e.Prenom)
            .NotEmpty().WithMessage("Le prénom est obligatoire")
            .Length(2, 50);

        RuleFor(e => e.Email)
            .NotEmpty().WithMessage("L'email est obligatoire")
            .EmailAddress().WithMessage("Format email invalide")
            .MustAsync(async (email, ct) =>
            {
                // Validation asynchrone (ex: vérifier en BDD)
                await Task.Delay(1); // Simulation
                return !email.StartsWith("spam");
            }).WithMessage("Domaine email non autorisé");

        RuleFor(e => e.Filiere)
            .NotEmpty()
            .Must(f => new[] { "Informatique", "Maths", "Physique", "Chimie" }.Contains(f))
            .WithMessage("Filière invalide. Choisir: Informatique, Maths, Physique, Chimie");
    }
}

public class ModifierMoyenneValidator : AbstractValidator<ModifierMoyenneDto>
{
    public ModifierMoyenneValidator()
    {
        RuleFor(x => x.Moyenne)
            .InclusiveBetween(0, 20).WithMessage("La moyenne doit être entre 0 et 20");
    }
}

// Enregistrement dans Program.cs :
/*
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddFluentValidationClientsideAdapters();
builder.Services.AddValidatorsFromAssemblyContaining<CreerEtudiantValidator>();
*/


// ----------------------------------------------------------------------------
// [SECURITE] GESTION GLOBALE DES ERREURS
// ----------------------------------------------------------------------------

/*
PROBLÈME : Exception non gérée = 500 avec stack trace exposé en production

SOLUTION : Middleware de gestion d'erreurs + ProblemDetails
*/

// Pattern ProblemDetails (RFC 7807 - Standard API)
/*
{
  "type": "https://tools.ietf.org/html/rfc7807",
  "title": "Ressource introuvable",
  "status": 404,
  "detail": "L'étudiant avec l'id 42 n'existe pas.",
  "instance": "/api/etudiants/42",
  "traceId": "0HMR7PQRS6M22:00000001"
}
*/

// Exception Handler avec ProblemDetails (ASP.NET Core 7+)
/*
Dans Program.cs :

builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = ctx =>
    {
        ctx.ProblemDetails.Extensions["traceId"] = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier;
        ctx.ProblemDetails.Extensions["timestamp"] = DateTime.UtcNow;
    };
});

// ...

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
}

app.UseStatusCodePages(); // Génère ProblemDetails pour 4xx sans corps
*/

// Exceptions métier personnalisées
public abstract class AppException : Exception
{
    public int StatusCode { get; }
    public string? Detail { get; }

    protected AppException(string message, int statusCode, string? detail = null)
        : base(message)
    {
        StatusCode = statusCode;
        Detail = detail;
    }
}

public class EntiteNotFoundException : AppException
{
    public EntiteNotFoundException(string entite, object id)
        : base($"{entite} non trouvé", 404, $"{entite} avec id '{id}' introuvable.") { }
}

public class EntiteDejaExisteException : AppException
{
    public EntiteDejaExisteException(string entite, string champ, object valeur)
        : base($"{entite} déjà existant", 409, $"Un {entite} avec {champ}='{valeur}' existe déjà.") { }
}

public class BusinessRuleException : AppException
{
    public BusinessRuleException(string message)
        : base("Règle métier violée", 422, message) { }
}

// Middleware de gestion d'erreur complet
public class AppExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<AppExceptionMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (AppException ex)
        {
            _logger.LogWarning("Exception applicative: {Message}", ex.Message);
            await EcrireProblemDetails(context, ex.StatusCode, ex.Message, ex.Detail);
        }
        catch (ValidationException ex) // FluentValidation
        {
            var erreurs = ex.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());

            _logger.LogWarning("Validation échouée: {Errors}", erreurs);
            context.Response.StatusCode = 400;
            context.Response.ContentType = "application/problem+json";
            await context.Response.WriteAsJsonAsync(new
            {
                Type = "https://tools.ietf.org/html/rfc7807",
                Title = "Une ou plusieurs erreurs de validation se sont produites.",
                Status = 400,
                Errors = erreurs,
                TraceId = context.TraceIdentifier
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erreur interne non gérée sur {Path}", context.Request.Path);
            await EcrireProblemDetails(context, 500, "Erreur interne du serveur",
                "Une erreur inattendue s'est produite. Veuillez réessayer plus tard.");
        }
    }

    private static async Task EcrireProblemDetails(
        HttpContext context, int status, string titre, string? detail)
    {
        context.Response.StatusCode = status;
        context.Response.ContentType = "application/problem+json";
        await context.Response.WriteAsJsonAsync(new
        {
            Type = $"https://httpstatuses.com/{status}",
            Title = titre,
            Status = status,
            Detail = detail,
            Instance = context.Request.Path.Value,
            TraceId = context.TraceIdentifier
        });
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 7 : FILTRES & MIDDLEWARE AVANCÉ
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer et utiliser des Action Filters
[OK] Créer des Exception Filters
[OK] Créer des Resource Filters
[OK] Comprendre le pipeline de filtres
*/


// ----------------------------------------------------------------------------
// [OBJECTIF] TYPES DE FILTRES
// ----------------------------------------------------------------------------

/*
ORDRE D'EXÉCUTION DES FILTRES :

Requête -> [Authorization] -> [Resource] -> [Action] -> [Exception] -> Réponse

1. IAuthorizationFilter   -> Vérification des autorisations
2. IResourceFilter        -> Avant le model binding (cache, court-circuit)
3. IActionFilter          -> Avant/après l'action (logging, validation)
4. IResultFilter          -> Avant/après le résultat
5. IExceptionFilter       -> Gestion des exceptions

Portées :
- Global    -> Toute l'application
- Controller-> Tout le controller
- Action    -> Action spécifique
*/

// ─── ACTION FILTER ─────────────────────────────────────────────────────────

// Filtre de logging
public class LoggingActionFilter : IActionFilter
{
    private readonly ILogger<LoggingActionFilter> _logger;

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

    // AVANT l'exécution de l'action
    public void OnActionExecuting(ActionExecutingContext context)
    {
        _logger.LogInformation(
            "[BLACK_RIGHT-POINTING_TRIANGLE] {Controller}.{Action} - Args: {Args}",
            context.RouteData.Values["controller"],
            context.RouteData.Values["action"],
            context.ActionArguments
        );
    }

    // APRÈS l'exécution de l'action
    public void OnActionExecuted(ActionExecutedContext context)
    {
        _logger.LogInformation(
            "[BLACK_LEFT-POINTING_TRIANGLE] {Controller}.{Action} - Status: {Status}",
            context.RouteData.Values["controller"],
            context.RouteData.Values["action"],
            (context.Result as ObjectResult)?.StatusCode
        );
    }
}

// Filtre de validation personnalisé
public class ValiderEntiteExisteAttribute : ActionFilterAttribute
{
    private readonly Type _typeEntite;
    public string ParametreId { get; set; } = "id";

    public ValiderEntiteExisteAttribute(Type typeEntite)
        => _typeEntite = typeEntite;

    public override async Task OnActionExecutionAsync(
        ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (context.ActionArguments.TryGetValue(ParametreId, out var idObj) && idObj is int id)
        {
            // Vérifier que l'entité existe
            // En vrai : injecter le repository et vérifier
            if (id <= 0)
            {
                context.Result = new BadRequestObjectResult($"Id invalide: {id}");
                return; // Court-circuit - n'exécute pas l'action
            }
        }

        await next(); // Continuer vers l'action
    }
}

// ─── EXCEPTION FILTER ──────────────────────────────────────────────────────
public class AppExceptionFilter : IExceptionFilter
{
    private readonly ILogger<AppExceptionFilter> _logger;

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

    public void OnException(ExceptionContext context)
    {
        if (context.Exception is EntiteNotFoundException notFound)
        {
            _logger.LogWarning("Entité introuvable: {Message}", notFound.Message);
            context.Result = new NotFoundObjectResult(new
            {
                notFound.Message,
                notFound.Detail,
                Timestamp = DateTime.UtcNow
            });
            context.ExceptionHandled = true; // Marquer comme géré
        }
    }
}

// ─── ENREGISTREMENT DES FILTRES ────────────────────────────────────────────
/*
Dans Program.cs (global) :

builder.Services.AddControllers(options =>
{
    options.Filters.Add<LoggingActionFilter>();      // Filtre global
    options.Filters.Add<AppExceptionFilter>();       // Exception filter global
});

Sur un controller :
[ServiceFilter(typeof(LoggingActionFilter))]
public class MonController : ControllerBase { ... }

Sur une action :
[ServiceFilter(typeof(LoggingActionFilter))]
[ValiderEntiteExiste(typeof(Etudiant))]
public async Task<IActionResult> Get(int id) { ... }
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE 5 - AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ :

Créez une API de gestion de cours universitaires avec :

1. Controller "CoursController" avec CRUD complet
   Cours : Id, Titre, Description, Credit, NombrePlaces, Professeur

2. Validator FluentValidation "CreerCoursValidator"

3. Filtre "VerifierProfesseurFilter" qui vérifie que le professeur
   n'est pas vide avant de créer un cours

4. Gestion d'erreur : lever EntiteNotFoundException si cours non trouvé

5. Endpoint GET /api/cours/{id}/inscrits qui retourne la liste des inscrits
*/

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

// Models
public record CoursEntity(int Id, string Titre, string Description, int Credit, int NombrePlaces, string Professeur);
public record CoursResponse(int Id, string Titre, string Description, int Credit, int NombrePlaces, string Professeur, int PlacesRestantes);
public record CreerCoursDto(string Titre, string Description, int Credit, int NombrePlaces, string Professeur);

// Validator
public class CreerCoursValidator : AbstractValidator<CreerCoursDto>
{
    public CreerCoursValidator()
    {
        RuleFor(c => c.Titre)
            .NotEmpty().WithMessage("Le titre est obligatoire")
            .Length(5, 100).WithMessage("Titre entre 5 et 100 caractères");

        RuleFor(c => c.Credit)
            .InclusiveBetween(1, 10).WithMessage("Les crédits doivent être entre 1 et 10");

        RuleFor(c => c.NombrePlaces)
            .GreaterThan(0).WithMessage("Le nombre de places doit être positif")
            .LessThanOrEqualTo(300).WithMessage("Maximum 300 places");

        RuleFor(c => c.Professeur)
            .NotEmpty().WithMessage("Le professeur est obligatoire")
            .MinimumLength(3).WithMessage("Nom du professeur trop court");
    }
}

// Filter
public class VerifierProfesseurFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (context.ActionArguments.TryGetValue("dto", out var dtoObj) && dtoObj is CreerCoursDto dto)
        {
            if (string.IsNullOrWhiteSpace(dto.Professeur))
            {
                context.Result = new BadRequestObjectResult(new
                {
                    Message = "Le professeur est obligatoire pour créer un cours"
                });
            }
        }
    }

    public void OnActionExecuted(ActionExecutedContext context) { }
}

// Service + Controller
public class ServiceCours
{
    private readonly List<CoursEntity> _cours = new()
    {
        new(1, "Algorithmique", "Introduction aux algorithmes", 6, 30, "Prof. Martin"),
        new(2, "Base de données", "SQL et NoSQL", 4, 25, "Prof. Dupont"),
    };
    private readonly Dictionary<int, List<string>> _inscrits = new()
    {
        { 1, new List<string> { "Alice Martin", "Bob Dupont" } }
    };
    private int _nextId = 3;

    private static CoursResponse ToResponse(CoursEntity c, int occupes) =>
        new(c.Id, c.Titre, c.Description, c.Credit, c.NombrePlaces, c.Professeur, c.NombrePlaces - occupes);

    public IEnumerable<CoursResponse> ObtenirTous() =>
        _cours.Select(c => ToResponse(c, _inscrits.GetValueOrDefault(c.Id, new()).Count));

    public CoursResponse ObtenirParId(int id)
    {
        var cours = _cours.FirstOrDefault(c => c.Id == id)
            ?? throw new EntiteNotFoundException("Cours", id);
        return ToResponse(cours, _inscrits.GetValueOrDefault(id, new()).Count);
    }

    public CoursResponse Creer(CreerCoursDto dto)
    {
        var nouveau = new CoursEntity(_nextId++, dto.Titre, dto.Description, dto.Credit, dto.NombrePlaces, dto.Professeur);
        _cours.Add(nouveau);
        return ToResponse(nouveau, 0);
    }

    public IEnumerable<string> ObtenirInscrits(int id)
    {
        _ = ObtenirParId(id); // Lance NotFoundException si absent
        return _inscrits.GetValueOrDefault(id, new());
    }
}

[ApiController]
[Route("api/[controller]")]
[ServiceFilter(typeof(AppExceptionFilter))]
public class CoursController : ControllerBase
{
    private readonly ServiceCours _service;

    public CoursController(ServiceCours service) => _service = service;

    [HttpGet]
    public ActionResult<IEnumerable<CoursResponse>> ObtenirTous()
        => Ok(_service.ObtenirTous());

    [HttpGet("{id:int}")]
    public ActionResult<CoursResponse> ObtenirParId(int id)
        => Ok(_service.ObtenirParId(id)); // NotFoundException géré par filtre

    [HttpPost]
    [ServiceFilter(typeof(VerifierProfesseurFilter))]
    public ActionResult<CoursResponse> Creer([FromBody] CreerCoursDto dto)
    {
        var cree = _service.Creer(dto);
        return CreatedAtAction(nameof(ObtenirParId), new { id = cree.Id }, cree);
    }

    [HttpGet("{id:int}/inscrits")]
    public ActionResult<IEnumerable<string>> ObtenirInscrits(int id)
        => Ok(_service.ObtenirInscrits(id));
}

/*
[DOCS] RÉCAPITULATIF PARTIE 2

[OK] REST : Principes, verbes HTTP, codes de statut
[OK] Controllers : Structure, routing, model binding, IActionResult
[OK] Swagger : Documentation automatique
[OK] Minimal APIs : MapGet/Post/Put/Delete, groupes
[OK] Validation : DataAnnotations, FluentValidation
[OK] Erreurs : Middleware, ProblemDetails, exceptions custom
[OK] Filtres : Action, Exception, Resource

-> PROCHAINE ÉTAPE : Partie 3 - Accès aux données avec EF Core [ARCHIVE]
*/

// ============================================================================
// [LIVRE] ASP.NET CORE - PARTIE 3 : ACCÈS AUX DONNÉES (Entity Framework Core)
// ============================================================================
//
// [OBJECTIF] CETTE PARTIE COUVRE :
// - Chapitre 8 : Introduction à Entity Framework Core
// - Chapitre 9 : Repositories & Unit of Work Pattern
// - Chapitre 10 : Optimisation Base de Données
//
// [TEMPS] TEMPS : ~8-10 heures
// [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 8 : ENTITY FRAMEWORK CORE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre ce qu'est EF Core et pourquoi l'utiliser
[OK] Configurer un DbContext
[OK] Créer des entités (modèles)
[OK] Effectuer des migrations
[OK] Faire des opérations CRUD complètes
[OK] Comprendre le Tracking et le NoTracking
*/


// ----------------------------------------------------------------------------
// [REFLEXION] QU'EST-CE QU'EF CORE ?
// ----------------------------------------------------------------------------

/*
[IDEE] ENTITY FRAMEWORK CORE = ORM pour .NET

COMMENT : Mappage entre classes C# <-> Tables SQL
POURQUOI :
  - Écrire du C# au lieu de SQL
  - Sécurité SQL Injection automatique
  - Migrations versionnées
  - Base de données portable (SQLite, PostgreSQL, SQL Server...)
  - Fortement typé (erreurs à la compilation, pas à l'exécution)

QUAND : Presque toujours pour les projets .NET

PACKAGES :
  dotnet add package Microsoft.EntityFrameworkCore.Sqlite    (SQLite)
  dotnet add package Microsoft.EntityFrameworkCore.SqlServer (SQL Server)
  dotnet add package Microsoft.EntityFrameworkCore.Npgsql    (PostgreSQL)
  dotnet add package Microsoft.EntityFrameworkCore.Design    (Migrations CLI)
  dotnet tool install --global dotnet-ef                     (Outil CLI)
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] ENTITÉS (MODÈLES DE BASE DE DONNÉES)
// ----------------------------------------------------------------------------

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

// ─── ENTITÉ SIMPLE ─────────────────────────────────────────────────────────
public class Produit
{
    public int Id { get; set; }                        // PK (convention: Id ou ClasseId)

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

    [Column(TypeName = "decimal(10,2)")]               // Précision décimale
    public decimal Prix { get; set; }

    [MaxLength(100)]
    public string? Categorie { get; set; }

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

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

    public DateTime? DateModification { get; set; }

    // Navigation property -> Relation One-to-Many
    public ICollection<AvisProduit> Avis { get; set; } = new List<AvisProduit>();

    // FK vers Categorie (si entité séparée)
    public int? CategorieId { get; set; }
    public CategorieProduit? CategorieNav { get; set; } // Navigation property
}

public class CategorieProduit
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public string? Description { get; set; }

    // Navigation property inverse
    public ICollection<Produit> Produits { get; set; } = new List<Produit>();
}

public class AvisProduit
{
    public int Id { get; set; }
    public string Texte { get; set; } = string.Empty;

    [Range(1, 5)]
    public int Note { get; set; }

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

    // FK - obligatoire (nullable = false par défaut si non nullable)
    public int ProduitId { get; set; }
    public Produit Produit { get; set; } = null!; // null! = sera initialisé par EF
}

// ─── CLASSE DE BASE AUDITABLE ──────────────────────────────────────────────
/*
PATTERN : Classe de base pour toutes les entités
Centralise les propriétés communes (créé par, modifié par, etc.)
*/
public abstract class EntiteAuditable
{
    public int Id { get; set; }
    public DateTime DateCreation { get; set; }
    public DateTime? DateModification { get; set; }
    public string CreePar { get; set; } = string.Empty;
    public string? ModifiePar { get; set; }
    public bool EstSupprime { get; set; } = false; // Soft delete
}

public class Utilisateur : EntiteAuditable
{
    [MaxLength(80)]
    public string Nom { get; set; } = string.Empty;

    [MaxLength(80)]
    public string Prenom { get; set; } = string.Empty;

    [MaxLength(256)]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;

    [MaxLength(100)]
    public string Role { get; set; } = "Utilisateur";

    // Relation One-to-One
    public ProfilUtilisateur? Profil { get; set; }

    // Relation One-to-Many
    public ICollection<Commande> Commandes { get; set; } = new List<Commande>();
}

public class ProfilUtilisateur
{
    public int Id { get; set; }
    public string? Bio { get; set; }
    public string? AvatarUrl { get; set; }
    public DateTime? DateNaissance { get; set; }

    // FK (One-to-One : FK + IsUnique dans OnModelCreating)
    public int UtilisateurId { get; set; }
    public Utilisateur Utilisateur { get; set; } = null!;
}

public class Commande : EntiteAuditable
{
    [Column(TypeName = "decimal(10,2)")]
    public decimal Total { get; set; }

    [MaxLength(50)]
    public string Statut { get; set; } = "EnAttente";

    public int UtilisateurId { get; set; }
    public Utilisateur Utilisateur { get; set; } = null!;

    // Relation Many-to-Many via table intermédiaire avec données
    public ICollection<LigneCommande> LignesCommande { get; set; } = new List<LigneCommande>();
}

public class LigneCommande
{
    public int Id { get; set; }
    public int Quantite { get; set; }

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

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

    public int ProduitId { get; set; }
    public Produit Produit { get; set; } = null!;
}


// ----------------------------------------------------------------------------
// [OUTIL] DBCONTEXT - LE CŒUR D'EF CORE
// ----------------------------------------------------------------------------

/*
[IDEE] DbContext = Gestionnaire de connexion + Unité de travail

RÔLES :
1. Point d'accès aux tables (DbSet<T>)
2. Suivi des modifications (Change Tracker)
3. Transaction implicite lors de SaveChanges
4. Configuration du mappage
5. Gestion de la connexion

LIFETIME : TOUJOURS Scoped (une instance par requête HTTP)
*/

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

    // ─── DbSets (= Tables) ─────────────────────────────────────────────────
    public DbSet<Produit> Produits { get; set; }
    public DbSet<CategorieProduit> Categories { get; set; }
    public DbSet<AvisProduit> Avis { get; set; }
    public DbSet<Utilisateur> Utilisateurs { get; set; }
    public DbSet<ProfilUtilisateur> Profils { get; set; }
    public DbSet<Commande> Commandes { get; set; }
    public DbSet<LigneCommande> LignesCommande { get; set; }

    // ─── CONFIGURATION FLUENT API ──────────────────────────────────────────
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // ══ Produit ══════════════════════════════════════════════════════════
        modelBuilder.Entity<Produit>(entity =>
        {
            entity.ToTable("produits");                        // Nom de table personnalisé
            entity.HasKey(p => p.Id);                         // Clé primaire
            entity.Property(p => p.Nom).IsRequired().HasMaxLength(200);
            entity.Property(p => p.Prix).HasColumnType("decimal(10,2)");
            entity.HasIndex(p => p.Nom);                      // Index simple
            entity.HasIndex(p => new { p.Nom, p.Categorie }); // Index composite

            // Relation One-to-Many avec Categorie
            entity.HasOne(p => p.CategorieNav)
                .WithMany(c => c.Produits)
                .HasForeignKey(p => p.CategorieId)
                .OnDelete(DeleteBehavior.SetNull); // Null si categorie supprimée

            // Relation One-to-Many avec Avis (cascade delete)
            entity.HasMany(p => p.Avis)
                .WithOne(a => a.Produit)
                .HasForeignKey(a => a.ProduitId)
                .OnDelete(DeleteBehavior.Cascade); // Supprimer avis si produit supprimé

            // Filtre global (Soft Delete)
            // entity.HasQueryFilter(p => !p.EstSupprime);
        });

        // ══ Utilisateur ════════════════════════════════════════════════════
        modelBuilder.Entity<Utilisateur>(entity =>
        {
            entity.HasIndex(u => u.Email).IsUnique(); // Email unique

            // One-to-One avec Profil
            entity.HasOne(u => u.Profil)
                .WithOne(p => p.Utilisateur)
                .HasForeignKey<ProfilUtilisateur>(p => p.UtilisateurId)
                .OnDelete(DeleteBehavior.Cascade);
        });

        // ══ Valeurs par défaut ══════════════════════════════════════════════
        modelBuilder.Entity<Produit>()
            .Property(p => p.DateCreation)
            .HasDefaultValueSql("GETUTCDATE()"); // SQL Server
        // .HasDefaultValueSql("now()");          // PostgreSQL
        // .HasDefaultValueSql("datetime('now')"); // SQLite

        // ══ Données de seed (données initiales) ════════════════════════════
        modelBuilder.Entity<CategorieProduit>().HasData(
            new CategorieProduit { Id = 1, Nom = "Informatique" },
            new CategorieProduit { Id = 2, Nom = "Mobilier" },
            new CategorieProduit { Id = 3, Nom = "Électronique" }
        );

        // ══ Appliquer configurations depuis classes séparées ════════════════
        // modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
    }

    // ─── OVERRIDE POUR AUDIT AUTOMATIQUE ───────────────────────────────────
    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        var maintenant = DateTime.UtcNow;

        foreach (var entry in ChangeTracker.Entries<EntiteAuditable>())
        {
            switch (entry.State)
            {
                case EntityState.Added:
                    entry.Entity.DateCreation = maintenant;
                    entry.Entity.CreePar = "système"; // En vrai: depuis ICurrentUserService
                    break;

                case EntityState.Modified:
                    entry.Entity.DateModification = maintenant;
                    entry.Entity.ModifiePar = "système";
                    break;
            }
        }

        return await base.SaveChangesAsync(ct);
    }
}

// ─── CONFIGURATION SÉPARÉE (MEILLEURE PRATIQUE) ────────────────────────────
public class ProduitConfiguration : IEntityTypeConfiguration<Produit>
{
    public void Configure(EntityTypeBuilder<Produit> builder)
    {
        builder.ToTable("produits");
        builder.HasKey(p => p.Id);
        builder.Property(p => p.Nom).IsRequired().HasMaxLength(200);
        builder.Property(p => p.Prix).HasColumnType("decimal(10,2)");
        // ... rest of configuration
    }
}
// À utiliser avec : modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());


// ----------------------------------------------------------------------------
// [CONFIG] CONFIGURATION DU DbContext DANS Program.cs
// ----------------------------------------------------------------------------

/*
Dans Program.cs :

// SQLite (développement)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))
           .EnableSensitiveDataLogging(builder.Environment.IsDevelopment())
           .EnableDetailedErrors(builder.Environment.IsDevelopment()));

// SQL Server (production)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"),
        sqlOptions =>
        {
            sqlOptions.EnableRetryOnFailure(
                maxRetryCount: 3,
                maxRetryDelay: TimeSpan.FromSeconds(30),
                errorNumbersToAdd: null);
        }));

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

appsettings.Development.json :
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=app.db"
  }
}

appsettings.json (SQL Server) :
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MonApp;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
*/


// ----------------------------------------------------------------------------
// [GRAPHIQUE] MIGRATIONS
// ----------------------------------------------------------------------------

/*
[IDEE] WORKFLOW DE MIGRATIONS

COMMANDES CLI :

1. Créer une migration :
   dotnet ef migrations add NomDeLaMigration

2. Appliquer les migrations :
   dotnet ef database update

3. Voir les migrations :
   dotnet ef migrations list

4. Annuler dernière migration (avant apply) :
   dotnet ef migrations remove

5. Revenir à une migration :
   dotnet ef database update NomMigration

6. Générer script SQL :
   dotnet ef migrations script  -> Tout
   dotnet ef migrations script MigrationA MigrationB -> Entre deux


EXEMPLE DE MIGRATION GÉNÉRÉE :

Migration_InitialCreate.cs :
──────────────────────────────

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "produits",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("Sqlite:Autoincrement", true),
                Nom = table.Column<string>(maxLength: 200, nullable: false),
                Prix = table.Column<decimal>(type: "decimal(10,2)", nullable: false),
                // ...
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "produits");
    }
}


MIGRATION CUSTOM (données) :

public partial class AjouterDonneesInitiales : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Insérer des données
        migrationBuilder.InsertData(
            table: "Categories",
            columns: new[] { "Id", "Nom" },
            values: new object[] { 1, "Informatique" });

        // Ou exécuter du SQL personnalisé
        migrationBuilder.Sql("UPDATE produits SET EnStock = 1 WHERE EnStock IS NULL");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DeleteData("Categories", "Id", 1);
    }
}
*/

// Appliquer migrations au démarrage (pratique pour dev/tests)
/*
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.Database.MigrateAsync(); // Applique migrations en attente
}
*/


// ----------------------------------------------------------------------------
// [SYNC] OPÉRATIONS CRUD AVEC EF CORE
// ----------------------------------------------------------------------------

public class RepositoireProduitEF
{
    private readonly AppDbContext _ctx;

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

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

    public async Task<Produit> AjouterAsync(Produit produit, CancellationToken ct = default)
    {
        // Méthode 1 : Add + SaveChanges
        _ctx.Produits.Add(produit);
        await _ctx.SaveChangesAsync(ct);
        return produit; // Id est maintenant peuplé par EF

        // Méthode 2 : AddAsync (légèrement différent pour sequences)
        // await _ctx.Produits.AddAsync(produit, ct);
        // await _ctx.SaveChangesAsync(ct);
    }

    public async Task AjouterPlusieursAsync(IEnumerable<Produit> produits, CancellationToken ct = default)
    {
        await _ctx.Produits.AddRangeAsync(produits, ct);
        await _ctx.SaveChangesAsync(ct);
    }

    // ─── READ ───────────────────────────────────────────────────────────────

    // Tous les produits (tracking activé)
    public async Task<List<Produit>> ObtenirTousAsync(CancellationToken ct = default)
    {
        return await _ctx.Produits.ToListAsync(ct);
    }

    // Par Id (null si absent)
    public async Task<Produit?> ObtenirParIdAsync(int id, CancellationToken ct = default)
    {
        return await _ctx.Produits.FindAsync(new object[] { id }, ct);
        // FindAsync : cherche d'abord dans le cache (Change Tracker), puis BDD
    }

    // Avec filtres LINQ
    public async Task<List<Produit>> RechercherAsync(
        string? terme = null,
        string? categorie = null,
        decimal? prixMin = null,
        decimal? prixMax = null,
        CancellationToken ct = default)
    {
        // IQueryable = requête non exécutée (DIFFÉRÉE)
        var query = _ctx.Produits.AsQueryable();

        if (!string.IsNullOrEmpty(terme))
            query = query.Where(p => p.Nom.Contains(terme));

        if (!string.IsNullOrEmpty(categorie))
            query = query.Where(p => p.Categorie == categorie);

        if (prixMin.HasValue)
            query = query.Where(p => p.Prix >= prixMin.Value);

        if (prixMax.HasValue)
            query = query.Where(p => p.Prix <= prixMax.Value);

        // ToListAsync() EXÉCUTE la requête SQL
        return await query
            .OrderBy(p => p.Nom)
            .ToListAsync(ct);
    }

    // Avec navigation properties (Include)
    public async Task<Produit?> ObtenirAvecAvisAsync(int id, CancellationToken ct = default)
    {
        return await _ctx.Produits
            .Include(p => p.Avis)                          // JOIN avec Avis
            .Include(p => p.CategorieNav)                  // JOIN avec Categorie
            .FirstOrDefaultAsync(p => p.Id == id, ct);
    }

    // Projection (sélectionner seulement certains champs)
    public async Task<List<object>> ObtenirResumeAsync(CancellationToken ct = default)
    {
        return await _ctx.Produits
            .Where(p => p.EnStock)
            .Select(p => new             // Projection -> SQL SELECT optimisé
            {
                p.Id,
                p.Nom,
                p.Prix,
                NombreAvis = p.Avis.Count()
            })
            .ToListAsync(ct);
    }

    // Pagination
    public async Task<(List<Produit> Items, int Total)> ObtenirPagineAsync(
        int page, int taille, CancellationToken ct = default)
    {
        var query = _ctx.Produits.AsQueryable();

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

        return (items, total);
    }

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

    // Méthode 1 : Modifier entité trackée
    public async Task<Produit?> ModifierAsync(int id, string nouveauNom, decimal nouveauPrix, CancellationToken ct = default)
    {
        var produit = await _ctx.Produits.FindAsync(new object[] { id }, ct);
        if (produit is null) return null;

        // EF Track les changements automatiquement
        produit.Nom = nouveauNom;
        produit.Prix = nouveauPrix;
        produit.DateModification = DateTime.UtcNow;

        // SaveChanges génère: UPDATE produits SET Nom=@p0, Prix=@p1 WHERE Id=@p2
        await _ctx.SaveChangesAsync(ct);
        return produit;
    }

    // Méthode 2 : Attacher entité détachée
    public async Task<Produit> RemplacerAsync(Produit produit, CancellationToken ct = default)
    {
        _ctx.Produits.Update(produit); // Marque toutes les props comme modified
        await _ctx.SaveChangesAsync(ct);
        return produit;
    }

    // Méthode 3 : ExecuteUpdateAsync (EF Core 7+ - pas de tracking)
    public async Task<int> ModifierPrixBulkAsync(string categorie, decimal nouveauPrix, CancellationToken ct = default)
    {
        return await _ctx.Produits
            .Where(p => p.Categorie == categorie)
            .ExecuteUpdateAsync(
                setters => setters
                    .SetProperty(p => p.Prix, nouveauPrix)
                    .SetProperty(p => p.DateModification, DateTime.UtcNow),
                ct
            );
        // SQL : UPDATE produits SET Prix=@p0, DateModif=@p1 WHERE Categorie=@p2
    }

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

    public async Task<bool> SupprimerAsync(int id, CancellationToken ct = default)
    {
        var produit = await _ctx.Produits.FindAsync(new object[] { id }, ct);
        if (produit is null) return false;

        _ctx.Produits.Remove(produit);
        await _ctx.SaveChangesAsync(ct);
        return true;
    }

    // EF Core 7+ - Suppression sans chargement
    public async Task<int> SupprimerBulkAsync(string categorie, CancellationToken ct = default)
    {
        return await _ctx.Produits
            .Where(p => p.Categorie == categorie)
            .ExecuteDeleteAsync(ct);
        // SQL : DELETE FROM produits WHERE Categorie=@p0
    }

    // Soft Delete (ne supprime pas réellement)
    public async Task<bool> ArchiversAsync(int id, CancellationToken ct = default)
    {
        var produit = await _ctx.Produits.FindAsync(new object[] { id }, ct);
        if (produit is null) return false;

        // produit.EstSupprime = true; // Avec la propriété de soft delete
        await _ctx.SaveChangesAsync(ct);
        return true;
    }
}


// ----------------------------------------------------------------------------
// [GRAPHIQUE] TRACKING vs NO-TRACKING
// ----------------------------------------------------------------------------

/*
[IDEE] DIFFERENCE TRACKING / NO-TRACKING

TRACKING (par défaut) :
[OK] Détecte les modifications automatiquement
[OK] SaveChanges génère les bons UPDATE
[X] Consomme plus de mémoire
[X] Plus lent pour lecture pure

NO-TRACKING :
[OK] Plus rapide (~30% pour lectures)
[OK] Moins de mémoire
[X] Ne détecte pas les modifications
[OK] Idéal pour lectures (GET) et projections

RÈGLE :
-> Lecture seule (GET) -> AsNoTracking()
-> Lecture + modification -> Tracking par défaut
*/

public class ExemplesTracking
{
    private readonly AppDbContext _ctx;

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

    // [OK] No-Tracking pour GET
    public async Task<List<Produit>> ObtenirPourAffichage()
    {
        return await _ctx.Produits
            .AsNoTracking()           // <- PERFORMANCE !
            .Where(p => p.EnStock)
            .ToListAsync();
    }

    // [OK] No-Tracking Identity Resolution (EF Core 5+)
    // Évite les doublons dans les requêtes avec Include
    public async Task<List<Commande>> ObtenirCommandesOptimise()
    {
        return await _ctx.Commandes
            .AsNoTrackingWithIdentityResolution()  // Déduplique les entités liées
            .Include(c => c.LignesCommande)
            .ThenInclude(l => l.Produit)
            .ToListAsync();
    }

    // [OK] Tracking pour modification
    public async Task<bool> MarquerEnRupture(int id)
    {
        var produit = await _ctx.Produits.FindAsync(id);  // Tracking !
        if (produit is null) return false;
        produit.EnStock = false;          // EF détecte le changement
        await _ctx.SaveChangesAsync();    // UPDATE automatique
        return true;
    }
}


// ============================================================================
// [COURS] EXERCICE PRATIQUE 6 - AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ :

Créez un système de gestion de films avec EF Core et SQLite.

Entités :
- Film : Id, Titre, AnnéeSortie, Genre, Note (0-10), Durée(minutes)
- Réalisateur : Id, Nom, Prenom, Nationalite
- Acteur : Id, Nom, Prenom
- CastingFilm (table intermédiaire) : FilmId, ActeurId, Role

1. Créez les entités avec les relations appropriées
2. Créez le DbContext avec configuration Fluent API
3. Créez un RepositoireFilm avec :
   - ObtenirTousAsync() avec pagination
   - ObtenirParIdAsync(id) avec Réalisateur et Acteurs
   - RechercherAsync(terme, genre, noteMin)
   - AjouterAsync(film)
   - SupprimerAsync(id)
4. Créez des données seed dans OnModelCreating
5. Ajoutez des index appropriés
*/

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

// Entités
public class Film
{
    public int Id { get; set; }
    public string Titre { get; set; } = string.Empty;
    public int AnneeSortie { get; set; }
    public string Genre { get; set; } = string.Empty;
    public double? Note { get; set; }
    public int DureeMinutes { get; set; }
    public string? Synopsis { get; set; }

    public int RealisateurId { get; set; }
    public Realisateur Realisateur { get; set; } = null!;

    public ICollection<CastingFilm> Castings { get; set; } = new List<CastingFilm>();
}

public class Realisateur
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public string Prenom { get; set; } = string.Empty;
    public string? Nationalite { get; set; }

    public ICollection<Film> Films { get; set; } = new List<Film>();
}

public class Acteur
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public string Prenom { get; set; } = string.Empty;

    public ICollection<CastingFilm> Castings { get; set; } = new List<CastingFilm>();
}

// Table intermédiaire AVEC données supplémentaires
public class CastingFilm
{
    public int FilmId { get; set; }
    public Film Film { get; set; } = null!;

    public int ActeurId { get; set; }
    public Acteur Acteur { get; set; } = null!;

    public string Role { get; set; } = string.Empty;      // Données extras
    public bool EstRolePrincipal { get; set; } = false;
}

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

    public DbSet<Film> Films { get; set; }
    public DbSet<Realisateur> Realisateurs { get; set; }
    public DbSet<Acteur> Acteurs { get; set; }
    public DbSet<CastingFilm> Castings { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configuration Film
        modelBuilder.Entity<Film>(entity =>
        {
            entity.HasIndex(f => f.Titre);
            entity.HasIndex(f => f.Genre);
            entity.Property(f => f.Note).HasColumnType("real");

            entity.HasOne(f => f.Realisateur)
                .WithMany(r => r.Films)
                .HasForeignKey(f => f.RealisateurId)
                .OnDelete(DeleteBehavior.Restrict); // Empêche suppression réalisateur si films
        });

        // Table intermédiaire avec clé composite
        modelBuilder.Entity<CastingFilm>(entity =>
        {
            entity.HasKey(c => new { c.FilmId, c.ActeurId });  // Clé composite

            entity.HasOne(c => c.Film)
                .WithMany(f => f.Castings)
                .HasForeignKey(c => c.FilmId);

            entity.HasOne(c => c.Acteur)
                .WithMany(a => a.Castings)
                .HasForeignKey(c => c.ActeurId);
        });

        // Seed data
        modelBuilder.Entity<Realisateur>().HasData(
            new Realisateur { Id = 1, Nom = "Nolan", Prenom = "Christopher", Nationalite = "Britannique" },
            new Realisateur { Id = 2, Nom = "Villeneuve", Prenom = "Denis", Nationalite = "Canadien" }
        );

        modelBuilder.Entity<Film>().HasData(
            new Film { Id = 1, Titre = "Inception", AnneeSortie = 2010, Genre = "SF", Note = 8.8, DureeMinutes = 148, RealisateurId = 1 },
            new Film { Id = 2, Titre = "Interstellar", AnneeSortie = 2014, Genre = "SF", Note = 8.6, DureeMinutes = 169, RealisateurId = 1 },
            new Film { Id = 3, Titre = "Dune", AnneeSortie = 2021, Genre = "SF", Note = 8.0, DureeMinutes = 155, RealisateurId = 2 }
        );
    }
}

// Repository
public interface IRepositoireFilm
{
    Task<(List<Film> Items, int Total)> ObtenirTousAsync(int page, int taille, CancellationToken ct = default);
    Task<Film?> ObtenirParIdAsync(int id, CancellationToken ct = default);
    Task<List<Film>> RechercherAsync(string? terme, string? genre, double? noteMin, CancellationToken ct = default);
    Task<Film> AjouterAsync(Film film, CancellationToken ct = default);
    Task<bool> SupprimerAsync(int id, CancellationToken ct = default);
}

public class RepositoireFilm : IRepositoireFilm
{
    private readonly CinemaDbContext _ctx;

    public RepositoireFilm(CinemaDbContext ctx) => _ctx = ctx;

    public async Task<(List<Film> Items, int Total)> ObtenirTousAsync(
        int page, int taille, CancellationToken ct = default)
    {
        var query = _ctx.Films.AsNoTracking();
        var total = await query.CountAsync(ct);
        var items = await query
            .Include(f => f.Realisateur)
            .OrderBy(f => f.Titre)
            .Skip((page - 1) * taille)
            .Take(taille)
            .ToListAsync(ct);
        return (items, total);
    }

    public async Task<Film?> ObtenirParIdAsync(int id, CancellationToken ct = default)
    {
        return await _ctx.Films
            .AsNoTracking()
            .Include(f => f.Realisateur)
            .Include(f => f.Castings).ThenInclude(c => c.Acteur)
            .FirstOrDefaultAsync(f => f.Id == id, ct);
    }

    public async Task<List<Film>> RechercherAsync(
        string? terme, string? genre, double? noteMin, CancellationToken ct = default)
    {
        var query = _ctx.Films.AsNoTracking().AsQueryable();

        if (!string.IsNullOrEmpty(terme))
            query = query.Where(f => f.Titre.Contains(terme));

        if (!string.IsNullOrEmpty(genre))
            query = query.Where(f => f.Genre == genre);

        if (noteMin.HasValue)
            query = query.Where(f => f.Note >= noteMin.Value);

        return await query.Include(f => f.Realisateur).OrderByDescending(f => f.Note).ToListAsync(ct);
    }

    public async Task<Film> AjouterAsync(Film film, CancellationToken ct = default)
    {
        _ctx.Films.Add(film);
        await _ctx.SaveChangesAsync(ct);
        return film;
    }

    public async Task<bool> SupprimerAsync(int id, CancellationToken ct = default)
    {
        var film = await _ctx.Films.FindAsync(new object[] { id }, ct);
        if (film is null) return false;
        _ctx.Films.Remove(film);
        await _ctx.SaveChangesAsync(ct);
        return true;
    }
}


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

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Implémenter le pattern Repository
[OK] Implémenter le pattern Unit of Work
[OK] Tester avec InMemory Database
[OK] Comprendre pourquoi et quand les utiliser
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] REPOSITORY PATTERN GÉNÉRIQUE
// ----------------------------------------------------------------------------

/*
[IDEE] POURQUOI REPOSITORY ?

SANS Repository :
  Controller -> DbContext directement
  [X] Difficile à tester (mocker DbContext = complexe)
  [X] Logique SQL dans les controllers
  [X] Couplage fort avec EF Core

AVEC Repository :
  Controller -> IRepository (interface) -> Implementation EF Core
  [OK] Facilement mockable pour les tests
  [OK] Logique de données centralisée
  [OK] Peut changer de technologie de stockage facilement
*/

// Interface générique
public interface IRepository<T> where T : class
{
    Task<T?> ObtenirParIdAsync(int id, CancellationToken ct = default);
    Task<IEnumerable<T>> ObtenirTousAsync(CancellationToken ct = default);
    Task<IEnumerable<T>> TrouverAsync(Expression<Func<T, bool>> predicate, CancellationToken ct = default);
    Task<T> AjouterAsync(T entite, CancellationToken ct = default);
    Task AjouterPlusieursAsync(IEnumerable<T> entites, CancellationToken ct = default);
    void Modifier(T entite);
    void Supprimer(T entite);
    void SupprimerPlusieur(IEnumerable<T> entites);
    Task<bool> ExisteAsync(Expression<Func<T, bool>> predicate, CancellationToken ct = default);
    Task<int> CompterAsync(Expression<Func<T, bool>>? predicate = null, CancellationToken ct = default);
}

// Implémentation générique
public class Repository<T> : IRepository<T> where T : class
{
    protected readonly AppDbContext _ctx;
    protected readonly DbSet<T> _dbSet;

    public Repository(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<IEnumerable<T>> ObtenirTousAsync(CancellationToken ct = default)
        => await _dbSet.AsNoTracking().ToListAsync(ct);

    public async Task<IEnumerable<T>> TrouverAsync(
        Expression<Func<T, bool>> predicate, CancellationToken ct = default)
        => await _dbSet.AsNoTracking().Where(predicate).ToListAsync(ct);

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

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

    public void Modifier(T entite)
        => _ctx.Entry(entite).State = EntityState.Modified;

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

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

    public async Task<bool> ExisteAsync(
        Expression<Func<T, bool>> predicate, CancellationToken ct = default)
        => await _dbSet.AnyAsync(predicate, ct);

    public async Task<int> CompterAsync(
        Expression<Func<T, bool>>? predicate = null, CancellationToken ct = default)
        => predicate is null
            ? await _dbSet.CountAsync(ct)
            : await _dbSet.CountAsync(predicate, ct);
}

// Repository spécialisé
public interface IRepositorireProduitSpec : IRepository<Produit>
{
    Task<List<Produit>> ObtenirAvecAvisAsync(CancellationToken ct = default);
    Task<List<Produit>> ObtenirParCategorieAsync(string categorie, CancellationToken ct = default);
    Task<List<Produit>> RechercherAsync(string terme, CancellationToken ct = default);
    Task<(List<Produit> Items, int Total)> ObtenirPagineAsync(
        int page, int taille, string? categorie = null, CancellationToken ct = default);
}

public class RepositorireProduitSpec : Repository<Produit>, IRepositorireProduitSpec
{
    public RepositorireProduitSpec(AppDbContext ctx) : base(ctx) { }

    public async Task<List<Produit>> ObtenirAvecAvisAsync(CancellationToken ct = default)
        => await _ctx.Produits.AsNoTracking()
            .Include(p => p.Avis)
            .Include(p => p.CategorieNav)
            .ToListAsync(ct);

    public async Task<List<Produit>> ObtenirParCategorieAsync(string categorie, CancellationToken ct = default)
        => await _ctx.Produits.AsNoTracking()
            .Where(p => p.Categorie == categorie)
            .ToListAsync(ct);

    public async Task<List<Produit>> RechercherAsync(string terme, CancellationToken ct = default)
        => await _ctx.Produits.AsNoTracking()
            .Where(p => p.Nom.Contains(terme) || (p.Categorie != null && p.Categorie.Contains(terme)))
            .ToListAsync(ct);

    public async Task<(List<Produit> Items, int Total)> ObtenirPagineAsync(
        int page, int taille, string? categorie = null, CancellationToken ct = default)
    {
        var query = _ctx.Produits.AsNoTracking().AsQueryable();
        if (!string.IsNullOrEmpty(categorie))
            query = query.Where(p => p.Categorie == categorie);
        var total = await query.CountAsync(ct);
        var items = await query.Skip((page - 1) * taille).Take(taille).ToListAsync(ct);
        return (items, total);
    }
}


// ----------------------------------------------------------------------------
// [LIEN] UNIT OF WORK PATTERN
// ----------------------------------------------------------------------------

/*
[IDEE] POURQUOI UNIT OF WORK ?

PROBLÈME SANS UoW :
  Chaque repository a son propre SaveChanges
  Si service utilise 2 repositories -> 2 transactions séparées
  Une réussit, l'autre échoue -> Base de données incohérente !

AVEC UoW :
  UN SEUL SaveChanges pour toute l'opération
  Transaction atomique (tout ou rien)
*/

public interface IUnitOfWork : IDisposable
{
    IRepositorireProduitSpec Produits { get; }
    IRepository<CategorieProduit> Categories { get; }
    IRepository<Utilisateur> Utilisateurs { get; }
    IRepository<Commande> Commandes { get; }

    Task<int> SauvegarderAsync(CancellationToken ct = default);
    Task<IDbContextTransaction> CommencerTransactionAsync(CancellationToken ct = default);
}

public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _ctx;

    private IRepositorireProduitSpec? _produits;
    private IRepository<CategorieProduit>? _categories;
    private IRepository<Utilisateur>? _utilisateurs;
    private IRepository<Commande>? _commandes;

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

    // Lazy initialization des repositories
    public IRepositorireProduitSpec Produits
        => _produits ??= new RepositorireProduitSpec(_ctx);

    public IRepository<CategorieProduit> Categories
        => _categories ??= new Repository<CategorieProduit>(_ctx);

    public IRepository<Utilisateur> Utilisateurs
        => _utilisateurs ??= new Repository<Utilisateur>(_ctx);

    public IRepository<Commande> Commandes
        => _commandes ??= new Repository<Commande>(_ctx);

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

    public async Task<IDbContextTransaction> CommencerTransactionAsync(CancellationToken ct = default)
        => await _ctx.Database.BeginTransactionAsync(ct);

    public void Dispose() => _ctx.Dispose();
}

// Utilisation dans un service
public class ServiceCommandeAvecUoW
{
    private readonly IUnitOfWork _uow;

    public ServiceCommandeAvecUoW(IUnitOfWork uow) => _uow = uow;

    public async Task<bool> PasserCommandeAsync(int userId, List<int> produitIds, CancellationToken ct = default)
    {
        await using var transaction = await _uow.CommencerTransactionAsync(ct);
        try
        {
            // Opération 1 : Vérifier stock
            var produits = new List<Produit>();
            foreach (var pId in produitIds)
            {
                var produit = await _uow.Produits.ObtenirParIdAsync(pId, ct);
                if (produit is null || !produit.EnStock)
                    throw new BusinessRuleException($"Produit {pId} indisponible");
                produits.Add(produit);
            }

            // Opération 2 : Créer commande
            var commande = new Commande
            {
                UtilisateurId = userId,
                Total = produits.Sum(p => p.Prix),
                Statut = "Validée"
            };
            await _uow.Commandes.AjouterAsync(commande, ct);

            // Opération 3 : Mettre à jour stock
            foreach (var produit in produits)
            {
                produit.EnStock = false;
                _uow.Produits.Modifier(produit);
            }

            // Sauvegarder TOUT en une transaction
            await _uow.SauvegarderAsync(ct);
            await transaction.CommitAsync(ct);

            return true;
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
}

// Enregistrement dans Program.cs :
/*
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IRepositorireProduitSpec, RepositorireProduitSpec>();
*/


// ============================================================================
// [GUIDE] CHAPITRE 10 : OPTIMISATION BASE DE DONNÉES
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Éviter les requêtes N+1
[OK] Utiliser Eager Loading, Lazy Loading, Explicit Loading
[OK] Créer et utiliser les index
[OK] Faire de la pagination efficace
[OK] Écrire des requêtes optimisées
*/


// ----------------------------------------------------------------------------
// [ATTENTION] LE PROBLÈME N+1 (À ABSOLUMENT ÉVITER)
// ----------------------------------------------------------------------------

public class ExemplesOptimisation
{
    private readonly AppDbContext _ctx;

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

    // [X] PROBLÈME N+1 : 1 requête + N requêtes pour les relations
    public async Task<List<object>> ProblemeNPlusUn()
    {
        var commandes = await _ctx.Commandes.ToListAsync(); // 1 requête

        return commandes.Select(c => new
        {
            c.Id,
            Utilisateur = c.Utilisateur.Nom // <- N requêtes (une par commande!)
        }).ToList();
    }

    // [OK] SOLUTION 1 : Eager Loading avec Include
    public async Task<List<object>> SolutionEagerLoading()
    {
        var commandes = await _ctx.Commandes
            .Include(c => c.Utilisateur)           // JOIN direct
            .Include(c => c.LignesCommande)        // JOIN lignes
            .ThenInclude(l => l.Produit)           // JOIN produits via lignes
            .AsNoTracking()
            .ToListAsync();                        // 1 requête SQL complexe

        return commandes.Select(c => new
        {
            c.Id,
            Utilisateur = c.Utilisateur.Nom,
            TotalLignes = c.LignesCommande.Count
        }).ToList();
    }

    // [OK] SOLUTION 2 : Projection directe (ENCORE MEILLEUR)
    public async Task<List<CommandeResume>> SolutionProjection()
    {
        // EF Core génère un SQL SELECT optimisé avec seulement les colonnes nécessaires
        return await _ctx.Commandes
            .AsNoTracking()
            .Select(c => new CommandeResume
            {
                Id = c.Id,
                NomUtilisateur = c.Utilisateur.Nom,
                Total = c.Total,
                NombreLignes = c.LignesCommande.Count()
            })
            .ToListAsync();
    }

    // [OK] SOLUTION 3 : Split Query (EF Core 5+) pour Many collections
    // Évite les produits cartésiens avec plusieurs Include
    public async Task<List<Commande>> SolutionSplitQuery()
    {
        return await _ctx.Commandes
            .AsNoTracking()
            .AsSplitQuery()             // Divise en plusieurs requêtes
            .Include(c => c.LignesCommande).ThenInclude(l => l.Produit)
            .Include(c => c.Utilisateur)
            .ToListAsync();
    }
}

public class CommandeResume
{
    public int Id { get; set; }
    public string NomUtilisateur { get; set; } = string.Empty;
    public decimal Total { get; set; }
    public int NombreLignes { get; set; }
}


// ----------------------------------------------------------------------------
// [GRAPHIQUE] REQUÊTES OPTIMISÉES
// ----------------------------------------------------------------------------

public class RequetesOptimisees
{
    private readonly AppDbContext _ctx;

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

    // COMPTER sans charger les données
    public async Task<int> CompterProduitsDisponibles()
        => await _ctx.Produits.CountAsync(p => p.EnStock);

    // VÉRIFIER EXISTENCE sans charger
    public async Task<bool> EmailExiste(string email)
        => await _ctx.Utilisateurs.AnyAsync(u => u.Email == email);

    // SOMME, MOYENNE, MAX/MIN
    public async Task<decimal> TotalVentes()
        => await _ctx.Commandes.SumAsync(c => c.Total);

    public async Task<double?> NoteMoyenne(int produitId)
        => await _ctx.Avis
            .Where(a => a.ProduitId == produitId)
            .AverageAsync(a => (double?)a.Note);

    // PREMIER ÉLÉMENT (optimisé vs First + ToList)
    public async Task<Produit?> ProduitLeMoinsCher()
        => await _ctx.Produits
            .AsNoTracking()
            .Where(p => p.EnStock)
            .OrderBy(p => p.Prix)
            .FirstOrDefaultAsync(); // Génère SELECT TOP 1

    // GROUP BY avec projection
    public async Task<List<StatCategorie>> StatsParCategorie()
        => await _ctx.Produits
            .AsNoTracking()
            .GroupBy(p => p.Categorie)
            .Select(g => new StatCategorie
            {
                Categorie = g.Key ?? "Inconnue",
                Nombre = g.Count(),
                PrixMoyen = g.Average(p => p.Prix),
                NombreDisponibles = g.Count(p => p.EnStock)
            })
            .ToListAsync();

    // REQUÊTE BRUTE SQL (pour cas complexes)
    public async Task<List<Produit>> RequeteSqlBrut(decimal prixMin)
        => await _ctx.Produits
            .FromSqlRaw("SELECT * FROM produits WHERE Prix >= {0}", prixMin)
            .AsNoTracking()
            .ToListAsync();

    // INTERPOLATED SQL (plus sûr - paramètre automatique)
    public async Task<List<Produit>> RequeteSqlInterpolee(decimal prixMin, string categorie)
        => await _ctx.Produits
            .FromSqlInterpolated($"SELECT * FROM produits WHERE Prix >= {prixMin} AND Categorie = {categorie}")
            .AsNoTracking()
            .ToListAsync();
}

public class StatCategorie
{
    public string Categorie { get; set; } = string.Empty;
    public int Nombre { get; set; }
    public double PrixMoyen { get; set; }
    public int NombreDisponibles { get; set; }
}


// ----------------------------------------------------------------------------
// [OLD_KEY] INDEX AVANCÉS
// ----------------------------------------------------------------------------

/*
TYPES D'INDEX :

1. Index simple (une colonne)
2. Index composite (plusieurs colonnes)
3. Index unique
4. Index filtré (condition WHERE)
5. Index covering (colonnes incluses)
*/

public class ProduitAvecIndex
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public string Categorie { get; set; } = string.Empty;
    public decimal Prix { get; set; }
    public bool EnStock { get; set; }
    public DateTime DateCreation { get; set; }
}

// Configuration des index dans OnModelCreating
/*
modelBuilder.Entity<ProduitAvecIndex>(entity =>
{
    // Index simple
    entity.HasIndex(p => p.Nom);

    // Index composite (ordre important!)
    entity.HasIndex(p => new { p.Categorie, p.Prix });

    // Index unique
    entity.HasIndex(p => p.Nom).IsUnique();

    // Index avec nom personnalisé
    entity.HasIndex(p => p.Categorie).HasDatabaseName("IX_Produits_Categorie");

    // Index filtré (PostgreSQL/SQL Server)
    entity.HasIndex(p => p.Prix)
        .HasFilter("[EnStock] = 1")  // Index seulement sur produits en stock
        .HasDatabaseName("IX_Produits_Prix_Dispo");
});
*/


// ============================================================================
// [COURS] EXERCICE PRATIQUE 7 (FINAL PARTIE 3) - AVEC CORRIGÉ
// ============================================================================

/*
ÉNONCÉ - Système de Bibliothèque Complet :

1. Entités : Livre, Auteur, Emprunteur, Emprunt
   - Auteur : Id, Nom, Prenom, Nationalite
   - Livre : Id, Titre, ISBN, Annee, Exemplaires, AutheurId
   - Emprunteur : Id, Nom, Email
   - Emprunt : Id, DateDebut, DateFin, EstRetourne, LivreId, EmprunteurId

2. DbContext avec Fluent API + index + seed data

3. Repository complet avec UoW

4. Service "BiblioService" avec :
   - EmprunterLivreAsync(livreId, emprunteurId) -> gère stock
   - RetournerLivreAsync(empruntId) -> met à jour retour
   - ObtenirLivresDisponiblesAsync() -> livres avec exemplaires > 0
   - ObtenirHistoriqueEmprunteurAsync(emprunteurId) -> historique complet
*/

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

public class Auteur
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public string Prenom { get; set; } = string.Empty;
    public string? Nationalite { get; set; }
    public ICollection<Livre> Livres { get; set; } = new List<Livre>();
}

public class LivreEntity
{
    public int Id { get; set; }
    public string Titre { get; set; } = string.Empty;
    public string ISBN { get; set; } = string.Empty;
    public int Annee { get; set; }
    public int Exemplaires { get; set; }
    public int AuteurId { get; set; }
    public Auteur Auteur { get; set; } = null!;
    public ICollection<Emprunt> Emprunts { get; set; } = new List<Emprunt>();
}

public class Emprunteur
{
    public int Id { get; set; }
    public string Nom { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public ICollection<Emprunt> Emprunts { get; set; } = new List<Emprunt>();
}

public class Emprunt
{
    public int Id { get; set; }
    public DateTime DateDebut { get; set; } = DateTime.UtcNow;
    public DateTime? DateFin { get; set; }
    public bool EstRetourne { get; set; } = false;
    public int LivreId { get; set; }
    public LivreEntity Livre { get; set; } = null!;
    public int EmprunteurId { get; set; }
    public Emprunteur Emprunteur { get; set; } = null!;
}

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

    public DbSet<LivreEntity> Livres { get; set; }
    public DbSet<Auteur> Auteurs { get; set; }
    public DbSet<Emprunteur> Emprunteurs { get; set; }
    public DbSet<Emprunt> Emprunts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<LivreEntity>(e =>
        {
            e.HasIndex(l => l.ISBN).IsUnique();
            e.HasIndex(l => l.Titre);
            e.HasOne(l => l.Auteur).WithMany(a => a.Livres)
                .HasForeignKey(l => l.AuteurId).OnDelete(DeleteBehavior.Restrict);
        });

        modelBuilder.Entity<Emprunteur>(e =>
            e.HasIndex(em => em.Email).IsUnique());

        modelBuilder.Entity<Auteur>().HasData(
            new Auteur { Id = 1, Nom = "Martin", Prenom = "Robert", Nationalite = "Américain" }
        );
        modelBuilder.Entity<LivreEntity>().HasData(
            new LivreEntity { Id = 1, Titre = "Clean Code", ISBN = "9780132350884", Annee = 2008, Exemplaires = 3, AuteurId = 1 },
            new LivreEntity { Id = 2, Titre = "Clean Architecture", ISBN = "9780134494166", Annee = 2017, Exemplaires = 2, AuteurId = 1 }
        );
    }
}

public class BiblioService
{
    private readonly BiblioDbContext _ctx;

    public BiblioService(BiblioDbContext ctx) => _ctx = ctx;

    public async Task<Emprunt> EmprunterLivreAsync(int livreId, int emprunteurId, CancellationToken ct = default)
    {
        var livre = await _ctx.Livres.FindAsync(new object[] { livreId }, ct)
            ?? throw new EntiteNotFoundException("Livre", livreId);

        if (livre.Exemplaires <= 0)
            throw new BusinessRuleException("Aucun exemplaire disponible pour ce livre");

        // Vérifier emprunteur n'a pas déjà ce livre
        var empruntEnCours = await _ctx.Emprunts
            .AnyAsync(e => e.LivreId == livreId && e.EmprunteurId == emprunteurId && !e.EstRetourne, ct);

        if (empruntEnCours)
            throw new BusinessRuleException("Cet emprunteur a déjà emprunté ce livre");

        // Transaction
        await using var transaction = await _ctx.Database.BeginTransactionAsync(ct);
        try
        {
            livre.Exemplaires--;
            var emprunt = new Emprunt { LivreId = livreId, EmprunteurId = emprunteurId, DateDebut = DateTime.UtcNow };
            _ctx.Emprunts.Add(emprunt);
            await _ctx.SaveChangesAsync(ct);
            await transaction.CommitAsync(ct);
            return emprunt;
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }

    public async Task RetournerLivreAsync(int empruntId, CancellationToken ct = default)
    {
        var emprunt = await _ctx.Emprunts
            .Include(e => e.Livre)
            .FirstOrDefaultAsync(e => e.Id == empruntId, ct)
            ?? throw new EntiteNotFoundException("Emprunt", empruntId);

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

        emprunt.EstRetourne = true;
        emprunt.DateFin = DateTime.UtcNow;
        emprunt.Livre.Exemplaires++;
        await _ctx.SaveChangesAsync(ct);
    }

    public async Task<List<LivreEntity>> ObtenirLivresDisponiblesAsync(CancellationToken ct = default)
        => await _ctx.Livres.AsNoTracking()
            .Include(l => l.Auteur)
            .Where(l => l.Exemplaires > 0)
            .OrderBy(l => l.Titre)
            .ToListAsync(ct);

    public async Task<List<Emprunt>> ObtenirHistoriqueEmprunteurAsync(int emprunteurId, CancellationToken ct = default)
        => await _ctx.Emprunts.AsNoTracking()
            .Include(e => e.Livre).ThenInclude(l => l.Auteur)
            .Where(e => e.EmprunteurId == emprunteurId)
            .OrderByDescending(e => e.DateDebut)
            .ToListAsync(ct);
}

/*
[DOCS] RÉCAPITULATIF PARTIE 3

[OK] EF Core : DbContext, DbSet, entités, migrations
[OK] CRUD complet : Add, Find, Update, Remove, SaveChanges
[OK] Tracking : AsNoTracking pour performances lectures
[OK] Repository Pattern : Interface + implémentation générique et spécialisée
[OK] Unit of Work : Transaction atomique sur plusieurs repositories
[OK] Optimisation : N+1, Include, projection, split query
[OK] Index : Simple, composite, unique, filtré
[OK] Raw SQL : FromSqlRaw pour cas complexes

-> PROCHAINE ÉTAPE : Partie 4 - Authentification & Sécurité [VERROUILLE]
*/

// ============================================================================
// [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
// PERFORMANCE, TESTS & DEVOPS
// ============================================================================
//
// [OBJECTIF] CETTE SECTION COUVRE :
// - Chapitre 20 : Performance API (Caching, Compression)
// - Chapitre 21 : Scalabilité
// - Chapitre 22 : Logging & Observabilité
// - Chapitre 23 : Unit Testing (xUnit + Moq)
// - Chapitre 24 : Integration Testing
// - Chapitre 25 : TDD
// - Chapitre 26 : Docker
// - Chapitre 27 : CI/CD (GitHub Actions)
// - Chapitre 28 : Déploiement
//
// [TEMPS] TEMPS : ~15-18 heures
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 20 : PERFORMANCE API
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Implémenter MemoryCache
[OK] Utiliser Redis pour le cache distribué
[OK] Configurer la compression des réponses
[OK] Utiliser le Response Caching
[OK] Optimiser les performances async
*/


// ----------------------------------------------------------------------------
// [SAUVEGARDE] MEMORYCACHE (CACHE EN MÉMOIRE)
// ----------------------------------------------------------------------------

/*
[IDEE] MemoryCache = Cache dans la mémoire du processus

COMMENT : Clé/Valeur en RAM
POURQUOI : Éviter les appels répétés à la BDD pour des données stables
QUAND : Données qui changent peu, une seule instance serveur

PACKAGES : Inclus dans ASP.NET Core (Microsoft.Extensions.Caching.Memory)
*/

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

// Service utilisant MemoryCache
public class ServiceProduitAvecCache
{
    private readonly IMemoryCache _cache;
    private readonly IProduitRepository _repo;
    private readonly ILogger<ServiceProduitAvecCache> _logger;

    // Clés de cache (éviter les fautes de frappe)
    private const string TOUS_PRODUITS_KEY = "produits:tous";
    private static string ProduitKey(int id) => $"produit:{id}";

    public ServiceProduitAvecCache(
        IMemoryCache cache,
        IProduitRepository repo,
        ILogger<ServiceProduitAvecCache> logger)
    {
        _cache = cache;
        _repo = repo;
        _logger = logger;
    }

    public async Task<IEnumerable<MonApp.Domain.Entities.ProduitDomain>> ObtenirTousAsync(CancellationToken ct = default)
    {
        // GetOrCreateAsync : Récupère depuis cache OU crée et met en cache
        return await _cache.GetOrCreateAsync(TOUS_PRODUITS_KEY, async entry =>
        {
            // Configuration du cache
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);  // Expire dans 5 min
            entry.SlidingExpiration = TimeSpan.FromMinutes(2);                 // Reset si accédé dans 2 min
            entry.Priority = CacheItemPriority.Normal;

            // Callback quand l'entrée est supprimée du cache
            entry.RegisterPostEvictionCallback((key, value, reason, state) =>
            {
                _logger.LogInformation("Cache évincé: {Key}, Raison: {Reason}", key, reason);
            });

            _logger.LogInformation("CACHE MISS: Chargement produits depuis BDD");
            return await _repo.ObtenirTousAsync(ct);
        }) ?? Enumerable.Empty<MonApp.Domain.Entities.ProduitDomain>();
    }

    public async Task<MonApp.Domain.Entities.ProduitDomain?> ObtenirParIdAsync(int id, CancellationToken ct = default)
    {
        var cle = ProduitKey(id);

        // TryGetValue : Vérifie sans créer
        if (_cache.TryGetValue(cle, out MonApp.Domain.Entities.ProduitDomain? cached))
        {
            _logger.LogInformation("CACHE HIT: Produit {Id}", id);
            return cached;
        }

        _logger.LogInformation("CACHE MISS: Produit {Id}", id);
        var produit = await _repo.ObtenirParIdAsync(id, ct);

        if (produit != null)
        {
            // Set avec options
            var options = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
                Size = 1  // Pour limiter la taille du cache
            };
            _cache.Set(cle, produit, options);
        }

        return produit;
    }

    // Invalider le cache quand les données changent
    public void InvaliderCacheProduit(int id)
    {
        _cache.Remove(ProduitKey(id));
        _cache.Remove(TOUS_PRODUITS_KEY); // Invalider aussi la liste
        _logger.LogInformation("Cache invalidé pour produit {Id}", id);
    }
}

// Configuration dans Program.cs :
/*
builder.Services.AddMemoryCache(options =>
{
    options.SizeLimit = 1024;       // Limite en unités (configurer avec .Size)
    options.CompactionPercentage = 0.25; // Compacter 25% quand limite atteinte
    options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
});
*/


// ----------------------------------------------------------------------------
// [ROUGE] REDIS (CACHE DISTRIBUÉ)
// ----------------------------------------------------------------------------

/*
[IDEE] REDIS = Cache distribué (Redis, SQL Server, Cosmos DB...)

COMMENT : Cache partagé entre plusieurs instances du serveur
POURQUOI : Ne pas perdre le cache lors du déploiement ou scaling horizontal
QUAND : Plusieurs instances serveur (load balancing), sessions partagées

PACKAGES :
  dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

CONFIGURATION :
  builder.Services.AddStackExchangeRedisCache(options =>
  {
      options.Configuration = builder.Configuration.GetConnectionString("Redis");
      options.InstanceName = "MonApp:";  // Préfixe pour éviter les conflits
  });
  // appsettings.json: "Redis": "localhost:6379"
*/

public class ServiceCacheRedis
{
    private readonly IDistributedCache _cache;
    private readonly ILogger<ServiceCacheRedis> _logger;

    public ServiceCacheRedis(IDistributedCache cache, ILogger<ServiceCacheRedis> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    // Wrapper générique pour simplifier l'utilisation
    public async Task<T?> ObtenirOuCreerAsync<T>(
        string cle,
        Func<Task<T>> fabriquer,
        TimeSpan? expiration = null,
        CancellationToken ct = default)
    {
        // Essayer depuis le cache
        var bytes = await _cache.GetAsync(cle, ct);

        if (bytes != null)
        {
            _logger.LogDebug("CACHE HIT: {Cle}", cle);
            return JsonSerializer.Deserialize<T>(bytes);
        }

        _logger.LogDebug("CACHE MISS: {Cle}", cle);

        // Fabriquer la valeur
        var valeur = await fabriquer();
        if (valeur == null) return default;

        // Sauvegarder dans le cache
        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(10),
            SlidingExpiration = TimeSpan.FromMinutes(2)
        };

        var serialise = JsonSerializer.SerializeToUtf8Bytes(valeur);
        await _cache.SetAsync(cle, serialise, options, ct);

        return valeur;
    }

    public async Task InvaliderAsync(string cle, CancellationToken ct = default)
    {
        await _cache.RemoveAsync(cle, ct);
        _logger.LogInformation("Cache invalidé: {Cle}", cle);
    }
}

// Pattern Cache-Aside (recommandé)
public class ServiceProduitsAvecRedis
{
    private readonly ServiceCacheRedis _cache;
    private readonly IProduitRepository _repo;

    public ServiceProduitsAvecRedis(ServiceCacheRedis cache, IProduitRepository repo)
    {
        _cache = cache;
        _repo = repo;
    }

    public async Task<MonApp.Domain.Entities.ProduitDomain?> ObtenirAsync(int id, CancellationToken ct = default)
    {
        return await _cache.ObtenirOuCreerAsync(
            cle: $"produit:{id}",
            fabriquer: () => _repo.ObtenirParIdAsync(id, ct)!,
            expiration: TimeSpan.FromMinutes(15),
            ct: ct);
    }
}


// ----------------------------------------------------------------------------
// [COMPRESSION] COMPRESSION
// ----------------------------------------------------------------------------

/*
[IDEE] COMPRESSION = Réduire la taille des réponses HTTP

COMMENT : Gzip ou Brotli (plus performant)
POURQUOI : Réduire la bande passante, améliorer les temps de chargement
QUAND : Toujours activer en production pour les APIs qui retournent du JSON
*/

/*
Dans Program.cs :

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;      // Activer pour HTTPS aussi
    options.Providers.Add<BrotliCompressionProvider>();   // Brotli en premier
    options.Providers.Add<GzipCompressionProvider>();     // Fallback Gzip
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
        new[] { "application/json", "text/json", "application/xml" });
});

builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
    options.Level = System.IO.Compression.CompressionLevel.Fastest);

builder.Services.Configure<GzipCompressionProviderOptions>(options =>
    options.Level = System.IO.Compression.CompressionLevel.SmallestSize);

// Dans le pipeline (avant UseRouting) :
app.UseResponseCompression();
*/


// ----------------------------------------------------------------------------
// [PACKAGE] RESPONSE CACHING
// ----------------------------------------------------------------------------

/*
[IDEE] RESPONSE CACHING = Cache HTTP côté client et proxy

COMMENT : Headers HTTP Cache-Control
POURQUOI : Éviter les requêtes côté client pour des données stables
QUAND : Ressources statiques, données changeant peu
*/

using Microsoft.AspNetCore.Mvc;

[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/[controller]")]
public class ProduitsAvecCacheController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    // Cache côté client 5 minutes, partagé (CDN/proxy)
    [HttpGet]
    [ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any)]
    public IActionResult ObtenirTous()
    {
        return Ok(new[] { "produit1", "produit2" });
    }

    // Cache privé (client seulement, pas CDN)
    [HttpGet("mon-panier")]
    [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Client)]
    public IActionResult ObtenirPanier()
    {
        return Ok("Mon panier");
    }

    // Pas de cache (données dynamiques)
    [HttpGet("{id}")]
    [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
    public IActionResult ObtenirParId(int id)
    {
        return Ok($"Produit {id}");
    }
}

// Configurer ResponseCaching dans Program.cs :
/*
builder.Services.AddResponseCaching(options =>
{
    options.MaximumBodySize = 1024;     // Max 1KB mis en cache côté serveur
    options.UseCaseSensitivePaths = true;
});

// Dans le pipeline (après UseRouting, avant UseAuthorization) :
app.UseResponseCaching();
*/


// ============================================================================
// [GUIDE] CHAPITRE 22 : LOGGING & OBSERVABILITÉ
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser le logging structuré avec Serilog
[OK] Configurer les sinks (fichier, console, Seq)
[OK] Implémenter les health checks
[OK] Monitorer l'application
*/


// ----------------------------------------------------------------------------
// [NOTE] SERILOG - LOGGING STRUCTURÉ
// ----------------------------------------------------------------------------

/*
[IDEE] SERILOG = Logging structuré pour .NET

COMMENT : Logs en JSON (parsables par des outils comme ELK, Seq, Splunk)
POURQUOI : Recherche et analyse des logs faciles
QUAND : Toujours en production

PACKAGES :
  dotnet add package Serilog.AspNetCore
  dotnet add package Serilog.Sinks.Console
  dotnet add package Serilog.Sinks.File
  dotnet add package Serilog.Sinks.Seq         (agrégateur local)
  dotnet add package Serilog.Enrichers.Environment
  dotnet add package Serilog.Enrichers.Thread
*/

/*
Configuration dans Program.cs :

using Serilog;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)       // Moins de bruit
    .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Information)
    .Enrich.FromLogContext()                                         // Context enrichissement
    .Enrich.WithMachineName()                                        // Nom de la machine
    .Enrich.WithThreadId()                                           // Thread ID
    .Enrich.WithProperty("Application", "MonApp")                    // Propriété fixe
    .Enrich.WithProperty("Version", "1.0.0")
    .WriteTo.Console(new Serilog.Formatting.Compact.CompactJsonFormatter()) // JSON Console
    .WriteTo.File(
        path: "logs/app-.log",
        rollingInterval: RollingInterval.Day,                        // Un fichier par jour
        retainedFileCountLimit: 30,                                  // Garder 30 fichiers
        fileSizeLimitBytes: 100_000_000,                             // Max 100MB par fichier
        formatter: new Serilog.Formatting.Compact.CompactJsonFormatter())
    .WriteTo.Seq("http://localhost:5341")                            // Seq (dev/staging)
    .CreateLogger();

try
{
    Log.Information("Démarrage de l'application");
    var builder = WebApplication.CreateBuilder(args);

    // Remplacer le logging .NET par Serilog
    builder.Host.UseSerilog();

    // ... rest of configuration
    var app = builder.Build();

    // Log les requêtes HTTP
    app.UseSerilogRequestLogging(options =>
    {
        options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} répondu {StatusCode} en {Elapsed:0.0000} ms";
        options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
            diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
            diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].ToString());
            diagnosticContext.Set("UserId", httpContext.User.FindFirst("sub")?.Value);
        };
    });

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminée de manière inattendue");
}
finally
{
    Log.CloseAndFlush(); // Important : vider le buffer
}
*/

// Utilisation dans les services
public class ServiceAvecLoggingStructure
{
    private readonly ILogger<ServiceAvecLoggingStructure> _logger;

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

    public async Task TraiterCommandeAsync(int commandeId, string email)
    {
        // [OK] Logging structuré (propriétés nommées)
        _logger.LogInformation("Traitement commande {CommandeId} pour {Email}", commandeId, email);

        // [OK] Avec données structurées complexes
        using var scope = _logger.BeginScope(new Dictionary<string, object>
        {
            ["CommandeId"] = commandeId,
            ["Email"] = email,
            ["Timestamp"] = DateTime.UtcNow
        });

        try
        {
            await Task.Delay(100); // Simulation traitement

            // [OK] Log de performance
            _logger.LogInformation("Commande {CommandeId} traitée avec succès", commandeId);
        }
        catch (Exception ex)
        {
            // [OK] Log d'erreur avec exception ET contexte
            _logger.LogError(ex, "Erreur lors du traitement de la commande {CommandeId}", commandeId);
            throw;
        }
    }

    // Niveaux de logs :
    /*
    _logger.LogTrace("Très détaillé, développement seulement");
    _logger.LogDebug("Débogage");
    _logger.LogInformation("Information normale");
    _logger.LogWarning("Avertissement, comportement inhabituel");
    _logger.LogError(ex, "Erreur gérée");
    _logger.LogCritical(ex, "Erreur fatale, app peut planter");
    */
}


// ----------------------------------------------------------------------------
// [HEAVY_BLACK_HEART] HEALTH CHECKS
// ----------------------------------------------------------------------------

/*
[IDEE] HEALTH CHECKS = Vérification de santé de l'application

COMMENT : Endpoints /health qui retournent l'état
POURQUOI :
  - Load balancer sait quand sortir une instance du pool
  - Kubernetes/Docker sait quand redémarrer
  - Monitoring peut alerter

TYPES :
  - Liveness  : L'app est-elle en vie ?
  - Readiness : L'app peut-elle traiter des requêtes ?

PACKAGES :
  dotnet add package AspNetCore.HealthChecks.UI
  dotnet add package AspNetCore.HealthChecks.SqlServer
  dotnet add package AspNetCore.HealthChecks.Redis
  dotnet add package AspNetCore.HealthChecks.Uris
*/

using Microsoft.Extensions.Diagnostics.HealthChecks;

// Health check personnalisé
public class BddHealthCheck : IHealthCheck
{
    private readonly AppIdentityDbContext _ctx;

    public BddHealthCheck(AppIdentityDbContext ctx) => _ctx = ctx;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            // Test de connexion BDD
            await _ctx.Database.CanConnectAsync(cancellationToken);
            return HealthCheckResult.Healthy("Base de données accessible.");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Base de données inaccessible.", ex);
        }
    }
}

// Health check service externe
public class ApiExterneHealthCheck : IHealthCheck
{
    private readonly HttpClient _httpClient;

    public ApiExterneHealthCheck(IHttpClientFactory factory)
        => _httpClient = factory.CreateClient();

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken ct = default)
    {
        try
        {
            var response = await _httpClient.GetAsync("https://api.externe.com/health", ct);
            return response.IsSuccessStatusCode
                ? HealthCheckResult.Healthy("API externe OK.")
                : HealthCheckResult.Degraded($"API externe répond {(int)response.StatusCode}.");
        }
        catch
        {
            return HealthCheckResult.Unhealthy("API externe inaccessible.");
        }
    }
}

/*
Configuration dans Program.cs :

builder.Services.AddHealthChecks()
    .AddCheck<BddHealthCheck>("base_de_donnees", tags: new[] { "db", "ready" })
    .AddSqlServer(                                              // SQL Server
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        name: "sql_server", tags: new[] { "db" })
    .AddRedis(                                                  // Redis
        redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
        name: "redis", tags: new[] { "cache" })
    .AddUrlGroup(                                               // URL externe
        new Uri("https://api.stripe.com/v1/charges"),
        name: "stripe_api", tags: new[] { "external" });

// Mapper les endpoints
app.MapHealthChecks("/health", new HealthCheckOptions
{
    Predicate = _ => true,          // Tous les checks
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse // JSON détaillé
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready"),
});

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false, // Vérification minimale (juste que l'app répond)
});
*/


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

/*
ÉNONCÉ — API de Météo avec Cache et Monitoring :

1. Créez WeatherService qui appelle une API météo externe
   (simulée) et met en cache le résultat 30 minutes avec IMemoryCache

2. Ajoutez du logging structuré Serilog :
   - Log à chaque CACHE HIT / CACHE MISS
   - Log le temps de réponse
   - Log les erreurs avec contexte

3. Ajoutez 2 Health Checks :
   - "meteo_api" : Vérifie que l'API météo externe répond
   - "cache" : Vérifie que le cache mémoire est accessible

4. Controller WeatherController :
   - GET /api/weather/{ville}
   - GET /health (déléguer aux health checks)
*/

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

public record MeteoDto(string Ville, double Temperature, string Description, DateTime MisAJourLe);

public class WeatherService
{
    private readonly IMemoryCache _cache;
    private readonly ILogger<WeatherService> _logger;

    public WeatherService(IMemoryCache cache, ILogger<WeatherService> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public async Task<MeteoDto?> ObtenirMeteoAsync(string ville, CancellationToken ct = default)
    {
        var cle = $"meteo:{ville.ToLower()}";
        var debut = DateTime.UtcNow;

        if (_cache.TryGetValue(cle, out MeteoDto? cached))
        {
            _logger.LogInformation("CACHE HIT: Météo pour {Ville} en {Ms}ms",
                ville, (DateTime.UtcNow - debut).TotalMilliseconds);
            return cached;
        }

        _logger.LogInformation("CACHE MISS: Appel API pour {Ville}", ville);

        try
        {
            // Simulation API météo externe
            await Task.Delay(200, ct);
            var meteo = new MeteoDto(
                ville,
                15.0 + new Random().NextDouble() * 20,
                "Partiellement nuageux",
                DateTime.UtcNow);

            _cache.Set(cle, meteo, TimeSpan.FromMinutes(30));

            _logger.LogInformation("Météo récupérée pour {Ville} en {Ms}ms",
                ville, (DateTime.UtcNow - debut).TotalMilliseconds);

            return meteo;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erreur récupération météo pour {Ville}", ville);
            throw;
        }
    }
}

// Health check API météo
public class MeteoApiHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext ctx, CancellationToken ct = default)
    {
        // Simulation : vérifier que l'API est accessible
        return Task.FromResult(HealthCheckResult.Healthy("API Météo accessible."));
    }
}

// Health check cache
public class CacheHealthCheck : IHealthCheck
{
    private readonly IMemoryCache _cache;

    public CacheHealthCheck(IMemoryCache cache) => _cache = cache;

    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext ctx, CancellationToken ct = default)
    {
        var cle = "health_check_test";
        _cache.Set(cle, DateTime.UtcNow, TimeSpan.FromSeconds(5));
        var ok = _cache.TryGetValue(cle, out _);
        return Task.FromResult(ok
            ? HealthCheckResult.Healthy("Cache mémoire fonctionnel.")
            : HealthCheckResult.Unhealthy("Cache mémoire défaillant."));
    }
}

[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/weather")]
public class WeatherController : Microsoft.AspNetCore.Mvc.ControllerBase
{
    private readonly WeatherService _service;

    public WeatherController(WeatherService service) => _service = service;

    [HttpGet("{ville}")]
    public async Task<IActionResult> Get(string ville, CancellationToken ct)
    {
        var meteo = await _service.ObtenirMeteoAsync(ville, ct);
        return meteo is null ? NotFound() : Ok(meteo);
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 23 : UNIT TESTING (xUnit + Moq)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Écrire des tests unitaires avec xUnit
[OK] Mocker les dépendances avec Moq
[OK] Utiliser FluentAssertions
[OK] Organiser les tests (AAA pattern)
[OK] Tester la logique de domaine
*/


// ----------------------------------------------------------------------------
// [TEST] SETUP DU PROJET DE TESTS
// ----------------------------------------------------------------------------

/*
CRÉATION :
  dotnet new xunit -n MonApp.Tests
  dotnet sln add MonApp.Tests/MonApp.Tests.csproj
  dotnet add MonApp.Tests reference src/MonApp.Application/...

PACKAGES :
  dotnet add package Moq
  dotnet add package FluentAssertions
  dotnet add package Microsoft.EntityFrameworkCore.InMemory (pour tester BDD)
  dotnet add package Bogus  (pour générer des données de test)
*/

using Xunit;
using Moq;
using FluentAssertions;

// ─── TESTS DU DOMAIN ────────────────────────────────────────────────────────

namespace MonApp.Tests.Domain
{
    public class ProduitDomainTests
    {
        // ─── PATTERN AAA : Arrange / Act / Assert ─────────────────────────

        [Fact]
        public void Creer_AvecDonneesValides_RetourneProduit()
        {
            // Arrange
            var nom = "Laptop";
            var prix = 999.99m;
            var stock = 10;
            var categorie = "Informatique";

            // Act
            var produit = ProduitDomain.Creer(nom, prix, stock, categorie);

            // Assert
            produit.Should().NotBeNull();
            produit.Nom.Should().Be(nom);
            produit.Prix.Should().Be(prix);
            produit.Stock.Should().Be(stock);
            produit.EstActif.Should().BeTrue();
        }

        [Theory]
        [InlineData("")]
        [InlineData(" ")]
        [InlineData(null)]
        public void Creer_AvecNomInvalide_LanceDomainException(string nomInvalide)
        {
            // Act
            var act = () => ProduitDomain.Creer(nomInvalide, 100m, 5, "Cat");

            // Assert
            act.Should().Throw<DomainException>()
                .WithMessage("*nom*");
        }

        [Fact]
        public void Creer_AvecPrixNegatif_LanceDomainException()
        {
            var act = () => ProduitDomain.Creer("Produit", -10m, 5, "Cat");
            act.Should().Throw<DomainException>()
                .WithMessage("*positif*");
        }

        [Fact]
        public void ModifierPrix_AvecPrixValide_MisAJour()
        {
            // Arrange
            var produit = ProduitDomain.Creer("Laptop", 999m, 10, "Cat");

            // Act
            produit.ModifierPrix(799m);

            // Assert
            produit.Prix.Should().Be(799m);
            produit.DateModification.Should().NotBeNull();
        }

        [Fact]
        public void RetirerDuStock_QuantiteInsuffisante_LanceException()
        {
            // Arrange
            var produit = ProduitDomain.Creer("Produit", 100m, 3, "Cat");

            // Act
            var act = () => produit.RetirerDuStock(5);

            // Assert
            act.Should().Throw<MonApp.Domain.Exceptions.StockInsuffisantException>();
        }

        [Fact]
        public void Creer_AjouteDomainEvent()
        {
            var produit = ProduitDomain.Creer("Laptop", 999m, 10, "Cat");

            produit.DomainEvents.Should().HaveCount(1);
            produit.DomainEvents[0].Should().BeOfType<MonApp.Domain.Events.ProduitCreéEvent>();
        }
    }

    // Tests des Value Objects
    public class EmailTests
    {
        [Fact]
        public void Creer_AvecEmailValide_RetourneEmail()
        {
            var email = Email.Creer("alice@example.com");
            email.Valeur.Should().Be("alice@example.com");
        }

        [Theory]
        [InlineData("pas-un-email")]
        [InlineData("@nodomain")]
        [InlineData("")]
        public void Creer_AvecEmailInvalide_LanceException(string emailInvalide)
        {
            var act = () => Email.Creer(emailInvalide);
            act.Should().Throw<DomainException>();
        }

        [Fact]
        public void DeuxEmails_MemeValeur_SontEgaux()
        {
            var email1 = Email.Creer("alice@example.com");
            var email2 = Email.Creer("alice@example.com");

            email1.Should().Be(email2);
            (email1 == email2).Should().BeTrue();
        }

        [Fact]
        public void DeuxEmails_Differents_SontInegaux()
        {
            var email1 = Email.Creer("alice@example.com");
            var email2 = Email.Creer("bob@example.com");

            email1.Should().NotBe(email2);
        }
    }
}

// ─── TESTS DES HANDLERS (APPLICATION) ──────────────────────────────────────

namespace MonApp.Tests.Application
{
    public class CreerProduitHandlerTests
    {
        private readonly Mock<MonApp.Domain.Interfaces.IUnitOfWork> _mockUow;
        private readonly Mock<IProduitRepository> _mockRepo;
        private readonly Mock<ILogger<MonApp.Application.Produits.Commands.CreerProduitHandler>> _mockLogger;
        private readonly MonApp.Application.Produits.Commands.CreerProduitHandler _handler;

        public CreerProduitHandlerTests()
        {
            _mockRepo = new Mock<IProduitRepository>();
            _mockUow = new Mock<MonApp.Domain.Interfaces.IUnitOfWork>();
            _mockLogger = new Mock<ILogger<MonApp.Application.Produits.Commands.CreerProduitHandler>>();

            _mockUow.Setup(u => u.Produits).Returns(_mockRepo.Object);

            _handler = new MonApp.Application.Produits.Commands.CreerProduitHandler(
                _mockUow.Object, _mockLogger.Object);
        }

        [Fact]
        public async Task Handle_CommandeValide_CreeProduitEtSauvegarde()
        {
            // Arrange
            var command = new MonApp.Application.Produits.Commands.CreerProduitCommand(
                "Laptop", 999m, 10, "Informatique");

            _mockRepo.Setup(r => r.NomExisteAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
                .ReturnsAsync(false);

            _mockRepo.Setup(r => r.AjouterAsync(It.IsAny<ProduitDomain>(), It.IsAny<CancellationToken>()))
                .Returns(Task.CompletedTask);

            _mockUow.Setup(u => u.SauvegarderAsync(It.IsAny<CancellationToken>()))
                .ReturnsAsync(1);

            // Act
            var result = await _handler.HandleAsync(command);

            // Assert
            result.Should().NotBeNull();
            result.Nom.Should().Be("Laptop");
            result.Prix.Should().Be(999m);
            result.Succes.Should().BeTrue();

            // Vérifier que les méthodes ont bien été appelées
            _mockRepo.Verify(r => r.AjouterAsync(It.IsAny<ProduitDomain>(), It.IsAny<CancellationToken>()), Times.Once);
            _mockUow.Verify(u => u.SauvegarderAsync(It.IsAny<CancellationToken>()), Times.Once);
        }

        [Fact]
        public async Task Handle_NomDejaPris_LanceDomainException()
        {
            // Arrange
            var command = new MonApp.Application.Produits.Commands.CreerProduitCommand(
                "Laptop", 999m, 10, "Informatique");

            _mockRepo.Setup(r => r.NomExisteAsync("Laptop", It.IsAny<CancellationToken>()))
                .ReturnsAsync(true); // Nom déjà pris !

            // Act
            var act = async () => await _handler.HandleAsync(command);

            // Assert
            await act.Should().ThrowAsync<DomainException>()
                .WithMessage("*existe déjà*");

            // Vérifier que SaveChanges N'a PAS été appelé
            _mockUow.Verify(u => u.SauvegarderAsync(It.IsAny<CancellationToken>()), Times.Never);
        }
    }

    // Tests avec InMemory Database
    public class RepositoireProduitTests : IDisposable
    {
        private readonly AppDbContext _ctx;

        public RepositoireProduitTests()
        {
            var options = new DbContextOptionsBuilder<AppDbContext>()
                .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) // BDD unique par test
                .Options;
            _ctx = new AppDbContext(options);
        }

        [Fact]
        public async Task AjouterAsync_ProduitValide_PersisteDansBDD()
        {
            // Arrange
            var repo = new MonApp.Infrastructure.Persistence.ProduitRepository(_ctx);
            var produit = ProduitDomain.Creer("Laptop", 999m, 10, "Cat");

            // Act
            await repo.AjouterAsync(produit);
            await _ctx.SaveChangesAsync();

            // Assert
            var saved = await repo.ObtenirParIdAsync(produit.Id);
            saved.Should().NotBeNull();
            saved!.Nom.Should().Be("Laptop");
        }

        [Fact]
        public async Task NomExisteAsync_NomPrisIRetourneTrue()
        {
            var repo = new MonApp.Infrastructure.Persistence.ProduitRepository(_ctx);
            var produit = ProduitDomain.Creer("Laptop", 999m, 10, "Cat");
            await repo.AjouterAsync(produit);
            await _ctx.SaveChangesAsync();

            var existe = await repo.NomExisteAsync("Laptop");

            existe.Should().BeTrue();
        }

        public void Dispose() => _ctx.Dispose();
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 24 : INTEGRATION TESTING
// ============================================================================

/*
[IDEE] INTEGRATION TESTING = Tester l'application dans son ensemble

COMMENT : WebApplicationFactory lance une vraie instance
POURQUOI : Tester les endpoints HTTP de bout en bout
QUAND : Tests fonctionnels, vérification des routes et serialisation

PACKAGES :
  dotnet add package Microsoft.AspNetCore.Mvc.Testing
*/

namespace MonApp.Tests.Integration
{
    // Fixture partagée entre les tests
    public class MonApiFactory : WebApplicationFactory<Program>
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                // Remplacer la BDD réelle par InMemory
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
                if (descriptor != null) services.Remove(descriptor);

                services.AddDbContext<AppDbContext>(options =>
                    options.UseInMemoryDatabase("IntegrationTests"));

                // Remplacer services externes par des mocks
                services.AddScoped<MonApp.Application.Interfaces.IEmailService, FakeEmailService>();
            });

            builder.UseEnvironment("Testing");
        }
    }

    // Service email factice pour les tests
    public class FakeEmailService : MonApp.Application.Interfaces.IEmailService
    {
        public List<(string Dest, string Sujet)> EmailsEnvoyés { get; } = new();

        public Task EnvoyerAsync(string dest, string sujet, string corps, CancellationToken ct = default)
        {
            EmailsEnvoyés.Add((dest, sujet));
            return Task.CompletedTask;
        }
    }

    // Tests d'intégration
    public class ProduitsIntegrationTests : IClassFixture<MonApiFactory>
    {
        private readonly HttpClient _client;

        public ProduitsIntegrationTests(MonApiFactory factory)
        {
            _client = factory.CreateClient();
        }

        [Fact]
        public async Task GET_Produits_RetourneListeVide()
        {
            // Act
            var response = await _client.GetAsync("/api/produits-cqrs");

            // Assert
            response.Should().BeSuccessful(); // FluentAssertions pour HttpResponseMessage
            var contenu = await response.Content.ReadAsStringAsync();
            contenu.Should().NotBeEmpty();
        }

        [Fact]
        public async Task POST_Produit_RetourneCreated()
        {
            // Arrange
            var command = new { Nom = "Test Produit", Prix = 99.99, Stock = 5, Categorie = "Test" };
            var json = JsonSerializer.Serialize(command);
            var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

            // Act
            var response = await _client.PostAsync("/api/produits-cqrs", content);

            // Assert
            response.StatusCode.Should().Be(System.Net.HttpStatusCode.Created);
            response.Headers.Location.Should().NotBeNull();
        }

        [Fact]
        public async Task GET_ProduitInexistant_Retourne404()
        {
            var response = await _client.GetAsync("/api/produits-cqrs/99999");

            response.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
        }

        // Test avec authentification
        [Fact]
        public async Task GET_EndpointProtege_SansAuth_Retourne401()
        {
            var response = await _client.GetAsync("/api/panier");

            response.StatusCode.Should().Be(System.Net.HttpStatusCode.Unauthorized);
        }
    }

    // Helper pour créer un client authentifié
    public static class TestAuthExtensions
    {
        public static HttpClient AvecJwtToken(this HttpClient client, string token)
        {
            client.DefaultRequestHeaders.Authorization =
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
            return client;
        }
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 25 : TDD (TEST DRIVEN DEVELOPMENT)
// ============================================================================

/*
TDD = Red -> Green -> Refactor

1. RED   : Écrire le test qui ÉCHOUE (fonctionnalité pas encore implémentée)
2. GREEN : Écrire le MINIMUM de code pour faire passer le test
3. REFACTOR : Améliorer le code sans casser les tests

EXEMPLE TDD : Implémentation du panier d'achat
*/

namespace MonApp.Tests.TDD
{
    public class PanierTddTests
    {
        // ─── RED : Écrire les tests avant le code ──────────────────────────

        [Fact]
        public void AjouterArticle_NouveauProduit_AjouteDansListe()
        {
            var panier = PanierAggregate.CreerPourUtilisateur("user1");
            panier.AjouterArticle(1, "Laptop", 999m, 1);

            panier.Articles.Should().HaveCount(1);
            panier.Articles[0].ProduitId.Should().Be(1);
            panier.Articles[0].NomProduit.Should().Be("Laptop");
        }

        [Fact]
        public void AjouterArticle_MemeProduitDeuxFois_AdditionneQuantites()
        {
            var panier = PanierAggregate.CreerPourUtilisateur("user1");
            panier.AjouterArticle(1, "Laptop", 999m, 1);
            panier.AjouterArticle(1, "Laptop", 999m, 2);

            panier.Articles.Should().HaveCount(1);
            panier.Articles[0].Quantite.Should().Be(3);
        }

        [Fact]
        public void Total_DeuxArticles_CalculeCorrectement()
        {
            var panier = PanierAggregate.CreerPourUtilisateur("user1");
            panier.AjouterArticle(1, "Laptop", 1000m, 2);
            panier.AjouterArticle(2, "Souris", 50m, 3);

            panier.Total.Should().Be(2150m); // (1000*2) + (50*3)
        }

        [Fact]
        public void Vider_PanierAvecArticles_SupprimesTout()
        {
            var panier = PanierAggregate.CreerPourUtilisateur("user1");
            panier.AjouterArticle(1, "Laptop", 999m, 1);
            panier.Vider();

            panier.Articles.Should().BeEmpty();
            panier.Total.Should().Be(0);
        }

        [Fact]
        public void Vider_PublieDomainEvent()
        {
            var panier = PanierAggregate.CreerPourUtilisateur("user1");
            panier.AjouterArticle(1, "Laptop", 999m, 1);
            panier.Vider();

            panier.DomainEvents.Should().ContainSingle(e => e is PanierVideEvent);
        }
    }
}


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

/*
ÉNONCÉ — Tests complets pour ServiceCommandes :

1. Tests unitaires avec Moq pour ServiceCommandeAvecUoW :
   - PasserCommandeAsync avec stock disponible -> succès
   - PasserCommandeAsync avec stock épuisé -> lève BusinessRuleException
   - Vérifier que SaveChanges est appelé une seule fois

2. Tests d'intégration :
   - POST /api/commandes -> 201 Created avec données valides
   - POST /api/commandes -> 422 avec produit épuisé

3. Tests TDD pour une nouvelle fonctionnalité :
   Calculateur de frais de livraison :
   - Gratuite si total > 50€
   - 5€ si total entre 20€ et 50€
   - 10€ si total < 20€
*/

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

// 3. TDD - Frais de livraison
namespace Exercice15.TDD
{
    // RED: Tests écrits avant l'implémentation
    public class CalculateurFraisLivraisonTests
    {
        [Theory]
        [InlineData(60, 0)]   // > 50€ -> gratuit
        [InlineData(50.01, 0)]
        [InlineData(50, 5)]   // 20-50€ -> 5€
        [InlineData(20, 5)]
        [InlineData(19.99, 10)] // < 20€ -> 10€
        [InlineData(0.01, 10)]
        public void CalculerFrais_RetourneValeurCorrecte(decimal total, decimal fraisAttendus)
        {
            var calc = new CalculateurFraisLivraison();
            var frais = calc.Calculer(total);
            frais.Should().Be(fraisAttendus);
        }

        [Fact]
        public void CalculerFrais_TotalNegatif_LanceException()
        {
            var calc = new CalculateurFraisLivraison();
            var act = () => calc.Calculer(-1);
            act.Should().Throw<ArgumentException>().WithMessage("*négatif*");
        }
    }

    // GREEN: Implémentation minimale
    public class CalculateurFraisLivraison
    {
        public decimal Calculer(decimal total)
        {
            if (total < 0) throw new ArgumentException("Le total ne peut pas être négatif.", nameof(total));

            return total switch
            {
                > 50 => 0m,
                >= 20 => 5m,
                _ => 10m
            };
        }
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 26 : DOCKER
// ============================================================================

/*
[IDEE] DOCKER = Containerisation de l'application

POURQUOI :
  - "Fonctionne sur ma machine" -> éliminé
  - Déploiement reproductible
  - Isolation des dépendances
  - Facilite le CI/CD

DOCKERFILE POUR ASP.NET CORE :
*/

/*
# ═══════════════════════════════════════════════════════════
# Dockerfile (à la racine du projet)
# ═══════════════════════════════════════════════════════════

# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copier les fichiers csproj et restaurer les packages (couche cachée)
COPY ["src/MonApp.API/MonApp.API.csproj", "src/MonApp.API/"]
COPY ["src/MonApp.Application/MonApp.Application.csproj", "src/MonApp.Application/"]
COPY ["src/MonApp.Domain/MonApp.Domain.csproj", "src/MonApp.Domain/"]
COPY ["src/MonApp.Infrastructure/MonApp.Infrastructure.csproj", "src/MonApp.Infrastructure/"]
RUN dotnet restore "src/MonApp.API/MonApp.API.csproj"

# Copier tout le code source
COPY . .

# Builder l'application
WORKDIR "/src/src/MonApp.API"
RUN dotnet build "MonApp.API.csproj" -c Release -o /app/build

# Stage 2: Publish
FROM build AS publish
RUN dotnet publish "MonApp.API.csproj" -c Release -o /app/publish \
    /p:UseAppHost=false \
    /p:PublishReadyToRun=true

# Stage 3: Runtime (image finale)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app

# Sécurité: utilisateur non-root
RUN addgroup --gid 1001 --system appgroup && \
    adduser --uid 1001 --system --ingroup appgroup appuser

# Copier les fichiers publiés depuis stage publish
COPY --from=publish /app/publish .

# Changer le propriétaire
RUN chown -R appuser:appgroup /app
USER appuser

# Configuration
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# Point d'entrée
ENTRYPOINT ["dotnet", "MonApp.API.dll"]
*/

/*
# ═══════════════════════════════════════════════════════════
# docker-compose.yml (développement)
# ═══════════════════════════════════════════════════════════

version: '3.8'

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: monapp-api
    ports:
      - "5001:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__DefaultConnection=Server=db;Database=MonApp;User=sa;Password=YourPassword123!;TrustServerCertificate=True
      - ConnectionStrings__Redis=redis:6379
      - JwtSettings__SecretKey=${JWT_SECRET_KEY}  # Depuis .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ./logs:/app/logs
    networks:
      - monapp-network
    restart: unless-stopped

  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    container_name: monapp-db
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourPassword123!
    ports:
      - "1433:1433"
    volumes:
      - sqlserver_data:/var/opt/mssql
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P YourPassword123! -Q 'SELECT 1'"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - monapp-network

  redis:
    image: redis:7-alpine
    container_name: monapp-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - monapp-network

  seq:
    image: datalust/seq:latest
    container_name: monapp-seq
    environment:
      - ACCEPT_EULA=Y
    ports:
      - "5341:80"
    networks:
      - monapp-network

volumes:
  sqlserver_data:
  redis_data:

networks:
  monapp-network:
    driver: bridge

# ═══════════════════════════════════════════
# COMMANDES DOCKER :
# docker build -t monapp:latest .
# docker-compose up -d
# docker-compose logs -f api
# docker-compose down
# docker-compose down -v   (supprimer volumes aussi)
# ═══════════════════════════════════════════
*/


// ============================================================================
// [GUIDE] CHAPITRE 27 : CI/CD AVEC GITHUB ACTIONS
// ============================================================================

/*
[IDEE] CI/CD = Intégration Continue / Déploiement Continu

CI = Vérifier automatiquement à chaque push :
  - Compilation
  - Tests unitaires
  - Tests d'intégration
  - Analyse de code

CD = Déployer automatiquement si CI passe :
  - Vers staging (automatique)
  - Vers production (manuel ou automatique)
*/

/*
# ═══════════════════════════════════════════════════════════
# .github/workflows/ci-cd.yml
# ═══════════════════════════════════════════════════════════

name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  DOTNET_VERSION: '8.0.x'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # ─── ÉTAPE 1 : Tests ─────────────────────────────────────
  tests:
    name: Tests & Build
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: testpassword
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: ${{ env.DOTNET_VERSION }}

    - name: Cache NuGet packages
      uses: actions/cache@v3
      with:
        path: ~/.nuget/packages
        key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
        restore-keys: ${{ runner.os }}-nuget-

    - name: Restore dependencies
      run: dotnet restore

    - name: Build
      run: dotnet build --no-restore --configuration Release

    - name: Run unit tests
      run: |
        dotnet test tests/MonApp.Tests/MonApp.Tests.csproj \
          --no-build \
          --configuration Release \
          --logger trx \
          --collect:"XPlat Code Coverage" \
          -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura

    - name: Run integration tests
      env:
        ConnectionStrings__TestDb: "Host=localhost;Database=testdb;Username=postgres;Password=testpassword"
      run: |
        dotnet test tests/MonApp.Integration.Tests/MonApp.Integration.Tests.csproj \
          --no-build \
          --configuration Release

    - name: Upload test results
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: test-results
        path: '**/*.trx'

    - name: Code Coverage Report
      uses: codecov/codecov-action@v3
      with:
        token: ${{ secrets.CODECOV_TOKEN }}

  # ─── ÉTAPE 2 : Build Docker Image ────────────────────────
  docker-build:
    name: Build Docker Image
    needs: tests
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    permissions:
      contents: read
      packages: write

    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Login to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=sha
          type=ref,event=branch
          latest

    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  # ─── ÉTAPE 3 : Déploiement Staging ───────────────────────
  deploy-staging:
    name: Deploy to Staging
    needs: docker-build
    runs-on: ubuntu-latest
    environment: staging

    steps:
    - name: Deploy to staging server
      uses: appleboy/ssh-action@v0.1.7
      with:
        host: ${{ secrets.STAGING_HOST }}
        username: ${{ secrets.STAGING_USER }}
        key: ${{ secrets.STAGING_SSH_KEY }}
        script: |
          cd /opt/monapp
          docker-compose pull api
          docker-compose up -d api
          docker system prune -f

  # ─── ÉTAPE 4 : Déploiement Production (manuel) ───────────
  deploy-production:
    name: Deploy to Production
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://monapp.com

    steps:
    - name: Deploy to production
      run: echo "Déploiement production déclenché manuellement"
*/


// ============================================================================
// [GUIDE] CHAPITRE 28 : DÉPLOIEMENT
// ============================================================================

/*
OPTIONS DE DÉPLOIEMENT :

1. Azure App Service (PaaS, le plus simple)
2. Linux VPS

// ============================================================================
// [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
═══════════════════════════════════════════════════════════════════
*/