

Creating the continuation of the Blazor guide - Parts 9 and 10
csharp

// ============================================================================
// [LIVRE] BLAZOR - PARTIE 9 : DEVOPS & PRODUCTION
// ============================================================================
//
// CHAPITRE 26 : Dockerisation
// CHAPITRE 27 : CI/CD (GitHub Actions, Azure DevOps)
// CHAPITRE 28 : Observabilité (Logging, Serilog, Monitoring)
//
// [TEMPS] TEMPS ESTIMÉ : ~10-12 heures
// [DOCS] PRÉREQUIS : Parties 1-8 complétées
// ============================================================================

/*
[OBJECTIF] PHILOSOPHIE DE CETTE PARTIE

POURQUOI DEVOPS ?
-> Votre application peut être parfaite localement...
  ...mais inutile si elle n'est pas déployée !
-> DevOps = Automatiser le chemin du code à la production
-> Zéro erreur humaine, déploiement reproductible, retour arrière facile

AVANT DEVOPS (manuel) :
  Développeur -> Compile sur sa machine -> Copie les fichiers sur le serveur ->
  Croise les doigts -> Problème de version -> Panic...

AVEC DEVOPS (automatisé) :
  Développeur -> git push -> Tests automatiques -> Build Docker -> Deploy -> [OK]

CE QUE VOUS ALLEZ CONSTRUIRE :
  - Dockeriser votre application Blazor + API
  - Pipeline CI/CD qui déploie automatiquement à chaque push
  - Monitoring avec Serilog + Application Insights
  - Alertes quand quelque chose se passe mal
*/


// ============================================================================
// [GUIDE] CHAPITRE 26 : DOCKERISATION
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre Docker et pourquoi l'utiliser
[OK] Créer un Dockerfile optimisé pour Blazor WASM
[OK] Créer un Dockerfile optimisé pour ASP.NET Core (API)
[OK] Orchestrer avec Docker Compose
[OK] Configurer un reverse proxy avec Nginx
[OK] Optimiser les images Docker (taille, sécurité)
*/


// ----------------------------------------------------------------------------
// [DOCKER] DOCKER — Comprendre les concepts fondamentaux
// ----------------------------------------------------------------------------

/*
QU'EST-CE QUE DOCKER ?

SANS DOCKER :
"Ça marche sur ma machine !"
-> Chaque serveur a des versions différentes (.NET, OS, bibliothèques)
-> Ce qui fonctionne chez le dev ne fonctionne pas en prod
-> Configuration manuelle de chaque serveur

AVEC DOCKER :
-> Un CONTENEUR = Boîte hermétique avec TOUT ce dont l'app a besoin
-> Identique partout (dev, staging, production)
-> Déployable en secondes
-> Isolation totale entre applications

ANALOGIE PARFAITE :
Conteneur maritime = Contient des marchandises emballées
-> Peut être chargé sur n'importe quel bateau
-> Le bateau ne sait pas ce qu'il y a dedans
-> Contenu identique peu importe le port de destination

VOCABULAIRE DOCKER :

IMAGE :     Le "moule" (template) d'un conteneur
            Fichier read-only avec l'app + dépendances + config
            Exemple : "monapp:v1.2.3"

CONTENEUR : L'instance en cours d'exécution d'une image
            Comme un processus isolé
            Peut être démarré, arrêté, copié, supprimé

REGISTRY :  Dépôt d'images Docker
            -> Docker Hub (public)
            -> Azure Container Registry (privé, recommandé)
            -> GitHub Container Registry

DOCKERFILE : Script qui décrit comment construire une image
             Chaque ligne = une couche de l'image

DOCKER COMPOSE : Outil pour orchestrer PLUSIEURS conteneurs
                 Exemple : App + BDD + Redis + Nginx

COMMANDES ESSENTIELLES :
docker build -t monapp:latest .          -> Construire une image
docker run -p 8080:80 monapp:latest      -> Lancer un conteneur
docker ps                                 -> Lister les conteneurs actifs
docker logs monconteneur                  -> Voir les logs
docker exec -it monconteneur bash        -> Entrer dans un conteneur
docker stop monconteneur                  -> Arrêter un conteneur
docker rm monconteneur                    -> Supprimer un conteneur
docker images                             -> Lister les images
docker rmi monapp:latest                  -> Supprimer une image
docker pull nginx:alpine                  -> Télécharger une image
docker push registry.io/monapp:latest    -> Envoyer vers un registry
*/


// ----------------------------------------------------------------------------
// [FICHIER] DOCKERFILE BLAZOR WEBASSEMBLY + ASP.NET CORE (HOSTED)
// ----------------------------------------------------------------------------

/*
STRATÉGIE MULTI-STAGE BUILD :

POURQUOI ?
-> Le build .NET nécessite le SDK (2 GB+)
-> L'exécution ne nécessite que le runtime (200 MB)
-> Multi-stage = Build avec SDK, copie uniquement le résultat dans l'image finale

IMAGE FINALE RÉSULTANTE :
-> ~150-200 MB au lieu de 2+ GB
-> Pas d'outils de build dans l'image de prod (sécurité)
-> Moins de surface d'attaque

SCHÉMA :

  Stage 1 "build" (SDK ~2GB)   Stage 2 "final" (~150MB)
  ┌──────────────────────┐     ┌──────────────────────┐
  │ .NET SDK             │     │ .NET Runtime seulement│
  │ dotnet restore       │ ->->-> │ Fichiers publiés      │
  │ dotnet publish       │     │ Configuration         │
  └──────────────────────┘     └──────────────────────┘
*/

/*
─────────────────────────────────────────────────────────────────
Dockerfile pour application Blazor WASM hébergée (Client + Server)
─────────────────────────────────────────────────────────────────
Fichier : Dockerfile (à la racine de la solution)
*/

/*
# ╔══════════════════════════════════════════════════════════════╗
# ║  STAGE 1 : BUILD                                             ║
# ║  Utiliser l'image SDK complète pour compiler l'application   ║
# ╚══════════════════════════════════════════════════════════════╝
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

# Définir le répertoire de travail dans le conteneur
WORKDIR /src

# ─── Optimisation du cache Docker ─────────────────────────────
# Copier UNIQUEMENT les fichiers .csproj en premier
# Docker cache chaque couche : si les .csproj n'ont pas changé,
# la restauration NuGet est mise en cache -> Build plus rapide !

# Copier les projets (adapter selon votre structure)
COPY ["MonApp.Server/MonApp.Server.csproj", "MonApp.Server/"]
COPY ["MonApp.Client/MonApp.Client.csproj", "MonApp.Client/"]
COPY ["MonApp.Shared/MonApp.Shared.csproj", "MonApp.Shared/"]

# Restaurer les dépendances NuGet (couche cachée si .csproj inchangé)
RUN dotnet restore "MonApp.Server/MonApp.Server.csproj"

# ─── Build de l'application ───────────────────────────────────
# Copier TOUT le reste du code source
COPY . .

# Compiler en Release (optimisations activées)
WORKDIR "/src/MonApp.Server"
RUN dotnet build "MonApp.Server.csproj" -c Release -o /app/build

# ─── Publication ──────────────────────────────────────────────
# Publier = Préparer les fichiers pour la production
# --no-restore : Pas besoin de restaurer à nouveau
RUN dotnet publish "MonApp.Server.csproj" \
    -c Release \
    -o /app/publish \
    --no-restore \
    /p:UseAppHost=false

# ╔══════════════════════════════════════════════════════════════╗
# ║  STAGE 2 : RUNTIME FINAL                                     ║
# ║  Image légère pour l'exécution en production                 ║
# ╚══════════════════════════════════════════════════════════════╝
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final

# ─── Configuration de sécurité ────────────────────────────────
# Ne PAS exécuter en tant que root (bonne pratique de sécurité)
# L'utilisateur "app" est créé par l'image aspnet de Microsoft
USER app

WORKDIR /app

# Exposer le port HTTP (pas HTTPS : le reverse proxy gère TLS)
EXPOSE 8080

# Copier les fichiers publiés depuis l'étape build
COPY --from=build /app/publish .

# Variables d'environnement
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:8080

# Commande de démarrage
ENTRYPOINT ["dotnet", "MonApp.Server.dll"]
*/


/*
─────────────────────────────────────────────────────────────────
.dockerignore — Fichiers à exclure (comme .gitignore)
─────────────────────────────────────────────────────────────────
Fichier : .dockerignore

**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
*/


// ----------------------------------------------------------------------------
// [OUTIL] DOCKER COMPOSE — Orchestrer plusieurs services
// ----------------------------------------------------------------------------

/*
DOCKER COMPOSE = Définir et lancer plusieurs conteneurs ensemble

NOTRE STACK DE PRODUCTION :

┌─────────────────────────────────────────────────────────┐
│                    Internet                             │
└──────────────────┬──────────────────────────────────────┘
                   │ HTTPS :443
┌──────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────────┐
│                 Nginx (Reverse Proxy)                   │
│  -> Terminaison TLS (HTTPS)                              │
│  -> Compression Gzip/Brotli                             │
│  -> Caching des fichiers statiques                       │
│  -> Load balancing (si plusieurs instances)              │
└──────────┬──────────────────────────────────────────────┘
           │ HTTP interne
┌──────────[BLACK_DOWN-POINTING_TRIANGLE]──────────┐    ┌──────────────────┐
│  ASP.NET Core API   │    │  PostgreSQL       │
│  + Blazor WASM      │[BLACK_LEFT-POINTING_POINTER]──[BLACK_RIGHT-POINTING_POINTER]│  (Base de données)│
└─────────────────────┘    └──────────────────┘
           │
┌──────────[BLACK_DOWN-POINTING_TRIANGLE]──────────┐
│  Redis              │
│  (Cache + Sessions) │
└─────────────────────┘
*/

/*
─────────────────────────────────────────────────────────────────
docker-compose.yml — Stack complète
─────────────────────────────────────────────────────────────────

version: '3.9'

# ─── Réseau interne (les services se parlent par leur nom) ────
networks:
  monapp_network:
    driver: bridge

# ─── Volumes persistants (données survivent aux redémarrages) ─
volumes:
  postgres_data:
  redis_data:
  nginx_certs:

services:

  # ══════════════════════════════════════════════════════════
  # APPLICATION ASP.NET CORE + BLAZOR WASM
  # ══════════════════════════════════════════════════════════
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: final
    image: monapp:latest
    container_name: monapp_api
    restart: unless-stopped         # Redémarre automatiquement si crash

    # Variables d'environnement (NE PAS METTRE LES SECRETS ICI !)
    # Utiliser un fichier .env ou Docker Secrets pour les secrets
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ASPNETCORE_URLS=http://+:8080

    # Fichier de secrets (NON commité dans Git !)
    env_file:
      - .env.production             # Contient les vrais secrets

    # Santé du conteneur : Docker redémarre si /health répond en erreur
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

    # Dépendances : Démarrer après la BDD et Redis
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

    networks:
      - monapp_network

    # Ressources (limiter pour éviter qu'une app monopolise le serveur)
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
        reservations:
          memory: 256M

  # ══════════════════════════════════════════════════════════
  # POSTGRESQL — Base de données
  # ══════════════════════════════════════════════════════════
  postgres:
    image: postgres:16-alpine       # Alpine = image légère (~30MB)
    container_name: monapp_postgres
    restart: unless-stopped

    environment:
      POSTGRES_USER_FILE: /run/secrets/db_user
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: monapp_prod
      PGDATA: /var/lib/postgresql/data/pgdata  # Sous-dossier pour les données

    volumes:
      - postgres_data:/var/lib/postgresql/data  # Persister les données

    # NE PAS exposer vers l'extérieur ! Seulement interne au réseau Docker
    # ports:
    #   - "5432:5432"  <- INTERDIT en production !

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U monapp -d monapp_prod"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

    networks:
      - monapp_network

    deploy:
      resources:
        limits:
          memory: 256M

  # ══════════════════════════════════════════════════════════
  # REDIS — Cache et sessions
  # ══════════════════════════════════════════════════════════
  redis:
    image: redis:7-alpine
    container_name: monapp_redis
    restart: unless-stopped

    command: >
      redis-server
      --requirepass ${REDIS_PASSWORD}
      --maxmemory 128mb
      --maxmemory-policy allkeys-lru
      --appendonly yes

    volumes:
      - redis_data:/data

    healthcheck:
      test: ["CMD", "redis-cli", "--no-auth-warning",
             "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

    networks:
      - monapp_network

    deploy:
      resources:
        limits:
          memory: 128M

  # ══════════════════════════════════════════════════════════
  # NGINX — Reverse Proxy
  # ══════════════════════════════════════════════════════════
  nginx:
    image: nginx:1.25-alpine
    container_name: monapp_nginx
    restart: unless-stopped

    ports:
      - "80:80"       # HTTP (redirige vers HTTPS)
      - "443:443"     # HTTPS

    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro      # Config Nginx
      - ./nginx/conf.d:/etc/nginx/conf.d:ro               # Sites
      - nginx_certs:/etc/nginx/ssl                        # Certificats TLS
      - ./nginx/www:/var/www/certbot:ro                   # Let's Encrypt

    depends_on:
      app:
        condition: service_healthy

    networks:
      - monapp_network

# ─── Docker Secrets (alternative sécurisée aux variables d'env) ─
secrets:
  db_user:
    file: ./secrets/db_user.txt
  db_password:
    file: ./secrets/db_password.txt
*/

/*
─────────────────────────────────────────────────────────────────
.env.production — Variables d'environnement (JAMAIS dans Git !)
─────────────────────────────────────────────────────────────────
Ajouter .env.production dans .gitignore !

ConnectionStrings__DefaultConnection=Host=postgres;Database=monapp_prod;Username=monapp;Password=VotreMotDePasse!
Jwt__Secret=VotreCleSecreteTresLonguePourLaProduction_MinimumSeize_Caracteres!
Jwt__Issuer=https://monapp.com
Jwt__Audience=https://app.monapp.com
Redis__ConnectionString=redis:6379,password=VotreMotDePasse
REDIS_PASSWORD=VotreMotDePasse
Stripe__SecretKey=sk_live_votre_cle
SendGrid__ApiKey=SG.votre_cle
ApplicationInsights__ConnectionString=InstrumentationKey=...
*/

/*
─────────────────────────────────────────────────────────────────
nginx/conf.d/monapp.conf — Configuration Nginx
─────────────────────────────────────────────────────────────────

# ─── Redirection HTTP -> HTTPS ─────────────────────────────────
server {
    listen 80;
    server_name monapp.com www.monapp.com app.monapp.com;

    # Let's Encrypt validation
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Tout le reste -> HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

# ─── Serveur HTTPS principal ───────────────────────────────────
server {
    listen 443 ssl http2;
    server_name app.monapp.com;

    # Certificats TLS (Let's Encrypt via Certbot)
    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    # Configuration TLS sécurisée
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP Stapling (vérifie validité du certificat)
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;

    # ─── Compression ──────────────────────────────────────────
    gzip on;
    gzip_vary on;
    gzip_min_length 256;
    gzip_types
        application/javascript
        application/wasm
        application/octet-stream
        text/css
        text/plain
        application/json;

    # Compression Brotli (si module installé - recommandé pour WASM !)
    # brotli on;
    # brotli_types application/wasm application/octet-stream text/css;

    # ─── Fichiers statiques Blazor WASM ───────────────────────
    # Cache agressif pour les fichiers avec hash dans le nom
    location ~* \.(wasm|dll|dat)$ {
        proxy_pass http://app:8080;
        add_header Cache-Control "public, max-age=604800, immutable";
        add_header Content-Encoding identity;  # Ne pas re-compresser
        gzip off;  # Blazor gère sa propre compression
    }

    # Cache moyen pour CSS/JS
    location ~* \.(css|js|svg|png|jpg|ico|woff2)$ {
        proxy_pass http://app:8080;
        add_header Cache-Control "public, max-age=86400";
    }

    # ─── Proxy vers l'application ─────────────────────────────
    location / {
        proxy_pass http://app:8080;
        proxy_http_version 1.1;

        # Headers pour que l'app connaisse l'IP réelle du client
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket (pour Blazor Server SignalR)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts
        proxy_connect_timeout 30s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;

        # Buffer (pour les grosses réponses)
        proxy_buffer_size          128k;
        proxy_buffers              4 256k;
        proxy_busy_buffers_size    256k;
    }

    # ─── Sécurité ──────────────────────────────────────────────
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Cacher la version Nginx
    server_tokens off;

    # ─── Rate Limiting Nginx ───────────────────────────────────
    # Défini dans nginx.conf : limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    location /api/auth/login {
        limit_req zone=api burst=5 nodelay;
        proxy_pass http://app:8080;
    }
}
*/


// ----------------------------------------------------------------------------
// [RAPIDE] COMMANDES DOCKER UTILES EN PRODUCTION
// ----------------------------------------------------------------------------

/*
CONSTRUIRE ET DÉMARRER L'APPLICATION :

# Construire les images et démarrer tous les services
docker compose up --build -d

# Voir les logs en temps réel
docker compose logs -f

# Voir les logs d'un service spécifique
docker compose logs -f app

# Redémarrer un service (après un déploiement)
docker compose up -d --no-deps --build app

# Arrêter tous les services
docker compose down

# Arrêter ET supprimer les volumes ([ATTENTION] supprime les données !)
docker compose down -v

# Vérifier l'état des services
docker compose ps

# Exécuter une commande dans un conteneur
docker compose exec app dotnet ef database update

# Ouvrir un shell dans un conteneur
docker compose exec app bash

# Appliquer les migrations EF Core
docker compose exec app dotnet ef database update \
  --project MonApp.Infrastructure \
  --startup-project MonApp.Server

# Backup de la base de données
docker compose exec postgres pg_dump \
  -U monapp monapp_prod > backup_$(date +%Y%m%d_%H%M%S).sql

# Mise à jour sans downtime (rolling update)
docker compose pull
docker compose up -d --no-deps --build app
*/


// ============================================================================
// [GUIDE] CHAPITRE 27 : CI/CD
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les concepts CI/CD
[OK] Créer un pipeline GitHub Actions complet
[OK] Automatiser les tests, build et déploiement
[OK] Gérer les secrets dans les pipelines
[OK] Déployer sur Azure App Service et VPS
[OK] Implémenter des stratégies de déploiement (Blue/Green, Rolling)
*/


// ----------------------------------------------------------------------------
// [SYNC] CI/CD — Les concepts fondamentaux
// ----------------------------------------------------------------------------

/*
CI = Continuous Integration (Intégration Continue)
CD = Continuous Delivery/Deployment (Livraison/Déploiement Continu)

POURQUOI CI/CD ?

SANS CI/CD :
  Développeur -> Compile localement -> Envoie par FTP -> Espère que ça marche
  -> Merge conflicts ignorés -> Bugs en prod -> "Fonctionne chez moi !"
  -> Déploiements stressants, souvent la nuit ou le weekend

AVEC CI/CD :
  git push -> Tests automatiques -> Build Docker -> Déploiement -> Notification
  -> Erreurs détectées immédiatement -> Rollback automatique si problème
  -> Déploiements quotidiens sans stress

FLUX CI/CD TYPIQUE :

  ┌────────────────────────────────────────────────────────────┐
  │                    PIPELINE CI/CD                          │
  │                                                            │
  │  git push       ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐   │
  │  ──────────────[BLACK_RIGHT-POINTING_POINTER]│Tests │─[BLACK_RIGHT-POINTING_POINTER]│Build │─[BLACK_RIGHT-POINTING_POINTER]│ Push │─[BLACK_RIGHT-POINTING_POINTER]│Deploy│   │
  │                 │Unit  │  │Docker│  │Regis.│  │ Prod │   │
  │                 │Integ.│  │Image │  │ ACR  │  │      │   │
  │                 └──────┘  └──────┘  └──────┘  └──────┘   │
  │                    │                              │        │
  │                    └──── Notification Email/Slack ┘        │
  └────────────────────────────────────────────────────────────┘

ÉTAPES DÉTAILLÉES :

1. TRIGGER : Push sur main/develop -> Pipeline démarre
2. LINT    : Vérifier le style de code (dotnet format)
3. BUILD   : Compiler l'application (dotnet build)
4. TEST    : Lancer les tests unitaires et d'intégration
5. ANALYSE : Analyse de sécurité des packages (vulnérabilités)
6. DOCKER  : Construire l'image Docker
7. PUSH    : Envoyer l'image vers le registry
8. DEPLOY  : Déployer sur le serveur de staging
9. SMOKE   : Tests de fumée (l'app répond-elle ?)
10. PROD   : Déployer en production (si staging OK)
11. NOTIFY : Notification de succès/échec
*/


// ----------------------------------------------------------------------------
// [OCTOPUS] GITHUB ACTIONS — Pipeline CI/CD complet
// ----------------------------------------------------------------------------

/*
GITHUB ACTIONS = CI/CD intégré directement dans GitHub
-> Gratuit pour les repos publics
-> 2000 minutes/mois pour les repos privés (plan gratuit)
-> Fichiers YAML dans .github/workflows/

CONCEPTS GITHUB ACTIONS :

WORKFLOW     : Pipeline complet (fichier .yml)
JOB          : Ensemble d'étapes exécutées sur la même machine
STEP         : Une action ou commande dans un Job
RUNNER       : La machine virtuelle qui exécute les Jobs
  ubuntu-latest -> Ubuntu Linux (le plus utilisé)
  windows-latest -> Windows Server
  macos-latest  -> macOS

TRIGGER (ON) : Événement qui déclenche le workflow
  push         -> À chaque push
  pull_request -> À chaque PR
  schedule     -> Périodiquement (cron)
  workflow_dispatch -> Manuel
*/

/*
─────────────────────────────────────────────────────────────────
.github/workflows/ci-cd.yml — Pipeline complet
─────────────────────────────────────────────────────────────────

name: [RAPIDE] CI/CD Pipeline

# ─── TRIGGERS : Quand ce pipeline se déclenche ────────────────
on:
  # Sur push vers les branches principales
  push:
    branches:
      - main          # Production
      - develop       # Staging
    paths-ignore:
      - '**.md'       # Ignorer les changements de documentation
      - '.gitignore'

  # Sur Pull Request (pour valider avant merge)
  pull_request:
    branches:
      - main
      - develop

  # Déclenchement manuel depuis l'interface GitHub
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environnement cible'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

# ─── VARIABLES GLOBALES ────────────────────────────────────────
env:
  DOTNET_VERSION: '8.0.x'
  REGISTRY: ghcr.io                           # GitHub Container Registry
  IMAGE_NAME: ${{ github.repository }}         # owner/repo-name

# ─── JOBS ──────────────────────────────────────────────────────
jobs:

  # ══════════════════════════════════════════════════════════════
  # JOB 1 : Tests et qualité du code
  # ══════════════════════════════════════════════════════════════
  tests:
    name: [TEST] Tests & Qualité
    runs-on: ubuntu-latest

    # Service PostgreSQL pour les tests d'intégration
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_PASSWORD: test_password
          POSTGRES_DB: monapp_test
        ports:
          - 5432:5432
        # Attendre que Postgres soit prêt
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      # ─── 1. Récupérer le code source ───────────────────────
      - name: [ENTREE] Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0   # Historique complet (pour l'analyse SonarCloud)

      # ─── 2. Configurer .NET ─────────────────────────────────
      - name: [OUTIL] Configurer .NET ${{ env.DOTNET_VERSION }}
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      # ─── 3. Cache NuGet ────────────────────────────────────
      # Cache les packages NuGet pour accélérer les builds suivants
      - name: [PACKAGE] Cache NuGet
        uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
          restore-keys: |
            ${{ runner.os }}-nuget-

      # ─── 4. Restaurer les packages ─────────────────────────
      - name: [PACKAGE] Restaurer les packages NuGet
        run: dotnet restore MonApp.sln

      # ─── 5. Vérifier le formatage ──────────────────────────
      - name: [DESIGN] Vérifier le style de code
        run: dotnet format MonApp.sln --verify-no-changes --severity warn
        continue-on-error: true  # Ne pas bloquer sur le style

      # ─── 6. Compiler ────────────────────────────────────────
      - name: [OUTIL] Compiler la solution
        run: >
          dotnet build MonApp.sln
          --no-restore
          -c Release
          -warnaserror   # Traiter les warnings comme des erreurs

      # ─── 7. Tests unitaires ─────────────────────────────────
      - name: [TEST] Exécuter les tests unitaires
        run: >
          dotnet test MonApp.Tests.Unit/MonApp.Tests.Unit.csproj
          --no-build
          -c Release
          --logger trx
          --results-directory TestResults/Unit
          --collect:"XPlat Code Coverage"
          /p:CollectCoverage=true
          /p:CoverletOutputFormat=opencover

      # ─── 8. Tests d'intégration ─────────────────────────────
      - name: [LIEN] Exécuter les tests d'intégration
        run: >
          dotnet test MonApp.Tests.Integration/MonApp.Tests.Integration.csproj
          --no-build
          -c Release
          --logger trx
          --results-directory TestResults/Integration
        env:
          ConnectionStrings__DefaultConnection: >-
            Host=localhost;
            Database=monapp_test;
            Username=postgres;
            Password=test_password

      # ─── 9. Publier les résultats de tests ──────────────────
      - name: [GRAPHIQUE] Publier les résultats de tests
        uses: dorny/test-reporter@v1
        if: success() || failure()   # Toujours publier (même si tests échouent)
        with:
          name: Tests .NET
          path: TestResults/**/*.trx
          reporter: dotnet-trx

      # ─── 10. Couverture du code ─────────────────────────────
      - name: [HAUSSE] Rapport de couverture
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: TestResults/**/*.xml

      # ─── 11. Analyse des vulnérabilités ─────────────────────
      - name: [VERROUILLE] Scanner les vulnérabilités NuGet
        run: dotnet list package --vulnerable --include-transitive
        continue-on-error: true  # Informationnel

  # ══════════════════════════════════════════════════════════════
  # JOB 2 : Build de l'image Docker
  # ══════════════════════════════════════════════════════════════
  build-docker:
    name: [DOCKER] Build & Push Docker
    runs-on: ubuntu-latest
    needs: tests          # N'exécuter QUE si les tests passent !

    # Seulement sur main et develop (pas sur les PRs)
    if: github.event_name != 'pull_request'

    outputs:
      # Passer le tag de l'image aux jobs suivants
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
      - name: [ENTREE] Checkout code
        uses: actions/checkout@v4

      # ─── 1. Se connecter au Container Registry ──────────────
      - name: [CLE] Login GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}   # Automatique !

      # ─── 2. Métadonnées de l'image (tags, labels) ───────────
      - name: [LABEL] Générer les métadonnées Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            # Tag avec le SHA court du commit (ex: sha-abc1234)
            type=sha,prefix=sha-,format=short
            # Tag avec le nom de la branche (ex: main, develop)
            type=ref,event=branch
            # Tag latest uniquement sur main
            type=raw,value=latest,enable={{is_default_branch}}
            # Tag avec la date (ex: 20240101)
            type=raw,value={{date 'YYYYMMDD'}},enable={{is_default_branch}}

      # ─── 3. Build et cache Docker ───────────────────────────
      - name: [OUTIL] Configurer Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: [DOCKER] Build et Push l'image Docker
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          # Cache des couches Docker entre les builds -> Build plus rapide !
          cache-from: type=gha
          cache-to: type=gha,mode=max
          # Build pour plusieurs architectures (linux/amd64 pour les serveurs)
          platforms: linux/amd64

      # ─── 4. Scan de sécurité de l'image ─────────────────────
      - name: [VERROUILLE] Scanner l'image avec Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          format: table
          severity: CRITICAL,HIGH
          exit-code: 0   # Ne pas bloquer (informationnel)

  # ══════════════════════════════════════════════════════════════
  # JOB 3 : Déploiement Staging
  # ══════════════════════════════════════════════════════════════
  deploy-staging:
    name: [WEB] Deploy -> Staging
    runs-on: ubuntu-latest
    needs: build-docker
    if: github.ref == 'refs/heads/develop'

    # Environnement GitHub (pour les règles d'approbation et les secrets)
    environment:
      name: staging
      url: https://staging.monapp.com

    steps:
      - name: [ENTREE] Checkout code
        uses: actions/checkout@v4

      # ─── Déploiement SSH sur VPS ─────────────────────────────
      - name: [RAPIDE] Déployer sur le VPS de staging
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.STAGING_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          port: 22
          script: |
            # Se placer dans le répertoire de l'app
            cd /opt/monapp-staging

            # Récupérer la nouvelle image
            echo ${{ secrets.GITHUB_TOKEN }} | \
              docker login ghcr.io -u ${{ github.actor }} --password-stdin

            # Mettre à jour la variable d'image
            export IMAGE_TAG=${{ needs.build-docker.outputs.image-tag }}

            # Mettre à jour et redémarrer (sans downtime)
            docker compose pull app
            docker compose up -d --no-deps app

            # Vérifier que l'app est démarrée
            sleep 15
            docker compose ps

            # Test de santé
            curl -f https://staging.monapp.com/health || \
              (docker compose logs app && exit 1)

      # ─── Tests de fumée sur staging ─────────────────────────
      - name: [HOT] Tests de fumée
        run: |
          # Vérifier que les pages principales répondent
          curl -sf https://staging.monapp.com | grep -q "MonApp" || exit 1
          curl -sf https://staging.monapp.com/api/health | grep -q '"status":"Healthy"' || exit 1
          echo "[OK] Tests de fumée réussis !"

  # ══════════════════════════════════════════════════════════════
  # JOB 4 : Déploiement Production
  # ══════════════════════════════════════════════════════════════
  deploy-production:
    name: [OBJECTIF] Deploy -> Production
    runs-on: ubuntu-latest
    needs: build-docker
    if: github.ref == 'refs/heads/main'

    # Protection : Nécessite approbation manuelle !
    environment:
      name: production
      url: https://monapp.com

    steps:
      - name: [ENTREE] Checkout code
        uses: actions/checkout@v4

      # ─── Déploiement Azure App Service ──────────────────────
      # Option A : Azure App Service (via GitHub Action officielle)
      - name: [CLE] Login Azure
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: [RAPIDE] Déployer sur Azure App Service
        uses: azure/webapps-deploy@v3
        with:
          app-name: monapp-production
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

      # ─── Tests de fumée en production ───────────────────────
      - name: [HOT] Tests de fumée production
        run: |
          sleep 30  # Attendre le démarrage
          curl -sf https://monapp.com/api/health | grep -q '"status":"Healthy"' || exit 1
          echo "[OK] Production en bonne santé !"

      # ─── Notification Slack ──────────────────────────────────
      - name: [SPEECH_BALLOON] Notification Slack
        if: always()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          channel-id: 'C1234567890'
          slack-message: |
            ${{ job.status == 'success' && '[OK]' || '[X]' }} Déploiement Production
            *Commit:* ${{ github.sha }}
            *Auteur:* ${{ github.actor }}
            *Message:* ${{ github.event.head_commit.message }}
            *Status:* ${{ job.status }}
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
*/


// ----------------------------------------------------------------------------
// [LISTE] CONFIGURER LES SECRETS GITHUB
// ----------------------------------------------------------------------------

/*
SECRETS GITHUB = Variables secrètes stockées dans GitHub
-> Jamais exposées dans les logs
-> Chiffrées au repos
-> Accessible via ${{ secrets.NOM_DU_SECRET }}

COMMENT AJOUTER DES SECRETS :
GitHub -> Repo -> Settings -> Secrets and variables -> Actions -> New repository secret

SECRETS NÉCESSAIRES POUR NOTRE PIPELINE :

Pour le VPS staging :
  STAGING_HOST       : IP ou domaine du serveur (ex: 192.168.1.100)
  STAGING_USER       : Utilisateur SSH (ex: ubuntu, deploy)
  STAGING_SSH_KEY    : Clé privée SSH (contenu du fichier ~/.ssh/id_rsa)

Pour Azure :
  AZURE_CREDENTIALS  : JSON des credentials Azure (via az ad sp create-for-rbac)

Pour les notifications :
  SLACK_BOT_TOKEN    : Token du bot Slack

GÉNÉRER UNE CLÉ SSH POUR LE DÉPLOIEMENT :
  ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key -N ""
  -> deploy_key      : Clé privée -> Mettre dans STAGING_SSH_KEY
  -> deploy_key.pub  : Clé publique -> Ajouter dans ~/.ssh/authorized_keys sur le serveur
*/


// ----------------------------------------------------------------------------
// [OBJECTIF] AZURE DEVOPS — Alternative enterprise à GitHub Actions
// ----------------------------------------------------------------------------

/*
AZURE DEVOPS = Suite Microsoft pour CI/CD, GitOps, gestion de projets
-> Idéal pour les équipes utilisant déjà l'écosystème Azure
-> Pipelines YAML ou classique (UI)

STRUCTURE D'UN PIPELINE AZURE DEVOPS :
*/

/*
─────────────────────────────────────────────────────────────────
azure-pipelines.yml — Pipeline Azure DevOps
─────────────────────────────────────────────────────────────────

# Déclencheurs
trigger:
  branches:
    include:
      - main
      - develop
  paths:
    exclude:
      - '**/*.md'

variables:
  buildConfiguration: 'Release'
  dotnetVersion: '8.0.x'
  imageRepository: 'monapp'
  containerRegistry: 'monappacr.azurecr.io'
  dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
  tag: '$(Build.BuildId)'

# Pool d'agents Microsoft (Ubuntu)
pool:
  vmImage: 'ubuntu-latest'

stages:

  # ═══════════════════════════════════════════════════════════
  # STAGE 1 : CI (Build + Tests)
  # ═══════════════════════════════════════════════════════════
  - stage: CI
    displayName: '[TEST] Build & Tests'
    jobs:
      - job: BuildAndTest
        displayName: 'Build et Tests .NET'
        steps:

          - task: UseDotNet@2
            displayName: 'Configurer .NET $(dotnetVersion)'
            inputs:
              version: $(dotnetVersion)

          - task: DotNetCoreCLI@2
            displayName: '[PACKAGE] Restaurer les packages'
            inputs:
              command: 'restore'
              projects: '**/*.sln'

          - task: DotNetCoreCLI@2
            displayName: '[OUTIL] Compiler'
            inputs:
              command: 'build'
              projects: '**/*.sln'
              arguments: '--configuration $(buildConfiguration) --no-restore'

          - task: DotNetCoreCLI@2
            displayName: '[TEST] Tests unitaires'
            inputs:
              command: 'test'
              projects: '**/*Tests.Unit.csproj'
              arguments: >-
                --configuration $(buildConfiguration)
                --no-build
                --collect "Code Coverage"
                --logger trx
                --results-directory $(Agent.TempDirectory)/TestResults

          - task: PublishTestResults@2
            displayName: '[GRAPHIQUE] Publier résultats tests'
            inputs:
              testResultsFormat: 'VSTest'
              testResultsFiles: '$(Agent.TempDirectory)/TestResults/*.trx'

  # ═══════════════════════════════════════════════════════════
  # STAGE 2 : Docker Build
  # ═══════════════════════════════════════════════════════════
  - stage: Docker
    displayName: '[DOCKER] Docker'
    dependsOn: CI
    condition: succeeded()
    jobs:
      - job: DockerBuild
        steps:

          - task: Docker@2
            displayName: '[CLE] Login Azure Container Registry'
            inputs:
              command: 'login'
              containerRegistry: 'AzureContainerRegistryServiceConnection'

          - task: Docker@2
            displayName: '[DOCKER] Build et Push image'
            inputs:
              command: 'buildAndPush'
              repository: $(imageRepository)
              dockerfile: $(dockerfilePath)
              containerRegistry: 'AzureContainerRegistryServiceConnection'
              tags: |
                $(tag)
                latest

  # ═══════════════════════════════════════════════════════════
  # STAGE 3 : Déploiement Staging
  # ═══════════════════════════════════════════════════════════
  - stage: DeployStaging
    displayName: '[WEB] Deploy Staging'
    dependsOn: Docker
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
    jobs:
      - deployment: DeployToStaging
        environment: 'staging'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebAppContainer@1
                  displayName: '[RAPIDE] Déployer Azure App Service'
                  inputs:
                    azureSubscription: 'AzureServiceConnection'
                    appName: 'monapp-staging'
                    containers: '$(containerRegistry)/$(imageRepository):$(tag)'

  # ═══════════════════════════════════════════════════════════
  # STAGE 4 : Déploiement Production (avec approbation)
  # ═══════════════════════════════════════════════════════════
  - stage: DeployProduction
    displayName: '[OBJECTIF] Deploy Production'
    dependsOn: DeployStaging
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployToProduction
        environment: 'production'  # Approbation manuelle configurée dans Azure DevOps
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebAppContainer@1
                  displayName: '[RAPIDE] Déployer Production'
                  inputs:
                    azureSubscription: 'AzureServiceConnection'
                    appName: 'monapp-production'
                    containers: '$(containerRegistry)/$(imageRepository):$(tag)'
*/


// ============================================================================
// [GUIDE] CHAPITRE 28 : OBSERVABILITÉ
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les 3 piliers de l'observabilité
[OK] Configurer Serilog (logging structuré)
[OK] Implémenter le health checking
[OK] Utiliser Application Insights (Azure)
[OK] Créer des dashboards de monitoring
[OK] Configurer des alertes
*/


// ----------------------------------------------------------------------------
// [EYE] LES 3 PILIERS DE L'OBSERVABILITÉ
// ----------------------------------------------------------------------------

/*
OBSERVABILITÉ = Comprendre l'état interne d'un système à partir de ses sorties

PILIER 1 : LOGS (Traces d'événements)
  QUOI ? Enregistrement textuel des événements qui se produisent
  QUAND ? Pour déboguer, auditer, suivre les actions
  EXEMPLE : "2024-01-15 14:32:01 INFO  Utilisateur 42 s'est connecté"

PILIER 2 : MÉTRIQUES (Données numériques)
  QUOI ? Mesures numériques agrégées dans le temps
  QUAND ? Pour monitorer la santé, détecter les tendances
  EXEMPLE : "CPU: 45%, RAM: 2.1GB, Requêtes/sec: 127, Erreurs: 0.2%"

PILIER 3 : TRACES DISTRIBUÉES (Suivi des requêtes)
  QUOI ? Suivi d'une requête à travers plusieurs services
  QUAND ? Pour identifier les goulots d'étranglement
  EXEMPLE : "/api/commandes -> (12ms) -> DB query -> (45ms) -> Email -> (200ms)"

ANALOGIE :
  Logs    = Journal de bord du capitaine
  Métriques = Tableau de bord de l'avion (altitude, vitesse, carburant)
  Traces  = GPS tracking du vol (où exactement à chaque instant)
*/


// ----------------------------------------------------------------------------
// [NOTE] SERILOG — Logging structuré professionnel
// ----------------------------------------------------------------------------

/*
POURQUOI SERILOG ET PAS MICROSOFT.EXTENSIONS.LOGGING SEUL ?

LOGS CLASSIQUES (non structurés) :
  logger.LogInformation($"Utilisateur {userId} a acheté {productName} pour {price}€");
  -> Sortie : "Utilisateur 42 a acheté Laptop pour 999.99€"
  -> Impossible de chercher/filtrer par userId ou price !

LOGS STRUCTURÉS (Serilog) :
  Log.Information("Achat effectué par {UserId} : {ProductName} à {Price}€",
                  userId, productName, price);
  -> Sortie JSON : { "UserId": 42, "ProductName": "Laptop", "Price": 999.99, ... }
  -> Requêtes Elasticsearch/Seq : WHERE UserId = 42 AND Price > 500 !

INSTALLATION :
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq                   -> Dashboard local
dotnet add package Serilog.Sinks.ApplicationInsights   -> Azure
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Process
*/

// Program.cs avec Serilog complet
public static class SerilogConfiguration
{
    public static void ConfigurerSerilog(WebApplicationBuilder builder)
    {
        /*
        Log.Logger = new LoggerConfiguration()

            // ─── Sources de configuration ──────────────────────────────
            // Serilog lit la config depuis appsettings.json
            .ReadFrom.Configuration(builder.Configuration)

            // ─── Enrichissement des logs ───────────────────────────────
            // Ajouter des propriétés supplémentaires à TOUS les logs
            .Enrich.FromLogContext()              // Properties poussées via LogContext
            .Enrich.WithMachineName()             // Nom du serveur
            .Enrich.WithEnvironmentName()         // Production/Staging/Development
            .Enrich.WithThreadId()                // ID du thread
            .Enrich.WithProcessId()               // ID du processus
            .Enrich.WithProperty("Application", "MonApp") // Nom de l'app
            .Enrich.WithProperty("Version", Assembly.GetExecutingAssembly()
                .GetName().Version?.ToString() ?? "0.0.0")

            // ─── Niveau minimum de log ──────────────────────────────────
            // Selon l'environnement
            .MinimumLevel.Is(builder.Environment.IsDevelopment()
                ? LogEventLevel.Debug
                : LogEventLevel.Information)

            // Réduire le bruit des logs Microsoft
            .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
            .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
            .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
            .MinimumLevel.Override("System", LogEventLevel.Warning)

            // ─── SINKS : Où écrire les logs ────────────────────────────

            // Console (développement) : Colorée et lisible par les humains
            .WriteTo.Console(
                theme: Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code,
                outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} " +
                                "{Properties:j}{NewLine}{Exception}")

            // Fichier rotatif (tous les jours, garder 30 jours)
            .WriteTo.File(
                path: "logs/monapp-.log",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30,
                outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} " +
                                "[{Level:u3}] {Message:lj}{NewLine}{Exception}",
                fileSizeLimitBytes: 10 * 1024 * 1024, // 10 MB max par fichier
                rollOnFileSizeLimit: true)

            // Fichier JSON (pour ingestion par des outils)
            .WriteTo.File(
                new CompactJsonFormatter(),
                path: "logs/monapp-json-.log",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 7)

            // Seq (dashboard local de développement)
            .WriteTo.Seq(
                serverUrl: builder.Configuration["Seq:ServerUrl"] ?? "http://localhost:5341",
                apiKey: builder.Configuration["Seq:ApiKey"],
                restrictedToMinimumLevel: LogEventLevel.Debug)

            // Application Insights (Azure, production)
            .WriteTo.ApplicationInsights(
                connectionString: builder.Configuration["ApplicationInsights:ConnectionString"],
                telemetryConverter: TelemetryConverter.Traces,
                restrictedToMinimumLevel: LogEventLevel.Information)

            .CreateLogger();

        builder.Host.UseSerilog();
        */
    }
}

/*
─────────────────────────────────────────────────────────────────
appsettings.json avec configuration Serilog
─────────────────────────────────────────────────────────────────

{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning",
        "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/monapp-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
    "Properties": {
      "Application": "MonApp",
      "Environment": "Production"
    }
  }
}
*/


// ----------------------------------------------------------------------------
// [GRAPHIQUE] LOGGING AVANCÉ — Patterns et bonnes pratiques
// ----------------------------------------------------------------------------

// Services/CommandeService.cs — Exemple de logging professionnel
public class CommandeServiceAvecLogs
{
    private readonly ILogger<CommandeServiceAvecLogs> _logger;
    private readonly AppDbContext _context;

    public CommandeServiceAvecLogs(
        ILogger<CommandeServiceAvecLogs> logger,
        AppDbContext context)
    {
        _logger = logger;
        _context = context;
    }

    public async Task<int> CreerCommandeAsync(int utilisateurId, List<int> produitIds)
    {
        // ─── LogInformation : Événement normal ──────────────────────────
        _logger.LogInformation(
            "Création commande pour utilisateur {UtilisateurId} avec {NombreProduits} produits",
            utilisateurId,
            produitIds.Count);

        // Les paramètres nommés ({UtilisateurId}) sont des propriétés structurées !
        // Pas de concaténation de string -> Plus performant + Cherchable

        var stopwatch = System.Diagnostics.Stopwatch.StartNew();

        try
        {
            // Simuler la création de commande
            await Task.Delay(100);
            var commandeId = new Random().Next(1, 1000);

            stopwatch.Stop();

            // ─── LogInformation : Succès avec durée ─────────────────────
            _logger.LogInformation(
                "Commande {CommandeId} créée en {DureeMs}ms pour utilisateur {UtilisateurId}",
                commandeId,
                stopwatch.ElapsedMilliseconds,
                utilisateurId);

            return commandeId;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();

            // ─── LogError : Erreur avec exception complète ───────────────
            _logger.LogError(
                ex,  // L'exception est le PREMIER paramètre
                "Échec création commande pour utilisateur {UtilisateurId} après {DureeMs}ms. " +
                "Produits demandés: {ProduitIds}",
                utilisateurId,
                stopwatch.ElapsedMilliseconds,
                produitIds);

            throw;
        }
    }

    public async Task<bool> TraiterPaiementAsync(int commandeId, decimal montant)
    {
        // ─── LogWarning : Événement inhabituel mais non critique ─────────
        if (montant > 10_000)
        {
            _logger.LogWarning(
                "Paiement élevé détecté : {Montant}€ pour commande {CommandeId}. " +
                "Vérification manuelle recommandée",
                montant,
                commandeId);
        }

        // ─── LogDebug : Informations de débogage ────────────────────────
        // NE s'affiche qu'en développement (MinimumLevel.Debug)
        _logger.LogDebug(
            "Tentative paiement : CommandeId={CommandeId}, Montant={Montant}, " +
            "Provider=Stripe",
            commandeId,
            montant);

        try
        {
            await Task.Delay(200); // Simuler appel Stripe
            return true;
        }
        catch (Exception ex) when (ex.Message.Contains("insufficient_funds"))
        {
            // ─── LogWarning : Erreur métier attendue ────────────────────
            _logger.LogWarning(
                "Paiement refusé (fonds insuffisants) : CommandeId={CommandeId}, Montant={Montant}",
                commandeId,
                montant);
            return false;
        }
    }
}

// Middleware de logging des requêtes HTTP
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        var requestId = Guid.NewGuid().ToString("N")[..8]; // ID court

        // Ajouter le requestId à tous les logs de cette requête
        using var scope = _logger.BeginScope(new Dictionary<string, object>
        {
            ["RequestId"] = requestId,
            ["ClientIp"] = context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        });

        _logger.LogInformation(
            "-> {Method} {Path}{QueryString}",
            context.Request.Method,
            context.Request.Path,
            context.Request.QueryString);

        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();

            var niveau = context.Response.StatusCode >= 500
                ? LogLevel.Error
                : context.Response.StatusCode >= 400
                ? LogLevel.Warning
                : LogLevel.Information;

            _logger.Log(
                niveau,
                "<- {StatusCode} {Method} {Path} ({DureeMs}ms)",
                context.Response.StatusCode,
                context.Request.Method,
                context.Request.Path,
                stopwatch.ElapsedMilliseconds);
        }
    }
}

// LogLevel (simulé)
public enum LogLevel { Trace, Debug, Information, Warning, Error, Critical }


// ----------------------------------------------------------------------------
// [HEAVY_BLACK_HEART] HEALTH CHECKS — Surveiller la santé de l'application
// ----------------------------------------------------------------------------

/*
HEALTH CHECKS = Endpoints qui indiquent si l'app est "en bonne santé"

UTILITÉ :
-> Docker : Redémarrer le conteneur si unhealthy
-> Kubernetes : Ne pas envoyer de trafic si not ready
-> Load Balancer : Retirer une instance défaillante
-> Monitoring : Alerter si unhealthy

ENDPOINTS TYPIQUES :
/health         -> Santé complète (pour monitoring humain)
/health/ready   -> L'app est prête à recevoir du trafic (Kubernetes readiness)
/health/live    -> L'app est vivante (Kubernetes liveness)

INSTALLATION :
dotnet add package AspNetCore.HealthChecks.NpgSql          -> PostgreSQL
dotnet add package AspNetCore.HealthChecks.Redis           -> Redis
dotnet add package AspNetCore.HealthChecks.Uris            -> URLs externes
dotnet add package AspNetCore.HealthChecks.UI              -> Dashboard web
*/

/*
─────────────────────────────────────────────────────────────────
Configuration des Health Checks dans Program.cs
─────────────────────────────────────────────────────────────────

builder.Services
    .AddHealthChecks()

    // ─── Base de données ────────────────────────────────────
    .AddNpgSql(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        name: "postgresql",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "ready" })

    // ─── Redis ──────────────────────────────────────────────
    .AddRedis(
        redisConnectionString: builder.Configuration["Redis:ConnectionString"]!,
        name: "redis",
        failureStatus: HealthStatus.Degraded,  // Dégradé mais pas mort
        tags: new[] { "cache", "ready" })

    // ─── URL externe (API Stripe, etc.) ─────────────────────
    .AddUrlGroup(
        uri: new Uri("https://api.stripe.com/v1/"),
        name: "stripe-api",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "external" })

    // ─── Check personnalisé ─────────────────────────────────
    .AddCheck<QueueHealthCheck>(
        name: "email-queue",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "queue" })

    // ─── Vérification de l'espace disque ────────────────────
    .AddDiskStorageHealthCheck(
        setup => setup.AddDrive(
            driveName: "/",
            minimumFreeMegabytes: 500),
        name: "disk-space",
        failureStatus: HealthStatus.Degraded)

    // ─── UI Web des health checks ────────────────────────────
    .AddHealthChecksUI(settings =>
    {
        settings.SetEvaluationTimeInSeconds(30);       // Vérifier toutes les 30s
        settings.MaximumHistoryEntriesPerEndpoint(60); // Garder 60 entrées
        settings.AddHealthCheckEndpoint(
            name: "MonApp",
            uri: "https://monapp.com/health");
    })
    .AddInMemoryStorage();

// Configurer les endpoints
app.MapHealthChecks("/health", new HealthCheckOptions
{
    // Afficher les détails complets en JSON
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
    // Inclure tous les checks
    Predicate = _ => true,
});

// Endpoint "ready" : Seulement les checks de démarrage (BDD, dépendances)
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready"),
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
});

// Endpoint "live" : Vérification minimale (l'app tourne-t-elle ?)
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false, // Aucun check = juste vérifier que l'app répond
});

// Dashboard UI
app.MapHealthChecksUI(options =>
{
    options.UIPath = "/health-ui";    // Accessible sur /health-ui
    options.ApiPath = "/health-api";  // API JSON pour le dashboard
});
*/

// Health Check personnalisé
public class QueueHealthCheck : IHealthCheck
{
    private readonly IEmailQueueService _queueService;

    public QueueHealthCheck(IEmailQueueService queueService)
    {
        _queueService = queueService;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        try
        {
            var taille = await _queueService.ObtenirTailleQueueAsync();

            var data = new Dictionary<string, object>
            {
                ["queue_size"] = taille,
                ["checked_at"] = DateTime.UtcNow,
            };

            if (taille > 10_000)
            {
                return HealthCheckResult.Degraded(
                    description: $"Queue email en retard : {taille} messages en attente",
                    data: data);
            }

            return HealthCheckResult.Healthy(
                description: $"Queue email OK : {taille} messages",
                data: data);
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                description: "Impossible de contacter la queue email",
                exception: ex);
        }
    }
}

// Interfaces (simulées)
public interface IHealthCheck
{
    Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct);
}
public class HealthCheckContext { }
public class HealthCheckResult
{
    public static HealthCheckResult Healthy(string? description = null, Dictionary<string, object>? data = null) => new();
    public static HealthCheckResult Degraded(string? description = null, Dictionary<string, object>? data = null) => new();
    public static HealthCheckResult Unhealthy(string? description = null, Exception? exception = null) => new();
}
public interface IEmailQueueService
{
    Task<int> ObtenirTailleQueueAsync();
}
public enum HealthStatus { Healthy, Degraded, Unhealthy }


// ----------------------------------------------------------------------------
// [GRAPHIQUE] APPLICATION INSIGHTS — Monitoring Azure complet
// ----------------------------------------------------------------------------

/*
APPLICATION INSIGHTS = Plateforme de monitoring Microsoft Azure

FONCTIONNALITÉS :
-> Logs centralisés (Serilog sink)
-> Métriques de performance (temps de réponse, taux d'erreur)
-> Traces distribuées (suivi end-to-end d'une requête)
-> Alertes (email/SMS si erreur > X% ou temps réponse > Y ms)
-> Live Metrics (métriques en temps réel)
-> Application Map (carte visuelle des dépendances)
-> Failure Analysis (analyse des erreurs)
-> Smart Detection (alertes intelligentes)

INSTALLATION :
dotnet add package Microsoft.ApplicationInsights.AspNetCore
dotnet add package Microsoft.ApplicationInsights.WorkerService

CONFIGURATION :
*/

/*
Program.cs avec Application Insights :

// Enregistrer Application Insights
builder.Services.AddApplicationInsightsTelemetry(options =>
{
    options.ConnectionString =
        builder.Configuration["ApplicationInsights:ConnectionString"];

    // Activer la détection des dépendances (HTTP, SQL, Redis, etc.)
    options.EnableDependencyTrackingTelemetryModule = true;

    // Activer le profilage des performances
    options.EnableAdaptiveSampling = true;
    options.EnableDiagnosticsTelemetryModule = true;
});

// Personnaliser le télémétrie
builder.Services.AddSingleton<ITelemetryInitializer, MonTelemetryInitializer>();

// Configurer le sampling (pour réduire le coût)
builder.Services.Configure<TelemetryConfiguration>(config =>
{
    config.DefaultTelemetrySink.TelemetryProcessorChainBuilder
        .UseAdaptiveSampling(
            maxTelemetryItemsPerSecond: 5,   // Max 5 items/sec
            excludedTypes: "Event");         // Ne pas sampler les events
});
*/

// Initializer personnalisé pour enrichir toutes les télémétries
public class MonTelemetryInitializer : ITelemetryInitializer
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public MonTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        var context = _httpContextAccessor.HttpContext;
        if (context is null) return;

        // Ajouter des propriétés à TOUS les événements Application Insights
        telemetry.Context.GlobalProperties["TenantId"] =
            context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "unknown";

        telemetry.Context.GlobalProperties["UserId"] =
            context.User.FindFirst("sub")?.Value ?? "anonymous";

        // Rôle du serveur (utile en multi-instance)
        telemetry.Context.Cloud.RoleName = "monapp-api";
        telemetry.Context.Cloud.RoleInstance = System.Environment.MachineName;
    }
}

// Interfaces Application Insights (simplifiées)
public interface ITelemetryInitializer
{
    void Initialize(ITelemetry telemetry);
}
public interface ITelemetry
{
    TelemetryContext Context { get; }
}
public class TelemetryContext
{
    public IDictionary<string, string> GlobalProperties { get; set; } = new Dictionary<string, string>();
    public CloudContext Cloud { get; set; } = new();
}
public class CloudContext
{
    public string? RoleName { get; set; }
    public string? RoleInstance { get; set; }
}

/*
─────────────────────────────────────────────────────────────────
Utiliser Application Insights dans le code
─────────────────────────────────────────────────────────────────
*/

/*
@inject TelemetryClient TelemetryClient

@code {
    private void TrackAchat(Produit produit, decimal montant)
    {
        // ─── Event personnalisé ─────────────────────────────
        TelemetryClient.TrackEvent("AchatEffectue", new Dictionary<string, string>
        {
            ["ProduitId"] = produit.Id.ToString(),
            ["Categorie"] = produit.Categorie,
        },
        new Dictionary<string, double>
        {
            ["Montant"] = (double)montant,
        });

        // ─── Métrique ────────────────────────────────────────
        TelemetryClient.TrackMetric("ValeurPanier", (double)montant);
    }

    private void TrackErreur(Exception ex, string contexte)
    {
        TelemetryClient.TrackException(ex, new Dictionary<string, string>
        {
            ["Contexte"] = contexte,
            ["UtilisateurId"] = _utilisateurId.ToString(),
        });
    }
}
*/


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

/*
═══════════════════════════════════════════════════════════════
EXERCICE : DÉPLOIEMENT COMPLET EN PRODUCTION
═══════════════════════════════════════════════════════════════

OBJECTIF : Déployer une application Blazor complète en production

ÉTAPES :

1. DOCKERISATION (2h) :
   a) Créer le Dockerfile multi-stage
   b) Créer le .dockerignore
   c) Créer docker-compose.yml avec app + postgres + redis + nginx
   d) Créer nginx.conf avec reverse proxy + compression + headers sécurité
   e) Créer .env.production (jamais commité !)
   f) Tester localement : docker compose up --build

2. HEALTH CHECKS (1h) :
   a) Installer les packages healthchecks
   b) Configurer PostgreSQL + Redis checks
   c) Créer un check personnalisé pour la queue email
   d) Configurer les endpoints /health, /health/ready, /health/live
   e) Vérifier : curl http://localhost:8080/health

3. LOGGING SERILOG (1h) :
   a) Installer Serilog + sinks (Console + File)
   b) Configurer dans Program.cs et appsettings.json
   c) Remplacer tous les ILogger.LogInformation par des logs structurés
   d) Ajouter RequestLoggingMiddleware
   e) Vérifier les logs structurés dans la console

4. CI/CD GITHUB ACTIONS (2h) :
   a) Créer .github/workflows/ci-cd.yml
   b) Job tests : build + tests unitaires
   c) Job docker : build + push vers GHCR
   d) Job staging : déploiement sur VPS de staging
   e) Configurer les secrets GitHub
   f) Pousser sur develop -> Vérifier que le pipeline passe

5. MONITORING (1h) :
   a) Créer un compte Application Insights (Azure Portal)
   b) Configurer le SDK dans l'application
   c) Déclencher quelques requêtes -> Vérifier dans Azure Portal
   d) Créer une alerte : "Notifier si taux d'erreur > 5%"

COMPÉTENCES :
[OK] Docker multi-stage build
[OK] Docker Compose avec stack complète
[OK] Nginx reverse proxy
[OK] Health Checks ASP.NET Core
[OK] Serilog logging structuré
[OK] GitHub Actions CI/CD
[OK] Application Insights
═══════════════════════════════════════════════════════════════
*/


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

[OK] CHAPITRE 26 - DOCKERISATION :
- Concepts : Image, Conteneur, Registry, Dockerfile, Compose
- Multi-stage build : Étape build (SDK ~2GB) -> Runtime final (~150MB)
- Dockerfile optimisé : Copier .csproj d'abord -> Cache NuGet
- USER app : Ne pas exécuter en root (sécurité)
- EXPOSE 8080 : Port HTTP interne (Nginx gère HTTPS)
- docker-compose.yml : App + PostgreSQL + Redis + Nginx
- Healthcheck Docker : Redémarrer si /health échoue
- Networks Docker : Services se parlent par leur nom
- Volumes persistants : Les données survivent aux redémarrages
- .dockerignore : Exclure bin, obj, .git pour builds plus rapides
- .env.production : Secrets JAMAIS dans Git

[OK] CHAPITRE 27 - CI/CD :
- CI = Intégration Continue : Tests + Build automatiques à chaque push
- CD = Déploiement Continu : Deploy automatique si CI passe
- GitHub Actions : Fichiers YAML dans .github/workflows/
- Workflow : Trigger -> Jobs -> Steps
- Cache NuGet : ${{ hashFiles('**/*.csproj') }} -> Build 5x plus rapide
- Container Registry : ghcr.io (GitHub) ou ACR (Azure)
- Metadata Action : Tags automatiques (sha, branche, latest)
- Deploy SSH : appleboy/ssh-action pour déployer sur VPS
- Deploy Azure : azure/webapps-deploy pour Azure App Service
- Environments : Staging + Production avec approbation manuelle
- Secrets GitHub : Jamais en clair dans le YAML
- Azure DevOps : Alternative enterprise avec stages et approbations

[OK] CHAPITRE 28 - OBSERVABILITÉ :
- 3 piliers : Logs (événements), Métriques (nombres), Traces (suivi)
- Serilog : Logging structuré avec propriétés nommées {UserId}
- Enrichissement : MachineName, Environment, ThreadId, ApplicationName
- Minimum Level Override : Réduire le bruit Microsoft/System
- Sinks : Console (colorée), File (JSON + texte), Seq, Application Insights
- LogContext.BeginScope : Ajouter des propriétés temporaires
- Health Checks : /health, /health/ready, /health/live
- PostgreSQL/Redis/URL checks + check personnalisé
- Application Insights : SDK, ITelemetryInitializer, TrackEvent, TrackMetric
- Alertes : Taux d'erreur, temps de réponse, disponibilité

[OBJECTIF] PROCHAINE ÉTAPE : PARTIE 10
- Blazor Hybrid avec .NET MAUI
- PWA et Service Workers
- WebSockets et temps réel avec SignalR
- Micro-frontends
- Librairie de composants commerciale
═══════════════════════════════════════════════════════════════
*/


// ============================================================================
// [LIVRE] BLAZOR - PARTIE 10 : AVANCÉ & EXPERT
// ============================================================================
//
// CHAPITRE 29 : Blazor Hybrid (.NET MAUI)
// CHAPITRE 30 : PWA avec Blazor
// CHAPITRE 31 : WebSockets & Temps réel (SignalR avancé)
// CHAPITRE 32 : Micro-frontends avec Blazor
// CHAPITRE 33 : Créer une bibliothèque de composants commerciale
// CHAPITRE 34 : Contribution open-source Blazor
//
// [TEMPS] TEMPS ESTIMÉ : ~15-20 heures
// [DOCS] PRÉREQUIS : Parties 1-9 complétées
// ============================================================================

/*
[OBJECTIF] PHILOSOPHIE DE CETTE PARTIE

Vous êtes maintenant un développeur Blazor avancé.
Cette partie vous amène au niveau EXPERT :
-> Applications desktop/mobile avec .NET MAUI
-> Applications offline-first avec les PWA
-> Communications temps réel (chat, notifications live)
-> Architecture micro-frontends pour les grandes équipes
-> Construire et vendre une bibliothèque de composants

À la fin de cette partie, vous serez capable de :
-> Créer n'importe quel type d'application avec Blazor
-> Choisir la bonne architecture pour chaque situation
-> Contribuer à l'écosystème Blazor open-source
*/


// ============================================================================
// [GUIDE] CHAPITRE 29 : BLAZOR HYBRID (.NET MAUI)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre ce qu'est Blazor Hybrid
[OK] Créer une app .NET MAUI avec Blazor
[OK] Partager du code entre Web et Desktop/Mobile
[OK] Accéder aux APIs natives (GPS, caméra, fichiers)
[OK] Distribuer sur Windows, macOS, iOS, Android
*/


// ----------------------------------------------------------------------------
// [WEB] BLAZOR HYBRID — Un seul code, toutes les plateformes
// ----------------------------------------------------------------------------

/*
QU'EST-CE QUE BLAZOR HYBRID ?

SANS HYBRID (Avant) :
  Web -> Blazor WASM                     (C#)
  Desktop -> WPF, WinForms               (C#, mais UI différente)
  Mobile -> Xamarin, MAUI                (C#, mais code différent)
  -> 3 bases de code différentes pour 3 plateformes !

AVEC BLAZOR HYBRID (.NET MAUI) :
  Web + Desktop + Mobile -> UN SEUL code Blazor !
  -> Composants Razor réutilisés partout
  -> Accès aux APIs natives via .NET MAUI

COMMENT ÇA MARCHE ?

  MAUI App (natif)
  ├── BlazorWebView        <- Composant MAUI qui héberge Blazor
  │   ├── Rendu dans WebView natif (WKWebView iOS, WebView2 Windows, etc.)
  │   ├── Exécution .NET LOCALE (pas de WebAssembly !)
  │   └── Accès directs aux ressources système
  └── Reste de l'UI MAUI (natif)
      └── Peut mixer Blazor et MAUI !

DIFFÉRENCES BLAZOR HYBRID vs WASM :

  WASM :   Code dans le navigateur, sandboxé, accès limité au système
  HYBRID : Code natif .NET, accès TOTAL au système (fichiers, GPS, etc.)

PLATEFORMES SUPPORTÉES :
  -> Windows 10/11 (WinUI 3)
  -> macOS 12+ (Catalyst)
  -> iOS 15+
  -> Android 7+

INSTALLATION :
  Visual Studio 2022+ avec la charge de travail "Mobile development"
  dotnet new maui-blazor -n MonHybridApp

STRUCTURE DU PROJET MAUI BLAZOR :

  MonHybridApp/
  ├── MauiProgram.cs         <- Configuration MAUI (comme Program.cs)
  ├── MainPage.xaml          <- Page principale avec BlazorWebView
  ├── MainPage.xaml.cs
  ├── Platforms/             <- Code spécifique par plateforme
  │   ├── Android/
  │   ├── iOS/
  │   ├── MacCatalyst/
  │   └── Windows/
  ├── Resources/             <- Images, fonts, splash screen
  ├── wwwroot/               <- Assets web (CSS, JS)
  ├── Pages/                 <- Vos composants Blazor (réutilisables !)
  └── Shared/
*/

/*
─────────────────────────────────────────────────────────────────
MauiProgram.cs — Configuration de l'application MAUI Hybrid
─────────────────────────────────────────────────────────────────

using Microsoft.AspNetCore.Components.WebView.Maui;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                fonts.AddFont("Inter-Regular.ttf", "Inter");
            });

        // ─── Enregistrer les services MAUI + Blazor ────────────────
        builder.Services.AddMauiBlazorWebView();

        #if DEBUG
        // Activer DevTools en développement (F12 dans l'app !)
        builder.Services.AddBlazorWebViewDeveloperTools();
        builder.Logging.AddDebug();
        #endif

        // ─── Vos services habituels ────────────────────────────────
        builder.Services.AddScoped<IProduitService, ProduitServiceHybrid>();
        builder.Services.AddSingleton<AppState>();

        // ─── Services MAUI (APIs natives) ─────────────────────────
        // Ces services accèdent au matériel réel de l'appareil
        builder.Services.AddSingleton<ICameraService, MauiCameraService>();
        builder.Services.AddSingleton<ILocationService, MauiLocationService>();
        builder.Services.AddSingleton<IFilePickerService, MauiFilePickerService>();
        builder.Services.AddSingleton<INotificationService, MauiNotificationService>();

        // HttpClient configuré pour l'API distante
        builder.Services.AddHttpClient("api", client =>
        {
            client.BaseAddress = new Uri("https://api.monapp.com/");
        });

        return builder.Build();
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
MainPage.xaml — Intégrer BlazorWebView dans MAUI
─────────────────────────────────────────────────────────────────

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MonHybridApp"
             x:Class="MonHybridApp.MainPage">

    <Grid>
        <!--
          BlazorWebView = Le composant qui héberge tout Blazor
          HostPage -> Le fichier HTML de base (dans wwwroot/)
        -->
        <BlazorWebView x:Name="blazorWebView"
                       HostPage="wwwroot/index.html">
            <BlazorWebView.RootComponents>
                <!--
                  ComponentType -> Le composant Blazor racine
                  Selector -> L'élément HTML où le monter (#app dans index.html)
                -->
                <RootComponent Selector="#app"
                               ComponentType="{x:Type local:Routes}" />
            </BlazorWebView.RootComponents>
        </BlazorWebView>
    </Grid>

</ContentPage>
*/


// ----------------------------------------------------------------------------
// [MOBILE] ACCÈS AUX APIS NATIVES DEPUIS BLAZOR
// ----------------------------------------------------------------------------

/*
MAGIE DE BLAZOR HYBRID :
Vos composants Blazor peuvent utiliser des services qui accèdent
au matériel physique de l'appareil !

Pattern :
1. Définir une interface (dans le projet partagé)
2. Implémenter pour MAUI (accès natif)
3. Implémenter pour le Web (si supporté)
4. Injecter dans les composants Blazor
*/

// Interfaces partagées (dans MonApp.Shared)
public interface ICameraService
{
    Task<byte[]?> PrendrePhotoAsync();
    Task<bool> EstDisponibleAsync();
}

public interface ILocationService
{
    Task<(double Latitude, double Longitude)?> ObtenirPositionAsync();
    Task<bool> EstAutoriseeAsync();
}

public interface IFilePickerService
{
    Task<(string Nom, byte[] Contenu)?> ChoisirFichierAsync(string[] types);
    Task SauvegarderFichierAsync(string nom, byte[] contenu);
}

public interface INotificationService
{
    Task EnvoyerNotificationLocaleAsync(string titre, string message);
    Task<bool> DemanderPermissionAsync();
}

/*
─────────────────────────────────────────────────────────────────
Implémentation MAUI de ICameraService
─────────────────────────────────────────────────────────────────
Fichier : Services/MauiCameraService.cs
*/

/*
// Ce fichier N'EXISTE QUE dans le projet MAUI (pas dans Shared ni Client Web)
public class MauiCameraService : ICameraService
{
    public async Task<bool> EstDisponibleAsync()
    {
        return MediaPicker.Default.IsCaptureSupported;
    }

    public async Task<byte[]?> PrendrePhotoAsync()
    {
        try
        {
            // Demander la permission si nécessaire
            var status = await Permissions.RequestAsync<Permissions.Camera>();
            if (status != PermissionStatus.Granted)
                return null;

            // Ouvrir la caméra native
            var photo = await MediaPicker.Default.CapturePhotoAsync(
                new MediaPickerOptions
                {
                    Title = "Prendre une photo"
                });

            if (photo is null) return null;

            // Lire les bytes de l'image
            await using var stream = await photo.OpenReadAsync();
            using var memoryStream = new MemoryStream();
            await stream.CopyToAsync(memoryStream);
            return memoryStream.ToArray();
        }
        catch (PermissionException)
        {
            return null;
        }
    }
}
*/

/*
─────────────────────────────────────────────────────────────────
Composant Blazor qui utilise la caméra (FONCTIONNE SUR MAUI !)
─────────────────────────────────────────────────────────────────
Fichier : Pages/ScannerProduit.razor (partagé Web + MAUI)

@page "/scanner"
@inject ICameraService CameraService
@inject IProduitService ProduitService

<div class="scanner-container">
    <h2>[CAMERA_WITH_FLASH] Scanner un produit</h2>

    @if (_photo is not null)
    {
        <!-- Afficher la photo prise -->
        <img src="@($"data:image/jpeg;base64,{Convert.ToBase64String(_photo)}")"
             class="photo-preview"
             alt="Photo scannée" />
        <button @onclick="AnalyserPhoto" class="btn btn-primary">
            Analyser
        </button>
    }
    else
    {
        <button @onclick="PrendrePhoto"
                disabled="@(!_cameraDisponible)"
                class="btn btn-success">
            [CAMERA] Prendre une photo
        </button>

        @if (!_cameraDisponible)
        {
            <p class="text-muted">Caméra non disponible sur cette plateforme</p>
        }
    }

    @if (_produitTrouve is not null)
    {
        <div class="produit-trouve mt-4">
            <h3>[OK] Produit trouvé</h3>
            <p><strong>@_produitTrouve.Nom</strong></p>
            <p>Prix: @_produitTrouve.Prix.ToString("C")</p>
        </div>
    }
</div>

@code {
    private byte[]? _photo;
    private bool _cameraDisponible = false;
    private Produit? _produitTrouve;

    protected override async Task OnInitializedAsync()
    {
        _cameraDisponible = await CameraService.EstDisponibleAsync();
    }

    private async Task PrendrePhoto()
    {
        _photo = await CameraService.PrendrePhotoAsync();
        // Sur desktop sans caméra -> _photo = null
    }

    private async Task AnalyserPhoto()
    {
        if (_photo is null) return;
        // Envoyer la photo à l'API pour reconnaissance
        _produitTrouve = await ProduitService.ReconnaitreParImageAsync(_photo);
    }
}
*/


// ----------------------------------------------------------------------------
// [SYNC] PARTAGE DE CODE WEB <-> MAUI
// ----------------------------------------------------------------------------

/*
STRATÉGIE DE PARTAGE DE CODE :

                     ┌─────────────────────────┐
                     │   MonApp.Shared          │
                     │   - Modèles (DTOs)       │
                     │   - Interfaces services  │
                     │   - Composants Blazor    │ <- Réutilisés partout !
                     │   - Validation           │
                     └─────────┬───────────────┘
                               │
               ┌───────────────┼───────────────────┐
               │               │                   │
  ┌────────────[BLACK_DOWN-POINTING_TRIANGLE]─────┐  ┌──────[BLACK_DOWN-POINTING_TRIANGLE]──────┐  ┌────────[BLACK_DOWN-POINTING_TRIANGLE]─────────┐
  │ MonApp.Client    │  │ MonApp.MAUI │  │ MonApp.Server    │
  │ (Blazor WASM)    │  │ (Desktop +  │  │ (ASP.NET Core)   │
  │                  │  │ Mobile)     │  │                  │
  │ Services via     │  │ Services    │  │ Services BDD     │
  │ HttpClient       │  │ natifs MAUI │  │ directs          │
  └──────────────────┘  └─────────────┘  └──────────────────┘

Les COMPOSANTS BLAZOR sont partagés entre Web et MAUI !
Seulement les IMPLÉMENTATIONS DES SERVICES diffèrent.
*/

// Exemple : Service produit adapté pour chaque plateforme

// Version Web (via HttpClient)
public class ProduitServiceWeb : IProduitService
{
    private readonly HttpClient _httpClient;

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

    public async Task<List<Produit>> ObtenirTousAsync()
        => await _httpClient.GetFromJsonAsync<List<Produit>>("api/produits")
           ?? new List<Produit>();

    public Task<Produit?> ObtenirParIdAsync(int id) => throw new NotImplementedException();
    public Task<Produit?> ReconnaitreParImageAsync(byte[] image) => throw new NotImplementedException();
}

// Version MAUI (accès direct à une BDD locale SQLite + sync)
public class ProduitServiceHybrid : IProduitService
{
    // SQLite local pour mode offline
    // private readonly SQLiteConnection _localDb;
    private readonly HttpClient? _httpClient; // Pour sync si connecté

    public ProduitServiceHybrid(IHttpClientFactory? factory = null)
    {
        _httpClient = factory?.CreateClient("api");
        // _localDb = new SQLiteConnection(Path.Combine(
        //     FileSystem.AppDataDirectory, "produits.db"));
    }

    public async Task<List<Produit>> ObtenirTousAsync()
    {
        // 1. Essayer de synchroniser depuis l'API
        try
        {
            if (_httpClient is not null && await VerifierConnexionAsync())
            {
                var produitsDistants = await _httpClient
                    .GetFromJsonAsync<List<Produit>>("api/produits");
                // SauvegarderLocalementAsync(produitsDistants);
                return produitsDistants ?? new();
            }
        }
        catch { /* Pas de connexion */ }

        // 2. Fallback : Données locales (SQLite)
        // return _localDb.Table<ProduitLocal>()
        //     .Select(p => p.ToModel()).ToList();

        return new List<Produit>(); // Simulation
    }

    private async Task<bool> VerifierConnexionAsync()
    {
        return Connectivity.Current.NetworkAccess == NetworkAccess.Internet;
    }

    public Task<Produit?> ObtenirParIdAsync(int id) => throw new NotImplementedException();
    public Task<Produit?> ReconnaitreParImageAsync(byte[] image) => throw new NotImplementedException();
}

// Enum MAUI (simulé)
public static class Connectivity
{
    public static ConnectivityCurrent Current { get; } = new();
}
public class ConnectivityCurrent
{
    public NetworkAccess NetworkAccess { get; } = NetworkAccess.Internet;
}
public enum NetworkAccess { None, Local, ConstrainedInternet, Internet, Unknown }


// ============================================================================
// [GUIDE] CHAPITRE 30 : PWA AVEC BLAZOR
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre ce qu'est une PWA
[OK] Configurer le Service Worker Blazor
[OK] Implémenter le mode offline
[OK] Gérer les stratégies de cache
[OK] Mettre à jour l'application automatiquement
[OK] Permettre l'installation sur l'écran d'accueil
*/


// ----------------------------------------------------------------------------
// [MOBILE] PWA — Application Web Progressive
// ----------------------------------------------------------------------------

/*
QU'EST-CE QU'UNE PWA ?

PWA = Progressive Web App
-> Site web qui SE COMPORTE COMME UNE APPLICATION NATIVE

CAPACITÉS D'UNE PWA :
[OK] Installable sur l'écran d'accueil (sans app store !)
[OK] Fonctionne HORS LIGNE
[OK] Notifications push
[OK] Accès aux fichiers locaux
[OK] Mise à jour automatique en arrière-plan
[OK] Splash screen au démarrage
[OK] Plein écran sans barre d'URL
[OK] Icône sur le bureau (Windows, macOS, Android, iOS)

POURQUOI UNE PWA PLUTÔT QU'UNE APP NATIVE ?
-> PAS de soumission à l'App Store
-> Mise à jour instantanée (l'utilisateur a toujours la dernière version)
-> Un seul code pour toutes les plateformes
-> Plus facile à maintenir
-> Partageable via une simple URL

BLAZOR WEBASSEMBLY = PARFAIT POUR LES PWA !
-> Code exécuté localement (pas de serveur requis hors ligne)
-> Service Worker inclus par défaut si vous activez l'option PWA

ACTIVER PWA LORS DE LA CRÉATION :
dotnet new blazorwasm --pwa -n MonAppPWA
*/

/*
─────────────────────────────────────────────────────────────────
manifest.webmanifest — Définir l'application installable
─────────────────────────────────────────────────────────────────
Fichier : wwwroot/manifest.webmanifest

{
  "name": "MonApp - Gestion Produits",
  "short_name": "MonApp",
  "description": "Application de gestion de produits et commandes",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#0f172a",
  "theme_color": "#3b82f6",
  "prefer_related_applications": false,
  "orientation": "any",

  "icons": [
    {
      "src": "icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ],

  "screenshots": [
    {
      "src": "screenshots/dashboard.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide",
      "label": "Dashboard principal"
    }
  ],

  "shortcuts": [
    {
      "name": "Nouveau Produit",
      "url": "/produits/ajouter",
      "icons": [{ "src": "icons/add.png", "sizes": "192x192" }]
    },
    {
      "name": "Commandes",
      "url": "/commandes",
      "icons": [{ "src": "icons/order.png", "sizes": "192x192" }]
    }
  ],

  "categories": ["business", "productivity"],
  "lang": "fr",
  "dir": "ltr"
}
*/

/*
─────────────────────────────────────────────────────────────────
service-worker.js — Service Worker personnalisé
─────────────────────────────────────────────────────────────────
Fichier : wwwroot/service-worker.js

// Ce fichier est publié sous forme de service-worker.published.js
// en production (avec la liste des fichiers à cacher)

// ─── Stratégies de cache ──────────────────────────────────────
//
// 1. CACHE FIRST (pour les assets statiques)
//    -> Chercher dans le cache d'abord
//    -> Aller au réseau seulement si absent du cache
//    -> Idéal pour : CSS, JS, images, polices
//
// 2. NETWORK FIRST (pour les données API)
//    -> Chercher sur le réseau d'abord (données fraîches)
//    -> Fallback sur le cache si réseau indisponible
//    -> Idéal pour : /api/produits, /api/commandes
//
// 3. STALE WHILE REVALIDATE (pour le contenu semi-dynamique)
//    -> Retourner le cache immédiatement (rapide !)
//    -> Mettre à jour le cache en arrière-plan
//    -> Idéal pour : pages HTML, contenus qui changent peu
//
// 4. CACHE ONLY (pour le mode offline strict)
//    -> Seulement depuis le cache
//    -> Jamais de requête réseau
//
// 5. NETWORK ONLY (pour les opérations critiques)
//    -> Jamais depuis le cache (POST, DELETE)
//    -> Échec si pas de réseau

// NOM ET VERSION DU CACHE
const CACHE_VERSION = 'v1.2.3';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const API_CACHE = `api-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`;

// Liste des fichiers essentiels pour le mode offline
const OFFLINE_FALLBACK_URL = '/offline.html';
const ESSENTIAL_FILES = [
  '/',
  '/index.html',
  '/css/app.css',
  '/_framework/blazor.webassembly.js',
  OFFLINE_FALLBACK_URL,
];

// ─── Installation du Service Worker ───────────────────────────
self.addEventListener('install', event => {
  console.log('[SW] Installation du service worker v' + CACHE_VERSION);

  event.waitUntil(
    caches.open(STATIC_CACHE).then(cache => {
      // Mettre en cache les fichiers essentiels
      return cache.addAll(ESSENTIAL_FILES);
    }).then(() => {
      // Prendre le contrôle immédiatement (sans attendre reload)
      self.skipWaiting();
    })
  );
});

// ─── Activation (nettoyage des anciens caches) ────────────────
self.addEventListener('activate', event => {
  console.log('[SW] Activation de la nouvelle version');

  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys
          // Supprimer les caches des ANCIENNES versions
          .filter(key => key !== STATIC_CACHE &&
                         key !== API_CACHE &&
                         key !== DYNAMIC_CACHE)
          .map(key => {
            console.log('[SW] Suppression ancien cache:', key);
            return caches.delete(key);
          })
      );
    }).then(() => {
      // Prendre le contrôle de tous les clients
      return self.clients.claim();
    })
  );
});

// ─── Interception des requêtes ────────────────────────────────
self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // Ne pas intercepter les requêtes POST/PUT/DELETE (mutations)
  if (event.request.method !== 'GET') {
    return; // Laisser passer normalement
  }

  // Stratégie selon le type de ressource
  if (url.pathname.startsWith('/api/')) {
    // API -> Network First (données fraîches en priorité)
    event.respondWith(networkFirst(event.request, API_CACHE));

  } else if (url.pathname.includes('/_framework/') ||
             url.pathname.includes('.wasm') ||
             url.pathname.includes('.dll')) {
    // Fichiers Blazor -> Cache First (changent avec la version de l'app)
    event.respondWith(cacheFirst(event.request, STATIC_CACHE));

  } else if (url.pathname.match(/\.(css|js|png|jpg|svg|ico|woff2)$/)) {
    // Assets statiques -> Cache First
    event.respondWith(cacheFirst(event.request, STATIC_CACHE));

  } else {
    // Pages HTML -> Stale While Revalidate
    event.respondWith(staleWhileRevalidate(event.request, DYNAMIC_CACHE));
  }
});

// ─── Stratégie : Network First ────────────────────────────────
async function networkFirst(request, cacheName) {
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open(cacheName);
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    // Réseau indisponible -> Chercher dans le cache
    const cachedResponse = await caches.match(request);
    if (cachedResponse) return cachedResponse;

    // Rien dans le cache -> Page offline
    if (request.destination === 'document') {
      return caches.match(OFFLINE_FALLBACK_URL);
    }
    throw error;
  }
}

// ─── Stratégie : Cache First ──────────────────────────────────
async function cacheFirst(request, cacheName) {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) return cachedResponse;

  // Pas dans le cache -> Aller au réseau ET mettre en cache
  const networkResponse = await fetch(request);
  if (networkResponse.ok) {
    const cache = await caches.open(cacheName);
    cache.put(request, networkResponse.clone());
  }
  return networkResponse;
}

// ─── Stratégie : Stale While Revalidate ──────────────────────
async function staleWhileRevalidate(request, cacheName) {
  const cache = await caches.open(cacheName);
  const cachedResponse = await cache.match(request);

  // Revalider en arrière-plan
  const networkFetch = fetch(request).then(networkResponse => {
    if (networkResponse.ok) {
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  });

  // Retourner le cache immédiatement si disponible, sinon attendre le réseau
  return cachedResponse || networkFetch;
}

// ─── Notifications Push ───────────────────────────────────────
self.addEventListener('push', event => {
  if (!event.data) return;

  const data = event.data.json();

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192x192.png',
      badge: '/icons/badge-72x72.png',
      vibrate: [200, 100, 200],
      data: { url: data.url || '/' },
      actions: data.actions || [],
    })
  );
});

// Clic sur une notification -> Ouvrir l'app à la bonne URL
self.addEventListener('notificationclick', event => {
  event.notification.close();

  const url = event.notification.data?.url || '/';

  event.waitUntil(
    clients.matchAll({ type: 'window' }).then(clientList => {
      // Si l'app est déjà ouverte, naviguer dans l'onglet existant
      for (const client of clientList) {
        if (client.url === url && 'focus' in client) {
          return client.focus();
        }
      }
      // Sinon ouvrir un nouvel onglet
      return clients.openWindow(url);
    })
  );
});
*/


// ----------------------------------------------------------------------------
// [SYNC] MISE À JOUR DE L'APPLICATION — ServiceWorkerUpdateManager
// ----------------------------------------------------------------------------

/*
PROBLÈME :
L'utilisateur a la version 1.0 de votre PWA en cache.
Vous déployez la version 1.1.
Comment l'utilisateur obtient-il la nouvelle version ?

RÉPONSE : Le Service Worker détecte automatiquement les changements
et propose à l'utilisateur de mettre à jour.

BLAZOR FOURNIT : ServiceWorkerUpdateManager
*/

/*
─────────────────────────────────────────────────────────────────
Composant AppUpdateNotifier.razor — Proposition de mise à jour
─────────────────────────────────────────────────────────────────

@implements IDisposable
@inject ServiceWorkerUpdateManager SwUpdateManager
@inject NavigationManager NavManager

@if (_miseAJourDisponible)
{
    <!-- Bandeau de mise à jour (discret mais visible) -->
    <div class="update-banner position-fixed bottom-0 start-0 end-0 z-1050 p-3"
         style="background: #1e293b; color: white; border-top: 3px solid #3b82f6;">
        <div class="container d-flex align-items-center justify-content-between">
            <div>
                <strong>🆕 Nouvelle version disponible !</strong>
                <span class="ms-2 text-muted">Rechargez pour obtenir les dernières améliorations.</span>
            </div>
            <div class="d-flex gap-2">
                <button @onclick="AppliquerMiseAJour"
                        class="btn btn-primary btn-sm">
                    (sync) Mettre à jour maintenant
                </button>
                <button @onclick="() => _miseAJourDisponible = false"
                        class="btn btn-outline-secondary btn-sm">
                    Plus tard
                </button>
            </div>
        </div>
    </div>
}

@code {
    private bool _miseAJourDisponible = false;

    protected override async Task OnInitializedAsync()
    {
        // S'abonner aux notifications de mise à jour
        SwUpdateManager.OnUpdateAvailable += NotifierMiseAJour;

        // Vérifier immédiatement s'il y a une mise à jour en attente
        await SwUpdateManager.CheckForUpdateAsync();
    }

    private void NotifierMiseAJour()
    {
        _miseAJourDisponible = true;
        InvokeAsync(StateHasChanged);
    }

    private async Task AppliquerMiseAJour()
    {
        // Demander au Service Worker d'activer la nouvelle version
        await SwUpdateManager.ActivateUpdateAsync();

        // Recharger la page pour appliquer la mise à jour
        NavManager.NavigateTo(NavManager.Uri, forceLoad: true);
    }

    public void Dispose()
    {
        SwUpdateManager.OnUpdateAvailable -= NotifierMiseAJour;
    }
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 31 : WEBSOCKETS & TEMPS RÉEL
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre SignalR avancé
[OK] Implémenter un chat en temps réel
[OK] Créer un système de notifications live
[OK] Gérer les groupes d'utilisateurs
[OK] Implémenter les indicateurs de présence
[OK] Optimiser les performances SignalR
*/


// ----------------------------------------------------------------------------
// [RESEAU] SIGNALR — Communication bidirectionnelle en temps réel
// ----------------------------------------------------------------------------

/*
QU'EST-CE QUE SIGNALR ?

HTTP classique :     Client -> Serveur (une seule direction, pull)
                     Client demande -> Serveur répond -> Connexion fermée

WebSocket / SignalR : Client <-> Serveur (bidirectionnel, push)
                      Connexion persistante
                      Serveur peut envoyer à n'importe quel moment !

POURQUOI SIGNALR PLUTÔT QUE WEBSOCKET RAW ?
-> SignalR est une abstraction au-dessus des WebSockets
-> Fallback automatique : WebSocket -> Server-Sent Events -> Long Polling
-> Reconnexion automatique
-> Groupes d'utilisateurs
-> Hub côté serveur (comme des contrôleurs pour les WebSockets)

EXEMPLES D'APPLICATIONS TEMPS RÉEL :
-> Chat / messagerie en temps réel
-> Notifications (email reçu, commande livrée, paiement reçu)
-> Tableaux de bord avec données en direct
-> Collaboration en temps réel (Google Docs-like)
-> Jeux multijoueurs
-> Mises à jour de prix (bourse, crypto)
-> Suivi de livraison en temps réel

INSTALLATION :
dotnet add package Microsoft.AspNetCore.SignalR.Client (côté client)
Les Hubs sont inclus dans ASP.NET Core (pas de package supplémentaire)
*/


// ----------------------------------------------------------------------------
// [ACCUEIL] HUB SIGNALR — Le serveur temps réel
// ----------------------------------------------------------------------------

// Hubs/ChatHub.cs (côté serveur)
public class ChatHub : Hub
{
    private readonly ILogger<ChatHub> _logger;
    private static readonly Dictionary<string, InfoUtilisateur> _utilisateursConnectes = new();

    public ChatHub(ILogger<ChatHub> logger)
    {
        _logger = logger;
    }

    // ─── Connexion/Déconnexion ──────────────────────────────────────────────

    // Appelé automatiquement quand un client se connecte
    public override async Task OnConnectedAsync()
    {
        _logger.LogInformation("Client connecté: {ConnectionId}", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    // Appelé automatiquement à la déconnexion
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        if (_utilisateursConnectes.TryGetValue(Context.ConnectionId, out var user))
        {
            _utilisateursConnectes.Remove(Context.ConnectionId);

            // Notifier TOUS les clients : Cet utilisateur s'est déconnecté
            await Clients.All.SendAsync("UtilisateurDeconnecte", user);
            _logger.LogInformation("Utilisateur déconnecté: {Nom}", user.Nom);
        }

        await base.OnDisconnectedAsync(exception);
    }

    // ─── Méthodes invocables par les clients ────────────────────────────────

    // Le client appelle cette méthode pour rejoindre le chat
    public async Task Rejoindre(string nomUtilisateur, string? salonId = null)
    {
        var utilisateur = new InfoUtilisateur(
            Context.ConnectionId,
            nomUtilisateur,
            DateTime.UtcNow);

        _utilisateursConnectes[Context.ConnectionId] = utilisateur;

        // Rejoindre un salon spécifique (groupe SignalR)
        var salon = salonId ?? "general";
        await Groups.AddToGroupAsync(Context.ConnectionId, salon);

        // Envoyer la liste des utilisateurs en ligne au nouveau venu
        var tousLesUsers = _utilisateursConnectes.Values.ToList();
        await Clients.Caller.SendAsync("InitialisationChat", new
        {
            Salon = salon,
            UtilisateursCourants = tousLesUsers,
            Historique = new List<MessageChat>() // Charger depuis BDD en vrai
        });

        // Notifier TOUT LE SALON : Quelqu'un vient d'arriver
        await Clients.Group(salon).SendAsync("UtilisateurRejoins", utilisateur);

        _logger.LogInformation("{Nom} a rejoint le salon {Salon}", nomUtilisateur, salon);
    }

    // Envoyer un message dans un salon
    public async Task EnvoyerMessage(string salon, string texte)
    {
        if (!_utilisateursConnectes.TryGetValue(Context.ConnectionId, out var expediteur))
            return; // Pas encore connecté avec un nom

        // Valider le message
        if (string.IsNullOrWhiteSpace(texte) || texte.Length > 2000)
            return;

        var message = new MessageChat(
            Id: Guid.NewGuid().ToString(),
            Salon: salon,
            Expediteur: expediteur,
            Texte: texte.Trim(),
            Horodatage: DateTime.UtcNow,
            Type: TypeMessage.Texte);

        // Envoyer à TOUS les membres du salon (y compris l'expéditeur)
        await Clients.Group(salon).SendAsync("NouveauMessage", message);

        // Sauvegarder dans la BDD en arrière-plan
        // await _messageRepository.SauvegarderAsync(message);
    }

    // Indicateur "est en train de taper..."
    public async Task SignalerEcritureProlongee(string salon, bool estEnTrainDeTaper)
    {
        if (!_utilisateursConnectes.TryGetValue(Context.ConnectionId, out var user))
            return;

        // Notifier TOUS SAUF l'expéditeur lui-même
        await Clients.GroupExcept(salon, Context.ConnectionId)
            .SendAsync("IndicateurEcriture", new
            {
                Utilisateur = user.Nom,
                EstEnTrainDeTaper = estEnTrainDeTaper
            });
    }

    // Envoyer une réaction sur un message ([BIEN], [HEAVY_BLACK_HEART], etc.)
    public async Task ReagirAuMessage(string salon, string messageId, string emoji)
    {
        if (!_utilisateursConnectes.TryGetValue(Context.ConnectionId, out var user))
            return;

        await Clients.Group(salon).SendAsync("NouvelleReaction", new
        {
            MessageId = messageId,
            Emoji = emoji,
            UtilisateurId = user.Id,
            UtilisateurNom = user.Nom
        });
    }

    // Créer ou rejoindre un salon privé (entre 2 personnes)
    public async Task OuvrirConversationPrivee(string destinataireConnectionId)
    {
        // Créer un ID de salon unique pour ces 2 utilisateurs
        var ids = new[] { Context.ConnectionId, destinataireConnectionId }.OrderBy(x => x);
        var salonPrive = $"prive_{string.Join("_", ids)}";

        // Ajouter les deux dans le salon privé
        await Groups.AddToGroupAsync(Context.ConnectionId, salonPrive);
        await Groups.AddToGroupAsync(destinataireConnectionId, salonPrive);

        var user = _utilisateursConnectes.GetValueOrDefault(Context.ConnectionId);
        await Clients.Group(salonPrive).SendAsync("ConversationPriveeOuverte", new
        {
            SalonId = salonPrive,
            Initiateur = user?.Nom
        });
    }
}

// Modèles
public record InfoUtilisateur(string Id, string Nom, DateTime ConnecteLe);

public record MessageChat(
    string Id,
    string Salon,
    InfoUtilisateur Expediteur,
    string Texte,
    DateTime Horodatage,
    TypeMessage Type);

public enum TypeMessage { Texte, Image, Fichier, Systeme }

// Hub SignalR (simulé)
public abstract class Hub
{
    protected IHubCallerClients Clients { get; } = default!;
    protected IGroupManager Groups { get; } = default!;
    protected HubCallerContext Context { get; } = default!;

    public virtual Task OnConnectedAsync() => Task.CompletedTask;
    public virtual Task OnDisconnectedAsync(Exception? exception) => Task.CompletedTask;
}

public interface IHubCallerClients
{
    IClientProxy All { get; }
    IClientProxy Caller { get; }
    IClientProxy Group(string groupName);
    IClientProxy GroupExcept(string groupName, string excludedConnectionId);
    IClientProxy Client(string connectionId);
}

public interface IClientProxy
{
    Task SendAsync(string method, object? arg1 = null, CancellationToken ct = default);
}

public interface IGroupManager
{
    Task AddToGroupAsync(string connectionId, string groupName, CancellationToken ct = default);
    Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken ct = default);
}

public class HubCallerContext
{
    public string ConnectionId { get; } = string.Empty;
}


// ----------------------------------------------------------------------------
// [SPEECH_BALLOON] COMPOSANT CHAT — Interface Blazor temps réel
// ----------------------------------------------------------------------------

/*
─────────────────────────────────────────────────────────────────
Fichier : Pages/Chat.razor — Interface de chat complète
─────────────────────────────────────────────────────────────────

@page "/chat"
@implements IAsyncDisposable
@inject NavigationManager NavManager
@inject AppState AppState

<div class="chat-container d-flex flex-column vh-100">

    <!-- En-tête du chat -->
    <div class="chat-header bg-primary text-white p-3 d-flex align-items-center gap-3">
        <h5 class="mb-0">[SPEECH_BALLOON] Chat - Salon #@_salonActuel</h5>
        <span class="badge bg-success ms-auto">@_utilisateursEnLigne.Count en ligne</span>

        <!-- Indicateur d'écriture -->
        @if (_utilisateursQuiEcrivent.Any())
        {
            <small class="text-white-50 fst-italic">
                @string.Join(", ", _utilisateursQuiEcrivent) écrit...
            </small>
        }
    </div>

    <!-- Corps du chat (messages) -->
    <div class="chat-messages flex-grow-1 overflow-auto p-3" @ref="_messagesDiv">
        @foreach (var message in _messages)
        {
            var estMoi = message.Expediteur.Nom == AppState.NomUtilisateur;

            <div class="message-container mb-3 @(estMoi ? "text-end" : "")">
                @if (!estMoi)
                {
                    <small class="text-muted d-block mb-1">@message.Expediteur.Nom</small>
                }
                <div class="message-bubble d-inline-block px-3 py-2 rounded-3
                            @(estMoi ? "bg-primary text-white" : "bg-light")">
                    @message.Texte
                </div>
                <small class="text-muted d-block mt-1">
                    @message.Horodatage.ToString("HH:mm")
                </small>
            </div>
        }
    </div>

    <!-- Zone de saisie -->
    <div class="chat-input border-top p-3">
        <div class="input-group">
            <input @bind="_texteMessage"
                   @bind:event="oninput"
                   @onkeyup="GererTouche"
                   @oninput="SignalerEcriture"
                   class="form-control"
                   placeholder="Écrire un message..."
                   maxlength="2000" />
            <button @onclick="EnvoyerMessage"
                    disabled="@(!_estConnecte || string.IsNullOrWhiteSpace(_texteMessage))"
                    class="btn btn-primary">
                Envoyer [DOWNWARDS_ARROW_WITH_CORNER_LEFTWARDS]
            </button>
        </div>
        <small class="text-muted">@_texteMessage.Length / 2000</small>
    </div>

    <!-- Panneau latéral : Utilisateurs en ligne -->
    <div class="utilisateurs-panel position-fixed end-0 top-0 h-100 bg-light border-start p-3"
         style="width: 200px; top: 64px !important;">
        <h6 class="border-bottom pb-2">[UTILISATEURS] En ligne</h6>
        @foreach (var user in _utilisateursEnLigne)
        {
            <div class="d-flex align-items-center gap-2 mb-2">
                <div class="rounded-circle bg-success" style="width: 8px; height: 8px;"></div>
                <span class="small @(user.Nom == AppState.NomUtilisateur ? "fw-bold" : "")">
                    @user.Nom
                </span>
            </div>
        }
    </div>

</div>

@code {
    private HubConnection? _hubConnection;
    private List<MessageChat> _messages = new();
    private List<InfoUtilisateur> _utilisateursEnLigne = new();
    private List<string> _utilisateursQuiEcrivent = new();
    private string _salonActuel = "general";
    private string _texteMessage = string.Empty;
    private bool _estConnecte = false;
    private ElementReference _messagesDiv;
    private Timer? _timerEcriture;

    protected override async Task OnInitializedAsync()
    {
        await ConnecterSignalRAsync();
    }

    private async Task ConnecterSignalRAsync()
    {
        // Créer la connexion SignalR
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(NavManager.ToAbsoluteUri("/hubs/chat"))
            .WithAutomaticReconnect(new[] { // Intervalles de reconnexion
                TimeSpan.Zero,
                TimeSpan.FromSeconds(2),
                TimeSpan.FromSeconds(5),
                TimeSpan.FromSeconds(10),
                TimeSpan.FromSeconds(30)
            })
            .Build();

        // ─── S'abonner aux événements serveur ─────────────────────

        // Nouveau message reçu
        _hubConnection.On<MessageChat>("NouveauMessage", async message =>
        {
            _messages.Add(message);
            await InvokeAsync(StateHasChanged);
            // Scroller vers le bas
            await ScrollerVersBas();
        });

        // Quelqu'un a rejoint le salon
        _hubConnection.On<InfoUtilisateur>("UtilisateurRejoins", async user =>
        {
            if (!_utilisateursEnLigne.Any(u => u.Id == user.Id))
                _utilisateursEnLigne.Add(user);

            // Message système
            _messages.Add(new MessageChat(
                Guid.NewGuid().ToString(), _salonActuel,
                new InfoUtilisateur("system", "Système", DateTime.UtcNow),
                $"[OK] {user.Nom} a rejoint le salon",
                DateTime.UtcNow, TypeMessage.Systeme));

            await InvokeAsync(StateHasChanged);
        });

        // Quelqu'un s'est déconnecté
        _hubConnection.On<InfoUtilisateur>("UtilisateurDeconnecte", async user =>
        {
            _utilisateursEnLigne.RemoveAll(u => u.Id == user.Id);

            _messages.Add(new MessageChat(
                Guid.NewGuid().ToString(), _salonActuel,
                new InfoUtilisateur("system", "Système", DateTime.UtcNow),
                $"[WAVING_HAND_SIGN] {user.Nom} a quitté le salon",
                DateTime.UtcNow, TypeMessage.Systeme));

            await InvokeAsync(StateHasChanged);
        });

        // Initialisation : Liste des utilisateurs et historique
        _hubConnection.On<object>("InitialisationChat", async data =>
        {
            // Charger l'historique et les utilisateurs en ligne
            await InvokeAsync(StateHasChanged);
        });

        // Indicateur "est en train de taper"
        _hubConnection.On<object>("IndicateurEcriture", async data =>
        {
            // Gérer l'affichage de l'indicateur d'écriture
            await InvokeAsync(StateHasChanged);
        });

        // Événements de reconnexion
        _hubConnection.Reconnecting += error =>
        {
            _estConnecte = false;
            InvokeAsync(StateHasChanged);
            return Task.CompletedTask;
        };

        _hubConnection.Reconnected += connectionId =>
        {
            _estConnecte = true;
            InvokeAsync(StateHasChanged);
            return Task.CompletedTask;
        };

        // Démarrer la connexion
        await _hubConnection.StartAsync();
        _estConnecte = true;

        // Rejoindre le salon par défaut
        var nomUtilisateur = AppState.NomUtilisateur ?? "Anonyme";
        await _hubConnection.InvokeAsync("Rejoindre", nomUtilisateur, _salonActuel);
    }

    private async Task EnvoyerMessage()
    {
        if (string.IsNullOrWhiteSpace(_texteMessage) || _hubConnection is null)
            return;

        var texte = _texteMessage;
        _texteMessage = string.Empty; // Vider l'input immédiatement

        await _hubConnection.InvokeAsync("EnvoyerMessage", _salonActuel, texte);
    }

    private async Task GererTouche(KeyboardEventArgs e)
    {
        if (e.Key == "Enter" && !e.ShiftKey)
            await EnvoyerMessage();
    }

    private async Task SignalerEcriture()
    {
        if (_hubConnection is null) return;

        // Notifier "est en train de taper"
        await _hubConnection.InvokeAsync("SignalerEcritureProlongee",
            _salonActuel, true);

        // Arrêter l'indicateur après 2 secondes d'inactivité
        _timerEcriture?.Dispose();
        _timerEcriture = new Timer(async _ =>
        {
            if (_hubConnection?.State == HubConnectionState.Connected)
            {
                await _hubConnection.InvokeAsync("SignalerEcritureProlongee",
                    _salonActuel, false);
            }
        }, null, TimeSpan.FromSeconds(2), Timeout.InfiniteTimeSpan);
    }

    private async Task ScrollerVersBas()
    {
        // Scroller la div des messages vers le bas
        // await JSRuntime.InvokeVoidAsync("scrollerVersBas", _messagesDiv);
    }

    public async ValueTask DisposeAsync()
    {
        _timerEcriture?.Dispose();
        if (_hubConnection is not null)
        {
            await _hubConnection.DisposeAsync();
        }
    }
}
*/

// HubConnection (simulée pour la compilation)
public class HubConnectionBuilder
{
    public HubConnectionBuilder WithUrl(Uri url) => this;
    public HubConnectionBuilder WithAutomaticReconnect(TimeSpan[] intervals) => this;
    public HubConnection Build() => new();
}

public class HubConnection : IAsyncDisposable
{
    public HubConnectionState State => HubConnectionState.Connected;

    public event Func<Exception?, Task>? Reconnecting;
    public event Func<string?, Task>? Reconnected;

    public IDisposable On<T>(string methodName, Func<T, Task> handler) => default!;

    public async Task StartAsync() => await Task.CompletedTask;
    public async Task InvokeAsync(string methodName, object? arg1 = null, object? arg2 = null)
        => await Task.CompletedTask;

    public async ValueTask DisposeAsync() => await ValueTask.CompletedTask;
}

public enum HubConnectionState { Disconnected, Connecting, Connected, Reconnecting }


// ----------------------------------------------------------------------------
// [NOTIF] NOTIFICATIONS PUSH SERVEUR -> CLIENTS SPÉCIFIQUES
// ----------------------------------------------------------------------------

// Services/NotificationService.cs (côté serveur)
public class NotificationHubService
{
    private readonly IHubContext<NotificationHub> _hubContext;
    private readonly ILogger<NotificationHubService> _logger;

    public NotificationHubService(
        IHubContext<NotificationHub> hubContext,
        ILogger<NotificationHubService> logger)
    {
        _hubContext = hubContext;
        _logger = logger;
    }

    // Notifier UN utilisateur spécifique
    public async Task NotifierUtilisateurAsync(
        string userId,
        string titre,
        string message,
        string? url = null)
    {
        await _hubContext.Clients
            .Group($"user_{userId}")  // Groupe par userId
            .SendAsync("NouvelleNotification", new
            {
                Id = Guid.NewGuid().ToString(),
                Titre = titre,
                Message = message,
                Url = url,
                Horodatage = DateTime.UtcNow,
                EstLue = false
            });

        _logger.LogInformation(
            "Notification envoyée à l'utilisateur {UserId}: {Titre}",
            userId, titre);
    }

    // Notifier TOUS les utilisateurs d'un tenant
    public async Task NotifierTenantAsync(
        string tenantId,
        string titre,
        string message)
    {
        await _hubContext.Clients
            .Group($"tenant_{tenantId}")
            .SendAsync("NouvelleNotification", new { Titre = titre, Message = message });
    }

    // Notifier TOUS les utilisateurs connectés (maintenance, etc.)
    public async Task NotifierTousAsync(string message)
    {
        await _hubContext.Clients.All
            .SendAsync("MessageSysteme", new
            {
                Message = message,
                Type = "warning",
                Horodatage = DateTime.UtcNow
            });
    }
}

// Hub pour les notifications
public class NotificationHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        // Ajouter l'utilisateur dans son groupe personnel
        var userId = Context.GetHttpContext()?.User.FindFirst("sub")?.Value;
        var tenantId = Context.GetHttpContext()?.Request.Headers["X-Tenant-Id"].FirstOrDefault();

        if (userId is not null)
            await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}");

        if (tenantId is not null)
            await Groups.AddToGroupAsync(Context.ConnectionId, $"tenant_{tenantId}");

        await base.OnConnectedAsync();
    }
}

// IHubContext (simulé)
public interface IHubContext<THub> where THub : Hub
{
    IHubClients Clients { get; }
}
public interface IHubClients
{
    IClientProxy All { get; }
    IClientProxy Group(string groupName);
    IClientProxy Client(string connectionId);
}


// ============================================================================
// [GUIDE] CHAPITRE 32 : MICRO-FRONTENDS AVEC BLAZOR
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre l'architecture micro-frontends
[OK] Implémenter des micro-frontends avec Blazor
[OK] Partager le contexte entre les micro-apps
[OK] Gérer le routing dans une architecture micro-frontend
*/


// ----------------------------------------------------------------------------
// [MODULE] MICRO-FRONTENDS — Architecture pour grandes équipes
// ----------------------------------------------------------------------------

/*
QU'EST-CE QUE LES MICRO-FRONTENDS ?

PROBLÈME DES GRANDES APPLICATIONS :
-> Une application = Une équipe = Un repo = Un déploiement
-> Après 2-3 ans, l'app devient MONOLITHIQUE
-> Merge conflicts, dépendances bloquantes, déploiement tous ensemble
-> Si l'équipe passe de 5 à 50 développeurs -> Chaos !

SOLUTION : Micro-frontends
-> L'application est divisée en MODULES INDÉPENDANTS
-> Chaque module = Une équipe = Un repo = Un déploiement indépendant
-> L'app principale "orchestre" les modules

ANALOGIE :
App comme un OS (Windows)
-> Windows = Coquille principale
-> Office = App indépendante intégrée
-> Explorer = App indépendante intégrée
-> Chaque app se déploie indépendamment !

EXEMPLE CONCRET :
MonApp (orchestrateur)
├── Module Catalogue    -> Équipe E-commerce (déploie seule)
├── Module Commandes    -> Équipe Logistique (déploie seule)
├── Module Paiement     -> Équipe Finance (déploie seule)
└── Module Analytique   -> Équipe Data (déploie seule)

APPROCHES BLAZOR POUR LES MICRO-FRONTENDS :

1. RAZOR CLASS LIBRARIES (RCL) — Approche recommandée pour Blazor
   -> Modules compilés en DLL
   -> Chargés dynamiquement avec Lazy Loading
   -> Partage de composants via NuGet privé

2. IFRAMES — Simple mais limité
   -> Isolation totale (sécurité)
   -> Communication via postMessage
   -> Mauvaise UX (scroll, cookies séparés)

3. WEB COMPONENTS — Standard navigateur
   -> Interopération avec d'autres frameworks
   -> Complex à implémenter en Blazor

PATTERN RECOMMANDÉ POUR BLAZOR : RCL + Lazy Loading
*/

/*
─────────────────────────────────────────────────────────────────
Structure d'une architecture micro-frontend Blazor
─────────────────────────────────────────────────────────────────

MonEntreprise/
├── ShellApp/                 <- Application principale (orchestrateur)
│   ├── App.razor             <- Routing + Lazy Loading
│   ├── Shared/
│   │   └── Navigation.razor  <- Ménu global
│   └── ShellApp.csproj       <- Référence les modules
│
├── Module.Catalogue/         <- Module RCL équipe E-commerce
│   ├── Pages/
│   │   ├── Catalogue.razor     (@page "/catalogue")
│   │   └── DetailProduit.razor (@page "/produits/{id}")
│   ├── Components/
│   └── Module.Catalogue.csproj
│
├── Module.Commandes/         <- Module RCL équipe Logistique
│   ├── Pages/
│   │   ├── MesCommandes.razor  (@page "/commandes")
│   │   └── Commande.razor      (@page "/commandes/{id}")
│   └── Module.Commandes.csproj
│
├── Module.Paiement/          <- Module RCL équipe Finance
│   └── ...
│
└── MonEntreprise.Shared/     <- Types partagés entre tous les modules
    ├── Models/
    ├── Interfaces/
    └── Events/               <- Communication inter-modules
*/

// MonEntreprise.Shared — Communication inter-modules
public class IntegrationEventBus
{
    private readonly Dictionary<Type, List<object>> _handlers = new();

    // Publier un événement (un module notifie les autres)
    public void Publier<T>(T evenement) where T : class
    {
        var type = typeof(T);
        if (!_handlers.ContainsKey(type)) return;

        foreach (var handler in _handlers[type].OfType<Action<T>>())
        {
            handler(evenement);
        }
    }

    // S'abonner à un événement (un module écoute les autres)
    public void Souscrire<T>(Action<T> handler) where T : class
    {
        var type = typeof(T);
        if (!_handlers.ContainsKey(type))
            _handlers[type] = new List<object>();

        _handlers[type].Add(handler);
    }

    // Se désabonner
    public void Desouscrire<T>(Action<T> handler) where T : class
    {
        var type = typeof(T);
        if (_handlers.ContainsKey(type))
            _handlers[type].Remove(handler);
    }
}

// Événements d'intégration inter-modules
public record ProduitAjouteAuPanierEvent(int ProduitId, int Quantite, decimal Prix);
public record CommandePasseeEvent(int CommandeId, decimal Total, int NombreArticles);
public record PaiementEffectueEvent(int CommandeId, decimal Montant, string MethodePaiement);

/*
─────────────────────────────────────────────────────────────────
App.razor avec chargement dynamique des modules
─────────────────────────────────────────────────────────────────

@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="@typeof(App).Assembly"
        AdditionalAssemblies="@_modulesCharges"
        OnNavigateAsync="@ChargerModulePourRoute">

    <Found Context="routeData">
        @if (_chargementModule)
        {
            <div class="module-loading">
                <div class="spinner-grow text-primary"></div>
                <p>Chargement du module...</p>
            </div>
        }
        else
        {
            <RouteView RouteData="@routeData"
                       DefaultLayout="@typeof(ShellLayout)" />
        }
    </Found>
    <NotFound>
        <Page404 />
    </NotFound>
</Router>

@code {
    private List<Assembly> _modulesCharges = new();
    private bool _chargementModule = false;

    // Table de routage : URL préfixe -> Module à charger
    private static readonly Dictionary<string, string[]> _routesModules = new()
    {
        ["/catalogue"]  = new[] { "Module.Catalogue.dll" },
        ["/produits"]   = new[] { "Module.Catalogue.dll" },
        ["/commandes"]  = new[] { "Module.Commandes.dll" },
        ["/paiement"]   = new[] { "Module.Paiement.dll" },
        ["/analytique"] = new[] { "Module.Analytique.dll", "ChartJs.Blazor.dll" },
    };

    private async Task ChargerModulePourRoute(NavigationContext ctx)
    {
        var prefixeRoute = "/" + ctx.Path.Split('/').Skip(1).FirstOrDefault();


Continuer les autres parties en utilisant le même niveau de détail des parties précédentes avec Pourquoi? Quand? Comment? ultra-détaillé les explications pour un grand débutant