# ============================================================================
# [LIVRE] NODE.JS - GUIDE ULTRA-DÉTAILLÉ POUR DÉBUTANTS EN GÉNIE LOGICIEL
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER NODE.JS DE ZÉRO À EXPERT
#
# Ce guide est organisé en 4 parties progressives :
#
# PARTIE 1 : FONDAMENTAUX (nodejs_partie1.txt)
# - Chapitre 0 : Introduction à Node.js
# - Chapitre 1 : Installation et Premier Programme
# - Chapitre 2 : Modules et require/import
# - Chapitre 3 : Le Système de Fichiers (fs)
# - Chapitre 4 : Événements et EventEmitter
#
# PARTIE 2 : SERVEURS ET APIs (nodejs_partie2.txt)
# - Chapitre 5 : Serveur HTTP natif
# - Chapitre 6 : Express.js - Fondamentaux
# - Chapitre 7 : Express.js - Middleware
# - Chapitre 8 : APIs REST avec Express
# - Chapitre 9 : Gestion des erreurs
#
# PARTIE 3 : DONNÉES ET AVANCÉ (nodejs_partie3.txt)
# - Chapitre 10 : Bases de données (MongoDB, PostgreSQL)
# - Chapitre 11 : Authentification (JWT, sessions)
# - Chapitre 12 : Streams et Buffers
# - Chapitre 13 : WebSockets (Socket.io)
# - Chapitre 14 : Tests (Jest, Supertest)
#
# PARTIE 4 : PRODUCTION (nodejs_partie4.txt)
# - Chapitre 15 : Variables d'environnement et config
# - Chapitre 16 : Sécurité
# - Chapitre 17 : Performance et Clustering
# - Chapitre 18 : Déploiement (Docker, Heroku, AWS)
# - Chapitre 19 : Logging et Monitoring
# - Chapitre 20 : Best Practices
#
# [TEMPS] TEMPS DE LECTURE TOTAL : ~25-30 heures
# [DOCS] PRÉREQUIS : JavaScript de base (variables, fonctions, tableaux)
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 0 : INTRODUCTION COMPLÈTE À NODE.JS
# ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est Node.js et comment il fonctionne
[OK] Pourquoi Node.js est différent des autres plateformes
[OK] L'Event Loop expliqué simplement
[OK] Quand utiliser Node.js
[OK] L'écosystème npm
*/


// ----------------------------------------------------------------------------
// [REFLEXION] QU'EST-CE QUE NODE.JS ?
// ----------------------------------------------------------------------------

/*
DÉFINITION SIMPLE

Node.js est un ENVIRONNEMENT D'EXÉCUTION JavaScript côté serveur.
Il permet d'exécuter du JavaScript EN DEHORS du navigateur.

[IDEE] ANALOGIE SIMPLE [CONSTRUCTION]

Navigateur Web    ->  Contexte JavaScript CLIENT
Node.js           ->  Contexte JavaScript SERVEUR

Avant Node.js :
  Front-end  ->  JavaScript (navigateur)
  Back-end   ->  PHP, Python, Ruby, Java, C#...

Après Node.js :
  Front-end  ->  JavaScript (navigateur)
  Back-end   ->  JavaScript (Node.js) [BRAVO]


COMPOSANTS CLÉS DE NODE.JS

1. V8 ENGINE (de Google Chrome)
   -> Compile et exécute JavaScript très rapidement

2. LIBUV
   -> Bibliothèque C++ qui gère :
   - L'Event Loop
   - Les opérations asynchrones
   - Les I/O (fichiers, réseau)
   - Les threads en arrière-plan

3. API NODE.JS
   -> Modules natifs : fs, http, path, crypto...
   -> Ce que vous utilisez directement


CARACTÉRISTIQUES FONDAMENTALES

[OK] SINGLE-THREADED
   Un seul thread JavaScript principal
   (Mais plusieurs threads I/O en arrière-plan via libuv)

[OK] NON-BLOQUANT (Non-blocking)
   Les opérations I/O ne bloquent pas l'exécution
   -> Pendant qu'un fichier se lit, d'autres requêtes sont traitées

[OK] ASYNCHRONE
   Résultat retourné plus tard via callbacks, Promises, async/await

[OK] EVENT-DRIVEN
   Tout repose sur des événements
*/


// ----------------------------------------------------------------------------
// [SYNC] L'EVENT LOOP EXPLIQUÉ
// ----------------------------------------------------------------------------

/*
[IDEE] L'EVENT LOOP = Le cœur de Node.js

POURQUOI ?
Sans Event Loop : chaque requête bloque le serveur
Avec Event Loop : des milliers de requêtes en parallèle


VISUALISATION SIMPLIFIÉE

                    ┌─────────────────────┐
                    │     Call Stack       │  <- Code JavaScript
                    │  (Pile d'appels)    │
                    └─────────┬───────────┘
                              │ Si vide
                              v
                    ┌─────────────────────┐
                    │    Event Queue      │  <- Callbacks en attente
                    │  (File d'attente)  │
                    └─────────────────────┘
                              ^
                    ┌─────────────────────┐
                    │   Background I/O    │  <- libuv threads
                    │ (Fichiers, réseau) │
                    └─────────────────────┘


EXEMPLE CONCRET :
*/

// 1. Code exécuté IMMÉDIATEMENT (synchrone)
console.log("1 - Début");

// 2. I/O enregistré, exécution CONTINUE
setTimeout(() => {
  console.log("3 - Après 1 seconde"); // Exécuté plus tard
}, 1000);

// 3. Code exécuté IMMÉDIATEMENT après
console.log("2 - Fin du code synchrone");

/*
OUTPUT :
1 - Début
2 - Fin du code synchrone
3 - Après 1 seconde

POURQUOI ?
-> setTimeout est non-bloquant
-> Node.js continue l'exécution pendant que le timer tourne
-> Après 1 seconde, le callback est mis dans la Event Queue
-> Quand le Call Stack est vide, il est exécuté


PHASES DE L'EVENT LOOP (ordre)

1. timers        -> setTimeout, setInterval
2. I/O callbacks -> callbacks d'I/O
3. idle/prepare  -> interne
4. poll          -> nouvelles I/O
5. check         -> setImmediate
6. close         -> socket.close, etc.


PROCESS.NEXTTICK vs SETIMMEDIATE vs SETTIMEOUT
*/

// process.nextTick -> Exécuté AVANT la prochaine itération de l'Event Loop
process.nextTick(() => console.log("nextTick"));

// setImmediate -> Exécuté à la fin de l'itération courante
setImmediate(() => console.log("setImmediate"));

// setTimeout(0) -> Exécuté dans la prochaine itération
setTimeout(() => console.log("setTimeout"), 0);

console.log("synchrone");

/*
OUTPUT :
synchrone
nextTick
setTimeout    (ou setImmediate selon les cas)
setImmediate
*/


// ----------------------------------------------------------------------------
// [SCALES] NODE.JS VS AUTRES PLATEFORMES BACKEND
// ----------------------------------------------------------------------------

/*
TABLEAU COMPARATIF

┌─────────────────┬──────────────┬──────────────┬──────────────┐
│                 │   NODE.JS    │    PYTHON    │     JAVA     │
├─────────────────┼──────────────┼──────────────┼──────────────┤
│ Langue          │ JavaScript   │  Python      │  Java        │
│ Concurrence     │ Event Loop   │ Async/Threads│  Threads     │
│ Performance I/O │ *****    │   ***       │   ***       │
│ CPU intense     │ **          │   ***       │   *****   │
│ Full-stack JS   │ [OK]           │   [X]         │   [X]         │
│ NPM Écosystème  │ 2M+ packages │  400k+ pip  │   Maven      │
│ Courbe appr.    │ Facile       │  Très facile │  Difficile   │
│ Microservices   │ *****    │   ***       │   ***       │
└─────────────────┴──────────────┴──────────────┴──────────────┘


NODE.JS - AVANTAGES [OK]

1. FULL-STACK JAVASCRIPT
   Même langage front et back -> équipe unifiée

2. PERFORMANCE I/O
   Idéal pour : chat, API REST, streaming, temps réel

3. NPM ÉCOSYSTÈME
   2 000 000+ packages disponibles

4. GRANDE COMMUNAUTÉ
   Stack Overflow, GitHub, nombreux tutoriels

5. JSON NATIF
   JSON = format natif JavaScript -> APIs faciles


NODE.JS - INCONVÉNIENTS [X]

1. PAS IDÉAL POUR CPU INTENSIF
   Calculs complexes -> Utiliser Worker Threads

2. CALLBACK HELL (historique)
   Résolu aujourd'hui avec async/await

3. SINGLE-THREADED
   Une erreur non gérée peut tuer tout le serveur

4. MATURITÉ
   Certains outils moins matures que Java/Python


QUAND UTILISER NODE.JS ? [OK]

[OK] APIs REST (CRUD)
[OK] Applications temps réel (chat, notifications)
[OK] Streaming (vidéo, données)
[OK] Microservices
[OK] Applications I/O intensives
[OK] Prototypes et MVPs rapides
[OK] Outils CLI
[OK] Full-stack avec React/Vue

QUAND NE PAS UTILISER ? [X]

[X] Calculs scientifiques / Machine Learning -> Python
[X] Applications enterprise complexes -> Java, C#
[X] Traitement d'images lourdes -> Python (PIL)
[X] Jeux avec logique serveur complexe -> C++


L'ÉCOSYSTÈME NPM

npm (Node Package Manager) = Gestionnaire de paquets
registry.npmjs.com = Registre de 2M+ packages

Alternatives :
- yarn -> Plus rapide, plus fiable
- pnpm -> Économise de l'espace disque
*/


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 0 : Comprendre Node.js
// ----------------------------------------------------------------------------

/*
QUESTIONS DE COMPRÉHENSION

1. Quelle est la différence entre JavaScript navigateur et Node.js ?
2. Pourquoi Node.js est-il non-bloquant ?
3. Donnez 3 cas d'usage pour Node.js
4. Qu'est-ce que l'Event Loop ?
5. Quel est le rôle de libuv ?

RÉPONSES

1. JavaScript navigateur : exécuté dans le navigateur, accès au DOM, window
   Node.js : exécuté côté serveur, accès aux fichiers, réseau, pas de DOM

2. Node.js utilise l'Event Loop + libuv pour traiter les I/O
   sans bloquer le thread principal

3. APIs REST, chat en temps réel, streaming vidéo

4. Mécanisme qui vérifie la Event Queue quand le Call Stack est vide,
   permettant l'exécution asynchrone

5. libuv gère les opérations I/O asynchrones en arrière-plan
   (multi-threaded), puis notifie l'Event Loop
*/


// ============================================================================
// [GUIDE] CHAPITRE 1 : INSTALLATION ET PREMIER PROGRAMME
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Installer Node.js correctement
[OK] Utiliser le REPL Node.js
[OK] Créer et exécuter votre premier programme
[OK] Comprendre package.json
[OK] Gérer les packages npm
*/


// ----------------------------------------------------------------------------
// [OUTILS] INSTALLATION
// ----------------------------------------------------------------------------

/*
MÉTHODE RECOMMANDÉE : NVM (Node Version Manager)

POURQUOI NVM ?
[OK] Gérer plusieurs versions de Node.js
[OK] Changer de version facilement
[OK] Isoler par projet
[OK] Standard en entreprise

INSTALLATION NVM

Linux/Mac :
  curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
  source ~/.bashrc  (ou ~/.zshrc)

Windows :
  nvm-windows : https://github.com/coreybutler/nvm-windows
  Télécharger nvm-setup.exe

COMMANDES NVM :
*/

// Terminal
nvm install node        // Dernière version
nvm install 20.10.0    // Version spécifique (LTS recommandée)
nvm install --lts      // Dernière LTS (Long Term Support)
nvm use 20.10.0        // Utiliser cette version
nvm list               // Lister versions installées
nvm current            // Version active
nvm alias default 20   // Version par défaut

/*
VÉRIFIER L'INSTALLATION
*/
node --version    // v20.10.0
npm --version     // 10.2.3

/*
VERSIONS NODE.JS

LTS (Long Term Support) -> Recommandé pour production
Current -> Dernière fonctionnalités (peut être instable)

Exemple :
v20.x.x -> LTS (pair = LTS)
v21.x.x -> Current (impair = Current)

TOUJOURS utiliser LTS en production !


INSTALLATION ALTERNATIVE (sans NVM)

Windows :
  nodejs.org -> Télécharger .msi
  Exécuter l'installateur

Mac :
  brew install node  (avec Homebrew)

Linux (Ubuntu/Debian) :
  sudo apt install nodejs npm

[ATTENTION] Sans NVM, difficile de gérer les versions
*/


// ----------------------------------------------------------------------------
// [ECRAN] LE REPL NODE.JS
// ----------------------------------------------------------------------------

/*
REPL = Read-Eval-Print Loop

C'est le terminal interactif de Node.js
Parfait pour tester du code rapidement
*/

// Lancer le REPL
// Terminal : node

/*
Dans le REPL :
> 2 + 2
4
> const name = "Node.js"
undefined
> `Bonjour ${name} !`
'Bonjour Node.js !'
> .help         -> Aide
> .exit         -> Quitter
> .clear        -> Vider
> .load file.js -> Charger un fichier

[IDEE] CTRL+C (2x) ou .exit pour quitter
*/


// ----------------------------------------------------------------------------
// [NOTE] PREMIER PROGRAMME : HELLO WORLD
// ----------------------------------------------------------------------------

// hello.js
console.log("Hello, Node.js ! [BRAVO]");
console.log("Je tourne côté serveur !");
console.log(`Version Node.js : ${process.version}`);
console.log(`OS : ${process.platform}`);

/*
EXÉCUTER :
  node hello.js

OUTPUT :
Hello, Node.js ! [BRAVO]
Je tourne côté serveur !
Version Node.js : v20.10.0
OS : linux (ou darwin, win32)


OBJET GLOBAL : process

process = Objet global Node.js (équivalent de window pour le navigateur)
*/

// Informations système
console.log(process.version);       // Version Node
console.log(process.versions);      // Versions de dépendances
console.log(process.platform);      // OS (linux, darwin, win32)
console.log(process.env);           // Variables d'environnement
console.log(process.argv);          // Arguments ligne de commande
console.log(process.cwd());         // Répertoire courant
console.log(process.pid);           // ID du processus

// Quitter le processus
process.exit(0);    // 0 = succès
process.exit(1);    // 1 = erreur

/*
ARGUMENTS LIGNE DE COMMANDE
*/

// args.js
const args = process.argv;
console.log("Tous les args :", args);
// process.argv[0] = chemin node
// process.argv[1] = chemin du script
// process.argv[2+] = arguments utilisateur

const name = process.argv[2] || "World";
console.log(`Hello, ${name}!`);

/*
node args.js Alice
-> Hello, Alice!

node args.js
-> Hello, World!
*/


// ----------------------------------------------------------------------------
// [PACKAGE] PACKAGE.JSON : LE CŒUR DE VOTRE PROJET
// ----------------------------------------------------------------------------

/*
package.json = Fichier de configuration du projet
Contient : métadonnées, dépendances, scripts

CRÉER package.json
*/

// Méthode 1 : Interactive
npm init

// Méthode 2 : Valeurs par défaut (rapide)
npm init -y

/*
CONTENU DE package.json
*/

// package.json
{
  "name": "mon-projet-nodejs",
  "version": "1.0.0",
  "description": "Mon premier projet Node.js",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest",
    "build": "node scripts/build.js"
  },
  "keywords": ["nodejs", "javascript", "backend"],
  "author": "Votre Nom <email@example.com>",
  "license": "MIT",
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^8.0.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.2",
    "jest": "^29.7.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

/*
[IDEE] EXPLICATION DES CHAMPS

name          -> Nom du projet (lowercase, no spaces)
version       -> Version sémantique (MAJOR.MINOR.PATCH)
main          -> Point d'entrée de l'application
scripts       -> Commandes personnalisées (npm run <name>)
dependencies  -> Packages nécessaires en production
devDependencies -> Packages de développement seulement
engines       -> Versions Node.js compatibles


VERSIONING SÉMANTIQUE (SemVer)

"express": "^4.18.2"
            │ │  │
            │ │  └── PATCH (corrections de bugs)
            │ └───── MINOR (nouvelles fonctionnalités rétrocompatibles)
            └─────── MAJOR (changements incompatibles)

^ (caret)  -> Compatible avec MINOR et PATCH
~ (tilde)  -> Compatible seulement avec PATCH
=          -> Version exacte
>          -> Supérieur à
>=         -> Supérieur ou égal
*          -> N'importe quelle version

Exemples :
"^4.18.2" -> 4.18.x à 4.x.x (pas 5.0.0)
"~4.18.2" -> 4.18.x (pas 4.19.0)
"4.18.2"  -> Exactement 4.18.2


SCRIPTS NPM
*/

// Lancer scripts
npm start           // npm run start (raccourci pour "start")
npm test            // npm run test (raccourci pour "test")
npm run dev         // Lancer "dev" script
npm run build       // Lancer "build" script

// Scripts courants
{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest --coverage",
    "lint": "eslint .",
    "format": "prettier --write .",
    "build": "tsc",
    "clean": "rm -rf dist"
  }
}


// ----------------------------------------------------------------------------
// [ENTREE] GESTION DES PACKAGES NPM
// ----------------------------------------------------------------------------

/*
INSTALLER DES PACKAGES
*/

npm install express              // Production dependency
npm install --save-dev nodemon   // Dev dependency (-D)
npm install -g nodemon           // Global (accessible partout)

/*
STRUCTURE APRÈS INSTALLATION :
node_modules/   <- Packages installés (NE PAS committer!)
package.json    <- Mis à jour avec la dépendance
package-lock.json <- Versions exactes (COMMITTER!)


COMMANDES NPM ESSENTIELLES
*/

npm install                  // Installer toutes les dépendances
npm install express@4.18.2   // Version spécifique
npm uninstall express        // Désinstaller
npm update                   // Mettre à jour
npm list                     // Lister packages installés
npm list --depth=0           // Seulement le niveau 0
npm outdated                 // Packages obsolètes
npm audit                    // Vérifier vulnérabilités
npm audit fix                // Corriger vulnérabilités
npm run <script>             // Lancer un script


/*
.GITIGNORE OBLIGATOIRE
*/

// .gitignore
node_modules/
.env
*.log
dist/
build/
.DS_Store
coverage/


/*
NODEMON : AUTO-RESTART

nodemon = Redémarre automatiquement Node.js quand fichier modifié
Comme flask debug=True (rechargement automatique)
*/

npm install --save-dev nodemon

// package.json
{
  "scripts": {
    "dev": "nodemon index.js"
  }
}

npm run dev   // Lance avec nodemon

/*
Quand vous modifiez index.js -> Nodemon redémarre automatiquement ! [SYNC]
*/


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 1 : Premier Projet Node.js
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer un projet Node.js complet avec npm

ÉTAPES :

1. Créer un dossier et initialiser le projet
2. Installer nodemon en développement
3. Créer index.js qui :
   - Affiche le nom du projet depuis package.json
   - Accepte un argument "--name" depuis la ligne de commande
   - Affiche "Bonjour <name>, bienvenue dans <projet>!"
4. Créer un script "dev" dans package.json

CORRIGÉ :
*/

// Terminal
mkdir mon-premier-projet
cd mon-premier-projet
npm init -y
npm install --save-dev nodemon

// index.js - CORRIGÉ
const packageJson = require('./package.json');

// Récupérer l'argument --name depuis la ligne de commande
const args = process.argv.slice(2);  // Ignorer node et script path
let name = "Monde";

for (let i = 0; i < args.length; i++) {
  if (args[i] === "--name" && args[i + 1]) {
    name = args[i + 1];
    break;
  }
}

const projectName = packageJson.name;
const projectVersion = packageJson.version;
const nodeVersion = process.version;

console.log("=".repeat(50));
console.log(`[BRAVO] Bonjour ${name}, bienvenue dans ${projectName}!`);
console.log(`[PACKAGE] Version du projet : ${projectVersion}`);
console.log(`[CONFIG]  Node.js version : ${nodeVersion}`);
console.log(`[CODE] Plateforme : ${process.platform}`);
console.log(`[DOSSIER] Répertoire : ${process.cwd()}`);
console.log("=".repeat(50));

// package.json scripts - CORRIGÉ
{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "start:name": "node index.js --name Alice"
  }
}

/*
TESTS :
  node index.js              -> Bonjour Monde, bienvenue dans mon-premier-projet!
  node index.js --name Alice -> Bonjour Alice, bienvenue dans mon-premier-projet!
  npm run dev                -> Lance avec nodemon (auto-restart)
*/


// ============================================================================
// [GUIDE] CHAPITRE 2 : MODULES ET REQUIRE/IMPORT
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le système de modules Node.js
[OK] Utiliser require() (CommonJS)
[OK] Utiliser import/export (ES Modules)
[OK] Créer et exporter vos propres modules
[OK] Utiliser les modules natifs (built-in)
[OK] Différence entre CommonJS et ESM
*/


// ----------------------------------------------------------------------------
// [REFLEXION] POURQUOI DES MODULES ?
// ----------------------------------------------------------------------------

/*
SANS MODULES : Code dans un seul fichier

// app.js - 5000 lignes [!]
function calculateTax() { ... }
function sendEmail() { ... }
function connectDB() { ... }
// ... 4900 autres fonctions

[X] PROBLÈMES :
- Impossible à maintenir
- Noms de variables qui entrent en conflit
- Pas de réutilisabilité

AVEC MODULES : Code séparé et organisé

utils/
  tax.js       -> Calcul de taxes
  email.js     -> Envoi d'emails
  database.js  -> Connexion DB
app.js         -> Point d'entrée

[OK] AVANTAGES :
- Code organisé
- Réutilisable
- Testable
- Équipe collaborative
*/


// ----------------------------------------------------------------------------
// [PACKAGE] COMMONJS (require/module.exports) - Le Système Historique
// ----------------------------------------------------------------------------

/*
CommonJS = Système de modules par défaut de Node.js
Utilise require() et module.exports
*/

// math.js - EXPORTER
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

const PI = 3.14159;

// EXPORTER : module.exports
module.exports = {
  add,
  multiply,
  PI
};

// Ou en une seule fois
module.exports.subtract = function(a, b) {
  return a - b;
};

// app.js - IMPORTER
const math = require('./math');  // .js est optionnel
console.log(math.add(2, 3));       // 5
console.log(math.multiply(4, 5));  // 20
console.log(math.PI);              // 3.14159

// Déstructuration (plus propre)
const { add, multiply, PI } = require('./math');
console.log(add(2, 3));    // 5
console.log(multiply(4, 5)); // 20

/*
[IDEE] DIFFÉRENTES FAÇONS D'EXPORTER


Méthode 1 : Exporter un objet
*/

// logger.js
const logger = {
  info: (msg) => console.log(`[INFO] ${msg}`),
  warn: (msg) => console.log(`[WARN] ${msg}`),
  error: (msg) => console.error(`[ERROR] ${msg}`)
};

module.exports = logger;

// Utilisation
const logger = require('./logger');
logger.info("Application démarrée");
logger.error("Une erreur est survenue");

/*
Méthode 2 : Exporter une classe
*/

// user.js
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
    this.createdAt = new Date();
  }

  greet() {
    return `Bonjour, je suis ${this.name}`;
  }

  toJSON() {
    return {
      name: this.name,
      email: this.email,
      createdAt: this.createdAt
    };
  }
}

module.exports = User;

// Utilisation
const User = require('./user');
const alice = new User("Alice", "alice@example.com");
console.log(alice.greet());  // Bonjour, je suis Alice

/*
Méthode 3 : Exporter une fonction
*/

// utils.js
function formatDate(date) {
  return date.toISOString().split('T')[0];
}

module.exports = formatDate;

// Utilisation
const formatDate = require('./utils');
console.log(formatDate(new Date()));  // 2024-12-18


/*
Méthode 4 : exports (alias de module.exports)
*/

// calculatrice.js
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
exports.multiply = (a, b) => a * b;
exports.divide = (a, b) => {
  if (b === 0) throw new Error("Division par zéro !");
  return a / b;
};

/*
[ATTENTION] ATTENTION : exports vs module.exports

exports       = référence vers module.exports
module.exports = l'objet réellement exporté

// [OK] OK : Ajouter des propriétés
exports.add = function() {...};

// [X] PROBLÈME : Remplacer exports détache le lien
exports = { add: function() {...} };  // Ne fonctionne PAS !

// [OK] OK : Remplacer module.exports
module.exports = { add: function() {...} };
*/


// ----------------------------------------------------------------------------
// [OUTIL] RÉSOLUTION DES MODULES (Comment require() trouve les fichiers)
// ----------------------------------------------------------------------------

/*
ORDRE DE RÉSOLUTION :

require('./utils')    -> Fichier local
  1. ./utils.js
  2. ./utils.json
  3. ./utils/index.js
  4. ./utils/package.json -> champ "main"

require('express')    -> Module npm
  1. node_modules/express/
  2. ../node_modules/express/
  3. ../../node_modules/express/
  ...remonte jusqu'à la racine

require('fs')         -> Module natif Node.js
  -> Retourné immédiatement (pas de recherche)

EXEMPLE DE STRUCTURE
*/

projet/
├── index.js
├── utils/
│   ├── index.js     // require('./utils') charge ceci
│   ├── math.js
│   └── string.js
└── config/
    ├── database.js
    └── server.js


// ----------------------------------------------------------------------------
// [PACKAGE] ES MODULES (import/export) - La Méthode Moderne
// ----------------------------------------------------------------------------

/*
ES Modules = Standard JavaScript moderne (ES6+)
Utilisé nativement dans les navigateurs ET Node.js (depuis v12+)

POUR ACTIVER ES MODULES dans Node.js :
1. Ajouter "type": "module" dans package.json
   OU
2. Utiliser l'extension .mjs
*/

// package.json
{
  "type": "module"  // Active ES Modules pour tout le projet
}

// math.mjs (ou math.js si "type": "module")
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

export const PI = 3.14159;

// Export par défaut (un seul par fichier)
export default function calculator(operation, a, b) {
  switch(operation) {
    case 'add': return add(a, b);
    case 'multiply': return multiply(a, b);
    default: throw new Error(`Operation inconnue: ${operation}`);
  }
}

// app.mjs - IMPORTER
import calculator, { add, multiply, PI } from './math.mjs';

console.log(add(2, 3));       // 5
console.log(multiply(4, 5));  // 20
console.log(PI);               // 3.14159
console.log(calculator('add', 10, 5));  // 15

// Importer tout
import * as math from './math.mjs';
console.log(math.add(2, 3));

// Renommer à l'import
import { add as addition } from './math.mjs';
console.log(addition(2, 3));  // 5


// ----------------------------------------------------------------------------
// [RAPIDE] COMMONJS VS ES MODULES : COMPARAISON
// ----------------------------------------------------------------------------

/*
┌─────────────────────┬──────────────────────┬──────────────────────┐
│  Aspect             │   CommonJS (CJS)     │   ES Modules (ESM)   │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ Syntaxe             │ require/module.exports│ import/export        │
│ Chargement          │ Synchrone            │ Asynchrone (top)     │
│ Default Node.js     │ [OK] Oui               │ Nécessite config     │
│ Navigateurs         │ [X] Non               │ [OK] Oui               │
│ Dynamic import      │ require() n'importe  │ import()             │
│ __dirname           │ [OK] Disponible        │ [X] Pas disponible    │
│ Tree shaking        │ [X] Difficile         │ [OK] Oui               │
│ Top-level await     │ [X] Non               │ [OK] Oui               │
└─────────────────────┴──────────────────────┴──────────────────────┘


[IDEE] QUELLE MÉTHODE CHOISIR ?

Nouveau projet 2024+ -> ES Modules [OK]
Projet existant -> CommonJS (ne pas migrer sans raison)
Packages npm -> CommonJS (compatibilité maximale)
Full-stack avec Bundler (Webpack, Vite) -> ES Modules


PROBLÈME : __dirname avec ES Modules
*/

// CommonJS : __dirname disponible directement
console.log(__dirname);   // /home/user/mon-projet
console.log(__filename);  // /home/user/mon-projet/index.js

// ES Modules : __dirname N'EXISTE PAS
// Solution avec import.meta.url
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log(__dirname);  // Maintenant disponible !


// ----------------------------------------------------------------------------
// [CLASSICAL_BUILDING] MODULES NATIFS (Built-in modules)
// ----------------------------------------------------------------------------

/*
Node.js inclut des modules natifs sans installation
*/

// Modules les plus utilisés :

// 1. fs - Système de fichiers
const fs = require('fs');

// 2. path - Manipulation de chemins
const path = require('path');

// 3. http - Serveur HTTP
const http = require('http');

// 4. https - Serveur HTTPS
const https = require('https');

// 5. os - Informations système
const os = require('os');
console.log(os.hostname());   // Nom de la machine
console.log(os.cpus());       // CPUs disponibles
console.log(os.totalmem());   // Mémoire totale
console.log(os.freemem());    // Mémoire libre

// 6. crypto - Cryptographie
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update('password').digest('hex');

// 7. url - Manipulation d'URLs
const { URL } = require('url');
const myUrl = new URL('https://example.com/path?q=test');
console.log(myUrl.hostname);   // example.com
console.log(myUrl.pathname);   // /path
console.log(myUrl.searchParams.get('q'));  // test

// 8. events - EventEmitter
const EventEmitter = require('events');

// 9. stream - Streams
const { Readable, Writable, Transform } = require('stream');

// 10. util - Utilitaires
const util = require('util');
const promisify = util.promisify;  // Convertir callback -> Promise


// ----------------------------------------------------------------------------
// [WORLD_MAP] MODULE PATH EN DÉTAIL
// ----------------------------------------------------------------------------

const path = require('path');

// Construire des chemins (cross-platform)
const filePath = path.join('/home', 'user', 'projets', 'app.js');
console.log(filePath);  // /home/user/projets/app.js

// [ATTENTION] TOUJOURS utiliser path.join(), jamais concatener les strings !
// [X] Mauvais
const bad = '/home' + '/' + 'user' + '/' + 'app.js';
// [OK] Bon
const good = path.join('/home', 'user', 'app.js');

// Chemin absolu
const absPath = path.resolve('config', 'database.json');
console.log(absPath);  // /current/working/dir/config/database.json

// Extraire parties d'un chemin
const p = '/home/user/projets/app.js';
console.log(path.dirname(p));   // /home/user/projets
console.log(path.basename(p));  // app.js
console.log(path.extname(p));   // .js
console.log(path.basename(p, '.js'));  // app (sans extension)

// Analyser un chemin
const parsed = path.parse(p);
console.log(parsed);
// { root: '/', dir: '/home/user/projets', base: 'app.js', ext: '.js', name: 'app' }

// Construire depuis objet
const built = path.format(parsed);
console.log(built);  // /home/user/projets/app.js

// __dirname avec path.join (pattern courant)
const configPath = path.join(__dirname, 'config', 'database.json');


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 2 : Système de Modules
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer un mini-framework de logging modulaire

STRUCTURE À CRÉER :
utils/
  logger.js    -> Module de logging
  formatter.js -> Module de formatage
  colors.js    -> Couleurs terminal
index.js       -> Programme principal

SPÉCIFICATIONS :
1. colors.js exporte des codes couleur ANSI
2. formatter.js utilise colors.js pour formater les messages
3. logger.js utilise formatter.js pour logger avec niveaux (info, warn, error)
4. index.js importe logger.js et teste toutes les fonctions

CORRIGÉ COMPLET :
*/

// utils/colors.js
const colors = {
  reset: '\x1b[0m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  cyan: '\x1b[36m',
  bold: '\x1b[1m'
};

module.exports = colors;

// utils/formatter.js
const colors = require('./colors');

function formatMessage(level, message, timestamp = new Date()) {
  const time = timestamp.toISOString().replace('T', ' ').substring(0, 19);
  
  const levelColors = {
    INFO:  colors.green,
    WARN:  colors.yellow,
    ERROR: colors.red,
    DEBUG: colors.cyan
  };
  
  const color = levelColors[level] || colors.reset;
  
  return `${colors.bold}[${time}]${colors.reset} ${color}[${level}]${colors.reset} ${message}`;
}

module.exports = { formatMessage };

// utils/logger.js
const { formatMessage } = require('./formatter');

const logger = {
  info(message) {
    console.log(formatMessage('INFO', message));
  },
  
  warn(message) {
    console.warn(formatMessage('WARN', message));
  },
  
  error(message, error = null) {
    console.error(formatMessage('ERROR', message));
    if (error) {
      console.error(error.stack || error);
    }
  },
  
  debug(message) {
    if (process.env.NODE_ENV !== 'production') {
      console.log(formatMessage('DEBUG', message));
    }
  },
  
  // Logger avec données supplémentaires
  withData(level, message, data) {
    this[level.toLowerCase()](message);
    console.log('  Data:', JSON.stringify(data, null, 2));
  }
};

module.exports = logger;

// index.js - Test
const logger = require('./utils/logger');

logger.info("Application démarrée");
logger.debug("Mode debug activé");
logger.warn("Mémoire faible : 89% utilisée");
logger.error("Connexion DB échouée", new Error("Connection timeout"));
logger.withData('info', "Utilisateur créé", { id: 1, name: "Alice" });


// ============================================================================
// [GUIDE] CHAPITRE 3 : LE SYSTÈME DE FICHIERS (fs)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Lire et écrire des fichiers (sync et async)
[OK] Utiliser les Promises avec fs/promises
[OK] Surveiller les fichiers (watch)
[OK] Manipuler les répertoires
[OK] Gérer les erreurs de fichiers
*/


// ----------------------------------------------------------------------------
// [DOSSIER] LE MODULE FS
// ----------------------------------------------------------------------------

/*
fs = File System = Module de gestion de fichiers

Trois APIs disponibles :
1. fs.readFile()       -> Callbacks (ancienne méthode)
2. fs.promises.readFile() -> Promises (méthode moderne)
3. fs.readFileSync()   -> Synchrone (bloquant, à éviter)
*/

const fs = require('fs');
const fsPromises = require('fs/promises');  // API Promises (Node 14+)
const path = require('path');


// ----------------------------------------------------------------------------
// [GUIDE] LIRE DES FICHIERS
// ----------------------------------------------------------------------------

/*
Méthode 1 : Callbacks (ancienne, à éviter)
*/

fs.readFile('./data.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Erreur lecture:', err.message);
    return;
  }
  console.log('Contenu:', data);
});

/*
Méthode 2 : Promises (RECOMMANDÉE)
*/

async function liresFichier() {
  try {
    const data = await fsPromises.readFile('./data.txt', 'utf8');
    console.log('Contenu:', data);
  } catch (err) {
    console.error('Erreur:', err.message);
  }
}

liresFichier();

/*
Méthode 3 : Synchrone (BLOQUER LE THREAD - éviter en prod)
Utiliser SEULEMENT pour la configuration au démarrage
*/

try {
  const data = fs.readFileSync('./config.json', 'utf8');
  const config = JSON.parse(data);
  console.log('Config:', config);
} catch (err) {
  console.error('Erreur config:', err.message);
  process.exit(1);
}

/*
LIRE UN FICHIER JSON
*/

async function lireJSON(filename) {
  const data = await fsPromises.readFile(filename, 'utf8');
  return JSON.parse(data);
}

// Avec JSON.parse intégré
async function lireConfig() {
  const configPath = path.join(__dirname, 'config.json');
  const content = await fsPromises.readFile(configPath, 'utf8');
  return JSON.parse(content);
}


// ----------------------------------------------------------------------------
// [EDIT] ÉCRIRE DES FICHIERS
// ----------------------------------------------------------------------------

/*
Écrire un fichier (écrase si existe)
*/

async function ecrireFichier() {
  const content = "Hello, Node.js!\nDeuxième ligne";
  
  await fsPromises.writeFile('./output.txt', content, 'utf8');
  console.log('Fichier écrit !');
}

/*
Ajouter à la fin d'un fichier (append)
*/

async function ajouterLog(message) {
  const timestamp = new Date().toISOString();
  const logEntry = `[${timestamp}] ${message}\n`;
  
  await fsPromises.appendFile('./app.log', logEntry);
  console.log('Log ajouté !');
}

/*
Écrire du JSON
*/

async function ecrireJSON(filename, data) {
  const jsonContent = JSON.stringify(data, null, 2);  // null, 2 = indentation
  await fsPromises.writeFile(filename, jsonContent, 'utf8');
  console.log(`${filename} écrit avec succès`);
}

// Exemple
const users = [
  { id: 1, name: "Alice", email: "alice@example.com" },
  { id: 2, name: "Bob", email: "bob@example.com" }
];

ecrireJSON('./users.json', users);


// ----------------------------------------------------------------------------
// [DOSSIER] MANIPULER LES RÉPERTOIRES
// ----------------------------------------------------------------------------

/*
CRÉER UN RÉPERTOIRE
*/

async function creerRepertoire(dirPath) {
  try {
    // recursive: true -> Crée parents si nécessaire (comme mkdir -p)
    await fsPromises.mkdir(dirPath, { recursive: true });
    console.log(`Répertoire créé : ${dirPath}`);
  } catch (err) {
    if (err.code !== 'EEXIST') {  // Ignorer si existe déjà
      throw err;
    }
  }
}

await creerRepertoire('./uploads/images/2024');

/*
LISTER UN RÉPERTOIRE
*/

async function listerRepertoire(dirPath) {
  const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
  
  for (const entry of entries) {
    const type = entry.isDirectory() ? '[DOSSIER]' : '[FICHIER]';
    console.log(`${type} ${entry.name}`);
  }
  
  return entries;
}

/*
RÉCURSIF : Lister tous les fichiers
*/

async function listerTousFichiers(dirPath, prefix = '') {
  const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
  
  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    
    if (entry.isDirectory()) {
      console.log(`${prefix}[DOSSIER] ${entry.name}/`);
      await listerTousFichiers(fullPath, prefix + '  ');
    } else {
      console.log(`${prefix}[FICHIER] ${entry.name}`);
    }
  }
}

/*
VÉRIFIER EXISTENCE
*/

async function verifierExistence(filePath) {
  try {
    const stats = await fsPromises.stat(filePath);
    
    console.log(`Existe : ${filePath}`);
    console.log(`Type : ${stats.isFile() ? 'Fichier' : 'Répertoire'}`);
    console.log(`Taille : ${stats.size} bytes`);
    console.log(`Modifié : ${stats.mtime}`);
    
    return true;
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log(`N'existe pas : ${filePath}`);
      return false;
    }
    throw err;
  }
}

/*
COPIER, DÉPLACER, SUPPRIMER
*/

// Copier
await fsPromises.copyFile('./source.txt', './destination.txt');

// Renommer / Déplacer
await fsPromises.rename('./old-name.txt', './new-name.txt');

// Supprimer fichier
await fsPromises.unlink('./fichier.txt');

// Supprimer répertoire (vide seulement)
await fsPromises.rmdir('./dossier');

// Supprimer répertoire et contenu (récursif)
await fsPromises.rm('./dossier', { recursive: true, force: true });


// ----------------------------------------------------------------------------
// [EYE] SURVEILLER DES FICHIERS
// ----------------------------------------------------------------------------

/*
WATCH : Surveiller changements
*/

const watcher = fs.watch('./config.json', (eventType, filename) => {
  console.log(`Événement : ${eventType} sur ${filename}`);
  
  if (eventType === 'change') {
    console.log('Configuration modifiée, rechargement...');
    // Recharger config
  }
});

// Arrêter la surveillance
setTimeout(() => {
  watcher.close();
  console.log('Surveillance arrêtée');
}, 30000);

/*
CHOKIDAR : Package plus robuste pour la surveillance
npm install chokidar
*/

const chokidar = require('chokidar');

const watcher = chokidar.watch('./uploads', {
  ignored: /^\./,  // Ignorer fichiers cachés
  persistent: true
});

watcher
  .on('add', path => console.log(`Fichier ajouté : ${path}`))
  .on('change', path => console.log(`Fichier modifié : ${path}`))
  .on('unlink', path => console.log(`Fichier supprimé : ${path}`))
  .on('addDir', path => console.log(`Dossier ajouté : ${path}`))
  .on('unlinkDir', path => console.log(`Dossier supprimé : ${path}`));


// ----------------------------------------------------------------------------
// [OUTIL] ERREURS COURANTES DE FICHIERS
// ----------------------------------------------------------------------------

/*
CODES D'ERREUR FS COURANTS

ENOENT  -> No such file or directory (fichier introuvable)
EACCES  -> Permission denied (permissions insuffisantes)
EEXIST  -> File already exists (fichier existe déjà)
EISDIR  -> Is a directory (opération sur répertoire au lieu de fichier)
ENOTDIR -> Not a directory (opération répertoire sur fichier)
EMFILE  -> Too many open files
ENOSPC  -> No space left on device

GESTIONNAIRE D'ERREURS ROBUSTE
*/

async function lireFichierSecurise(filePath) {
  try {
    const content = await fsPromises.readFile(filePath, 'utf8');
    return { success: true, data: content };
  } catch (err) {
    switch (err.code) {
      case 'ENOENT':
        return { success: false, error: `Fichier introuvable : ${filePath}` };
      case 'EACCES':
        return { success: false, error: `Permission refusée : ${filePath}` };
      case 'EISDIR':
        return { success: false, error: `${filePath} est un répertoire` };
      default:
        return { success: false, error: `Erreur inattendue : ${err.message}` };
    }
  }
}

const result = await lireFichierSecurise('./data.txt');
if (result.success) {
  console.log(result.data);
} else {
  console.error(result.error);
}


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 3 : Gestionnaire de Fichiers JSON
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer un gestionnaire de base de données JSON simple

SPÉCIFICATIONS :
Créer jsondb.js avec les fonctions suivantes :
1. readDB(filename)    -> Lire la base de données JSON
2. writeDB(filename, data) -> Écrire la base de données
3. findById(filename, id) -> Trouver un item par ID
4. insert(filename, item) -> Insérer un item (auto-increment ID)
5. update(filename, id, updates) -> Mettre à jour un item
6. deleteItem(filename, id) -> Supprimer un item

CORRIGÉ :
*/

// jsondb.js
const fs = require('fs/promises');
const path = require('path');

class JsonDB {
  constructor(filename) {
    this.filepath = path.resolve(filename);
  }

  // Lire toute la DB
  async readDB() {
    try {
      const content = await fs.readFile(this.filepath, 'utf8');
      return JSON.parse(content);
    } catch (err) {
      if (err.code === 'ENOENT') {
        // Fichier n'existe pas -> retourner tableau vide
        return [];
      }
      throw new Error(`Erreur lecture DB : ${err.message}`);
    }
  }

  // Écrire toute la DB
  async writeDB(data) {
    await fs.writeFile(this.filepath, JSON.stringify(data, null, 2), 'utf8');
  }

  // Trouver par ID
  async findById(id) {
    const data = await this.readDB();
    return data.find(item => item.id === id) || null;
  }

  // Insérer
  async insert(item) {
    const data = await this.readDB();
    const maxId = data.length > 0 ? Math.max(...data.map(d => d.id)) : 0;
    const newItem = {
      id: maxId + 1,
      ...item,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };
    data.push(newItem);
    await this.writeDB(data);
    return newItem;
  }

  // Mettre à jour
  async update(id, updates) {
    const data = await this.readDB();
    const index = data.findIndex(item => item.id === id);
    if (index === -1) return null;
    
    data[index] = {
      ...data[index],
      ...updates,
      id: data[index].id,  // Protéger l'ID
      updatedAt: new Date().toISOString()
    };
    
    await this.writeDB(data);
    return data[index];
  }

  // Supprimer
  async delete(id) {
    const data = await this.readDB();
    const index = data.findIndex(item => item.id === id);
    if (index === -1) return false;
    
    data.splice(index, 1);
    await this.writeDB(data);
    return true;
  }

  // Chercher avec filtre
  async find(predicate) {
    const data = await this.readDB();
    return data.filter(predicate);
  }

  // Compter
  async count() {
    const data = await this.readDB();
    return data.length;
  }
}

module.exports = JsonDB;

// test-jsondb.js - TEST
const JsonDB = require('./jsondb');

async function main() {
  const db = new JsonDB('./users.json');

  // Insérer des utilisateurs
  const alice = await db.insert({ name: "Alice", email: "alice@example.com", role: "admin" });
  const bob = await db.insert({ name: "Bob", email: "bob@example.com", role: "user" });
  console.log("Insérés :", alice, bob);

  // Trouver par ID
  const found = await db.findById(1);
  console.log("Trouvé :", found);

  // Mettre à jour
  const updated = await db.update(1, { role: "superadmin" });
  console.log("Mis à jour :", updated);

  // Chercher avec filtre
  const admins = await db.find(user => user.role === 'admin' || user.role === 'superadmin');
  console.log("Admins :", admins);

  // Compter
  const total = await db.count();
  console.log("Total :", total);

  // Supprimer
  await db.delete(2);
  console.log("Bob supprimé");
  console.log("Restants :", await db.readDB());
}

main().catch(console.error);


// ============================================================================
// [GUIDE] CHAPITRE 4 : ÉVÉNEMENTS ET EVENTEMITTER
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le pattern Observer/Event
[OK] Utiliser EventEmitter
[OK] Créer des événements personnalisés
[OK] Gérer les erreurs d'événements
[OK] Utiliser les événements dans des classes
*/


// ----------------------------------------------------------------------------
// [OBJECTIF] QU'EST-CE QU'UN ÉVÉNEMENT ?
// ----------------------------------------------------------------------------

/*
PATTERN ÉVÉNEMENT = Observer Pattern

Composants :
1. ÉMETTEUR (EventEmitter) -> Déclenche les événements
2. ÉVÉNEMENT (Event)       -> Ce qui s'est passé
3. LISTENER (Listener)     -> Réagit à l'événement

ANALOGIE : Alarm Clock
- Alarm Clock = Émetteur
- "alarm" = Événement
- Tu qui te réveilles = Listener

TOUT NODE.JS EST BASÉ SUR CE PATTERN !
- HTTP server émet des événements
- Streams émettent des événements
- Process émet des événements
*/


// ----------------------------------------------------------------------------
// [OUTIL] EVENTEMITTER DE BASE
// ----------------------------------------------------------------------------

const EventEmitter = require('events');

// Créer un émetteur
const emitter = new EventEmitter();

// ÉCOUTER un événement (before emit!)
emitter.on('saluer', (nom) => {
  console.log(`Bonjour ${nom} !`);
});

// ÉMETTRE un événement
emitter.emit('saluer', 'Alice');  // -> Bonjour Alice !
emitter.emit('saluer', 'Bob');    // -> Bonjour Bob !

/*
MÉTHODES ESSENTIELLES
*/

// on() -> Écouter (peut être appelé plusieurs fois)
emitter.on('event', (data) => {
  console.log('Listener 1:', data);
});
emitter.on('event', (data) => {
  console.log('Listener 2:', data);
});

// once() -> Écouter UNE seule fois
emitter.once('init', () => {
  console.log('Initialisation ! (appelé une seule fois)');
});

// emit() -> Déclencher
emitter.emit('init');  // 'Initialisation !'
emitter.emit('init');  // Rien ! (déjà consommé)

// off() / removeListener() -> Retirer un listener
function monListener(data) {
  console.log(data);
}

emitter.on('test', monListener);
emitter.emit('test', 'premier');  // Déclenché
emitter.off('test', monListener);
emitter.emit('test', 'deuxième');  // Rien !

// removeAllListeners() -> Retirer tous les listeners
emitter.removeAllListeners('event');
emitter.removeAllListeners();  // Tout supprimer

// listenerCount() -> Compter les listeners
console.log(emitter.listenerCount('event'));

// eventNames() -> Noms des événements
console.log(emitter.eventNames());

// prependListener() -> Ajouter en PREMIER
emitter.prependListener('test', () => console.log('Je suis premier !'));


// ----------------------------------------------------------------------------
// [CONSTRUCTION] CRÉER DES CLASSES AVEC EVENTEMITTER
// ----------------------------------------------------------------------------

/*
PATTERN STANDARD : Hériter de EventEmitter
*/

const EventEmitter = require('events');

class Utilisateur extends EventEmitter {
  constructor(nom, email) {
    super();  // [ATTENTION] OBLIGATOIRE - Appel du constructeur parent
    this.nom = nom;
    this.email = email;
    this.connecte = false;
  }

  seConnecter() {
    this.connecte = true;
    // Émettre événement DEPUIS la classe
    this.emit('connexion', {
      utilisateur: this.nom,
      timestamp: new Date()
    });
  }

  seDeconnecter() {
    this.connecte = false;
    this.emit('deconnexion', {
      utilisateur: this.nom,
      timestamp: new Date()
    });
  }

  envoyerMessage(message) {
    if (!this.connecte) {
      this.emit('erreur', new Error('Utilisateur non connecté !'));
      return;
    }
    this.emit('message', {
      de: this.nom,
      contenu: message,
      timestamp: new Date()
    });
  }
}

// Utilisation
const alice = new Utilisateur('Alice', 'alice@example.com');

// Écouter les événements
alice.on('connexion', (data) => {
  console.log(`[OK] ${data.utilisateur} connecté à ${data.timestamp}`);
});

alice.on('deconnexion', (data) => {
  console.log(`[X] ${data.utilisateur} déconnecté`);
});

alice.on('message', (data) => {
  console.log(`[SPEECH_BALLOON] ${data.de}: "${data.contenu}"`);
});

alice.on('erreur', (err) => {
  console.error(`[ROUGE] Erreur: ${err.message}`);
});

// Interactions
alice.seConnecter();         // [OK] Alice connectée
alice.envoyerMessage("Hello!");  // [SPEECH_BALLOON] Alice: "Hello!"
alice.seDeconnecter();       // [X] Alice déconnectée
alice.envoyerMessage("test"); // [ROUGE] Erreur: Utilisateur non connecté !


// ----------------------------------------------------------------------------
// [ATTENTION] GESTION DES ERREURS AVEC EVENTS
// ----------------------------------------------------------------------------

/*
RÈGLE CRITIQUE : L'événement 'error' est SPÉCIAL

Si un 'error' est émis et qu'il n'y a PAS de listener 'error' :
-> Node.js lance une EXCEPTION non gérée
-> Peut planter votre application !
*/

const emitter = new EventEmitter();

// [X] DANGEREUX : Pas de listener 'error'
emitter.emit('error', new Error('Problème!'));  // -> CRASH !

// [OK] SAFE : Toujours écouter 'error'
emitter.on('error', (err) => {
  console.error('Erreur capturée:', err.message);
});

emitter.emit('error', new Error('Problème!'));  // -> Géré proprement


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 4 : Système de Notifications
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer un système de notifications basé sur EventEmitter

SPÉCIFICATIONS :
Créer notifications.js avec :
1. Classe NotificationSystem héritant de EventEmitter
2. Méthodes : notify(), subscribe(), unsubscribe(), getHistory()
3. Types de notifications : 'info', 'warn', 'error', 'success'
4. Historique des 50 dernières notifications
5. Filtre par type dans subscribe()

CORRIGÉ :
*/

// notifications.js
const EventEmitter = require('events');

class NotificationSystem extends EventEmitter {
  constructor(maxHistory = 50) {
    super();
    this.maxHistory = maxHistory;
    this.history = [];
    this.subscribers = new Map();  // id -> { types, callback }
    this.nextId = 1;
  }

  // Envoyer une notification
  notify(type, title, message, data = {}) {
    const notification = {
      id: Date.now(),
      type,
      title,
      message,
      data,
      timestamp: new Date().toISOString()
    };

    // Ajouter à l'historique
    this.history.push(notification);
    if (this.history.length > this.maxHistory) {
      this.history.shift();  // Supprimer le plus ancien
    }

    // Émettre l'événement
    this.emit('notification', notification);
    this.emit(`notification:${type}`, notification);

    return notification;
  }

  // S'abonner aux notifications
  subscribe(callback, types = ['info', 'warn', 'error', 'success']) {
    const id = this.nextId++;

    const listener = (notification) => {
      if (types.includes(notification.type)) {
        callback(notification);
      }
    };

    this.on('notification', listener);
    this.subscribers.set(id, { types, callback, listener });

    return id;  // Retourner l'ID pour se désabonner
  }

  // Se désabonner
  unsubscribe(id) {
    const subscriber = this.subscribers.get(id);
    if (!subscriber) return false;

    this.off('notification', subscriber.listener);
    this.subscribers.delete(id);
    return true;
  }

  // Obtenir l'historique
  getHistory(type = null) {
    if (type) {
      return this.history.filter(n => n.type === type);
    }
    return [...this.history];
  }

  // Raccourcis
  info(title, message, data) {
    return this.notify('info', title, message, data);
  }
  
  warn(title, message, data) {
    return this.notify('warn', title, message, data);
  }
  
  error(title, message, data) {
    return this.notify('error', title, message, data);
  }
  
  success(title, message, data) {
    return this.notify('success', title, message, data);
  }
}

module.exports = NotificationSystem;

// test-notifications.js
const NotificationSystem = require('./notifications');

const ns = new NotificationSystem(10);

// Abonnement global
const sub1 = ns.subscribe((notif) => {
  const icons = { info: 'ℹ', warn: '[ATTENTION]', error: '[X]', success: '[OK]' };
  console.log(`${icons[notif.type]} [${notif.type.toUpperCase()}] ${notif.title}: ${notif.message}`);
});

// Abonnement seulement aux erreurs
const sub2 = ns.subscribe((notif) => {
  console.log(`[ALERTE] ALERTE ADMIN: ${notif.title}`);
}, ['error']);

// Envoyer des notifications
ns.info("Démarrage", "Application démarrée avec succès");
ns.warn("Mémoire", "Utilisation mémoire à 80%");
ns.error("Database", "Connexion DB perdue");
ns.success("Backup", "Sauvegarde automatique réussie");
ns.error("Sécurité", "Tentative de connexion suspecte", { ip: "192.168.1.100" });

// Désabonner sub2
ns.unsubscribe(sub2);
ns.error("Test", "Cette erreur ne sera pas vue par sub2");

// Historique
console.log("\n[LISTE] Historique complet :", ns.getHistory().length, "notifications");
console.log("[X] Erreurs :", ns.getHistory('error').length);


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

/*
[OK] CE QUE VOUS AVEZ APPRIS

Chapitre 0 : Introduction à Node.js
[OK] Node.js = Runtime JavaScript serveur
[OK] Event Loop et Non-blocking I/O
[OK] Single-threaded mais concurrent via libuv
[OK] Quand utiliser Node.js

Chapitre 1 : Installation et Premier Programme
[OK] NVM pour gérer les versions
[OK] package.json et npm
[OK] Versioning sémantique
[OK] nodemon pour développement

Chapitre 2 : Modules
[OK] CommonJS (require/module.exports)
[OK] ES Modules (import/export)
[OK] Modules natifs (os, path, fs, crypto)
[OK] Résolution de modules

Chapitre 3 : Système de Fichiers
[OK] fs/promises (méthode moderne)
[OK] Lecture, écriture, suppression
[OK] Manipulation de répertoires
[OK] Watch de fichiers

Chapitre 4 : EventEmitter
[OK] Pattern Observer
[OK] on(), emit(), once(), off()
[OK] Héritage de EventEmitter
[OK] Gestion des erreurs

[OBJECTIF] PROJETS SUGGÉRÉS APRÈS PARTIE 1
-> CLI tool de gestion de notes (fichiers JSON)
-> Watcher de dossier avec notifications
-> Système de logging personnalisé

-> PROCHAINE ÉTAPE : nodejs_partie2.txt
-> Serveurs HTTP, Express.js, APIs REST
*/

# ============================================================================
# [LIVRE] NODE.JS - PARTIE 2 : SERVEURS ET APIs
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 5 : Serveur HTTP natif
# - Chapitre 6 : Express.js - Fondamentaux
# - Chapitre 7 : Express.js - Middleware
# - Chapitre 8 : APIs REST avec Express
# - Chapitre 9 : Gestion des erreurs
#
# [TEMPS] TEMPS : ~8-10 heures
# [DOCS] PRÉREQUIS : Partie 1 complétée
# ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 5 : SERVEUR HTTP NATIF
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer un serveur HTTP avec le module natif
[OK] Comprendre les objets req et res
[OK] Router les requêtes manuellement
[OK] Gérer les méthodes HTTP
[OK] Envoyer différents types de réponses
*/


// ----------------------------------------------------------------------------
// [WEB] CRÉER UN SERVEUR HTTP BASIQUE
// ----------------------------------------------------------------------------

const http = require('http');

/*
http.createServer(callback)

callback = (req, res) =>
  req -> IncomingMessage = Requête du client
  res -> ServerResponse = Réponse à envoyer
*/

const server = http.createServer((req, res) => {
  // 1. Définir le status code et les headers
  res.writeHead(200, {
    'Content-Type': 'text/plain; charset=utf-8'
  });

  // 2. Écrire le corps de la réponse
  res.write("Bonjour depuis Node.js !");
  res.end();  // [ATTENTION] TOUJOURS appeler .end() !
});

// Démarrer le serveur
const PORT = 3000;
server.listen(PORT, () => {
  console.log(`[RAPIDE] Serveur démarré sur http://localhost:${PORT}`);
});

/*
TESTER :
  curl http://localhost:3000
  -> Bonjour depuis Node.js !


L'OBJET REQ (IncomingMessage)
*/

const server = http.createServer((req, res) => {
  console.log('Méthode :', req.method);      // GET, POST, PUT, DELETE
  console.log('URL :', req.url);             // /path?query=value
  console.log('Headers :', req.headers);     // Object avec tous les headers
  console.log('Hôte :', req.headers.host);   // localhost:3000
  console.log('User-Agent :', req.headers['user-agent']);
  
  // Parser l'URL
  const url = new URL(req.url, `http://${req.headers.host}`);
  console.log('Pathname :', url.pathname);    // /path
  console.log('Query :', url.searchParams.get('q'));  // valeur de ?q=
});

/*
L'OBJET RES (ServerResponse)
*/

const server = http.createServer((req, res) => {
  // Méthodes de response

  // Définir status et headers ensemble
  res.writeHead(200, {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'ma-valeur',
    'Access-Control-Allow-Origin': '*'
  });

  // Ou définir header individuellement
  res.setHeader('Content-Type', 'application/json');
  res.statusCode = 200;

  // Écrire corps
  res.write('{"message": "Hello"}');
  res.end();

  // Ou tout en une fois
  res.end(JSON.stringify({ message: "Hello" }));
});


// ----------------------------------------------------------------------------
// [WORLD_MAP] ROUTING MANUEL
// ----------------------------------------------------------------------------

/*
PROBLÈME : Sans framework, le routing est manuel
*/

const http = require('http');
const url = require('url');

// Données simulées
let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
];

const server = http.createServer(async (req, res) => {
  const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
  const path = parsedUrl.pathname;
  const method = req.method;

  // Configurer les headers CORS et Content-Type
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Access-Control-Allow-Origin', '*');

  // Helper pour envoyer JSON
  const sendJSON = (statusCode, data) => {
    res.writeHead(statusCode);
    res.end(JSON.stringify(data));
  };

  // Helper pour lire le body de la requête
  const getBody = () => {
    return new Promise((resolve, reject) => {
      let body = '';
      req.on('data', chunk => body += chunk.toString());
      req.on('end', () => {
        try {
          resolve(body ? JSON.parse(body) : {});
        } catch (err) {
          reject(new Error('JSON invalide'));
        }
      });
      req.on('error', reject);
    });
  };

  try {
    // Route : GET /
    if (path === '/' && method === 'GET') {
      sendJSON(200, { message: 'API Node.js native', version: '1.0.0' });
    }

    // Route : GET /users
    else if (path === '/users' && method === 'GET') {
      sendJSON(200, { users, total: users.length });
    }

    // Route : GET /users/:id
    else if (path.match(/^\/users\/\d+$/) && method === 'GET') {
      const id = parseInt(path.split('/')[2]);
      const user = users.find(u => u.id === id);

      if (!user) {
        sendJSON(404, { error: 'Utilisateur non trouvé' });
      } else {
        sendJSON(200, user);
      }
    }

    // Route : POST /users
    else if (path === '/users' && method === 'POST') {
      const body = await getBody();

      if (!body.name || !body.email) {
        sendJSON(400, { error: 'name et email sont requis' });
        return;
      }

      const newUser = {
        id: users.length + 1,
        name: body.name,
        email: body.email
      };

      users.push(newUser);
      sendJSON(201, newUser);
    }

    // Route : DELETE /users/:id
    else if (path.match(/^\/users\/\d+$/) && method === 'DELETE') {
      const id = parseInt(path.split('/')[2]);
      const index = users.findIndex(u => u.id === id);

      if (index === -1) {
        sendJSON(404, { error: 'Utilisateur non trouvé' });
        return;
      }

      users.splice(index, 1);
      res.writeHead(204);
      res.end();
    }

    // Route non trouvée
    else {
      sendJSON(404, { error: 'Route non trouvée', path, method });
    }

  } catch (err) {
    console.error('Erreur serveur:', err);
    sendJSON(500, { error: 'Erreur interne du serveur' });
  }
});

server.listen(3000, () => {
  console.log('Serveur sur http://localhost:3000');
});

/*
[IDEE] PROBLÈMES AVEC LE HTTP NATIF

[X] Routing complexe et verbeux
[X] Parsing du body manuel
[X] Pas de middleware
[X] Gestion d'erreurs répétitive
[X] Headers répétés partout

-> C'est pourquoi Express.js existe ! [OK]
*/


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 5 : Serveur de Fichiers Statiques
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer un serveur HTTP natif qui sert des fichiers statiques

SPÉCIFICATIONS :
1. Servir les fichiers du dossier ./public
2. Détecter automatiquement le Content-Type (html, css, js, json, png, jpg)
3. Retourner 404 si fichier introuvable
4. Lister le contenu du dossier si URL = '/'
5. Gérer les erreurs

CORRIGÉ :
*/

// static-server.js
const http = require('http');
const fs = require('fs/promises');
const path = require('path');

const MIME_TYPES = {
  '.html': 'text/html; charset=utf-8',
  '.css': 'text/css; charset=utf-8',
  '.js': 'application/javascript; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.gif': 'image/gif',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
  '.txt': 'text/plain; charset=utf-8'
};

const PUBLIC_DIR = path.join(__dirname, 'public');

const server = http.createServer(async (req, res) => {
  const urlPath = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);

  try {
    // Index : lister les fichiers
    if (urlPath === '/') {
      const files = await fs.readdir(PUBLIC_DIR, { withFileTypes: true });
      const listing = files.map(f => ({
        name: f.name,
        type: f.isDirectory() ? 'dossier' : 'fichier',
        url: `/${f.name}`
      }));

      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ files: listing }, null, 2));
      return;
    }

    // Construire le chemin du fichier
    const filePath = path.join(PUBLIC_DIR, urlPath);

    // Sécurité : empêcher path traversal (../../etc/passwd)
    if (!filePath.startsWith(PUBLIC_DIR)) {
      res.writeHead(403, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Accès interdit' }));
      return;
    }

    // Lire le fichier
    const content = await fs.readFile(filePath);
    const ext = path.extname(filePath).toLowerCase();
    const contentType = MIME_TYPES[ext] || 'application/octet-stream';

    const stats = await fs.stat(filePath);

    res.writeHead(200, {
      'Content-Type': contentType,
      'Content-Length': stats.size,
      'Last-Modified': stats.mtime.toUTCString()
    });
    res.end(content);

  } catch (err) {
    if (err.code === 'ENOENT') {
      res.writeHead(404, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: `Fichier non trouvé : ${urlPath}` }));
    } else if (err.code === 'EISDIR') {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Ceci est un répertoire' }));
    } else {
      console.error('Erreur serveur:', err);
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Erreur serveur interne' }));
    }
  }
});

server.listen(3000, () => {
  console.log('[DOSSIER] Serveur de fichiers statiques : http://localhost:3000');
});


// ============================================================================
// [GUIDE] CHAPITRE 6 : EXPRESS.JS - FONDAMENTAUX
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Installer et configurer Express.js
[OK] Créer des routes basiques
[OK] Utiliser les paramètres d'URL
[OK] Envoyer différents types de réponses
[OK] Structurer une application Express
*/


// ----------------------------------------------------------------------------
// [RAPIDE] INSTALLATION ET CONFIGURATION
// ----------------------------------------------------------------------------

/*
npm install express

POURQUOI EXPRESS ?
[OK] Routing simple et élégant
[OK] Middleware puissant
[OK] Grande communauté
[OK] Performant et léger
[OK] Base de nombreux frameworks (NestJS, etc.)
*/


// ----------------------------------------------------------------------------
// [DEMARRAGE] PREMIÈRE APPLICATION EXPRESS
// ----------------------------------------------------------------------------

const express = require('express');
const app = express();

// Configuration de base
app.use(express.json());          // Parser JSON bodies
app.use(express.urlencoded({ extended: true }));  // Parser form data

// Routes
app.get('/', (req, res) => {
  res.send('Hello, Express!');
});

// Démarrer le serveur
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`[RAPIDE] Serveur Express sur http://localhost:${PORT}`);
});

/*
ANATOMIE D'EXPRESS :

app.METHODE(CHEMIN, HANDLER)
   │         │       │
   │         │       └── Fonction (req, res) => {}
   │         └────────── URL path
   └──────────────────── HTTP Method (get, post, put, delete, patch)
*/


// ----------------------------------------------------------------------------
// [WORLD_MAP] ROUTING AVEC EXPRESS
// ----------------------------------------------------------------------------

/*
Routes de base
*/

app.get('/hello', (req, res) => {
  res.send('GET /hello');
});

app.post('/data', (req, res) => {
  res.send('POST /data');
});

app.put('/update', (req, res) => {
  res.send('PUT /update');
});

app.delete('/remove', (req, res) => {
  res.send('DELETE /remove');
});

// Toutes méthodes
app.all('/any', (req, res) => {
  res.send(`Méthode: ${req.method}`);
});

/*
PARAMÈTRES D'URL

:param -> Paramètre requis
:param? -> Paramètre optionnel
*/

app.get('/user/:id', (req, res) => {
  const id = req.params.id;
  res.json({ message: `Utilisateur ${id}` });
});

app.get('/user/:id/posts/:postId', (req, res) => {
  const { id, postId } = req.params;
  res.json({ userId: id, postId });
});

// Wildcards
app.get('/files/*', (req, res) => {
  const filePath = req.params[0];  // Capture après /files/
  res.json({ file: filePath });
});

/*
QUERY STRING

/search?q=node&page=2&limit=10
*/

app.get('/search', (req, res) => {
  const { q, page = 1, limit = 10 } = req.query;

  if (!q) {
    return res.status(400).json({ error: 'Le paramètre q est requis' });
  }

  res.json({
    query: q,
    page: parseInt(page),
    limit: parseInt(limit),
    results: []  // Simulated
  });
});


// ----------------------------------------------------------------------------
// [SORTIE] ENVOYER DES RÉPONSES
// ----------------------------------------------------------------------------

/*
MÉTHODES DE RÉPONSE EXPRESS
*/

app.get('/exemples', (req, res) => {
  // Envoyer texte
  res.send('Hello World');

  // Envoyer JSON (Content-Type: application/json)
  res.json({ message: 'OK', data: { users: [] } });

  // Envoyer HTML
  res.send('<h1>Titre</h1>');

  // Envoyer un fichier
  res.sendFile(path.join(__dirname, 'public', 'index.html'));

  // Télécharger un fichier
  res.download('./rapport.pdf', 'rapport-2024.pdf');

  // Redirection
  res.redirect('/nouvelle-url');           // 302 par défaut
  res.redirect(301, '/nouvelle-url');       // 301 Permanent
  res.redirect('https://www.google.com');

  // Status seulement
  res.sendStatus(200);  // "OK"
  res.sendStatus(404);  // "Not Found"

  // Status avec body
  res.status(201).json({ message: 'Créé', id: 42 });
  res.status(404).json({ error: 'Non trouvé' });
  res.status(500).send('Erreur interne');
});

/*
HEADERS DE RÉPONSE
*/

app.get('/avec-headers', (req, res) => {
  // Définir headers
  res.set('X-Custom-Header', 'ma-valeur');
  res.set({
    'X-Header-1': 'valeur1',
    'X-Header-2': 'valeur2'
  });

  // Content-Type manuel
  res.type('text/plain');

  // Cookie
  res.cookie('session', 'abc123', {
    maxAge: 86400000,  // 1 jour en ms
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
  });

  // Supprimer cookie
  res.clearCookie('session');

  res.json({ ok: true });
});

/*
L'OBJET REQ EN EXPRESS
*/

app.post('/body-exemple', (req, res) => {
  // Corps de la requête (nécessite middleware)
  console.log(req.body);        // { name: "Alice", ... }

  // Paramètres URL
  console.log(req.params);      // { id: "123" }

  // Query string
  console.log(req.query);       // { page: "1", limit: "10" }

  // Headers
  console.log(req.headers);
  console.log(req.get('Content-Type'));
  console.log(req.get('Authorization'));

  // IP du client
  console.log(req.ip);
  console.log(req.ips);         // Proxies

  // Protocole
  console.log(req.protocol);    // http ou https
  console.log(req.secure);      // true si HTTPS

  // Méthode et path
  console.log(req.method);      // POST
  console.log(req.path);        // /body-exemple

  // Cookies (nécessite cookie-parser)
  console.log(req.cookies);

  res.json({ received: req.body });
});


// ----------------------------------------------------------------------------
// [DOSSIER] ROUTER EXPRESS
// ----------------------------------------------------------------------------

/*
POURQUOI LES ROUTERS ?

Organiser les routes par module/fonctionnalité
Exactement comme les Blueprints Flask

STRUCTURE AVEC ROUTERS :
routes/
  users.js    -> Routes /users
  posts.js    -> Routes /posts
  auth.js     -> Routes /auth
app.js        -> Enregistrement des routers
*/

// routes/users.js
const express = require('express');
const router = express.Router();

// Ces routes seront préfixées avec /users (défini dans app.js)
router.get('/', (req, res) => {
  res.json({ users: [] });  // GET /users
});

router.get('/:id', (req, res) => {
  res.json({ user: { id: req.params.id } });  // GET /users/:id
});

router.post('/', (req, res) => {
  res.status(201).json({ message: 'Créé', user: req.body });  // POST /users
});

router.put('/:id', (req, res) => {
  res.json({ message: 'Modifié', id: req.params.id });  // PUT /users/:id
});

router.delete('/:id', (req, res) => {
  res.sendStatus(204);  // DELETE /users/:id
});

module.exports = router;

// app.js
const express = require('express');
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');

const app = express();
app.use(express.json());

// Enregistrer avec préfixe
app.use('/users', usersRouter);
app.use('/posts', postsRouter);

/*
ROUTES COMPLÈTES :
GET    /users        -> usersRouter -> '/'
GET    /users/:id    -> usersRouter -> '/:id'
POST   /users        -> usersRouter -> '/'
PUT    /users/:id    -> usersRouter -> '/:id'
DELETE /users/:id    -> usersRouter -> '/:id'
*/


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 6 : API Todo List
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer une API complète de gestion de tâches avec Express

ENDPOINTS :
GET    /api/todos          -> Lister (avec ?completed=true/false)
GET    /api/todos/:id      -> Récupérer une tâche
POST   /api/todos          -> Créer une tâche
PUT    /api/todos/:id      -> Modifier une tâche
DELETE /api/todos/:id      -> Supprimer
DELETE /api/todos/completed -> Supprimer toutes les tâches terminées

CORRIGÉ COMPLET :
*/

// routes/todos.js
const express = require('express');
const router = express.Router();

// Simulation d'une base de données en mémoire
let todos = [
  { id: 1, title: 'Apprendre Node.js', completed: false, createdAt: new Date() },
  { id: 2, title: 'Créer une API REST', completed: false, createdAt: new Date() },
  { id: 3, title: 'Lire la doc Express', completed: true, createdAt: new Date() }
];
let nextId = 4;

// Validation middleware
function validateTodo(req, res, next) {
  const { title } = req.body;
  if (!title || typeof title !== 'string' || title.trim().length === 0) {
    return res.status(400).json({ error: 'Le champ title est requis et doit être une chaîne non vide' });
  }
  if (title.length > 200) {
    return res.status(400).json({ error: 'title ne peut pas dépasser 200 caractères' });
  }
  next();
}

// GET /api/todos
router.get('/', (req, res) => {
  const { completed, search, sort = 'createdAt' } = req.query;
  let result = [...todos];

  // Filtrer par statut
  if (completed !== undefined) {
    const isCompleted = completed === 'true';
    result = result.filter(t => t.completed === isCompleted);
  }

  // Recherche
  if (search) {
    result = result.filter(t =>
      t.title.toLowerCase().includes(search.toLowerCase())
    );
  }

  // Trier
  if (sort === 'title') {
    result.sort((a, b) => a.title.localeCompare(b.title));
  } else {
    result.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  }

  res.json({
    todos: result,
    total: result.length,
    completed: result.filter(t => t.completed).length,
    pending: result.filter(t => !t.completed).length
  });
});

// GET /api/todos/:id
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);

  if (isNaN(id)) {
    return res.status(400).json({ error: 'ID invalide' });
  }

  const todo = todos.find(t => t.id === id);
  if (!todo) {
    return res.status(404).json({ error: 'Tâche non trouvée', id });
  }

  res.json(todo);
});

// POST /api/todos
router.post('/', validateTodo, (req, res) => {
  const { title, priority = 'medium' } = req.body;

  const newTodo = {
    id: nextId++,
    title: title.trim(),
    completed: false,
    priority,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };

  todos.push(newTodo);
  res.status(201).json(newTodo);
});

// PUT /api/todos/:id
router.put('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = todos.findIndex(t => t.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'Tâche non trouvée', id });
  }

  const { title, completed, priority } = req.body;
  const updates = {};

  if (title !== undefined) {
    if (!title.trim()) return res.status(400).json({ error: 'title ne peut pas être vide' });
    updates.title = title.trim();
  }
  if (completed !== undefined) updates.completed = Boolean(completed);
  if (priority !== undefined) updates.priority = priority;

  todos[index] = {
    ...todos[index],
    ...updates,
    updatedAt: new Date().toISOString()
  };

  res.json(todos[index]);
});

// DELETE /api/todos/completed (avant /:id!)
router.delete('/completed', (req, res) => {
  const before = todos.length;
  todos = todos.filter(t => !t.completed);
  const deleted = before - todos.length;

  res.json({ message: `${deleted} tâche(s) supprimée(s)`, remaining: todos.length });
});

// DELETE /api/todos/:id
router.delete('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = todos.findIndex(t => t.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'Tâche non trouvée', id });
  }

  todos.splice(index, 1);
  res.status(204).send();
});

module.exports = router;

// app.js
const express = require('express');
const todosRouter = require('./routes/todos');

const app = express();
app.use(express.json());

// Info API
app.get('/', (req, res) => {
  res.json({
    name: 'Todo API',
    version: '1.0.0',
    endpoints: {
      'GET /api/todos': 'Lister les tâches',
      'GET /api/todos/:id': 'Récupérer une tâche',
      'POST /api/todos': 'Créer une tâche',
      'PUT /api/todos/:id': 'Modifier une tâche',
      'DELETE /api/todos/:id': 'Supprimer une tâche',
      'DELETE /api/todos/completed': 'Supprimer les tâches terminées'
    }
  });
});

app.use('/api/todos', todosRouter);

app.listen(3000, () => console.log('API Todo sur http://localhost:3000'));

/*
TEST AVEC CURL :
curl http://localhost:3000/api/todos
curl http://localhost:3000/api/todos?completed=false
curl http://localhost:3000/api/todos?search=node
curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Nouvelle tâche", "priority": "high"}'
curl -X PUT http://localhost:3000/api/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'
curl -X DELETE http://localhost:3000/api/todos/1
*/


// ============================================================================
// [GUIDE] CHAPITRE 7 : EXPRESS.JS - MIDDLEWARE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le concept de middleware
[OK] Créer des middlewares personnalisés
[OK] Utiliser les middlewares Express intégrés
[OK] Utiliser les middlewares tiers populaires
[OK] Gérer l'ordre des middlewares
*/


// ----------------------------------------------------------------------------
// [REFLEXION] QU'EST-CE QU'UN MIDDLEWARE ?
// ----------------------------------------------------------------------------

/*
MIDDLEWARE = Fonction entre la requête et la réponse

REQUÊTE -> [MW1] -> [MW2] -> [MW3] -> HANDLER -> RÉPONSE
                                      │
                           Peut court-circuiter avec res.send()

SIGNATURE : (req, res, next) => {}

next() -> Passer au middleware suivant
Si next() n'est pas appelé -> La chaîne s'arrête !

ANALOGIE : Aéroport

Passager (req) ->
  [1. Vérification billets] ->
  [2. Contrôle des bagages] ->
  [3. Contrôle passeport] ->
  [4. Embarquement] ->
  Avion (res)

Chaque contrôle = un middleware
Peut bloquer si problème, sinon passe au suivant
*/


// ----------------------------------------------------------------------------
// [OUTIL] CRÉER UN MIDDLEWARE
// ----------------------------------------------------------------------------

/*
Middleware APPLICATION (s'applique à toutes les routes)
*/

const express = require('express');
const app = express();

// Logger de requêtes
app.use((req, res, next) => {
  const start = Date.now();

  // Intercepter la fin de la réponse
  res.on('finish', () => {
    const duration = Date.now() - start;
    const status = res.statusCode;
    const color = status >= 500 ? '[ROUGE]' : status >= 400 ? '[JAUNE]' : '[VERT]';
    console.log(`${color} ${req.method} ${req.path} -> ${status} (${duration}ms)`);
  });

  next();  // [ATTENTION] NE PAS OUBLIER next() !
});

/*
Middleware de ROUTE (s'applique à une route spécifique)
*/

app.get('/protected', authMiddleware, (req, res) => {
  res.json({ message: 'Contenu protégé' });
});

/*
Middleware sur ROUTER
*/

const router = express.Router();

// S'applique à toutes les routes du router
router.use((req, res, next) => {
  console.log(`Router middleware : ${req.method} ${req.path}`);
  next();
});


// ----------------------------------------------------------------------------
// [OUTILS] MIDDLEWARES PERSONNALISÉS ESSENTIELS
// ----------------------------------------------------------------------------

/*
1. MIDDLEWARE D'AUTHENTIFICATION
*/

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).json({ error: 'Token manquant' });
  }

  if (!authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Format: Bearer <token>' });
  }

  const token = authHeader.slice(7);

  // Vérification simplifiée (en vrai : vérifier JWT)
  if (token !== 'mon-token-secret') {
    return res.status(403).json({ error: 'Token invalide' });
  }

  // Attacher l'utilisateur à la requête
  req.user = { id: 1, name: 'Alice', role: 'admin' };
  next();
}

// Utilisation
app.get('/api/protected', authMiddleware, (req, res) => {
  res.json({ message: `Bonjour ${req.user.name}` });
});

/*
2. MIDDLEWARE DE VALIDATION
*/

function validateBody(schema) {
  return (req, res, next) => {
    const errors = [];

    for (const [field, rules] of Object.entries(schema)) {
      const value = req.body[field];

      if (rules.required && (value === undefined || value === null || value === '')) {
        errors.push(`${field} est requis`);
        continue;
      }

      if (value === undefined) continue;

      if (rules.type && typeof value !== rules.type) {
        errors.push(`${field} doit être de type ${rules.type}`);
      }

      if (rules.minLength && value.length < rules.minLength) {
        errors.push(`${field} doit avoir au moins ${rules.minLength} caractères`);
      }

      if (rules.maxLength && value.length > rules.maxLength) {
        errors.push(`${field} ne peut pas dépasser ${rules.maxLength} caractères`);
      }

      if (rules.min !== undefined && value < rules.min) {
        errors.push(`${field} doit être >= ${rules.min}`);
      }

      if (rules.pattern && !rules.pattern.test(value)) {
        errors.push(`${field} format invalide`);
      }
    }

    if (errors.length > 0) {
      return res.status(400).json({ errors });
    }

    next();
  };
}

// Utilisation
const userSchema = {
  name: { required: true, type: 'string', minLength: 2, maxLength: 50 },
  email: { required: true, type: 'string', pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
  age: { required: false, min: 0 }
};

app.post('/users', validateBody(userSchema), (req, res) => {
  res.status(201).json({ message: 'Utilisateur créé', data: req.body });
});

/*
3. MIDDLEWARE DE RATE LIMITING
*/

function rateLimit(options = {}) {
  const { maxRequests = 100, windowMs = 60000 } = options;
  const requests = new Map();

  return (req, res, next) => {
    const ip = req.ip;
    const now = Date.now();
    const windowStart = now - windowMs;

    // Nettoyer les anciennes requêtes
    if (!requests.has(ip)) {
      requests.set(ip, []);
    }

    const userRequests = requests.get(ip).filter(t => t > windowStart);
    requests.set(ip, userRequests);

    if (userRequests.length >= maxRequests) {
      const resetTime = Math.ceil((userRequests[0] + windowMs - now) / 1000);
      res.set('X-RateLimit-Limit', maxRequests);
      res.set('X-RateLimit-Remaining', 0);
      res.set('X-RateLimit-Reset', resetTime);
      return res.status(429).json({
        error: 'Trop de requêtes',
        retryAfter: resetTime
      });
    }

    userRequests.push(now);
    res.set('X-RateLimit-Limit', maxRequests);
    res.set('X-RateLimit-Remaining', maxRequests - userRequests.length);
    next();
  };
}

// Appliquer globalement
app.use(rateLimit({ maxRequests: 100, windowMs: 60000 }));

// Ou seulement sur les routes d'auth
app.post('/auth/login', rateLimit({ maxRequests: 5, windowMs: 900000 }), loginHandler);

/*
4. MIDDLEWARE CORS MANUEL
*/

function corsMiddleware(options = {}) {
  const {
    origins = '*',
    methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    headers = ['Content-Type', 'Authorization']
  } = options;

  return (req, res, next) => {
    const origin = req.headers.origin;

    if (origins === '*') {
      res.set('Access-Control-Allow-Origin', '*');
    } else if (Array.isArray(origins) && origins.includes(origin)) {
      res.set('Access-Control-Allow-Origin', origin);
    }

    res.set('Access-Control-Allow-Methods', methods.join(', '));
    res.set('Access-Control-Allow-Headers', headers.join(', '));
    res.set('Access-Control-Max-Age', '86400');

    // Requête preflight OPTIONS -> Répondre immédiatement
    if (req.method === 'OPTIONS') {
      return res.status(204).send();
    }

    next();
  };
}

app.use(corsMiddleware({ origins: ['http://localhost:3001', 'https://monapp.com'] }));


// ----------------------------------------------------------------------------
// [PACKAGE] MIDDLEWARES TIERS POPULAIRES
// ----------------------------------------------------------------------------

/*
INSTALLER :
npm install cors helmet morgan compression cookie-parser express-rate-limit
*/

// 1. CORS
const cors = require('cors');
app.use(cors({
  origin: ['http://localhost:3001', 'https://monapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true
}));

// 2. HELMET - Headers de sécurité
const helmet = require('helmet');
app.use(helmet());

// 3. MORGAN - Logging HTTP
const morgan = require('morgan');
app.use(morgan('combined'));      // Log complet (production)
app.use(morgan('dev'));           // Log développement coloré

// Log personnalisé
app.use(morgan(':method :url :status :response-time ms - :res[content-length]'));

// 4. COMPRESSION - Gzip
const compression = require('compression');
app.use(compression());

// 5. COOKIE-PARSER
const cookieParser = require('cookie-parser');
app.use(cookieParser());

// Lire cookies
app.get('/cookies', (req, res) => {
  console.log(req.cookies);
  res.cookie('session', 'abc123', { httpOnly: true });
  res.json({ cookies: req.cookies });
});

// 6. EXPRESS-RATE-LIMIT
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,                   // 100 requêtes par window
  message: { error: 'Trop de requêtes, réessayez dans 15 minutes' },
  standardHeaders: true,
  legacyHeaders: false
});

app.use(limiter);

// 7. MULTER - Upload de fichiers
const multer = require('multer');

const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, 'uploads/'),
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, uniqueSuffix + path.extname(file.originalname));
  }
});

const upload = multer({
  storage,
  limits: { fileSize: 10 * 1024 * 1024 },  // 10 MB
  fileFilter: (req, file, cb) => {
    if (file.mimetype.startsWith('image/')) {
      cb(null, true);
    } else {
      cb(new Error('Seules les images sont acceptées'));
    }
  }
});

app.post('/upload', upload.single('photo'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'Aucun fichier' });
  }
  res.json({ message: 'Uploadé !', file: req.file.filename });
});

// Upload multiple
app.post('/upload-many', upload.array('photos', 10), (req, res) => {
  res.json({ files: req.files.map(f => f.filename) });
});


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 7 : Middleware Stack Complet
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer une API avec un stack de middlewares professionnel

MIDDLEWARES À CRÉER :
1. requestId - Ajouter un ID unique à chaque requête
2. timing - Mesurer le temps de réponse
3. apiKey - Vérifier une clé API dans le header X-API-Key
4. parseFilters - Parser les filtres de query string en objet structuré
5. paginate - Normaliser la pagination

CORRIGÉ :
*/

// middlewares/index.js
const { v4: uuidv4 } = require('uuid');  // npm install uuid

// 1. Request ID
const requestId = (req, res, next) => {
  req.id = uuidv4();
  res.set('X-Request-ID', req.id);
  next();
};

// 2. Timing
const timing = (req, res, next) => {
  const start = process.hrtime.bigint();
  
  res.on('finish', () => {
    const end = process.hrtime.bigint();
    const durationMs = Number(end - start) / 1e6;
    console.log(`[${req.id?.slice(0, 8)}] ${req.method} ${req.path} -> ${res.statusCode} (${durationMs.toFixed(2)}ms)`);
  });
  
  next();
};

// 3. API Key
const VALID_KEYS = new Set(['key-dev-123', 'key-prod-456']);

const apiKey = (req, res, next) => {
  const key = req.headers['x-api-key'];
  
  if (!key) {
    return res.status(401).json({
      error: 'Clé API manquante',
      hint: 'Ajouter X-API-Key dans les headers'
    });
  }
  
  if (!VALID_KEYS.has(key)) {
    return res.status(403).json({ error: 'Clé API invalide' });
  }
  
  next();
};

// 4. Parse Filters
const parseFilters = (req, res, next) => {
  const { filter, sort, fields } = req.query;
  
  req.filters = {};
  req.sort = {};
  req.fields = null;
  
  // Exemple: ?filter=age:>:18,name:like:alice
  if (filter) {
    filter.split(',').forEach(condition => {
      const [field, op, value] = condition.split(':');
      req.filters[field] = { op, value };
    });
  }
  
  // Exemple: ?sort=name:asc,age:desc
  if (sort) {
    sort.split(',').forEach(s => {
      const [field, order = 'asc'] = s.split(':');
      req.sort[field] = order;
    });
  }
  
  // Exemple: ?fields=id,name,email
  if (fields) {
    req.fields = fields.split(',');
  }
  
  next();
};

// 5. Pagination
const paginate = (req, res, next) => {
  const page = Math.max(1, parseInt(req.query.page) || 1);
  const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 10));
  const offset = (page - 1) * limit;
  
  req.pagination = { page, limit, offset };
  
  // Helper pour créer les métadonnées de pagination
  res.paginate = (data, total) => {
    const totalPages = Math.ceil(total / limit);
    res.json({
      data,
      pagination: {
        page,
        limit,
        total,
        totalPages,
        hasNext: page < totalPages,
        hasPrev: page > 1
      }
    });
  };
  
  next();
};

module.exports = { requestId, timing, apiKey, parseFilters, paginate };

// app.js
const express = require('express');
const { requestId, timing, apiKey, parseFilters, paginate } = require('./middlewares');

const app = express();
app.use(express.json());
app.use(requestId);
app.use(timing);
app.use(cors());
app.use(helmet());

// Routes protégées par API key
const apiRouter = express.Router();
apiRouter.use(apiKey);
apiRouter.use(parseFilters);
apiRouter.use(paginate);

// Données de test
const products = Array.from({ length: 50 }, (_, i) => ({
  id: i + 1,
  name: `Produit ${i + 1}`,
  price: Math.floor(Math.random() * 1000),
  category: ['electronics', 'books', 'clothing'][i % 3]
}));

apiRouter.get('/products', (req, res) => {
  const { page, limit, offset } = req.pagination;
  
  let filtered = [...products];
  
  // Appliquer les filtres
  for (const [field, { op, value }] of Object.entries(req.filters)) {
    filtered = filtered.filter(p => {
      if (op === 'like') return String(p[field]).toLowerCase().includes(value.toLowerCase());
      if (op === '>') return p[field] > Number(value);
      if (op === '<') return p[field] < Number(value);
      if (op === '=') return String(p[field]) === value;
      return true;
    });
  }
  
  const total = filtered.length;
  const pageData = filtered.slice(offset, offset + limit);
  
  // Filtrer les champs si nécessaire
  const result = req.fields
    ? pageData.map(p => Object.fromEntries(req.fields.map(f => [f, p[f]])))
    : pageData;
  
  res.paginate(result, total);
});

app.use('/api', apiRouter);

app.listen(3000, () => console.log('API sur http://localhost:3000'));

/*
TEST :
curl -H "X-API-Key: key-dev-123" "http://localhost:3000/api/products?page=2&limit=5"
curl -H "X-API-Key: key-dev-123" "http://localhost:3000/api/products?filter=category:=:books&sort=price:desc"
curl -H "X-API-Key: key-dev-123" "http://localhost:3000/api/products?fields=id,name,price"
*/


// ============================================================================
// [GUIDE] CHAPITRE 8 : APIs REST AVEC EXPRESS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Concevoir une API REST complète
[OK] Respecter les conventions REST
[OK] Implémenter CRUD complet
[OK] Validation et sérialisation
[OK] Documentation API
*/


// ----------------------------------------------------------------------------
// [MESURE] CONVENTIONS REST
// ----------------------------------------------------------------------------

/*
REST = Representational State Transfer

PRINCIPES CLÉS :

1. RESSOURCES -> URLs parlantes (noms, pas verbes)
   [OK] /users, /posts, /products
   [X] /getUsers, /createPost, /deleteProduct

2. VERBES HTTP -> Actions
   GET    -> Lire (idempotent)
   POST   -> Créer
   PUT    -> Remplacer complètement
   PATCH  -> Modifier partiellement
   DELETE -> Supprimer

3. STATUS CODES -> Résultat
   200 OK            -> Succès GET/PUT/PATCH
   201 Created       -> Succès POST
   204 No Content    -> Succès DELETE
   400 Bad Request   -> Données invalides
   401 Unauthorized  -> Non authentifié
   403 Forbidden     -> Non autorisé
   404 Not Found     -> Ressource introuvable
   409 Conflict      -> Conflit (doublon)
   422 Unprocessable -> Validation échouée
   429 Too Many Req  -> Rate limit
   500 Internal Error -> Erreur serveur

4. REPRÉSENTATIONS -> JSON (le plus courant)


TABLE DE ROUTING REST STANDARD :

┌─────────────────────────┬────────────┬─────────────────────────┐
│ URL                     │ Méthode    │ Action                  │
├─────────────────────────┼────────────┼─────────────────────────┤
│ /users                  │ GET        │ Lister tous             │
│ /users                  │ POST       │ Créer un nouveau        │
│ /users/:id              │ GET        │ Récupérer un            │
│ /users/:id              │ PUT        │ Remplacer un            │
│ /users/:id              │ PATCH      │ Modifier partiellement  │
│ /users/:id              │ DELETE     │ Supprimer un            │
├─────────────────────────┼────────────┼─────────────────────────┤
│ /users/:id/posts        │ GET        │ Posts d'un utilisateur  │
│ /users/:id/posts        │ POST       │ Créer post pour user    │
│ /users/:id/posts/:postId│ GET        │ Post spécifique d'un user│
└─────────────────────────┴────────────┴─────────────────────────┘
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] API REST COMPLÈTE
// ----------------------------------------------------------------------------

// Structure du projet :
projet/
├── app.js
├── routes/
│   ├── users.js
│   └── posts.js
├── controllers/
│   ├── userController.js
│   └── postController.js
├── models/
│   ├── User.js
│   └── Post.js
├── middlewares/
│   ├── auth.js
│   └── validate.js
└── utils/
    └── response.js


// utils/response.js - Helpers standardisés
const successResponse = (res, data, options = {}) => {
  const { statusCode = 200, message = 'Succès', meta = null } = options;
  
  const response = { success: true, message, data };
  if (meta) response.meta = meta;
  
  return res.status(statusCode).json(response);
};

const errorResponse = (res, error, options = {}) => {
  const { statusCode = 500 } = options;
  
  return res.status(statusCode).json({
    success: false,
    error: typeof error === 'string' ? error : error.message,
    ...(process.env.NODE_ENV === 'development' && error.stack && { stack: error.stack })
  });
};

const paginatedResponse = (res, data, total, pagination) => {
  const { page, limit } = pagination;
  const totalPages = Math.ceil(total / limit);
  
  return res.status(200).json({
    success: true,
    data,
    pagination: {
      page,
      limit,
      total,
      totalPages,
      hasNext: page < totalPages,
      hasPrev: page > 1
    }
  });
};

module.exports = { successResponse, errorResponse, paginatedResponse };

// controllers/userController.js
const { successResponse, errorResponse, paginatedResponse } = require('../utils/response');

// "Base de données" en mémoire
let users = [];
let nextId = 1;

class UserController {
  // GET /users
  static async getAll(req, res) {
    try {
      const { page = 1, limit = 10, search, sort = 'createdAt', order = 'desc' } = req.query;
      
      let result = [...users];
      
      if (search) {
        result = result.filter(u =>
          u.name.toLowerCase().includes(search.toLowerCase()) ||
          u.email.toLowerCase().includes(search.toLowerCase())
        );
      }
      
      // Trier
      result.sort((a, b) => {
        const aVal = a[sort];
        const bVal = b[sort];
        return order === 'asc' ? aVal > bVal ? 1 : -1 : aVal < bVal ? 1 : -1;
      });
      
      const total = result.length;
      const pageNum = parseInt(page);
      const limitNum = parseInt(limit);
      const paginated = result.slice((pageNum - 1) * limitNum, pageNum * limitNum);
      
      return paginatedResponse(res, paginated, total, { page: pageNum, limit: limitNum });
    } catch (err) {
      return errorResponse(res, err);
    }
  }

  // GET /users/:id
  static async getOne(req, res) {
    try {
      const id = parseInt(req.params.id);
      const user = users.find(u => u.id === id);
      
      if (!user) {
        return errorResponse(res, 'Utilisateur non trouvé', { statusCode: 404 });
      }
      
      return successResponse(res, user);
    } catch (err) {
      return errorResponse(res, err);
    }
  }

  // POST /users
  static async create(req, res) {
    try {
      const { name, email, role = 'user' } = req.body;
      
      // Vérifier doublon email
      const exists = users.find(u => u.email === email);
      if (exists) {
        return errorResponse(res, 'Cet email est déjà utilisé', { statusCode: 409 });
      }
      
      const newUser = {
        id: nextId++,
        name: name.trim(),
        email: email.toLowerCase().trim(),
        role,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString()
      };
      
      users.push(newUser);
      return successResponse(res, newUser, { statusCode: 201, message: 'Utilisateur créé' });
    } catch (err) {
      return errorResponse(res, err);
    }
  }

  // PUT /users/:id (remplacement complet)
  static async replace(req, res) {
    try {
      const id = parseInt(req.params.id);
      const index = users.findIndex(u => u.id === id);
      
      if (index === -1) {
        return errorResponse(res, 'Utilisateur non trouvé', { statusCode: 404 });
      }
      
      const { name, email, role } = req.body;
      
      // PUT = remplacement total (sauf id, createdAt)
      users[index] = {
        id,
        name: name.trim(),
        email: email.toLowerCase().trim(),
        role,
        createdAt: users[index].createdAt,
        updatedAt: new Date().toISOString()
      };
      
      return successResponse(res, users[index], { message: 'Utilisateur remplacé' });
    } catch (err) {
      return errorResponse(res, err);
    }
  }

  // PATCH /users/:id (modification partielle)
  static async update(req, res) {
    try {
      const id = parseInt(req.params.id);
      const index = users.findIndex(u => u.id === id);
      
      if (index === -1) {
        return errorResponse(res, 'Utilisateur non trouvé', { statusCode: 404 });
      }
      
      // PATCH = uniquement ce qui est envoyé
      const allowed = ['name', 'email', 'role'];
      const updates = {};
      
      for (const key of allowed) {
        if (req.body[key] !== undefined) {
          updates[key] = req.body[key];
        }
      }
      
      users[index] = {
        ...users[index],
        ...updates,
        updatedAt: new Date().toISOString()
      };
      
      return successResponse(res, users[index], { message: 'Utilisateur modifié' });
    } catch (err) {
      return errorResponse(res, err);
    }
  }

  // DELETE /users/:id
  static async remove(req, res) {
    try {
      const id = parseInt(req.params.id);
      const index = users.findIndex(u => u.id === id);
      
      if (index === -1) {
        return errorResponse(res, 'Utilisateur non trouvé', { statusCode: 404 });
      }
      
      users.splice(index, 1);
      return res.status(204).send();
    } catch (err) {
      return errorResponse(res, err);
    }
  }
}

module.exports = UserController;

// routes/users.js
const express = require('express');
const router = express.Router();
const UserController = require('../controllers/userController');

router.get('/', UserController.getAll);
router.get('/:id', UserController.getOne);
router.post('/', UserController.create);
router.put('/:id', UserController.replace);
router.patch('/:id', UserController.update);
router.delete('/:id', UserController.remove);

module.exports = router;


// ============================================================================
// [GUIDE] CHAPITRE 9 : GESTION DES ERREURS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer des classes d'erreurs personnalisées
[OK] Utiliser le middleware d'erreur Express
[OK] Gérer les erreurs async
[OK] Centraliser la gestion des erreurs
[OK] Erreurs de validation (Joi/Zod)
*/


// ----------------------------------------------------------------------------
// [ATTENTION] MIDDLEWARE D'ERREUR EXPRESS
// ----------------------------------------------------------------------------

/*
RÈGLE : Middleware avec 4 paramètres = Middleware d'erreur
  (err, req, res, next) -> Middleware d'erreur
  (req, res, next)      -> Middleware normal

Le middleware d'erreur doit être DÉCLARÉ EN DERNIER !
*/

// Erreurs personnalisées
class AppError extends Error {
  constructor(message, statusCode = 500, isOperational = true) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.isOperational = isOperational;  // Erreur "prévue" vs "bug"
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Ressource') {
    super(`${resource} non trouvé(e)`, 404);
  }
}

class ValidationError extends AppError {
  constructor(message, details = []) {
    super(message, 422);
    this.details = details;
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Non authentifié') {
    super(message, 401);
  }
}

class ForbiddenError extends AppError {
  constructor(message = 'Accès refusé') {
    super(message, 403);
  }
}

class ConflictError extends AppError {
  constructor(message = 'Conflit de données') {
    super(message, 409);
  }
}

// Middleware d'erreur centralisé
function errorHandler(err, req, res, next) {
  // Log l'erreur
  if (err.isOperational) {
    console.warn(`[ATTENTION] ${err.name}: ${err.message}`);
  } else {
    console.error('[ROUGE] ERREUR CRITIQUE:', err.stack);
  }

  // Erreurs de validation Mongoose
  if (err.name === 'ValidationError') {
    const details = Object.values(err.errors).map(e => ({
      field: e.path,
      message: e.message
    }));
    return res.status(422).json({ success: false, error: 'Validation échouée', details });
  }

  // Erreurs JWT
  if (err.name === 'JsonWebTokenError') {
    return res.status(401).json({ success: false, error: 'Token invalide' });
  }

  if (err.name === 'TokenExpiredError') {
    return res.status(401).json({ success: false, error: 'Token expiré' });
  }

  // Erreurs de syntaxe JSON
  if (err instanceof SyntaxError && err.status === 400) {
    return res.status(400).json({ success: false, error: 'JSON invalide' });
  }

  // Erreurs personnalisées
  if (err instanceof AppError) {
    const response = {
      success: false,
      error: err.message,
      ...(err.details && { details: err.details })
    };

    // Stack trace en développement seulement
    if (process.env.NODE_ENV === 'development') {
      response.stack = err.stack;
    }

    return res.status(err.statusCode).json(response);
  }

  // Erreur inconnue
  res.status(500).json({
    success: false,
    error: process.env.NODE_ENV === 'production'
      ? 'Erreur interne du serveur'
      : err.message
  });
}

// Gestion des routes inexistantes
function notFoundHandler(req, res, next) {
  next(new NotFoundError(`Route ${req.method} ${req.path}`));
}


// ----------------------------------------------------------------------------
// [SYNC] GÉRER LES ERREURS ASYNC
// ----------------------------------------------------------------------------

/*
PROBLÈME : Les erreurs async ne sont PAS attrapées automatiquement
*/

// [X] PROBLÈME
app.get('/users', async (req, res, next) => {
  const users = await User.findAll();  // Si ça throw, crash !
  res.json(users);
});

// [OK] SOLUTION 1 : try/catch partout (verbeux)
app.get('/users', async (req, res, next) => {
  try {
    const users = await User.findAll();
    res.json(users);
  } catch (err) {
    next(err);  // Passer à errorHandler
  }
});

// [OK] SOLUTION 2 : Wrapper asyncHandler (RECOMMANDÉ)
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Utilisation
app.get('/users', asyncHandler(async (req, res) => {
  const users = await User.findAll();
  res.json(users);
}));

// Pas besoin de try/catch ! Erreurs automatiquement passées à next()

// [OK] SOLUTION 3 : Express 5 (beta - gère async natifement)
// Pas encore stable, mais en cours

// Application complète avec gestion d'erreurs
const express = require('express');
const app = express();

app.use(express.json());

// Routes
app.get('/users/:id', asyncHandler(async (req, res) => {
  const id = parseInt(req.params.id);
  if (isNaN(id)) throw new ValidationError('ID invalide');
  
  const user = await findUserById(id);
  if (!user) throw new NotFoundError('Utilisateur');
  
  res.json({ success: true, data: user });
}));

app.post('/users', asyncHandler(async (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    throw new ValidationError('Champs manquants', [
      !name && { field: 'name', message: 'requis' },
      !email && { field: 'email', message: 'requis' }
    ].filter(Boolean));
  }
  
  // Vérifier doublon
  const exists = await findUserByEmail(email);
  if (exists) throw new ConflictError('Email déjà utilisé');
  
  const user = await createUser({ name, email });
  res.status(201).json({ success: true, data: user });
}));

// ORDRE IMPORTANT : not-found AVANT error handler
app.use(notFoundHandler);   // 404
app.use(errorHandler);       // Erreurs générales

app.listen(3000);


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 9 : API avec Gestion d'Erreurs Complète
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer une API de gestion d'articles de blog avec gestion d'erreurs professionnelle

ENDPOINTS :
GET    /api/articles        -> Lister avec pagination et filtres
GET    /api/articles/:id    -> Récupérer un article
POST   /api/articles        -> Créer (authentification requise)
PATCH  /api/articles/:id    -> Modifier (seulement l'auteur)
DELETE /api/articles/:id    -> Supprimer (auteur ou admin)
GET    /api/articles/:id/related -> Articles similaires (même tag)

GESTION D'ERREURS :
- Article non trouvé -> 404
- Données invalides -> 422 avec détails
- Non authentifié -> 401
- Pas propriétaire -> 403
- Slug dupliqué -> 409

CORRIGÉ :
*/

// errors/index.js
class AppError extends Error {
  constructor(message, statusCode = 500, details = null) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.details = details;
    this.isOperational = true;
  }
}

class NotFoundError extends AppError {
  constructor(resource) { super(`${resource} non trouvé(e)`, 404); }
}

class ValidationError extends AppError {
  constructor(details) { super('Validation échouée', 422, details); }
}

class UnauthorizedError extends AppError {
  constructor(msg = 'Authentification requise') { super(msg, 401); }
}

class ForbiddenError extends AppError {
  constructor(msg = 'Action non autorisée') { super(msg, 403); }
}

class ConflictError extends AppError {
  constructor(msg) { super(msg, 409); }
}

module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError, ConflictError };

// controllers/articleController.js
const {
  NotFoundError, ValidationError, UnauthorizedError, ForbiddenError, ConflictError
} = require('../errors');

let articles = [];
let nextId = 1;

// Génère un slug unique depuis le titre
function generateSlug(title) {
  return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
}

// Validation d'un article
function validateArticle(data) {
  const errors = [];
  
  if (!data.title?.trim()) errors.push({ field: 'title', message: 'requis' });
  else if (data.title.length < 5) errors.push({ field: 'title', message: 'minimum 5 caractères' });
  else if (data.title.length > 200) errors.push({ field: 'title', message: 'maximum 200 caractères' });
  
  if (!data.content?.trim()) errors.push({ field: 'content', message: 'requis' });
  else if (data.content.length < 50) errors.push({ field: 'content', message: 'minimum 50 caractères' });
  
  if (data.tags && !Array.isArray(data.tags)) errors.push({ field: 'tags', message: 'doit être un tableau' });
  
  if (errors.length > 0) throw new ValidationError(errors);
}

const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

const ArticleController = {
  getAll: asyncHandler(async (req, res) => {
    const { page = 1, limit = 10, tag, author, q } = req.query;
    let result = [...articles];
    
    if (tag) result = result.filter(a => a.tags.includes(tag));
    if (author) result = result.filter(a => a.authorId === parseInt(author));
    if (q) {
      const search = q.toLowerCase();
      result = result.filter(a =>
        a.title.toLowerCase().includes(search) || a.content.toLowerCase().includes(search)
      );
    }
    
    const total = result.length;
    const p = parseInt(page), l = Math.min(100, parseInt(limit));
    const data = result.slice((p - 1) * l, p * l);
    
    res.json({
      success: true,
      data,
      pagination: { page: p, limit: l, total, totalPages: Math.ceil(total / l) }
    });
  }),

  getOne: asyncHandler(async (req, res) => {
    const id = parseInt(req.params.id);
    if (isNaN(id)) throw new ValidationError([{ field: 'id', message: 'doit être un entier' }]);
    
    const article = articles.find(a => a.id === id);
    if (!article) throw new NotFoundError('Article');
    
    res.json({ success: true, data: article });
  }),

  create: asyncHandler(async (req, res) => {
    if (!req.user) throw new UnauthorizedError();
    
    validateArticle(req.body);
    
    const { title, content, tags = [] } = req.body;
    const slug = generateSlug(title);
    
    if (articles.find(a => a.slug === slug)) {
      throw new ConflictError(`Slug '${slug}' déjà utilisé`);
    }
    
    const article = {
      id: nextId++,
      title: title.trim(),
      slug,
      content: content.trim(),
      tags,
      authorId: req.user.id,
      authorName: req.user.name,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };
    
    articles.push(article);
    res.status(201).json({ success: true, data: article, message: 'Article créé' });
  }),

  update: asyncHandler(async (req, res) => {
    if (!req.user) throw new UnauthorizedError();
    
    const id = parseInt(req.params.id);
    const index = articles.findIndex(a => a.id === id);
    if (index === -1) throw new NotFoundError('Article');
    
    const article = articles[index];
    if (article.authorId !== req.user.id && req.user.role !== 'admin') {
      throw new ForbiddenError('Vous ne pouvez modifier que vos propres articles');
    }
    
    const { title, content, tags } = req.body;
    const updates = {};
    
    if (title !== undefined) {
      if (title.trim().length < 5) throw new ValidationError([{ field: 'title', message: 'minimum 5 caractères' }]);
      updates.title = title.trim();
      updates.slug = generateSlug(title);
      
      const slugExists = articles.find((a, i) => i !== index && a.slug === updates.slug);
      if (slugExists) throw new ConflictError(`Slug '${updates.slug}' déjà utilisé`);
    }
    
    if (content !== undefined) {
      if (content.trim().length < 50) throw new ValidationError([{ field: 'content', message: 'minimum 50 caractères' }]);
      updates.content = content.trim();
    }
    
    if (tags !== undefined) updates.tags = tags;
    
    articles[index] = { ...article, ...updates, updatedAt: new Date().toISOString() };
    res.json({ success: true, data: articles[index] });
  }),

  remove: asyncHandler(async (req, res) => {
    if (!req.user) throw new UnauthorizedError();
    
    const id = parseInt(req.params.id);
    const index = articles.findIndex(a => a.id === id);
    if (index === -1) throw new NotFoundError('Article');
    
    if (articles[index].authorId !== req.user.id && req.user.role !== 'admin') {
      throw new ForbiddenError('Vous ne pouvez supprimer que vos propres articles');
    }
    
    articles.splice(index, 1);
    res.status(204).send();
  }),

  related: asyncHandler(async (req, res) => {
    const id = parseInt(req.params.id);
    const article = articles.find(a => a.id === id);
    if (!article) throw new NotFoundError('Article');
    
    const related = articles
      .filter(a => a.id !== id && a.tags.some(t => article.tags.includes(t)))
      .slice(0, 5);
    
    res.json({ success: true, data: related, count: related.length });
  })
};

module.exports = ArticleController;

// app.js final
const express = require('express');
const ArticleController = require('./controllers/articleController');
const { AppError } = require('./errors');

const app = express();
app.use(express.json());

// Simuler authentification
app.use((req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (token === 'admin-token') req.user = { id: 1, name: 'Alice', role: 'admin' };
  else if (token === 'user-token') req.user = { id: 2, name: 'Bob', role: 'user' };
  next();
});

// Routes
app.get('/api/articles', ArticleController.getAll);
app.get('/api/articles/:id/related', ArticleController.related);
app.get('/api/articles/:id', ArticleController.getOne);
app.post('/api/articles', ArticleController.create);
app.patch('/api/articles/:id', ArticleController.update);
app.delete('/api/articles/:id', ArticleController.remove);

// 404
app.use((req, res, next) => next(new AppError(`Route ${req.method} ${req.path} introuvable`, 404)));

// Error handler
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json({
    success: false,
    error: err.message,
    ...(err.details && { details: err.details }),
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
});

app.listen(3000, () => console.log('Blog API sur http://localhost:3000'));


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

/*
[OK] CE QUE VOUS AVEZ APPRIS

Chapitre 5 : Serveur HTTP natif
[OK] http.createServer()
[OK] Objets req et res
[OK] Routing manuel
[OK] Lire le body de requête

Chapitre 6 : Express.js Fondamentaux
[OK] Installation et configuration
[OK] Routing (GET, POST, PUT, DELETE)
[OK] Paramètres URL et query string
[OK] Express Router

Chapitre 7 : Middleware
[OK] Concept et ordre d'exécution
[OK] Middlewares personnalisés
[OK] Middlewares tiers (cors, helmet, morgan)
[OK] Rate limiting, auth, validation

Chapitre 8 : APIs REST
[OK] Conventions REST
[OK] Architecture MVC (controllers)
[OK] CRUD complet
[OK] Réponses standardisées

Chapitre 9 : Gestion des erreurs
[OK] Classes d'erreurs personnalisées
[OK] Middleware d'erreur centralisé
[OK] asyncHandler wrapper
[OK] Gestion des erreurs en production

[OBJECTIF] PROJETS SUGGÉRÉS APRÈS PARTIE 2
-> API de gestion de librairie
-> Clone d'API Reddit simplifié
-> API de gestion de projets (Trello-like)

-> PROCHAINE ÉTAPE : nodejs_partie3.txt
-> Bases de données, Auth JWT, WebSockets, Tests
*/

# ============================================================================
# [LIVRE] NODE.JS - PARTIE 3 : DONNÉES ET FONCTIONNALITÉS AVANCÉES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 10 : Bases de données (MongoDB/Mongoose, PostgreSQL/pg)
# - Chapitre 11 : Authentification (JWT, bcrypt, sessions)
# - Chapitre 12 : Streams et Buffers
# - Chapitre 13 : WebSockets avec Socket.io
# - Chapitre 14 : Tests (Jest, Supertest)
#
# [TEMPS] TEMPS : ~10-12 heures
# [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
# ============================================================================


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

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Connecter Node.js à MongoDB avec Mongoose
[OK] Créer des schemas et modèles Mongoose
[OK] Opérations CRUD MongoDB
[OK] Connecter à PostgreSQL avec pg
[OK] Requêtes SQL depuis Node.js
[OK] Transactions et connexions pooling
*/


// ----------------------------------------------------------------------------
// [LEAF_FLUTTERING_IN_WIND] MONGODB AVEC MONGOOSE
// ----------------------------------------------------------------------------

/*
INSTALLATION :
npm install mongoose

DÉMARRER MONGODB :
- Local : mongod (si installé)
- Cloud : MongoDB Atlas (https://cloud.mongodb.com) - gratuit
*/

const mongoose = require('mongoose');

// CONNEXION
async function connectDB() {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp', {
      // Options recommandées (Mongoose 6+ les gère automatiquement)
    });

    console.log(`[OK] MongoDB connecté : ${conn.connection.host}`);

    // Événements de connexion
    mongoose.connection.on('error', err => console.error('[X] MongoDB erreur:', err));
    mongoose.connection.on('disconnected', () => console.warn('[ATTENTION] MongoDB déconnecté'));

  } catch (err) {
    console.error('[X] Connexion MongoDB échouée:', err.message);
    process.exit(1);
  }
}

// Appel au démarrage de l'app
connectDB();


// ----------------------------------------------------------------------------
// [GRAPHIQUE] SCHEMAS ET MODÈLES MONGOOSE
// ----------------------------------------------------------------------------

/*
Schema = Structure d'un document
Modèle = Classe pour interagir avec une collection
*/

const { Schema, model, Types } = mongoose;

// Schéma de base
const userSchema = new Schema({
  // Types : String, Number, Boolean, Date, Array, ObjectId, Mixed
  
  name: {
    type: String,
    required: [true, 'Le nom est requis'],
    trim: true,
    minlength: [2, 'Minimum 2 caractères'],
    maxlength: [50, 'Maximum 50 caractères']
  },
  
  email: {
    type: String,
    required: [true, 'L\'email est requis'],
    unique: true,
    lowercase: true,   // Automatiquement en lowercase
    trim: true,
    match: [/^\S+@\S+\.\S+$/, 'Format email invalide']
  },
  
  password: {
    type: String,
    required: true,
    minlength: 8,
    select: false    // Jamais retourné dans les requêtes par défaut
  },
  
  age: {
    type: Number,
    min: [0, 'L\'âge ne peut pas être négatif'],
    max: [150, 'Âge irréaliste']
  },
  
  role: {
    type: String,
    enum: ['user', 'moderator', 'admin'],
    default: 'user'
  },
  
  avatar: String,  // Optionnel, type String court
  
  tags: [String],  // Tableau de strings
  
  address: {       // Objet imbriqué
    street: String,
    city: String,
    country: { type: String, default: 'France' },
    zipCode: String
  },
  
  isActive: {
    type: Boolean,
    default: true
  },
  
  lastLogin: Date,
  
  // Référence à une autre collection
  posts: [{
    type: Types.ObjectId,
    ref: 'Post'
  }]
}, {
  timestamps: true,       // Ajoute createdAt et updatedAt automatiquement
  versionKey: false        // Supprime __v
});


// INDEXES - Accélèrent les requêtes
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ name: 'text', 'address.city': 'text' });  // Full-text search
userSchema.index({ createdAt: -1 });    // -1 = décroissant
userSchema.index({ role: 1, isActive: 1 });  // Index composite


// VIRTUAL - Champs calculés (pas stockés en DB)
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

// Pour inclure les virtuals dans JSON
userSchema.set('toJSON', { virtuals: true });
userSchema.set('toObject', { virtuals: true });


// METHODS - Méthodes d'instance (sur un document)
userSchema.methods.greet = function() {
  return `Bonjour, je suis ${this.name}`;
};

userSchema.methods.hasRole = function(role) {
  if (role === 'admin') return this.role === 'admin';
  if (role === 'moderator') return ['admin', 'moderator'].includes(this.role);
  return true;
};


// STATICS - Méthodes de classe (sur le Modèle)
userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email: email.toLowerCase() });
};

userSchema.statics.findActiveUsers = function() {
  return this.find({ isActive: true });
};


// MIDDLEWARE MONGOOSE (hooks pre/post)

// Avant de sauvegarder
userSchema.pre('save', async function(next) {
  // this = le document
  if (this.isModified('password')) {
    const bcrypt = require('bcrypt');
    this.password = await bcrypt.hash(this.password, 12);
  }
  next();
});

// Après suppression
userSchema.post('findOneAndDelete', async function(doc) {
  if (doc) {
    // Supprimer les posts associés
    await mongoose.model('Post').deleteMany({ author: doc._id });
    console.log(`Posts de ${doc.name} supprimés`);
  }
});


// CRÉER LE MODÈLE
const User = model('User', userSchema);

module.exports = User;


// ----------------------------------------------------------------------------
// [RECHERCHE] OPÉRATIONS CRUD MONGOOSE
// ----------------------------------------------------------------------------

const User = require('./models/User');

/*
CREATE
*/

// Méthode 1 : new + save()
const user = new User({
  name: 'Alice',
  email: 'alice@example.com',
  password: 'password123',
  role: 'user'
});

await user.save();
console.log(user._id);  // ObjectId généré

// Méthode 2 : create() (plus simple)
const user = await User.create({
  name: 'Bob',
  email: 'bob@example.com',
  password: 'password456'
});

// Créer plusieurs
const users = await User.create([
  { name: 'Alice', email: 'alice@example.com', password: 'pass1' },
  { name: 'Bob', email: 'bob@example.com', password: 'pass2' }
]);


/*
READ
*/

// Trouver tous
const allUsers = await User.find();

// Avec conditions
const activeAdmins = await User.find({ role: 'admin', isActive: true });

// Par ID
const user = await User.findById('60d...');

// Par ID ou null
const user = await User.findById(id).exec();

// Premier résultat
const user = await User.findOne({ email: 'alice@example.com' });

// Opérateurs de comparaison
const users = await User.find({
  age: { $gte: 18, $lte: 65 },          // 18 <= age <= 65
  role: { $in: ['admin', 'moderator'] }, // role dans la liste
  name: { $regex: /^A/i },              // Commence par A
  'address.city': { $exists: true }      // Champ existe
});

// Select (choisir colonnes)
const users = await User.find()
  .select('name email role -_id');       // + pour inclure, - pour exclure

// Sort
const users = await User.find()
  .sort({ createdAt: -1, name: 1 });     // -1 = desc, 1 = asc

// Pagination
const page = 2, limit = 10;
const users = await User.find()
  .skip((page - 1) * limit)
  .limit(limit);

// Compter
const total = await User.countDocuments({ isActive: true });

// Populate (jointure)
const user = await User.findById(id)
  .populate('posts', 'title createdAt');    // Charger les posts

// Populate imbriqué
const user = await User.findById(id)
  .populate({
    path: 'posts',
    select: 'title content',
    populate: {
      path: 'comments',
      select: 'text author'
    }
  });


/*
UPDATE
*/

// Trouver et modifier (retourne le NOUVEAU document)
const updated = await User.findByIdAndUpdate(
  id,
  { $set: { name: 'Alice Updated', 'address.city': 'Paris' } },
  { new: true, runValidators: true }  // new: true = retourner le nouveau doc
);

// Opérateurs de mise à jour
await User.findByIdAndUpdate(id, {
  $set: { name: 'Nouveau nom' },      // Modifier
  $unset: { avatar: 1 },              // Supprimer champ
  $push: { tags: 'nodejs' },           // Ajouter à tableau
  $pull: { tags: 'old-tag' },          // Retirer du tableau
  $addToSet: { tags: 'unique-tag' },   // Ajouter si pas déjà présent
  $inc: { loginCount: 1 }              // Incrémenter
});

// Modifier plusieurs
await User.updateMany(
  { role: 'user', createdAt: { $lt: new Date('2020-01-01') } },
  { $set: { isActive: false } }
);

// Modifier sur instance
const user = await User.findById(id);
user.name = 'Nouveau nom';
user.lastLogin = new Date();
await user.save();  // Déclenche les middleware pre/post save


/*
DELETE
*/

// Supprimer par ID
await User.findByIdAndDelete(id);

// Supprimer premier correspondant
await User.findOneAndDelete({ email: 'spam@example.com' });

// Supprimer plusieurs
await User.deleteMany({ isActive: false });


// ----------------------------------------------------------------------------
// [POSTGRES] POSTGRESQL AVEC PG
// ----------------------------------------------------------------------------

/*
npm install pg pg-hstore

DEUX APPROCHES :
1. pg (pg-pool) - SQL direct
2. Sequelize - ORM
*/

const { Pool } = require('pg');

// Pool de connexions (recommandé pour production)
const pool = new Pool({
  host: process.env.DB_HOST || 'localhost',
  port: process.env.DB_PORT || 5432,
  database: process.env.DB_NAME || 'myapp',
  user: process.env.DB_USER || 'postgres',
  password: process.env.DB_PASSWORD,
  max: 20,              // Max 20 connexions dans le pool
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});

// Tester la connexion
async function testConnection() {
  const client = await pool.connect();
  try {
    const result = await client.query('SELECT NOW() as current_time');
    console.log('[OK] PostgreSQL connecté :', result.rows[0].current_time);
  } finally {
    client.release();  // [ATTENTION] Toujours release !
  }
}

// Helper de requête (utilise le pool automatiquement)
const query = (text, params) => pool.query(text, params);


// OPÉRATIONS CRUD POSTGRESQL
// ============================================

// CREATE TABLE (migration)
await query(`
  CREATE TABLE IF NOT EXISTS users (
    id          SERIAL PRIMARY KEY,
    name        VARCHAR(50) NOT NULL,
    email       VARCHAR(120) UNIQUE NOT NULL,
    password    VARCHAR(128) NOT NULL,
    role        VARCHAR(20) DEFAULT 'user',
    is_active   BOOLEAN DEFAULT true,
    created_at  TIMESTAMP DEFAULT NOW(),
    updated_at  TIMESTAMP DEFAULT NOW()
  )
`);

// INSERT
const result = await query(
  `INSERT INTO users (name, email, password, role)
   VALUES ($1, $2, $3, $4)
   RETURNING *`,
  ['Alice', 'alice@example.com', hashedPassword, 'user']
);
const newUser = result.rows[0];

// SELECT
const { rows: users } = await query(
  `SELECT id, name, email, role, created_at
   FROM users
   WHERE is_active = true
   ORDER BY created_at DESC
   LIMIT $1 OFFSET $2`,
  [limit, offset]
);

// SELECT avec JOIN
const { rows: posts } = await query(
  `SELECT p.id, p.title, p.content, p.created_at,
          u.name as author_name, u.email as author_email
   FROM posts p
   INNER JOIN users u ON p.user_id = u.id
   WHERE p.id = $1`,
  [postId]
);

// UPDATE
const { rows: [updated] } = await query(
  `UPDATE users
   SET name = $1, updated_at = NOW()
   WHERE id = $2
   RETURNING *`,
  [newName, userId]
);

// DELETE
await query('DELETE FROM users WHERE id = $1', [userId]);

// COUNT
const { rows: [{ count }] } = await query(
  'SELECT COUNT(*) FROM users WHERE is_active = true'
);
const total = parseInt(count);


// TRANSACTIONS
// ============================================

async function transferMoney(fromId, toId, amount) {
  const client = await pool.connect();
  
  try {
    await client.query('BEGIN');  // Démarrer transaction
    
    // Vérifier solde suffisant
    const { rows: [sender] } = await client.query(
      'SELECT balance FROM accounts WHERE id = $1 FOR UPDATE',
      [fromId]
    );
    
    if (sender.balance < amount) {
      throw new Error('Solde insuffisant');
    }
    
    // Débiter
    await client.query(
      'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
      [amount, fromId]
    );
    
    // Créditer
    await client.query(
      'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
      [amount, toId]
    );
    
    // Log de transaction
    await client.query(
      `INSERT INTO transactions (from_id, to_id, amount, created_at)
       VALUES ($1, $2, $3, NOW())`,
      [fromId, toId, amount]
    );
    
    await client.query('COMMIT');  // Confirmer
    console.log('[OK] Transfert réussi');
    
  } catch (err) {
    await client.query('ROLLBACK');  // Annuler tout !
    console.error('[X] Transaction annulée:', err.message);
    throw err;
  } finally {
    client.release();
  }
}


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 10 : API avec MongoDB et Mongoose
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer une API de gestion d'une bibliothèque

MODÈLES :
- Book : title, author, isbn, genre, year, availableCopies
- Member : name, email, memberNumber, borrowedBooks[]
- Loan : book, member, borrowedAt, dueDate, returnedAt

ENDPOINTS :
GET    /api/books         -> Lister (filtres: genre, year, available)
POST   /api/books         -> Ajouter un livre
POST   /api/loans         -> Emprunter un livre (décrémente availableCopies)
PUT    /api/loans/:id/return -> Rendre un livre
GET    /api/members/:id/history -> Historique d'emprunts

CORRIGÉ :
*/

// models/Book.js
const mongoose = require('mongoose');

const bookSchema = new mongoose.Schema({
  title: { type: String, required: true, trim: true },
  author: { type: String, required: true, trim: true },
  isbn: { type: String, unique: true, required: true },
  genre: {
    type: String,
    enum: ['fiction', 'non-fiction', 'science', 'history', 'biography', 'children', 'other'],
    default: 'other'
  },
  year: { type: Number, min: 1000, max: new Date().getFullYear() },
  totalCopies: { type: Number, default: 1, min: 0 },
  availableCopies: { type: Number, default: 1, min: 0 }
}, { timestamps: true });

bookSchema.virtual('available').get(function() {
  return this.availableCopies > 0;
});

bookSchema.set('toJSON', { virtuals: true });

module.exports = mongoose.model('Book', bookSchema);

// models/Loan.js
const loanSchema = new mongoose.Schema({
  book: { type: mongoose.Types.ObjectId, ref: 'Book', required: true },
  member: { type: mongoose.Types.ObjectId, ref: 'Member', required: true },
  borrowedAt: { type: Date, default: Date.now },
  dueDate: { type: Date, required: true },
  returnedAt: { type: Date, default: null },
  status: {
    type: String,
    enum: ['active', 'returned', 'overdue'],
    default: 'active'
  }
}, { timestamps: true });

// Calculer si overdue automatiquement
loanSchema.pre('find', function() {
  this.where({ returnedAt: null, dueDate: { $lt: new Date() } })
    .updateMany({ status: 'overdue' });
});

module.exports = mongoose.model('Loan', loanSchema);

// controllers/libraryController.js
const Book = require('../models/Book');
const Loan = require('../models/Loan');
const Member = require('../models/Member');

const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

// Lister les livres
const getBooks = asyncHandler(async (req, res) => {
  const { genre, year, available, search, page = 1, limit = 10 } = req.query;
  const filter = {};

  if (genre) filter.genre = genre;
  if (year) filter.year = parseInt(year);
  if (available === 'true') filter.availableCopies = { $gt: 0 };
  if (search) filter.$text = { $search: search };

  const [books, total] = await Promise.all([
    Book.find(filter)
      .sort({ title: 1 })
      .skip((page - 1) * limit)
      .limit(parseInt(limit)),
    Book.countDocuments(filter)
  ]);

  res.json({
    success: true,
    data: books,
    pagination: { page: parseInt(page), limit: parseInt(limit), total }
  });
});

// Emprunter un livre
const borrowBook = asyncHandler(async (req, res) => {
  const { bookId, memberId, dueDays = 14 } = req.body;

  // Vérifier livre disponible
  const book = await Book.findById(bookId);
  if (!book) throw new Error('Livre non trouvé');
  if (book.availableCopies <= 0) {
    return res.status(409).json({ success: false, error: 'Aucune copie disponible' });
  }

  // Vérifier que le membre n'a pas déjà ce livre
  const existingLoan = await Loan.findOne({
    book: bookId, member: memberId, status: 'active'
  });
  if (existingLoan) {
    return res.status(409).json({ success: false, error: 'Vous avez déjà ce livre emprunté' });
  }

  // Créer l'emprunt et décrémenter le stock
  const dueDate = new Date();
  dueDate.setDate(dueDate.getDate() + parseInt(dueDays));

  const [loan] = await Promise.all([
    Loan.create({ book: bookId, member: memberId, dueDate }),
    Book.findByIdAndUpdate(bookId, { $inc: { availableCopies: -1 } })
  ]);

  const populatedLoan = await loan.populate([
    { path: 'book', select: 'title author isbn' },
    { path: 'member', select: 'name email' }
  ]);

  res.status(201).json({ success: true, data: populatedLoan });
});

// Rendre un livre
const returnBook = asyncHandler(async (req, res) => {
  const loan = await Loan.findById(req.params.id);
  if (!loan) throw new Error('Emprunt non trouvé');
  if (loan.status === 'returned') {
    return res.status(400).json({ success: false, error: 'Livre déjà rendu' });
  }

  const isOverdue = new Date() > loan.dueDate;

  await Promise.all([
    Loan.findByIdAndUpdate(req.params.id, {
      returnedAt: new Date(),
      status: 'returned'
    }),
    Book.findByIdAndUpdate(loan.book, { $inc: { availableCopies: 1 } })
  ]);

  res.json({
    success: true,
    message: isOverdue ? 'Livre rendu (en retard)' : 'Livre rendu à temps',
    wasOverdue: isOverdue
  });
});

module.exports = { getBooks, borrowBook, returnBook };


// ============================================================================
// [GUIDE] CHAPITRE 11 : AUTHENTIFICATION (JWT, bcrypt)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Hasher les mots de passe avec bcrypt
[OK] Créer et vérifier des tokens JWT
[OK] Implémenter inscription/connexion
[OK] Protéger les routes avec JWT
[OK] Refresh tokens
[OK] Révoquer les tokens
*/


// ----------------------------------------------------------------------------
// [SECURISE] BCRYPT - HASHAGE DES MOTS DE PASSE
// ----------------------------------------------------------------------------

/*
npm install bcrypt (ou bcryptjs pour pure JS)

RÈGLE : JAMAIS stocker un mot de passe en clair !
bcrypt = Algorithme de hashage adaptatif (résistant aux brute force)
*/

const bcrypt = require('bcrypt');

/*
HASHING
*/

const SALT_ROUNDS = 12;  // 2^12 = 4096 itérations (recommandé)
// Plus élevé = Plus sécurisé mais Plus lent
// 10 : ~100ms, 12 : ~300ms, 14 : ~1200ms

async function hashPassword(plainPassword) {
  return bcrypt.hash(plainPassword, SALT_ROUNDS);
}

/*
VÉRIFICATION
*/

async function verifyPassword(plainPassword, hashedPassword) {
  return bcrypt.compare(plainPassword, hashedPassword);
}

/*
EXEMPLE COMPLET
*/

async function registerUser(name, email, password) {
  // 1. Hasher le mot de passe
  const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);

  // 2. Stocker l'utilisateur (hashedPassword, pas password !)
  const user = await User.create({
    name,
    email,
    password: hashedPassword
  });

  return user;
}

async function loginUser(email, password) {
  // 1. Trouver l'utilisateur (avec son mot de passe)
  const user = await User.findOne({ email }).select('+password');

  if (!user) {
    throw new Error('Email ou mot de passe incorrect');
  }

  // 2. Comparer les mots de passe
  const isMatch = await bcrypt.compare(password, user.password);

  if (!isMatch) {
    throw new Error('Email ou mot de passe incorrect');
  }

  return user;
}


// ----------------------------------------------------------------------------
// [TICKET] JWT - JSON WEB TOKENS
// ----------------------------------------------------------------------------

/*
npm install jsonwebtoken

JWT = Token auto-contenu et signé
Structure : header.payload.signature

POURQUOI JWT ?
[OK] Sans état (stateless) - pas de session serveur
[OK] Portable - fonctionne cross-domain
[OK] Auto-contenu - contient les données de l'utilisateur
[OK] Signé - impossible à falsifier

INCONVÉNIENTS :
[X] Impossible de révoquer avant expiration (sans liste noire)
[X] Taille plus grande qu'un ID de session
*/

const jwt = require('jsonwebtoken');

const JWT_SECRET = process.env.JWT_SECRET || 'votre-secret-ultra-long-et-aleatoire';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'refresh-secret-different';
const JWT_REFRESH_EXPIRES_IN = '30d';


// CRÉER UN TOKEN
function generateToken(payload) {
  return jwt.sign(payload, JWT_SECRET, {
    expiresIn: JWT_EXPIRES_IN,
    issuer: 'mon-app',       // Qui a créé le token
    audience: 'mon-app-clients'  // Pour qui
  });
}

function generateRefreshToken(payload) {
  return jwt.sign(payload, JWT_REFRESH_SECRET, {
    expiresIn: JWT_REFRESH_EXPIRES_IN
  });
}


// VÉRIFIER UN TOKEN
function verifyToken(token) {
  return jwt.verify(token, JWT_SECRET);
  // Throw si invalide ou expiré
}

function verifyRefreshToken(token) {
  return jwt.verify(token, JWT_REFRESH_SECRET);
}


// DÉCODER SANS VÉRIFIER (pas sécurisé !)
function decodeToken(token) {
  return jwt.decode(token);  // Ne vérifie PAS la signature
}


// ----------------------------------------------------------------------------
// [VERROUILLE] SYSTÈME D'AUTHENTIFICATION COMPLET
// ----------------------------------------------------------------------------

// middlewares/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({
      success: false,
      error: 'Token manquant. Format: Authorization: Bearer <token>'
    });
  }

  const token = authHeader.slice(7);

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.userId = decoded.userId;
    req.userRole = decoded.role;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ success: false, error: 'Token expiré' });
    }
    if (err.name === 'JsonWebTokenError') {
      return res.status(401).json({ success: false, error: 'Token invalide' });
    }
    return res.status(500).json({ success: false, error: 'Erreur interne' });
  }
}

// Charger l'utilisateur depuis le token
async function loadUser(req, res, next) {
  if (!req.userId) return next();
  
  try {
    req.user = await User.findById(req.userId).lean();
    next();
  } catch (err) {
    next(err);
  }
}

// Vérification de rôle
function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ success: false, error: 'Non authentifié' });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        error: `Rôle requis : ${roles.join(' ou ')}`
      });
    }
    
    next();
  };
}

module.exports = { authMiddleware, loadUser, requireRole };

// controllers/authController.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

// INSCRIPTION
const register = asyncHandler(async (req, res) => {
  const { name, email, password } = req.body;

  // Validation
  if (!name || !email || !password) {
    return res.status(400).json({ success: false, error: 'Tous les champs sont requis' });
  }

  if (password.length < 8) {
    return res.status(400).json({ success: false, error: 'Mot de passe: minimum 8 caractères' });
  }

  // Vérifier doublon
  const exists = await User.findOne({ email: email.toLowerCase() });
  if (exists) {
    return res.status(409).json({ success: false, error: 'Email déjà utilisé' });
  }

  // Hasher le mot de passe
  const hashedPassword = await bcrypt.hash(password, 12);

  // Créer l'utilisateur
  const user = await User.create({
    name: name.trim(),
    email: email.toLowerCase().trim(),
    password: hashedPassword
  });

  // Générer les tokens
  const tokenPayload = { userId: user._id, role: user.role };
  const accessToken = jwt.sign(tokenPayload, process.env.JWT_SECRET, { expiresIn: '15m' });
  const refreshToken = jwt.sign(tokenPayload, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' });

  // Stocker le refresh token (hashé)
  user.refreshToken = await bcrypt.hash(refreshToken, 10);
  await user.save();

  // Retourner sans le mot de passe
  const userObj = user.toObject();
  delete userObj.password;
  delete userObj.refreshToken;

  res.status(201).json({
    success: true,
    message: 'Compte créé avec succès',
    data: {
      user: userObj,
      tokens: { accessToken, refreshToken }
    }
  });
});

// CONNEXION
const login = asyncHandler(async (req, res) => {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ success: false, error: 'Email et mot de passe requis' });
  }

  // Trouver l'utilisateur (avec password)
  const user = await User.findOne({ email: email.toLowerCase() }).select('+password +refreshToken');

  // Message générique (sécurité : ne pas révéler si email existe)
  const invalidMsg = 'Email ou mot de passe incorrect';
  if (!user) return res.status(401).json({ success: false, error: invalidMsg });

  // Vérifier le mot de passe
  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) return res.status(401).json({ success: false, error: invalidMsg });

  if (!user.isActive) {
    return res.status(403).json({ success: false, error: 'Compte désactivé' });
  }

  // Générer nouveaux tokens
  const tokenPayload = { userId: user._id, role: user.role };
  const accessToken = jwt.sign(tokenPayload, process.env.JWT_SECRET, { expiresIn: '15m' });
  const refreshToken = jwt.sign(tokenPayload, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' });

  // Mettre à jour refresh token et lastLogin
  user.refreshToken = await bcrypt.hash(refreshToken, 10);
  user.lastLogin = new Date();
  await user.save();

  const userObj = user.toObject();
  delete userObj.password;
  delete userObj.refreshToken;

  res.json({
    success: true,
    data: {
      user: userObj,
      tokens: { accessToken, refreshToken, expiresIn: '15m' }
    }
  });
});

// REFRESH TOKEN
const refreshTokens = asyncHandler(async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(400).json({ success: false, error: 'Refresh token requis' });
  }

  try {
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    const user = await User.findById(decoded.userId).select('+refreshToken');
    
    if (!user) return res.status(401).json({ success: false, error: 'Utilisateur non trouvé' });

    // Vérifier que le refresh token correspond
    const isValid = await bcrypt.compare(refreshToken, user.refreshToken);
    if (!isValid) return res.status(401).json({ success: false, error: 'Refresh token invalide' });

    // Générer nouveaux tokens (rotation)
    const tokenPayload = { userId: user._id, role: user.role };
    const newAccessToken = jwt.sign(tokenPayload, process.env.JWT_SECRET, { expiresIn: '15m' });
    const newRefreshToken = jwt.sign(tokenPayload, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' });

    // Mettre à jour (rotation des refresh tokens)
    user.refreshToken = await bcrypt.hash(newRefreshToken, 10);
    await user.save();

    res.json({
      success: true,
      tokens: { accessToken: newAccessToken, refreshToken: newRefreshToken }
    });
  } catch (err) {
    return res.status(401).json({ success: false, error: 'Refresh token invalide ou expiré' });
  }
});

// DÉCONNEXION
const logout = asyncHandler(async (req, res) => {
  // Invalider le refresh token
  await User.findByIdAndUpdate(req.userId, { refreshToken: null });
  res.json({ success: true, message: 'Déconnecté avec succès' });
});

// PROFIL
const getProfile = asyncHandler(async (req, res) => {
  const user = await User.findById(req.userId);
  if (!user) return res.status(404).json({ success: false, error: 'Utilisateur non trouvé' });
  res.json({ success: true, data: user });
});

module.exports = { register, login, refreshTokens, logout, getProfile };

// routes/auth.js
const express = require('express');
const router = express.Router();
const { register, login, refreshTokens, logout, getProfile } = require('../controllers/authController');
const { authMiddleware } = require('../middlewares/auth');

router.post('/register', register);
router.post('/login', login);
router.post('/refresh', refreshTokens);
router.post('/logout', authMiddleware, logout);
router.get('/profile', authMiddleware, getProfile);

module.exports = router;

/*
UTILISATION :

1. Inscription :
POST /auth/register
{ "name": "Alice", "email": "alice@example.com", "password": "password123" }

2. Connexion :
POST /auth/login
{ "email": "alice@example.com", "password": "password123" }
-> Retourne { accessToken, refreshToken }

3. Route protégée :
GET /api/profile
Headers: Authorization: Bearer <accessToken>

4. Renouveler le token :
POST /auth/refresh
{ "refreshToken": "..." }

5. Déconnexion :
POST /auth/logout
Headers: Authorization: Bearer <accessToken>
*/


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 11 : Système d'Auth Complet avec Niveaux de Permission
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer un système d'authentification avec :
1. Inscription avec vérification email (simulation)
2. Connexion avec brute-force protection
3. Roles : user, premium, admin
4. Middleware requirePermission(permission)
5. Profil avec modification du mot de passe

CORRIGÉ ABRÉGÉ :
*/

// Permissions par rôle
const PERMISSIONS = {
  user: ['read:own', 'write:own', 'delete:own'],
  premium: ['read:own', 'read:all', 'write:own', 'delete:own', 'export:data'],
  admin: ['read:own', 'read:all', 'write:own', 'write:all', 'delete:own', 'delete:all', 'export:data', 'manage:users']
};

function requirePermission(permission) {
  return (req, res, next) => {
    const userPermissions = PERMISSIONS[req.user?.role] || [];
    
    if (!userPermissions.includes(permission)) {
      return res.status(403).json({
        success: false,
        error: `Permission requise : ${permission}`,
        yourRole: req.user?.role,
        yourPermissions: userPermissions
      });
    }
    next();
  };
}

// Exemple d'utilisation
app.get('/api/users', authMiddleware, loadUser, requirePermission('read:all'), getAllUsers);
app.delete('/api/users/:id', authMiddleware, loadUser, requirePermission('delete:all'), deleteUser);


// ============================================================================
// [GUIDE] CHAPITRE 12 : STREAMS ET BUFFERS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les Buffers
[OK] Utiliser les Streams (Readable, Writable, Transform)
[OK] Piper des Streams
[OK] Streamer des fichiers
[OK] Créer des streams personnalisés
*/


// ----------------------------------------------------------------------------
// [ICE_CUBE] BUFFERS
// ----------------------------------------------------------------------------

/*
Buffer = Zone mémoire pour données binaires
Utilisé pour : fichiers, réseau, crypto
*/

// Créer un Buffer
const buf1 = Buffer.from('Hello Node.js', 'utf8');
const buf2 = Buffer.alloc(10);       // 10 octets à zéro
const buf3 = Buffer.allocUnsafe(10); // 10 octets (contenu aléatoire)

// Lire un Buffer
console.log(buf1.toString());         // 'Hello Node.js'
console.log(buf1.toString('hex'));    // Représentation hex
console.log(buf1.toString('base64')); // Représentation base64
console.log(buf1.length);             // 13 (octets)

// Opérations
const combined = Buffer.concat([buf1, Buffer.from('!')]);
const slice = buf1.slice(0, 5);      // 'Hello'

// Comparer
console.log(buf1.equals(buf1));       // true


// ----------------------------------------------------------------------------
// [WATER_WAVE] STREAMS
// ----------------------------------------------------------------------------

/*
Streams = Données en flux (chunk par chunk)

POURQUOI LES STREAMS ?
Sans streams : Charger tout le fichier en mémoire (dangereux pour gros fichiers)
Avec streams : Traiter morceau par morceau (efficace)

TYPES :
1. Readable  -> Lire des données (fs.createReadStream)
2. Writable  -> Écrire des données (fs.createWriteStream)
3. Duplex    -> Les deux (net.Socket)
4. Transform -> Modifier les données (zlib.createGzip)
*/

const fs = require('fs');
const { Transform, pipeline } = require('stream');
const { promisify } = require('util');

const pipelineAsync = promisify(pipeline);

// READABLE STREAM - Lire un fichier de 1GB sans problème mémoire
const readStream = fs.createReadStream('./gros-fichier.txt', {
  encoding: 'utf8',
  highWaterMark: 64 * 1024  // 64KB par chunk
});

readStream.on('data', (chunk) => {
  console.log(`Chunk reçu : ${chunk.length} bytes`);
});

readStream.on('end', () => console.log('Lecture terminée'));
readStream.on('error', (err) => console.error('Erreur lecture:', err));

// WRITABLE STREAM
const writeStream = fs.createWriteStream('./output.txt', { flags: 'a' });

writeStream.write('Ligne 1\n');
writeStream.write('Ligne 2\n');
writeStream.end('Dernière ligne\n');

writeStream.on('finish', () => console.log('Écriture terminée'));

// PIPE - Connecter streams
const readStream = fs.createReadStream('./input.txt');
const writeStream = fs.createWriteStream('./output.txt');

readStream.pipe(writeStream);  // Simple pipe

// Avec gzip
const zlib = require('zlib');
const readStream = fs.createReadStream('./input.txt');
const gzipStream = zlib.createGzip();
const writeStream = fs.createWriteStream('./output.txt.gz');

// Pipeline (gère les erreurs proprement)
await pipelineAsync(readStream, gzipStream, writeStream);
console.log('Compression terminée !');

// TRANSFORM STREAM - Modifier les données
class UpperCaseTransform extends Transform {
  _transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  }
}

// Utiliser le Transform
const upper = new UpperCaseTransform();

await pipelineAsync(
  fs.createReadStream('./input.txt'),
  upper,
  fs.createWriteStream('./output-upper.txt')
);

// STREAM HTTP - Réponse en streaming
app.get('/download/big-file', (req, res) => {
  const filePath = path.join(__dirname, 'files', 'rapport-annuel.pdf');
  
  const stat = fs.statSync(filePath);
  
  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Length', stat.size);
  res.setHeader('Content-Disposition', 'attachment; filename=rapport-annuel.pdf');
  
  // Stream le fichier directement dans la réponse
  const readStream = fs.createReadStream(filePath);
  readStream.pipe(res);
  
  readStream.on('error', (err) => {
    console.error('Erreur stream:', err);
    if (!res.headersSent) res.status(500).send('Erreur');
  });
});


// ============================================================================
// [GUIDE] CHAPITRE 13 : WEBSOCKETS AVEC SOCKET.IO
// ============================================================================

/*
npm install socket.io
*/

const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: { origin: '*', methods: ['GET', 'POST'] },
  pingTimeout: 60000
});

// Données en mémoire
const users = new Map();     // socketId -> { username, room }
const rooms = new Map();     // roomName -> Set(socketId)

// Événements de connexion
io.on('connection', (socket) => {
  console.log(`[OK] Connecté: ${socket.id}`);

  // Rejoindre une room
  socket.on('join', ({ username, room }) => {
    // Quitter les autres rooms
    for (const [roomName, members] of rooms) {
      if (members.has(socket.id)) {
        members.delete(socket.id);
        socket.leave(roomName);
        io.to(roomName).emit('user_left', {
          username: users.get(socket.id)?.username,
          room: roomName,
          usersCount: members.size
        });
      }
    }

    // Rejoindre la nouvelle room
    socket.join(room);
    users.set(socket.id, { username, room });
    
    if (!rooms.has(room)) rooms.set(room, new Set());
    rooms.get(room).add(socket.id);

    // Notifier la room
    io.to(room).emit('user_joined', {
      username,
      room,
      usersCount: rooms.get(room).size
    });

    // Confirmer à l'utilisateur
    socket.emit('join_success', {
      room,
      usersCount: rooms.get(room).size
    });
  });

  // Message reçu
  socket.on('message', ({ content, room }) => {
    const user = users.get(socket.id);
    if (!user) return;

    const message = {
      id: Date.now(),
      content,
      username: user.username,
      room,
      timestamp: new Date().toISOString()
    };

    // Envoyer à tous dans la room (y compris l'expéditeur)
    io.to(room).emit('new_message', message);
  });

  // Message privé
  socket.on('private_message', ({ toUsername, content }) => {
    const sender = users.get(socket.id);
    if (!sender) return;

    // Trouver le socket de destination
    const targetEntry = [...users.entries()].find(([, v]) => v.username === toUsername);
    if (!targetEntry) {
      socket.emit('error', { message: `Utilisateur ${toUsername} non trouvé` });
      return;
    }

    const [targetSocketId] = targetEntry;
    const message = {
      content,
      from: sender.username,
      timestamp: new Date().toISOString(),
      private: true
    };

    // Envoyer aux deux
    socket.emit('private_message', message);
    io.to(targetSocketId).emit('private_message', message);
  });

  // Typing indicator
  socket.on('typing', ({ room, isTyping }) => {
    const user = users.get(socket.id);
    if (!user) return;
    socket.to(room).emit('typing', { username: user.username, isTyping });
  });

  // Déconnexion
  socket.on('disconnect', () => {
    const user = users.get(socket.id);
    if (user) {
      const { username, room } = user;
      
      // Retirer de la room
      if (rooms.has(room)) {
        rooms.get(room).delete(socket.id);
        io.to(room).emit('user_left', {
          username,
          room,
          usersCount: rooms.get(room).size
        });
      }
      
      users.delete(socket.id);
    }
    console.log(`[X] Déconnecté: ${socket.id}`);
  });
});

// Route API pour les stats
app.get('/api/rooms', (req, res) => {
  const stats = Object.fromEntries(
    [...rooms.entries()].map(([name, members]) => [name, members.size])
  );
  res.json({ rooms: stats, totalUsers: users.size });
});

httpServer.listen(3000, () => console.log('[PLUGIN] WebSocket sur http://localhost:3000'));


// ============================================================================
// [GUIDE] CHAPITRE 14 : TESTS AVEC JEST ET SUPERTEST
// ============================================================================

/*
npm install --save-dev jest supertest @types/jest

Configuration package.json :
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "jest": {
    "testEnvironment": "node",
    "testMatch": ["**/__tests__/**/*.js", "**/*.test.js"]
  }
}
*/

// ----------------------------------------------------------------------------
// [TEST] TESTS UNITAIRES AVEC JEST
// ----------------------------------------------------------------------------

// utils/math.js
const math = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => {
    if (b === 0) throw new Error('Division par zéro');
    return a / b;
  },
  factorial: (n) => {
    if (n < 0) throw new Error('n doit être >= 0');
    if (n === 0) return 1;
    return n * math.factorial(n - 1);
  }
};

module.exports = math;

// __tests__/math.test.js
const math = require('../utils/math');

describe('Math utilities', () => {
  describe('add()', () => {
    test('should add two positive numbers', () => {
      expect(math.add(2, 3)).toBe(5);
    });
    
    test('should add negative numbers', () => {
      expect(math.add(-1, -2)).toBe(-3);
    });
    
    test('should add zero', () => {
      expect(math.add(5, 0)).toBe(5);
    });
  });
  
  describe('divide()', () => {
    test('should divide correctly', () => {
      expect(math.divide(10, 2)).toBe(5);
    });
    
    test('should throw on division by zero', () => {
      expect(() => math.divide(10, 0)).toThrow('Division par zéro');
    });
    
    test('should handle decimals', () => {
      expect(math.divide(1, 3)).toBeCloseTo(0.333, 2);
    });
  });
  
  describe('factorial()', () => {
    test.each([
      [0, 1],
      [1, 1],
      [5, 120],
      [10, 3628800]
    ])('factorial(%i) should be %i', (n, expected) => {
      expect(math.factorial(n)).toBe(expected);
    });
    
    test('should throw for negative numbers', () => {
      expect(() => math.factorial(-1)).toThrow();
    });
  });
});

/*
JEST MATCHERS ESSENTIELS

Égalité :
expect(value).toBe(5)               // ===
expect(value).toEqual({ a: 1 })     // Deep equality
expect(value).toStrictEqual({})      // Strict deep equality

Vrai/Faux :
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
expect(value).toBeDefined()
expect(value).toBeUndefined()

Nombres :
expect(value).toBeGreaterThan(5)
expect(value).toBeLessThanOrEqual(10)
expect(value).toBeCloseTo(0.333, 2)

Strings :
expect(str).toContain('Node')
expect(str).toMatch(/node/i)
expect(str).toHaveLength(5)

Arrays/Objects :
expect(arr).toContain('item')
expect(arr).toHaveLength(3)
expect(obj).toHaveProperty('name', 'Alice')
expect(obj).toMatchObject({ name: 'Alice' })

Erreurs :
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('message')
expect(() => fn()).toThrow(TypeError)

Async :
await expect(asyncFn()).resolves.toBe(5)
await expect(asyncFn()).rejects.toThrow('error')
*/


// ----------------------------------------------------------------------------
// [WEB] TESTS D'INTÉGRATION AVEC SUPERTEST
// ----------------------------------------------------------------------------

// app.js - Application testable
const express = require('express');

function createApp() {
  const app = express();
  app.use(express.json());
  
  let todos = [{ id: 1, title: 'Test', completed: false }];
  let nextId = 2;
  
  app.get('/api/todos', (req, res) => {
    res.json({ success: true, data: todos });
  });
  
  app.post('/api/todos', (req, res) => {
    const { title } = req.body;
    if (!title) return res.status(400).json({ success: false, error: 'title requis' });
    const todo = { id: nextId++, title, completed: false };
    todos.push(todo);
    res.status(201).json({ success: true, data: todo });
  });
  
  app.delete('/api/todos/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) return res.status(404).json({ success: false, error: 'Non trouvé' });
    todos.splice(index, 1);
    res.status(204).send();
  });
  
  return app;
}

module.exports = createApp;

// __tests__/todos.test.js
const request = require('supertest');
const createApp = require('../app');

describe('Todos API', () => {
  let app;
  
  beforeEach(() => {
    app = createApp();  // Nouvelle instance pour chaque test
  });

  describe('GET /api/todos', () => {
    it('should return todos list', async () => {
      const res = await request(app)
        .get('/api/todos')
        .expect(200)
        .expect('Content-Type', /json/);
      
      expect(res.body.success).toBe(true);
      expect(Array.isArray(res.body.data)).toBe(true);
      expect(res.body.data.length).toBeGreaterThan(0);
    });
  });

  describe('POST /api/todos', () => {
    it('should create a new todo', async () => {
      const res = await request(app)
        .post('/api/todos')
        .send({ title: 'New Todo' })
        .expect(201)
        .expect('Content-Type', /json/);
      
      expect(res.body.success).toBe(true);
      expect(res.body.data.title).toBe('New Todo');
      expect(res.body.data.completed).toBe(false);
      expect(res.body.data.id).toBeDefined();
    });

    it('should return 400 if title is missing', async () => {
      const res = await request(app)
        .post('/api/todos')
        .send({})
        .expect(400);
      
      expect(res.body.success).toBe(false);
      expect(res.body.error).toBe('title requis');
    });

    it('should return 400 if body is empty', async () => {
      await request(app)
        .post('/api/todos')
        .expect(400);
    });
  });

  describe('DELETE /api/todos/:id', () => {
    it('should delete existing todo', async () => {
      await request(app)
        .delete('/api/todos/1')
        .expect(204);
    });

    it('should return 404 for non-existing todo', async () => {
      const res = await request(app)
        .delete('/api/todos/999')
        .expect(404);
      
      expect(res.body.success).toBe(false);
    });
  });

  describe('Authorization tests', () => {
    it('should return 401 without token for protected routes', async () => {
      const protectedApp = createApp();
      // Ajouter middleware d'auth...
      
      await request(protectedApp)
        .get('/api/protected')
        .expect(401);
    });
  });
});

// MOCKING avec Jest
describe('Auth Controller tests', () => {
  let User;
  let bcrypt;
  
  beforeEach(() => {
    jest.resetModules();
    jest.mock('../models/User');
    jest.mock('bcrypt');
    
    User = require('../models/User');
    bcrypt = require('bcrypt');
  });

  it('should return 409 if email already exists', async () => {
    User.findOne.mockResolvedValue({ email: 'exists@example.com' });
    
    const app = require('../app')();
    const res = await request(app)
      .post('/auth/register')
      .send({ name: 'Test', email: 'exists@example.com', password: 'pass123' });
    
    expect(res.status).toBe(409);
    expect(User.findOne).toHaveBeenCalledWith({ email: 'exists@example.com' });
  });
});


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 14 : Tests Complets d'une API
// ----------------------------------------------------------------------------

/*
OBJECTIF : Écrire des tests complets pour une API de gestion de contacts

TESTS À ÉCRIRE :
1. GET /contacts -> Retourne tableau vide initialement, puis avec données
2. POST /contacts -> Création réussie, validation email, doublon
3. GET /contacts/:id -> Trouvé, non trouvé, ID invalide
4. PUT /contacts/:id -> Modification réussie, non trouvé
5. DELETE /contacts/:id -> Suppression réussie, non trouvé
6. Tests de pagination

CORRIGÉ :
*/

// app.test.js (tests complets)
const request = require('supertest');
const express = require('express');

// Créer une app de test fraîche
function createTestApp() {
  const app = express();
  app.use(express.json());
  
  let contacts = [];
  let nextId = 1;
  
  app.get('/contacts', (req, res) => {
    const { page = 1, limit = 10, search } = req.query;
    let result = search
      ? contacts.filter(c => c.name.includes(search) || c.email.includes(search))
      : contacts;
    
    const total = result.length;
    const p = parseInt(page), l = parseInt(limit);
    result = result.slice((p - 1) * l, p * l);
    
    res.json({ data: result, total, page: p, limit: l });
  });
  
  app.post('/contacts', (req, res) => {
    const { name, email, phone } = req.body;
    const errors = [];
    
    if (!name?.trim()) errors.push({ field: 'name', message: 'requis' });
    if (!email?.trim()) errors.push({ field: 'email', message: 'requis' });
    else if (!/^\S+@\S+\.\S+$/.test(email)) errors.push({ field: 'email', message: 'format invalide' });
    
    if (errors.length) return res.status(400).json({ errors });
    
    if (contacts.find(c => c.email === email)) {
      return res.status(409).json({ error: 'Email déjà utilisé' });
    }
    
    const contact = { id: nextId++, name: name.trim(), email, phone: phone || null };
    contacts.push(contact);
    res.status(201).json(contact);
  });
  
  app.get('/contacts/:id', (req, res) => {
    const id = parseInt(req.params.id);
    if (isNaN(id)) return res.status(400).json({ error: 'ID invalide' });
    const contact = contacts.find(c => c.id === id);
    if (!contact) return res.status(404).json({ error: 'Non trouvé' });
    res.json(contact);
  });
  
  app.put('/contacts/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const index = contacts.findIndex(c => c.id === id);
    if (index === -1) return res.status(404).json({ error: 'Non trouvé' });
    contacts[index] = { ...contacts[index], ...req.body, id };
    res.json(contacts[index]);
  });
  
  app.delete('/contacts/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const index = contacts.findIndex(c => c.id === id);
    if (index === -1) return res.status(404).json({ error: 'Non trouvé' });
    contacts.splice(index, 1);
    res.status(204).send();
  });
  
  return app;
}

describe('Contacts API Tests', () => {
  let app;
  
  beforeEach(() => { app = createTestApp(); });
  
  describe('GET /contacts', () => {
    it('returns empty array initially', async () => {
      const res = await request(app).get('/contacts').expect(200);
      expect(res.body.data).toEqual([]);
      expect(res.body.total).toBe(0);
    });
    
    it('returns contacts after creation', async () => {
      await request(app).post('/contacts').send({ name: 'Alice', email: 'alice@test.com' });
      const res = await request(app).get('/contacts').expect(200);
      expect(res.body.data).toHaveLength(1);
      expect(res.body.total).toBe(1);
    });
    
    it('supports search', async () => {
      await request(app).post('/contacts').send({ name: 'Alice', email: 'alice@test.com' });
      await request(app).post('/contacts').send({ name: 'Bob', email: 'bob@test.com' });
      
      const res = await request(app).get('/contacts?search=alice').expect(200);
      expect(res.body.data).toHaveLength(1);
      expect(res.body.data[0].name).toBe('Alice');
    });
    
    it('supports pagination', async () => {
      // Créer 15 contacts
      for (let i = 1; i <= 15; i++) {
        await request(app).post('/contacts')
          .send({ name: `Contact ${i}`, email: `contact${i}@test.com` });
      }
      
      const res = await request(app).get('/contacts?page=2&limit=5').expect(200);
      expect(res.body.data).toHaveLength(5);
      expect(res.body.total).toBe(15);
    });
  });
  
  describe('POST /contacts', () => {
    it('creates a contact', async () => {
      const data = { name: 'Alice', email: 'alice@test.com', phone: '0612345678' };
      const res = await request(app).post('/contacts').send(data).expect(201);
      
      expect(res.body.id).toBeDefined();
      expect(res.body.name).toBe('Alice');
      expect(res.body.email).toBe('alice@test.com');
    });
    
    it('rejects missing name', async () => {
      const res = await request(app)
        .post('/contacts').send({ email: 'test@test.com' }).expect(400);
      
      expect(res.body.errors).toContainEqual(expect.objectContaining({ field: 'name' }));
    });
    
    it('rejects invalid email', async () => {
      const res = await request(app)
        .post('/contacts').send({ name: 'Alice', email: 'not-an-email' }).expect(400);
      
      expect(res.body.errors).toContainEqual(
        expect.objectContaining({ field: 'email', message: 'format invalide' })
      );
    });
    
    it('rejects duplicate email', async () => {
      await request(app).post('/contacts').send({ name: 'Alice', email: 'alice@test.com' });
      const res = await request(app)
        .post('/contacts').send({ name: 'Alice2', email: 'alice@test.com' }).expect(409);
      
      expect(res.body.error).toBe('Email déjà utilisé');
    });
  });
  
  describe('GET /contacts/:id', () => {
    it('returns a contact by id', async () => {
      const created = await request(app)
        .post('/contacts').send({ name: 'Alice', email: 'alice@test.com' });
      
      const res = await request(app)
        .get(`/contacts/${created.body.id}`).expect(200);
      
      expect(res.body.name).toBe('Alice');
    });
    
    it('returns 404 for non-existing id', async () => {
      await request(app).get('/contacts/999').expect(404);
    });
    
    it('returns 400 for invalid id', async () => {
      await request(app).get('/contacts/abc').expect(400);
    });
  });
  
  describe('DELETE /contacts/:id', () => {
    it('deletes a contact', async () => {
      const { body: contact } = await request(app)
        .post('/contacts').send({ name: 'Alice', email: 'alice@test.com' });
      
      await request(app).delete(`/contacts/${contact.id}`).expect(204);
      await request(app).get(`/contacts/${contact.id}`).expect(404);
    });
    
    it('returns 404 for non-existing contact', async () => {
      await request(app).delete('/contacts/999').expect(404);
    });
  });
});

/*
LANCER LES TESTS :
npm test
npm test -- --coverage
npm test -- --watch
*/


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

/*
[OK] CE QUE VOUS AVEZ APPRIS

Chapitre 10 : Bases de données
[OK] MongoDB/Mongoose : schemas, modèles, CRUD
[OK] PostgreSQL/pg : pool, requêtes, transactions
[OK] Indexes et relations

Chapitre 11 : Authentification
[OK] bcrypt pour hasher les mots de passe
[OK] JWT access + refresh tokens
[OK] Middleware d'authentification
[OK] Système de permissions basé sur les rôles

Chapitre 12 : Streams
[OK] Buffers pour données binaires
[OK] Readable, Writable, Transform streams
[OK] Pipeline et pipe
[OK] Streaming de fichiers et HTTP

Chapitre 13 : WebSockets
[OK] Socket.io server setup
[OK] Rooms et namespaces
[OK] Messages en temps réel
[OK] Typing indicators

Chapitre 14 : Tests
[OK] Jest matchers
[OK] Tests unitaires
[OK] Tests d'intégration avec Supertest
[OK] Mocking

[OBJECTIF] PROJETS SUGGÉRÉS APRÈS PARTIE 3
-> Chat en temps réel avec rooms
-> API complète avec auth JWT et MongoDB
-> API REST avec tests complets (>80% coverage)

-> PROCHAINE ÉTAPE : nodejs_partie4.txt
-> Variables d'env, Sécurité, Performance, Déploiement
*/

// ============================================================================
// [VERT] GUIDE NODE.JS COMPLET - PARTIE 4 : PRODUCTION
// ============================================================================
//
// CHAPITRES :
//   Chapitre 15 : Variables d'environnement et Configuration
//   Chapitre 16 : Sécurité
//   Chapitre 17 : Performance
//   Chapitre 18 : Déploiement (PM2, Docker, Nginx)
//   Chapitre 19 : Logging et Monitoring
//   Chapitre 20 : Best Practices et Récapitulatif Final
//
// PRÉREQUIS : Avoir suivi les parties 1, 2 et 3
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 15 : VARIABLES D'ENVIRONNEMENT ET CONFIGURATION
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser dotenv pour charger les variables d'environnement
[OK] Créer un module de configuration centralisé
[OK] Valider les variables requises au démarrage
[OK] Gérer plusieurs environnements (dev, test, prod)
[OK] Sécuriser les secrets

[REFLEXION] POURQUOI les variables d'environnement ?

PROBLÈME sans variables d'env :
*/

// [X] JAMAIS faire ça
const db = require('mongodb').connect('mongodb+srv://admin:SuperSecret123@cluster.mongodb.net');
const jwt = require('jsonwebtoken');
const token = jwt.sign({}, 'ma-cle-secrete-hardcodee');
// -> Secret visible dans le code
// -> Compromis si le repo est public
// -> Impossible de changer sans modifier le code

/*
SOLUTION : variables d'environnement
-> Les secrets restent hors du code
-> Différentes valeurs selon l'environnement
-> Pas de secrets dans Git
*/


// ----------------------------------------------------------------------------
// [PACKAGE] DOTENV
// ----------------------------------------------------------------------------

// Installation
// npm install dotenv

// .env (JAMAIS committer ce fichier !)
/*
NODE_ENV=development
PORT=3000

# Base de données
MONGODB_URI=mongodb://localhost:27017/monapp
REDIS_URL=redis://localhost:6379

# JWT
JWT_SECRET=mon-secret-tres-long-et-complexe-pour-dev
JWT_REFRESH_SECRET=autre-secret-pour-refresh-token
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

# Email
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=2525
SMTP_USER=monuser
SMTP_PASS=monpass

# Sentry
SENTRY_DSN=https://xxxxx@sentry.io/yyyyy
*/

// .env.example (CE fichier est commité - template sans valeurs réelles)
/*
NODE_ENV=development
PORT=3000

MONGODB_URI=mongodb://localhost:27017/nomdeladb

JWT_SECRET=changez-moi-en-production
JWT_EXPIRES_IN=15m

SMTP_HOST=
SMTP_PORT=
SMTP_USER=
SMTP_PASS=
*/

// .gitignore
/*
.env
.env.local
.env.production
node_modules/
logs/
uploads/
*/

// Chargement de dotenv (DOIT être fait en tout premier)
// src/server.js
require('dotenv').config();  // Charge .env dans process.env

const app = require('./app');
// ... reste du code

// [ATTENTION] ATTENTION : dotenv ne remplace pas des variables déjà définies
// Si PORT est déjà dans le système, .env ne l'écrasera pas
// C'est voulu : les variables système ont priorité sur .env


// ----------------------------------------------------------------------------
// [CONFIG] MODULE DE CONFIGURATION CENTRALISÉ
// ----------------------------------------------------------------------------

/*
POURQUOI centraliser la configuration ?

Sans module config :
*/

// [X] Variables éparpillées partout
// auth.js
const secret = process.env.JWT_SECRET;

// database.js
const uri = process.env.MONGODB_URI || 'mongodb://localhost/dev';

// email.js
const port = parseInt(process.env.SMTP_PORT);
// -> Dupplication
// -> Pas de validation
// -> Pas de valeurs par défaut cohérentes

// [OK] Module config centralisé
// src/config/index.js
const config = {
  // Environnement
  env: process.env.NODE_ENV || 'development',
  isDev: process.env.NODE_ENV === 'development',
  isProd: process.env.NODE_ENV === 'production',
  isTest: process.env.NODE_ENV === 'test',

  // Serveur
  server: {
    port: parseInt(process.env.PORT, 10) || 3000,
    host: process.env.HOST || '0.0.0.0',
    corsOrigins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000']
  },

  // Base de données
  db: {
    uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp',
    options: {
      maxPoolSize: parseInt(process.env.DB_POOL_SIZE, 10) || 10
    }
  },

  // JWT
  jwt: {
    secret: process.env.JWT_SECRET,
    refreshSecret: process.env.JWT_REFRESH_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '15m',
    refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d'
  },

  // Redis
  redis: {
    url: process.env.REDIS_URL || 'redis://localhost:6379',
    ttl: parseInt(process.env.REDIS_DEFAULT_TTL, 10) || 3600
  },

  // Email
  smtp: {
    host: process.env.SMTP_HOST,
    port: parseInt(process.env.SMTP_PORT, 10) || 587,
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,
    from: process.env.SMTP_FROM || 'noreply@monapp.com'
  },

  // Upload
  upload: {
    maxSize: parseInt(process.env.UPLOAD_MAX_SIZE, 10) || 5 * 1024 * 1024, // 5MB
    allowedTypes: (process.env.UPLOAD_ALLOWED_TYPES || 'image/jpeg,image/png').split(','),
    dest: process.env.UPLOAD_DEST || 'uploads/'
  },

  // Logging
  log: {
    level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
    file: process.env.LOG_FILE || 'logs/app.log'
  }
};

module.exports = config;

// Utilisation dans n'importe quel fichier
const config = require('./config');

app.listen(config.server.port, () => {
  console.log(`Serveur sur port ${config.server.port} en mode ${config.env}`);
});


// ----------------------------------------------------------------------------
// [OK] VALIDATION DES VARIABLES AU DÉMARRAGE
// ----------------------------------------------------------------------------

/*
POURQUOI valider ?

Sans validation :
-> Serveur démarre avec config incomplète
-> Erreur cryptique plus tard ("Cannot read property of undefined")
-> Difficile à déboguer

Avec validation :
-> Erreur claire au démarrage
-> Liste de tout ce qui manque
*/

// src/config/validate.js
function validateConfig(config) {
  const errors = [];

  // Variables OBLIGATOIRES en production
  if (config.isProd) {
    const required = [
      ['jwt.secret', config.jwt.secret],
      ['jwt.refreshSecret', config.jwt.refreshSecret],
      ['db.uri', config.db.uri],
      ['smtp.host', config.smtp.host]
    ];

    for (const [key, value] of required) {
      if (!value) {
        errors.push(`Variable manquante : ${key}`);
      }
    }

    // Vérifier la longueur du secret JWT
    if (config.jwt.secret && config.jwt.secret.length < 32) {
      errors.push('JWT_SECRET doit faire au moins 32 caractères');
    }
  }

  // Variables TOUJOURS requises
  if (!config.db.uri) {
    errors.push('MONGODB_URI est requis');
  }

  if (errors.length > 0) {
    console.error('[X] Erreurs de configuration :');
    errors.forEach(e => console.error(`   • ${e}`));
    console.error('\nVérifiez votre fichier .env');
    process.exit(1);
  }

  console.log('[OK] Configuration validée');
}

// Intégration dans config/index.js
const config = { /* ... */ };

if (process.env.NODE_ENV !== 'test') {
  validateConfig(config);
}

module.exports = config;


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

// Fichiers .env par environnement
// .env                 -> Développement (defaut)
// .env.test            -> Tests
// .env.production      -> Production
// .env.local           -> Surcharges locales (ignoré par git)

// Charger le bon .env
require('dotenv').config({
  path: `.env.${process.env.NODE_ENV || 'development'}`
});

// Ou avec dotenv-flow (gère l'ordre de priorité automatiquement)
// npm install dotenv-flow
require('dotenv-flow').config();
// Ordre de chargement : .env -> .env.development -> .env.development.local
// Les fichiers .local ont priorité (parfait pour les surcharges perso)

// [ATTENTION] Ne jamais committer les fichiers .env et .env.*.local !

// Exemple .env.test
/*
NODE_ENV=test
PORT=3001
MONGODB_URI=mongodb://localhost:27017/myapp_test
JWT_SECRET=secret-test-pas-besoin-detre-complexe
JWT_REFRESH_SECRET=refresh-test
LOG_LEVEL=silent
*/

// Dans jest.config.js, charger le bon .env
module.exports = {
  testEnvironment: 'node',
  setupFiles: ['dotenv/config'],  // Charge .env
  globalSetup: './tests/global-setup.js'
};

// tests/global-setup.js
module.exports = async () => {
  process.env.NODE_ENV = 'test';
  process.env.MONGODB_URI = 'mongodb://localhost:27017/myapp_test';
};


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 15 : Configuration multi-environnements
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer un système de configuration robuste

TÂCHES :
1. Créer les fichiers .env.development, .env.test, .env.production
2. Créer src/config/index.js avec toutes les valeurs
3. Ajouter validation au démarrage (en prod)
4. Tester que la validation détecte les variables manquantes

CORRIGÉ :
*/

// config/index.js complet
require('dotenv').config({
  path: process.env.NODE_ENV === 'test'
    ? '.env.test'
    : process.env.NODE_ENV === 'production'
      ? '.env.production'
      : '.env.development'
});

const config = {
  env: process.env.NODE_ENV || 'development',
  get isDev() { return this.env === 'development'; },
  get isProd() { return this.env === 'production'; },
  get isTest() { return this.env === 'test'; },

  server: {
    port: parseInt(process.env.PORT, 10) || 3000
  },

  db: {
    uri: process.env.MONGODB_URI
  },

  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '15m'
  }
};

// Validation
function validate(cfg) {
  const required = cfg.isProd
    ? ['db.uri', 'jwt.secret']
    : ['db.uri'];

  const missing = required.filter(key => {
    const [obj, prop] = key.split('.');
    return !cfg[obj]?.[prop];
  });

  if (missing.length) {
    throw new Error(`Variables manquantes : ${missing.join(', ')}`);
  }
}

if (!config.isTest) {
  try {
    validate(config);
    console.log(`[OK] Config OK [${config.env}]`);
  } catch (err) {
    console.error(`[X] ${err.message}`);
    process.exit(1);
  }
}

module.exports = config;


// ============================================================================
// [GUIDE] CHAPITRE 16 : SÉCURITÉ
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Configurer les headers de sécurité (Helmet)
[OK] Protéger contre les injections (NoSQL, XSS, SQL)
[OK] Implémenter le rate limiting
[OK] Valider et sanitizer les inputs
[OK] Gérer les secrets correctement
[OK] Appliquer une checklist de sécurité complète

[REFLEXION] POURQUOI la sécurité dès le début ?

"Shift left" = intégrer la sécurité dès le développement
-> Moins coûteux de corriger tôt
-> Les failles de sécurité peuvent être catastrophiques (RGPD, réputation)
-> Node.js/Express n'est pas sécurisé par défaut

OWASP Top 10 pour Node.js :
1. Injection (NoSQL, SQL, LDAP)
2. Broken Authentication
3. Sensitive Data Exposure
4. XSS (Cross-Site Scripting)
5. Security Misconfiguration
6. Broken Access Control
7. Using Components with Known Vulnerabilities
8. Insufficient Logging
9. Mass Assignment
10. SSRF (Server-Side Request Forgery)
*/


// ----------------------------------------------------------------------------
// [SECURITE] HELMET - HEADERS DE SÉCURITÉ
// ----------------------------------------------------------------------------

/*
npm install helmet

Helmet configure automatiquement ces headers HTTP :
- Content-Security-Policy : limite les sources de contenu
- X-DNS-Prefetch-Control : désactive le prefetch DNS
- X-Frame-Options : empêche le clickjacking (iframes)
- X-Powered-By : supprime la signature Express
- Strict-Transport-Security : force HTTPS
- X-Download-Options : protège le téléchargement IE
- X-Content-Type-Options : empêche le MIME sniffing
- Referrer-Policy : contrôle l'en-tête Referer
*/

const helmet = require('helmet');
const express = require('express');
const app = express();

// Configuration simple (recommandée pour débuter)
app.use(helmet());

// Configuration fine
app.use(helmet({
  // Content Security Policy
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],           // Par défaut, seulement le même origine
      scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.monapp.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"]
    }
  },

  // HSTS (force HTTPS)
  hsts: {
    maxAge: 31536000,         // 1 an
    includeSubDomains: true,
    preload: true
  },

  // Pas de clickjacking
  frameguard: { action: 'deny' },

  // Désactiver X-Powered-By
  hidePoweredBy: true,

  // Protection XSS IE
  xssFilter: true,

  // Pas de MIME sniffing
  noSniff: true
}));


// ----------------------------------------------------------------------------
// [NETTOYAGE] PROTECTION CONTRE LES INJECTIONS
// ----------------------------------------------------------------------------

// 1. INJECTION NOSQL
// npm install express-mongo-sanitize
const mongoSanitize = require('express-mongo-sanitize');

app.use(mongoSanitize());
// Supprime les $ et . des inputs pour bloquer les opérateurs MongoDB

// EXEMPLE D'ATTAQUE BLOQUÉE :
// POST /login { "email": { "$gt": "" }, "password": { "$gt": "" } }
// -> Sans protection : se connecte sans mot de passe !
// -> Avec express-mongo-sanitize : les $ sont supprimés

// Protection manuelle dans les requêtes Mongoose
// [X] DANGEREUX
User.findOne({ email: req.body.email });

// [OK] SÛRS
User.findOne({ email: String(req.body.email) });  // Cast en string
// Ou validator.isEmail() pour valider d'abord

// 2. PROTECTION XSS
// npm install xss-clean
const xss = require('xss-clean');
app.use(xss());
// Sanitize les inputs HTML (remplace <script> par &lt;script&gt;)

// 3. PROTECTION DANS LES REQUÊTES MONGOOSE
// Utiliser lean() et select() pour limiter les données exposées
const user = await User.findById(id)
  .select('-password -__v -refreshToken')  // Exclure les champs sensibles
  .lean();  // Retourne un objet JS simple, plus rapide

// 4. MASS ASSIGNMENT
// [X] DANGEREUX : accepter tous les champs du body
const user = new User(req.body);

// [OK] Whitelister les champs autorisés
const { name, email, bio } = req.body;  // Seulement ces champs
const user = new User({ name, email, bio });

// Ou avec une fonction de filtrage
function filterBody(body, allowedFields) {
  const filtered = {};
  allowedFields.forEach(field => {
    if (body[field] !== undefined) {
      filtered[field] = body[field];
    }
  });
  return filtered;
}

const safeData = filterBody(req.body, ['name', 'email', 'bio']);
await User.findByIdAndUpdate(id, safeData, { new: true });


// ----------------------------------------------------------------------------
// [TEMPS] RATE LIMITING
// ----------------------------------------------------------------------------

// npm install express-rate-limit

const rateLimit = require('express-rate-limit');

// Rate limiter général
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 500,                   // 500 requêtes par IP
  standardHeaders: true,      // Headers RateLimit-*
  legacyHeaders: false,
  message: {
    error: 'Trop de requêtes. Réessayez dans 15 minutes.'
  }
});

// Rate limiter strict pour l'auth
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,  // Seulement 10 tentatives de login
  skipSuccessfulRequests: true,  // Ne compte pas les succès
  message: {
    error: 'Trop de tentatives de connexion. Compte temporairement bloqué.'
  },
  // Clé personnalisée (par IP + email)
  keyGenerator: (req) => `${req.ip}:${req.body.email || 'unknown'}`
});

// Rate limiter pour les uploads
const uploadLimiter = rateLimit({
  windowMs: 60 * 60 * 1000,  // 1 heure
  max: 20,  // 20 uploads par heure
  message: { error: 'Limite d\'upload atteinte' }
});

// Application
app.use(globalLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
app.use('/api/upload', uploadLimiter);

// Rate limiting avec Redis (partagé entre instances)
// npm install rate-limit-redis
const RedisStore = require('rate-limit-redis');
const redis = require('./config/redis');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  store: new RedisStore({
    sendCommand: (...args) => redis.sendCommand(args)
  })
});


// ----------------------------------------------------------------------------
// [OK] VALIDATION COMPLÈTE DES INPUTS
// ----------------------------------------------------------------------------

// npm install express-validator

const { body, param, query, validationResult } = require('express-validator');

// Règles de validation réutilisables
const userValidation = {
  create: [
    body('name')
      .trim()
      .notEmpty().withMessage('Le nom est requis')
      .isLength({ min: 2, max: 50 }).withMessage('Nom : 2-50 caractères')
      .escape(),  // Échappe les caractères HTML

    body('email')
      .trim()
      .notEmpty().withMessage('L\'email est requis')
      .isEmail().withMessage('Email invalide')
      .normalizeEmail()  // lowercase, supprime les dots Gmail...
      .isLength({ max: 255 }),

    body('password')
      .notEmpty().withMessage('Mot de passe requis')
      .isLength({ min: 8 }).withMessage('Minimum 8 caractères')
      .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
      .withMessage('Doit contenir majuscule, minuscule et chiffre'),

    body('age')
      .optional()
      .isInt({ min: 0, max: 150 }).withMessage('Âge invalide')
      .toInt()  // Convertit en nombre entier
  ],

  update: [
    param('id')
      .isMongoId().withMessage('ID invalide'),

    body('name')
      .optional()
      .trim()
      .isLength({ min: 2, max: 50 })
      .escape(),

    body('email')
      .optional()
      .trim()
      .isEmail()
      .normalizeEmail()
  ]
};

// Middleware pour traiter les erreurs de validation
const handleValidation = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({
      success: false,
      errors: errors.array().map(({ field, msg }) => ({ field, message: msg }))
    });
  }
  next();
};

// Application dans les routes
router.post('/', ...userValidation.create, handleValidation, createUser);
router.put('/:id', ...userValidation.update, handleValidation, updateUser);

// Validation des paramètres de pagination
const paginationValidation = [
  query('page').optional().isInt({ min: 1 }).toInt().default(1),
  query('limit').optional().isInt({ min: 1, max: 100 }).toInt().default(10),
  query('sort').optional().isIn(['createdAt', 'name', 'email']).default('createdAt'),
  query('order').optional().isIn(['asc', 'desc']).default('desc')
];


// ----------------------------------------------------------------------------
// [VERROUILLE] GESTION SÉCURISÉE DES SECRETS
// ----------------------------------------------------------------------------

const crypto = require('crypto');

// Générer des secrets sécurisés
const generateSecret = (length = 64) => crypto.randomBytes(length).toString('hex');

// En ligne de commande pour générer les secrets .env :
// node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

// Comparer des tokens de manière sécurisée (évite les timing attacks)
const safeCompare = (a, b) => {
  const bufA = Buffer.from(String(a));
  const bufB = Buffer.from(String(b));
  if (bufA.length !== bufB.length) return false;
  return crypto.timingSafeEqual(bufA, bufB);
};

// Hacher les données sensibles dans les logs
function maskSensitive(obj, fields = ['password', 'token', 'secret', 'credit_card']) {
  const masked = { ...obj };
  fields.forEach(field => {
    if (masked[field]) masked[field] = '***';
  });
  return masked;
}

logger.info('Requête reçue', { body: maskSensitive(req.body) });


// ----------------------------------------------------------------------------
// [LISTE] CHECKLIST SÉCURITÉ COMPLÈTE
// ----------------------------------------------------------------------------

/*
[OK] HEADERS HTTP :
[ ] helmet() configuré
[ ] CSP configuré (Content-Security-Policy)
[ ] CORS restrictif (pas de *)
[ ] X-Frame-Options: DENY

[OK] AUTHENTIFICATION :
[ ] bcrypt (rounds >= 12)
[ ] JWT avec secret fort (>= 32 chars)
[ ] Tokens d'accès courts (15min)
[ ] Refresh tokens avec rotation
[ ] Rate limiting sur le login (max 10 tentatives)

[OK] DONNÉES :
[ ] Validation de tous les inputs (express-validator/Joi)
[ ] Sanitization HTML (xss-clean)
[ ] Protection NoSQL injection (mongo-sanitize)
[ ] Pas de mass assignment (whitelist des champs)
[ ] Champs sensibles exclus des réponses (select())

[OK] RÉSEAU :
[ ] HTTPS en production (TLS 1.2+)
[ ] Rate limiting global
[ ] Rate limiting strict sur auth
[ ] Nginx comme reverse proxy

[OK] CODE :
[ ] Pas de secrets dans le code
[ ] .env non commité (.gitignore)
[ ] npm audit régulier
[ ] Dépendances à jour
[ ] Erreurs génériques (pas de stack trace en prod)

[OK] BASE DE DONNÉES :
[ ] Utilisateur DB avec droits minimum
[ ] Réseau privé (pas exposé publiquement)
[ ] Backups automatiques
[ ] Chiffrement au repos
*/


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 16 : API sécurisée
// ----------------------------------------------------------------------------

/*
OBJECTIF : Sécuriser une API Express complète

TÂCHES :
1. Ajouter Helmet avec CSP personnalisé
2. Rate limiting (global 500/15min, auth 10/15min)
3. Validation avec express-validator
4. Protection NoSQL injection
5. Tester les protections avec des inputs malicieux

CORRIGÉ - Configuration sécurité complète :
*/

// security.js - Middleware de sécurité complet
const helmet = require('helmet');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const rateLimit = require('express-rate-limit');

function applySecurity(app) {
  // Headers
  app.use(helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        objectSrc: ["'none'"]
      }
    }
  }));

  // Injection protection
  app.use(mongoSanitize({ replaceWith: '_' }));
  app.use(xss());

  // Rate limiting
  app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 500 }));

  app.use(['/api/auth/login', '/api/auth/register'],
    rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 10,
      skipSuccessfulRequests: true,
      message: { success: false, error: 'Trop de tentatives. Attendez 15 minutes.' }
    })
  );
}

module.exports = applySecurity;


// ============================================================================
// [GUIDE] CHAPITRE 17 : PERFORMANCE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser le clustering pour exploiter tous les CPUs
[OK] Mettre en cache avec Redis
[OK] Utiliser les Worker Threads pour les tâches CPU-intensives
[OK] Optimiser les requêtes MongoDB
[OK] Mesurer les performances (profiling)

[REFLEXION] POURQUOI optimiser ?

Node.js est single-thread = 1 CPU utilisé par défaut
-> Clustering = utiliser tous les CPUs
-> Cache Redis = éviter les requêtes DB répétées
-> Worker Threads = tâches CPU sans bloquer le thread principal
*/


// ----------------------------------------------------------------------------
// [CONFIG] CLUSTERING
// ----------------------------------------------------------------------------

// src/cluster.js
const cluster = require('cluster');
const os = require('os');
const path = require('path');

const NUM_CPUS = os.cpus().length;

if (cluster.isPrimary) {
  console.log(`[ECRAN]  ${NUM_CPUS} CPUs disponibles`);
  console.log(`[RAPIDE] Démarrage de ${NUM_CPUS} workers...`);

  // Créer un worker par CPU
  for (let i = 0; i < NUM_CPUS; i++) {
    cluster.fork();
  }

  // Redémarrer les workers qui crashent
  cluster.on('exit', (worker, code, signal) => {
    console.log(`[IMPACT] Worker ${worker.process.pid} mort (code: ${code}, signal: ${signal})`);
    console.log('[SYNC] Redémarrage d\'un nouveau worker...');
    cluster.fork();
  });

  cluster.on('online', (worker) => {
    console.log(`[OK] Worker ${worker.process.pid} démarré`);
  });

} else {
  // Chaque worker charge l'app Express
  require('./server');
  console.log(`Worker ${process.pid} démarré`);
}

// Lancer avec clustering
// node src/cluster.js

/*
[ATTENTION] ATTENTION au clustering :
-> Chaque worker = processus indépendant = pas de mémoire partagée
-> Les sessions en mémoire ne fonctionnent pas (utiliser Redis)
-> Les WebSockets nécessitent sticky sessions ou Redis pub/sub
-> PM2 gère le clustering automatiquement (recommandé en prod)
*/


// ----------------------------------------------------------------------------
// [ROUGE] CACHE REDIS
// ----------------------------------------------------------------------------

// npm install ioredis

// config/redis.js
const Redis = require('ioredis');
const config = require('./index');

const redis = new Redis(config.redis.url, {
  retryStrategy: (times) => {
    if (times > 10) return null;  // Arrêter après 10 tentatives
    return Math.min(times * 50, 2000);  // Délai exponentiel
  },
  maxRetriesPerRequest: 3
});

redis.on('connect', () => console.log('[OK] Redis connecté'));
redis.on('error', (err) => console.error('[X] Redis erreur:', err.message));

module.exports = redis;

// Fonctions utilitaires de cache
const redis = require('../config/redis');

const cache = {
  async get(key) {
    const value = await redis.get(key);
    return value ? JSON.parse(value) : null;
  },

  async set(key, value, ttlSeconds = 3600) {
    await redis.setex(key, ttlSeconds, JSON.stringify(value));
  },

  async del(key) {
    await redis.del(key);
  },

  async delPattern(pattern) {
    // Supprimer toutes les clés qui matchent un pattern
    const keys = await redis.keys(pattern);
    if (keys.length > 0) {
      await redis.del(...keys);
    }
  }
};

// Middleware de cache pour les routes GET
function cacheMiddleware(ttl = 300) {
  return async (req, res, next) => {
    if (req.method !== 'GET') return next();

    const key = `cache:${req.originalUrl}`;

    try {
      const cached = await cache.get(key);
      if (cached) {
        return res.json({ ...cached, _cached: true });
      }

      // Intercepter res.json pour mettre en cache
      const originalJson = res.json.bind(res);
      res.json = async (data) => {
        await cache.set(key, data, ttl);
        return originalJson(data);
      };

      next();
    } catch (err) {
      // Si Redis échoue, continuer sans cache
      console.error('Cache error:', err.message);
      next();
    }
  };
}

// Utilisation
router.get('/articles', cacheMiddleware(300), getAllArticles);  // Cache 5 min
router.get('/stats', cacheMiddleware(60), getStats);           // Cache 1 min

// Invalider le cache lors d'une modification
router.post('/articles', async (req, res, next) => {
  const article = await Article.create(req.body);
  await cache.delPattern('cache:/api/articles*');  // Invalider la liste
  res.status(201).json(article);
});

// Cache avec clé personnalisée
async function getUser(id) {
  const cacheKey = `user:${id}`;

  const cached = await cache.get(cacheKey);
  if (cached) return cached;

  const user = await User.findById(id).lean();
  if (user) {
    await cache.set(cacheKey, user, 1800);  // Cache 30 min
  }
  return user;
}

// Invalider le cache utilisateur lors d'une mise à jour
async function updateUser(id, data) {
  const user = await User.findByIdAndUpdate(id, data, { new: true });
  await cache.del(`user:${id}`);  // Invalider le cache
  return user;
}


// ----------------------------------------------------------------------------
// [SPOOL_OF_THREAD] WORKER THREADS - TÂCHES CPU-INTENSIVES
// ----------------------------------------------------------------------------

/*
QUAND utiliser Worker Threads ?
-> Traitement d'images
-> Cryptographie lourde
-> Calculs mathématiques complexes
-> Parsing de gros fichiers
-> Compression/décompression

[ATTENTION] PAS pour les I/O (réseau, fichiers) -> utiliser async/await
*/

// workers/imageProcessor.js (le worker)
const { parentPort, workerData } = require('worker_threads');
const sharp = require('sharp');  // npm install sharp

async function processImage(inputPath, outputPath, options) {
  const { width, height, quality, format } = options;

  await sharp(inputPath)
    .resize(width, height, { fit: 'cover' })
    .toFormat(format || 'jpeg', { quality: quality || 80 })
    .toFile(outputPath);

  return { success: true, outputPath };
}

// Écouter les messages du thread principal
parentPort.on('message', async (task) => {
  try {
    const result = await processImage(task.input, task.output, task.options);
    parentPort.postMessage({ id: task.id, result });
  } catch (err) {
    parentPort.postMessage({ id: task.id, error: err.message });
  }
});

// services/imageWorker.js (pool de workers)
const { Worker } = require('worker_threads');
const path = require('path');
const os = require('os');

class WorkerPool {
  constructor(workerPath, poolSize = os.cpus().length) {
    this.workerPath = workerPath;
    this.poolSize = poolSize;
    this.workers = [];
    this.queue = [];
    this.pendingCallbacks = new Map();
    this.taskCounter = 0;

    // Créer les workers
    for (let i = 0; i < poolSize; i++) {
      this._createWorker();
    }
  }

  _createWorker() {
    const worker = new Worker(this.workerPath);
    worker.busy = false;

    worker.on('message', ({ id, result, error }) => {
      const { resolve, reject } = this.pendingCallbacks.get(id);
      this.pendingCallbacks.delete(id);
      worker.busy = false;

      if (error) {
        reject(new Error(error));
      } else {
        resolve(result);
      }

      // Traiter la tâche suivante dans la queue
      if (this.queue.length > 0) {
        const nextTask = this.queue.shift();
        this._runTask(worker, nextTask);
      }
    });

    worker.on('error', (err) => {
      console.error('Worker error:', err);
      this.workers = this.workers.filter(w => w !== worker);
      this._createWorker();  // Remplacer le worker mort
    });

    this.workers.push(worker);
  }

  _runTask(worker, { task, resolve, reject }) {
    const id = ++this.taskCounter;
    this.pendingCallbacks.set(id, { resolve, reject });
    worker.busy = true;
    worker.postMessage({ ...task, id });
  }

  run(task) {
    return new Promise((resolve, reject) => {
      const freeWorker = this.workers.find(w => !w.busy);

      if (freeWorker) {
        this._runTask(freeWorker, { task, resolve, reject });
      } else {
        this.queue.push({ task, resolve, reject });
      }
    });
  }

  destroy() {
    this.workers.forEach(w => w.terminate());
  }
}

// Pool de workers pour le traitement d'images
const imagePool = new WorkerPool(
  path.join(__dirname, '../workers/imageProcessor.js'),
  4  // 4 workers
);

// Utilisation dans une route
router.post('/upload', upload.single('image'), async (req, res) => {
  const inputPath = req.file.path;
  const outputPath = inputPath.replace('original', 'processed');

  // Traitement en parallèle sans bloquer l'event loop
  const result = await imagePool.run({
    input: inputPath,
    output: outputPath,
    options: { width: 800, height: 600, quality: 85, format: 'webp' }
  });

  res.json({ success: true, path: result.outputPath });
});


// ----------------------------------------------------------------------------
// [RAPIDE] OPTIMISATIONS MONGODB
// ----------------------------------------------------------------------------

// 1. INDEX (déjà vu dans ch.10, rappel)
// Toujours indexer les champs de recherche
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ role: 1, createdAt: -1 });

// 2. LEAN() - Retourne du JSON simple
// [X] Lent : retourne un document Mongoose avec méthodes
const users = await User.find();

// [OK] Rapide : retourne des objets JS simples
const users = await User.find().lean();
// ~30% plus rapide, utilise moins de mémoire

// 3. SELECT() - Limiter les champs
const user = await User.findById(id)
  .select('name email role createdAt')  // Seulement ces champs
  .lean();

// 4. PAGINATION avec cursor (grands datasets)
// [X] Lent pour les grandes pages
const users = await User.find().skip(10000).limit(20);

// [OK] Cursor-based pagination
const users = await User.find({
  _id: { $gt: lastId }  // Commencer après le dernier ID
}).limit(20).lean();

// 5. EXPLAIN pour analyser les requêtes
const result = await User.find({ email: 'test@test.com' })
  .explain('executionStats');
console.log(result.executionStats);
// Vérifier que "IXSCAN" apparaît (et pas "COLLSCAN")

// 6. PROJECTION dans aggregate
await User.aggregate([
  { $match: { role: 'admin' } },
  { $project: { name: 1, email: 1 } },  // Limiter les champs
  { $limit: 100 }
]);

// 7. populate avec select
const posts = await Post.find()
  .populate('author', 'name email')  // Seulement name et email
  .lean();


// ----------------------------------------------------------------------------
// [GRAPHIQUE] PROFILING ET MESURE DES PERFORMANCES
// ----------------------------------------------------------------------------

// Mesurer le temps d'exécution
console.time('operation');
const result = await heavyOperation();
console.timeEnd('operation');  // "operation: 123.45ms"

// Middleware de timing
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`);
    if (duration > 1000) {
      console.warn(`[ATTENTION] Requête lente: ${req.path} (${duration}ms)`);
    }
  });
  next();
});

// Node.js --inspect pour profiling avec Chrome DevTools
// node --inspect src/server.js
// -> Ouvrir chrome://inspect dans Chrome
// -> Profiler CPU et mémoire


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 17 : Cache Redis et Clustering
// ----------------------------------------------------------------------------

/*
OBJECTIF : Optimiser une API avec Redis et clustering

TÂCHES :
1. Créer un middleware de cache Redis (TTL paramétrable)
2. Ajouter invalidation automatique du cache
3. Mesurer l'amélioration des performances
4. Configurer le clustering

CORRIGÉ - Cache Redis complet :
*/

// middlewares/cache.js
const redis = require('../config/redis');
const logger = require('../utils/logger');

const cache = {
  async get(key) {
    try {
      const val = await redis.get(key);
      return val ? JSON.parse(val) : null;
    } catch (err) {
      logger.warn('Cache GET failed:', err.message);
      return null;
    }
  },

  async set(key, value, ttl = 300) {
    try {
      await redis.setex(key, ttl, JSON.stringify(value));
      return true;
    } catch (err) {
      logger.warn('Cache SET failed:', err.message);
      return false;
    }
  },

  async invalidate(pattern) {
    try {
      const keys = await redis.keys(pattern);
      if (keys.length > 0) {
        await redis.del(...keys);
        logger.debug(`Cache invalidé: ${keys.length} clés (${pattern})`);
      }
    } catch (err) {
      logger.warn('Cache INVALIDATE failed:', err.message);
    }
  }
};

function withCache(ttl = 300, keyFn = null) {
  return async (req, res, next) => {
    const key = keyFn ? keyFn(req) : `cache:${req.url}`;

    const cached = await cache.get(key);
    if (cached) {
      return res.json({ ...cached, _fromCache: true });
    }

    const originalJson = res.json.bind(res);
    res.json = async (data) => {
      if (res.statusCode < 400) {
        await cache.set(key, data, ttl);
      }
      return originalJson(data);
    };

    next();
  };
}

module.exports = { cache, withCache };

// Utilisation
const { cache, withCache } = require('../middlewares/cache');

router.get('/articles',
  withCache(300, req => `articles:${JSON.stringify(req.query)}`),
  getArticles
);

router.post('/articles', async (req, res, next) => {
  const article = await Article.create(req.body);
  await cache.invalidate('articles:*');
  res.status(201).json({ success: true, data: article });
});


// ============================================================================
// [GUIDE] CHAPITRE 18 : DÉPLOIEMENT
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Préparer l'app pour la production
[OK] Configurer PM2 (process manager)
[OK] Créer un Dockerfile multi-stage
[OK] Orchestrer avec docker-compose
[OK] Configurer Nginx comme reverse proxy
[OK] Déployer sur Heroku / VPS
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] PRÉPARER L'APP POUR LA PRODUCTION
// ----------------------------------------------------------------------------

// Package.json - scripts essentiels
{
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "cluster": "node src/cluster.js",
    "test": "jest --forceExit",
    "test:coverage": "jest --coverage --forceExit",
    "lint": "eslint src/",
    "build": "echo 'No build step for Node.js'",
    "prestart": "node -e \"require('./src/config')\"  "
  }
}

// Graceful shutdown - Important pour ne pas perdre de requêtes
// src/server.js
const config = require('./config');
const db = require('./config/database');
const createApp = require('./app');
const logger = require('./utils/logger');

const app = createApp();
let server;

async function startServer() {
  try {
    // Connexion DB avant de démarrer
    await db.connect();

    server = app.listen(config.server.port, config.server.host, () => {
      logger.info(`[RAPIDE] Serveur démarré [PID: ${process.pid}]`, {
        port: config.server.port,
        env: config.env,
        nodeVersion: process.version
      });
    });

    // Timeout pour éviter les connexions qui traînent
    server.keepAliveTimeout = 65000;
    server.headersTimeout = 66000;

  } catch (err) {
    logger.error('[X] Démarrage échoué:', err);
    process.exit(1);
  }
}

async function gracefulShutdown(signal) {
  logger.info(`${signal} reçu. Arrêt gracieux...`);

  if (!server) return process.exit(0);

  server.close(async () => {
    logger.info('Connexions HTTP fermées');

    try {
      await db.disconnect();
      logger.info('Base de données déconnectée');
      process.exit(0);
    } catch (err) {
      logger.error('Erreur lors de la déconnexion:', err);
      process.exit(1);
    }
  });

  // Forcer l'arrêt après 30 secondes
  setTimeout(() => {
    logger.error('Arrêt forcé après timeout');
    process.exit(1);
  }, 30000);
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));  // Docker stop, PM2
process.on('SIGINT', () => gracefulShutdown('SIGINT'));    // Ctrl+C

startServer();


// ----------------------------------------------------------------------------
// [SYNC] PM2 - PROCESS MANAGER
// ----------------------------------------------------------------------------

/*
INSTALLATION :
npm install -g pm2

PM2 = gestionnaire de processus pour Node.js en production
-> Redémarrage automatique si crash
-> Cluster mode (tous les CPUs)
-> Logs gérés
-> Monitoring intégré
-> Démarrage automatique au boot
*/

// ecosystem.config.js (à la racine du projet)
module.exports = {
  apps: [{
    name: 'mon-api',
    script: 'src/server.js',

    // Mode cluster (utilise tous les CPUs)
    instances: 'max',       // ou un nombre : 4
    exec_mode: 'cluster',

    // Variables d'environnement
    env: {
      NODE_ENV: 'development',
      PORT: 3000
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000
    },

    // Stabilité
    max_memory_restart: '500M',  // Restart si > 500MB RAM
    min_uptime: '10s',            // Doit rester en vie au moins 10s
    max_restarts: 10,             // Abandon après 10 crashs rapides
    restart_delay: 4000,          // Attendre 4s avant restart

    // Logs
    out_file: 'logs/out.log',
    error_file: 'logs/error.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    combine_logs: true,

    // Zero-downtime reload
    kill_timeout: 10000,    // 10s avant SIGKILL
    listen_timeout: 5000    // 5s pour démarrer
  }]
};

/*
COMMANDES PM2 :

pm2 start ecosystem.config.js --env production  # Démarrer
pm2 reload mon-api                              # Reload sans downtime
pm2 restart mon-api                             # Restart
pm2 stop mon-api                               # Arrêter
pm2 delete mon-api                             # Supprimer
pm2 list                                        # Lister
pm2 logs mon-api                               # Logs temps réel
pm2 monit                                       # Monitoring
pm2 save                                        # Sauvegarder la config
pm2 startup                                     # Démarrer au boot
pm2 status                                      # Statut
*/


// ----------------------------------------------------------------------------
// [DOCKER] DOCKER
// ----------------------------------------------------------------------------

// Dockerfile - Multi-stage build
/*
# ===== STAGE BASE =====
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./

# ===== DÉVELOPPEMENT =====
FROM base AS development
ENV NODE_ENV=development
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

# ===== TEST =====
FROM base AS test
ENV NODE_ENV=test
RUN npm ci
COPY . .
CMD ["npm", "test"]

# ===== PRODUCTION - INSTALLER DEPS =====
FROM base AS deps-prod
RUN npm ci --only=production

# ===== PRODUCTION FINALE =====
FROM node:20-alpine AS production

# Sécurité : utilisateur non-root
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 --ingroup nodejs nodeapp

WORKDIR /app

# Copier uniquement ce qui est nécessaire
COPY --from=deps-prod --chown=nodeapp:nodejs /app/node_modules ./node_modules
COPY --chown=nodeapp:nodejs . .

# Changer d'utilisateur (sécurité)
USER nodeapp

EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/ping', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"

CMD ["node", "src/server.js"]
*/

// .dockerignore
/*
node_modules
npm-debug.log
.env
.env.*
logs/
uploads/
.git
.gitignore
README.md
coverage/
.nyc_output
*/

// docker-compose.yml
/*
version: '3.8'

services:
  app:
    build:
      context: .
      target: production
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      PORT: 3000
      MONGODB_URI: mongodb://mongo:27017/myapp
      REDIS_URL: redis://redis:6379
    env_file:
      - .env.production
    depends_on:
      mongo:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - app-network
    volumes:
      - ./uploads:/app/uploads
      - ./logs:/app/logs

  mongo:
    image: mongo:7-jammy
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
    volumes:
      - mongo-data:/data/db
    healthcheck:
      test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
      - ./static:/var/www/static:ro
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - app-network

volumes:
  mongo-data:
  redis-data:

networks:
  app-network:
    driver: bridge
*/

// Commandes Docker utiles
/*
# Build
docker build -t mon-api .
docker build --target development -t mon-api:dev .
docker build --target test -t mon-api:test .

# Run
docker run -p 3000:3000 --env-file .env mon-api
docker run --rm mon-api:test  # Tests dans container

# Docker Compose
docker-compose up -d           # Démarrer en arrière-plan
docker-compose up -d --build   # Rebuild avant démarrer
docker-compose logs -f app     # Voir les logs de l'app
docker-compose exec app sh     # Shell dans le container
docker-compose down            # Arrêter
docker-compose down -v         # Arrêter + supprimer volumes
docker-compose ps              # Statut des services
*/


// ----------------------------------------------------------------------------
// [WEB] NGINX COMME REVERSE PROXY
// ----------------------------------------------------------------------------

// nginx.conf
/*
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # Compression gzip
    gzip on;
    gzip_vary on;
    gzip_types text/plain application/json application/javascript text/css application/xml;
    gzip_min_length 1024;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    # Upstream Node.js
    upstream nodejs {
        server app:3000;
        keepalive 32;
    }

    # Redirect HTTP -> HTTPS
    server {
        listen 80;
        server_name monapp.com www.monapp.com;
        return 301 https://$host$request_uri;
    }

    # HTTPS
    server {
        listen 443 ssl http2;
        server_name monapp.com www.monapp.com;

        # Certificats SSL (Let's Encrypt)
        ssl_certificate     /etc/nginx/certs/fullchain.pem;
        ssl_certificate_key /etc/nginx/certs/privkey.pem;
        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_timeout 1d;
        ssl_session_cache shared:MozSSL:10m;

        # Headers de sécurité
        add_header Strict-Transport-Security "max-age=63072000" always;
        add_header X-Frame-Options DENY always;
        add_header X-Content-Type-Options nosniff always;

        # Taille max des uploads
        client_max_body_size 10M;

        # Fichiers statiques (servis directement par Nginx)
        location /static/ {
            alias /var/www/static/;
            expires 30d;
            add_header Cache-Control "public, immutable";
        }

        # API avec rate limiting
        location /api/ {
            limit_req zone=api burst=20 nodelay;

            proxy_pass http://nodejs;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Connection '';
            proxy_cache_bypass $http_upgrade;
            proxy_read_timeout 60s;
            proxy_connect_timeout 10s;
        }

        # WebSocket
        location /socket.io/ {
            proxy_pass http://nodejs;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # Health check (pas de logs)
        location /ping {
            proxy_pass http://nodejs;
            access_log off;
        }
    }
}
*/


// ----------------------------------------------------------------------------
// [CLOUD] DÉPLOIEMENT SUR HEROKU
// ----------------------------------------------------------------------------

// Procfile
/*
web: node src/server.js
worker: node src/workers/email.js
*/

// Heroku - Commandes de déploiement
/*
# Installer Heroku CLI : https://devcenter.heroku.com/articles/heroku-cli

# Créer et configurer
heroku create mon-api-node
heroku config:set NODE_ENV=production
heroku config:set JWT_SECRET=$(node -e "console.log(require('crypto').randomBytes(64).toString('hex'))")
heroku config:set JWT_REFRESH_SECRET=$(node -e "console.log(require('crypto').randomBytes(64).toString('hex'))")

# Add-ons
heroku addons:create mongolab:sandbox       # MongoDB Atlas
heroku addons:create heroku-redis:mini      # Redis

# Déployer
heroku git:remote -a mon-api-node
git push heroku main

# Monitoring
heroku logs --tail
heroku ps
heroku ps:scale web=2                       # 2 dynos (payant)

# Base de données
heroku run node scripts/seed.js            # Exécuter un script
*/


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 18 : Dockeriser une API
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer un environnement Docker complet

TÂCHES :
1. Dockerfile multi-stage (dev, test, prod)
2. docker-compose avec App + MongoDB + Redis
3. Health checks sur tous les services
4. Script d'attente des dépendances

CORRIGÉ - Script wait-for.sh :
*/

// scripts/wait-for-mongo.js
const mongoose = require('mongoose');

async function waitForMongo(uri, retries = 30, delay = 2000) {
  for (let i = 0; i < retries; i++) {
    try {
      await mongoose.connect(uri, { serverSelectionTimeoutMS: 2000 });
      console.log('[OK] MongoDB prêt !');
      await mongoose.disconnect();
      return;
    } catch {
      console.log(`[HOURGLASS_WITH_FLOWING_SAND] Attente MongoDB... (${i + 1}/${retries})`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  console.error('[X] MongoDB non disponible');
  process.exit(1);
}

waitForMongo(process.env.MONGODB_URI || 'mongodb://mongo:27017/myapp');

// Dans le Dockerfile/docker-compose, lancer avant l'app :
// CMD ["sh", "-c", "node scripts/wait-for-mongo.js && node src/server.js"]


// ============================================================================
// [GUIDE] CHAPITRE 19 : LOGGING ET MONITORING
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Configurer Winston pour un logging professionnel
[OK] Structurer les logs en JSON (pour agrégation)
[OK] Ajouter des health checks complets
[OK] Intégrer Sentry pour le suivi des erreurs
[OK] Exposer des métriques Prometheus
*/


// ----------------------------------------------------------------------------
// [NOTE] WINSTON - LOGGING PROFESSIONNEL
// ----------------------------------------------------------------------------

/*
npm install winston winston-daily-rotate-file
*/

const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

const { combine, timestamp, printf, colorize, json, errors, metadata } = winston.format;

// Format lisible pour le développement
const devFormat = combine(
  colorize({ all: true }),
  timestamp({ format: 'HH:mm:ss' }),
  errors({ stack: true }),
  printf(({ level, message, timestamp: ts, stack, ...meta }) => {
    const metaStr = Object.keys(meta).length
      ? `\n  ${JSON.stringify(meta, null, 2)}`
      : '';
    return `${ts} [${level}]: ${message}${stack ? '\n' + stack : ''}${metaStr}`;
  })
);

// Format JSON pour la production (agrégation ELK, Datadog...)
const prodFormat = combine(
  timestamp(),
  errors({ stack: true }),
  metadata({ fillExcept: ['message', 'level', 'timestamp'] }),
  json()
);

const isDev = process.env.NODE_ENV !== 'production';

const transports = [
  new winston.transports.Console({
    format: isDev ? devFormat : prodFormat,
    level: isDev ? 'debug' : 'info'
  })
];

// Fichiers rotatifs en production
if (!isDev) {
  transports.push(
    new DailyRotateFile({
      filename: 'logs/error-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      level: 'error',
      maxFiles: '30d',
      maxSize: '20m',
      format: prodFormat,
      zippedArchive: true
    }),
    new DailyRotateFile({
      filename: 'logs/combined-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxFiles: '14d',
      maxSize: '50m',
      format: prodFormat,
      zippedArchive: true
    })
  );
}

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
  transports,
  exitOnError: false
});

// Ajouter le contexte de requête facilement
logger.request = function(req) {
  return logger.child({
    requestId: req.id,
    method: req.method,
    path: req.path,
    ip: req.ip,
    userId: req.user?.id
  });
};

module.exports = logger;

// Middleware de logging des requêtes (plus précis que morgan)
const logger = require('../utils/logger');
const { v4: uuidv4 } = require('uuid');  // npm install uuid

app.use((req, res, next) => {
  req.id = uuidv4();                    // ID unique par requête
  req.startTime = Date.now();

  const reqLogger = logger.request(req);
  req.log = reqLogger;

  reqLogger.http(`-> ${req.method} ${req.path}`, {
    query: req.query,
    contentLength: req.headers['content-length']
  });

  res.on('finish', () => {
    const duration = Date.now() - req.startTime;
    const level = res.statusCode >= 500 ? 'error'
      : res.statusCode >= 400 ? 'warn'
        : 'http';

    reqLogger[level](`<- ${res.statusCode} ${duration}ms`, {
      statusCode: res.statusCode,
      duration
    });
  });

  next();
});


// ----------------------------------------------------------------------------
// [STETHOSCOPE] HEALTH CHECKS COMPLETS
// ----------------------------------------------------------------------------

const mongoose = require('mongoose');
const os = require('os');

app.get('/health', async (req, res) => {
  const checks = {};
  let status = 'healthy';

  // MongoDB
  try {
    if (mongoose.connection.readyState === 1) {
      await mongoose.connection.db.admin().ping();
      checks.mongodb = { status: 'ok' };
    } else {
      checks.mongodb = { status: 'disconnected' };
      status = 'degraded';
    }
  } catch (err) {
    checks.mongodb = { status: 'error', error: err.message };
    status = 'unhealthy';
  }

  // Redis
  try {
    const redis = require('./config/redis');
    const ping = await redis.ping();
    checks.redis = { status: ping === 'PONG' ? 'ok' : 'error' };
  } catch (err) {
    checks.redis = { status: 'error', error: err.message };
    status = 'degraded';
  }

  // Mémoire
  const mem = process.memoryUsage();
  const heapUsedMB = Math.round(mem.heapUsed / 1024 / 1024);
  const heapTotalMB = Math.round(mem.heapTotal / 1024 / 1024);
  const heapPct = Math.round((heapUsedMB / heapTotalMB) * 100);
  checks.memory = {
    status: heapPct < 85 ? 'ok' : 'high',
    heapUsedMB,
    heapTotalMB,
    heapPercent: heapPct
  };
  if (heapPct >= 85) status = 'degraded';

  const httpStatus = status === 'unhealthy' ? 503 : status === 'degraded' ? 207 : 200;

  res.status(httpStatus).json({
    status,
    timestamp: new Date().toISOString(),
    uptime: Math.floor(process.uptime()),
    pid: process.pid,
    version: process.env.npm_package_version || '1.0.0',
    environment: process.env.NODE_ENV,
    checks,
    system: {
      cpus: os.cpus().length,
      loadAvg: os.loadavg().map(l => Math.round(l * 100) / 100),
      freeMemoryMB: Math.round(os.freemem() / 1024 / 1024)
    }
  });
});

// Endpoint simple pour load balancers
app.get('/ping', (req, res) => res.json({ status: 'ok', timestamp: Date.now() }));


// ----------------------------------------------------------------------------
// [TELESCOPE] SENTRY - TRACKING DES ERREURS
// ----------------------------------------------------------------------------

/*
npm install @sentry/node

Sentry = service de tracking des erreurs
-> Capture automatiquement les exceptions
-> Agrège les erreurs similaires
-> Alerte par email/Slack
-> Stack traces avec contexte
*/

const Sentry = require('@sentry/node');

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  release: `${process.env.npm_package_name}@${process.env.npm_package_version}`,

  // Intégrations
  integrations: [
    new Sentry.Integrations.Http({ tracing: true }),
    new Sentry.Integrations.Express({ app }),
    new Sentry.Integrations.Mongo(),
  ],

  // % de requêtes tracées (0 = aucune, 1 = toutes)
  tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,

  // Ne pas envoyer en test
  enabled: process.env.NODE_ENV !== 'test'
});

// [ATTENTION] ORDRE IMPORTANT : Sentry AVANT les routes
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

// Vos routes ici...

// Sentry error handler AVANT votre error handler custom
app.use(Sentry.Handlers.errorHandler({
  shouldHandleError(error) {
    // Envoyer seulement les erreurs 500+ à Sentry
    return error.status >= 500;
  }
}));

// Votre error handler...

// Capturer manuellement une erreur avec contexte
try {
  await riskyOperation();
} catch (err) {
  Sentry.withScope(scope => {
    scope.setUser({ id: req.user?.id, email: req.user?.email });
    scope.setTag('operation', 'riskyOperation');
    scope.setContext('request', { body: req.body });
    Sentry.captureException(err);
  });
  next(err);
}


// ----------------------------------------------------------------------------
// [GRAPHIQUE] MÉTRIQUES PROMETHEUS
// ----------------------------------------------------------------------------

/*
npm install prom-client

Prometheus = collecteur de métriques open source
-> Scrape /metrics toutes les N secondes
-> Grafana pour la visualisation
-> Alertmanager pour les alertes
*/

const promClient = require('prom-client');

// Collecter les métriques système par défaut
promClient.collectDefaultMetrics({
  prefix: 'node_',
  labels: { app: 'mon-api' }
});

// Métriques personnalisées
const metrics = {
  // Compteur de requêtes
  httpRequests: new promClient.Counter({
    name: 'http_requests_total',
    help: 'Total HTTP requests',
    labelNames: ['method', 'route', 'status']
  }),

  // Histogramme de durée
  httpDuration: new promClient.Histogram({
    name: 'http_request_duration_ms',
    help: 'HTTP request duration in milliseconds',
    labelNames: ['method', 'route'],
    buckets: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]
  }),

  // Gauge de connexions actives
  activeConnections: new promClient.Gauge({
    name: 'active_connections_total',
    help: 'Current number of active connections'
  }),

  // Compteur d'erreurs métier
  businessErrors: new promClient.Counter({
    name: 'business_errors_total',
    help: 'Total business logic errors',
    labelNames: ['type', 'operation']
  })
};

// Middleware de collecte
app.use((req, res, next) => {
  const end = metrics.httpDuration.startTimer();
  metrics.activeConnections.inc();

  res.on('finish', () => {
    const route = req.route?.path || 'unknown';
    metrics.httpRequests.inc({ method: req.method, route, status: res.statusCode });
    end({ method: req.method, route });
    metrics.activeConnections.dec();
  });

  next();
});

// Endpoint /metrics
app.get('/metrics', async (req, res) => {
  // Protéger en production (IP whitelist ou token)
  if (process.env.NODE_ENV === 'production') {
    const authHeader = req.headers.authorization;
    if (authHeader !== `Bearer ${process.env.METRICS_TOKEN}`) {
      return res.status(403).json({ error: 'Unauthorized' });
    }
  }

  res.set('Content-Type', promClient.register.contentType);
  res.send(await promClient.register.metrics());
});

// Utiliser les métriques
metrics.businessErrors.inc({ type: 'validation', operation: 'createUser' });


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 19 : Logging et Health Check complets
// ----------------------------------------------------------------------------

/*
OBJECTIF : Configurer un système de logging et monitoring complet

TÂCHES :
1. Winston avec format JSON en prod, colorisé en dev
2. Middleware d'ID de requête unique
3. Health check avec vérification MongoDB + Redis + mémoire
4. Un endpoint /metrics basique

CORRIGÉ - Système complet :
*/

// utils/logger.js (version finale)
const winston = require('winston');
const { combine, timestamp, printf, colorize, json, errors } = winston.format;

const isDev = process.env.NODE_ENV !== 'production';
const isTest = process.env.NODE_ENV === 'test';

const logger = winston.createLogger({
  silent: isTest,
  level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
  transports: [
    new winston.transports.Console({
      format: isDev
        ? combine(
          colorize({ all: true }),
          timestamp({ format: 'HH:mm:ss' }),
          errors({ stack: true }),
          printf(({ level, message, timestamp: ts, stack, ...meta }) =>
            `${ts} [${level}]: ${message}${Object.keys(meta).length ? ' ' + JSON.stringify(meta) : ''}${stack ? '\n' + stack : ''}`
          )
        )
        : combine(timestamp(), errors({ stack: true }), json())
    })
  ]
});

logger.withReq = (req) => logger.child({
  reqId: req.id,
  method: req.method,
  path: req.path,
  userId: req.user?.id
});

module.exports = logger;


// ============================================================================
// [GUIDE] CHAPITRE 20 : BEST PRACTICES ET RÉCAPITULATIF
// ============================================================================

/*
[OBJECTIF] CE CHAPITRE FINAL RASSEMBLE :
[OK] Les patterns architecturaux recommandés
[OK] La checklist complète de production
[OK] Un projet exemple complet
[OK] Les ressources pour aller plus loin
*/


// ----------------------------------------------------------------------------
// [CONSTRUCTION] ARCHITECTURE FINALE RECOMMANDÉE
// ----------------------------------------------------------------------------

/*
STRUCTURE DE FICHIERS (scalable)

mon-api/
├── src/
│   ├── server.js               <- Point d'entrée + graceful shutdown
│   ├── app.js                  <- Factory Express (testable)
│   ├── cluster.js              <- Mode cluster PM2
│   │
│   ├── config/
│   │   ├── index.js            <- Config unifiée + validation
│   │   ├── database.js         <- Connexion MongoDB
│   │   └── redis.js            <- Connexion Redis
│   │
│   ├── models/                 <- Schémas Mongoose
│   │   ├── User.js
│   │   └── Article.js
│   │
│   ├── controllers/            <- Logique métier
│   │   ├── auth.controller.js
│   │   └── article.controller.js
│   │
│   ├── routes/                 <- Routing Express
│   │   ├── index.js            <- Enregistrement de toutes les routes
│   │   ├── auth.routes.js
│   │   └── article.routes.js
│   │
│   ├── middlewares/            <- Middlewares
│   │   ├── auth.js             <- JWT verify
│   │   ├── validate.js         <- Wrapper validation
│   │   ├── cache.js            <- Cache Redis
│   │   └── errorHandler.js     <- Gestion centralisée des erreurs
│   │
│   ├── services/               <- Services métier
│   │   ├── email.service.js    <- Envoi d'emails (nodemailer)
│   │   └── upload.service.js   <- Gestion des fichiers
│   │
│   ├── validators/             <- Règles de validation
│   │   ├── auth.validator.js
│   │   └── article.validator.js
│   │
│   └── utils/                  <- Utilitaires
│       ├── logger.js
│       ├── asyncHandler.js
│       └── ApiResponse.js
│
├── tests/
│   ├── setup.js
│   ├── auth.test.js
│   └── articles.test.js
│
├── scripts/
│   ├── seed.js                 <- Données de test
│   └── wait-for-mongo.js
│
├── .env.development
├── .env.test
├── .env.example
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── ecosystem.config.js
└── package.json
*/


// ----------------------------------------------------------------------------
// [MESURE] PATTERNS ESSENTIELS
// ----------------------------------------------------------------------------

// 1. ASYNC HANDLER - toujours utiliser
// utils/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

module.exports = asyncHandler;

// 2. RÉPONSES API STANDARDISÉES
// utils/ApiResponse.js
class ApiResponse {
  static success(res, data, { statusCode = 200, message = 'Succès', meta } = {}) {
    const body = { success: true, message, data };
    if (meta) body.meta = meta;
    return res.status(statusCode).json(body);
  }

  static created(res, data, message = 'Créé avec succès') {
    return this.success(res, data, { statusCode: 201, message });
  }

  static noContent(res) {
    return res.status(204).send();
  }

  static paginated(res, data, total, { page, limit }) {
    return res.json({
      success: true,
      data,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
        hasNext: page * limit < total,
        hasPrev: page > 1
      }
    });
  }
}

module.exports = ApiResponse;

// 3. GESTION D'ERREURS CENTRALISÉE
// middlewares/errorHandler.js
class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

const errors = {
  notFound: (resource = 'Ressource') => new AppError(`${resource} introuvable`, 404),
  unauthorized: (msg = 'Non autorisé') => new AppError(msg, 401),
  forbidden: (msg = 'Accès interdit') => new AppError(msg, 403),
  conflict: (msg = 'Conflit') => new AppError(msg, 409),
  badRequest: (msg = 'Requête invalide') => new AppError(msg, 400)
};

function errorHandler(err, req, res, next) {
  const logger = require('./utils/logger');

  // Erreurs connues (opérationnelles)
  if (err.isOperational) {
    logger.warn('Operational error:', { message: err.message, path: req.path });
    return res.status(err.statusCode).json({
      success: false,
      error: err.message
    });
  }

  // Erreurs Mongoose
  if (err.name === 'ValidationError') {
    const messages = Object.values(err.errors).map(e => ({
      field: e.path,
      message: e.message
    }));
    return res.status(422).json({ success: false, errors: messages });
  }

  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return res.status(409).json({
      success: false,
      error: `${field} déjà utilisé`
    });
  }

  if (err.name === 'CastError') {
    return res.status(400).json({ success: false, error: 'ID invalide' });
  }

  // Erreurs JWT
  if (err.name === 'JsonWebTokenError') {
    return res.status(401).json({ success: false, error: 'Token invalide' });
  }
  if (err.name === 'TokenExpiredError') {
    return res.status(401).json({ success: false, error: 'Token expiré' });
  }

  // Erreur inconnue (bug !)
  logger.error('Unexpected error:', {
    error: err.message,
    stack: err.stack,
    path: req.path
  });

  res.status(500).json({
    success: false,
    error: process.env.NODE_ENV === 'production'
      ? 'Une erreur est survenue'
      : err.message
  });
}

function notFoundHandler(req, res) {
  res.status(404).json({
    success: false,
    error: `Route ${req.method} ${req.path} introuvable`
  });
}

module.exports = { AppError, errors, errorHandler, notFoundHandler };

// 4. PAGINATION RÉUTILISABLE
// utils/pagination.js
async function paginate(Model, query = {}, options = {}) {
  const {
    page = 1,
    limit = 10,
    sort = '-createdAt',
    select = '',
    populate = null
  } = options;

  const skip = (page - 1) * limit;

  const [data, total] = await Promise.all([
    Model.find(query)
      .select(select)
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .populate(populate || [])
      .lean(),
    Model.countDocuments(query)
  ]);

  return {
    data,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
      hasNext: page * limit < total,
      hasPrev: page > 1
    }
  };
}

module.exports = { paginate };

// Utilisation
const { paginate } = require('../utils/pagination');

const result = await paginate(Article, { status: 'published' }, {
  page: parseInt(req.query.page),
  limit: parseInt(req.query.limit),
  sort: '-createdAt',
  select: 'title author createdAt',
  populate: { path: 'author', select: 'name email' }
});

return ApiResponse.paginated(res, result.data, result.pagination.total, result.pagination);


// ----------------------------------------------------------------------------
// [LISTE] CHECKLIST COMPLÈTE DE PRODUCTION
// ----------------------------------------------------------------------------

/*
═══════════════════════════════════════════════════════
[OK] CODE QUALITY
═══════════════════════════════════════════════════════
[ ] ESLint configuré et sans erreurs (npm run lint)
[ ] Prettier pour le formatage cohérent
[ ] Tests écrits avec coverage > 80% (npm test:coverage)
[ ] Pas de console.log (remplacés par logger)
[ ] Toutes les fonctions async ont asyncHandler ou try/catch
[ ] Pas de code commenté inutile
[ ] Variables explicites (pas de i, x, tmp)

═══════════════════════════════════════════════════════
[OK] SÉCURITÉ
═══════════════════════════════════════════════════════
[ ] Helmet() configuré avec CSP
[ ] CORS restrictif (domaines précis, pas *)
[ ] Rate limiting (global + auth + upload)
[ ] Validation de tous les inputs (express-validator ou Joi)
[ ] Protection XSS (xss-clean)
[ ] Protection NoSQL injection (mongo-sanitize)
[ ] Pas de mass assignment
[ ] bcrypt avec rounds >= 12
[ ] JWT avec secret fort (64+ chars aléatoires)
[ ] Access token court (15min), refresh token long (7j)
[ ] Champs sensibles exclus des réponses
[ ] Erreurs génériques en production (pas de stack trace)
[ ] npm audit --production sans vulnérabilités critiques

═══════════════════════════════════════════════════════
[OK] CONFIGURATION
═══════════════════════════════════════════════════════
[ ] Pas de secrets dans le code
[ ] .env dans .gitignore
[ ] .env.example commité (sans valeurs réelles)
[ ] Config centralisée et validée au démarrage
[ ] Variables d'env différentes par environnement

═══════════════════════════════════════════════════════
[OK] PERFORMANCE
═══════════════════════════════════════════════════════
[ ] Compression gzip activée
[ ] Redis pour le cache des endpoints lourds
[ ] Index MongoDB sur les champs de recherche
[ ] lean() sur les requêtes Mongoose
[ ] select() pour limiter les champs retournés
[ ] Pagination sur toutes les listes
[ ] PM2 en cluster mode (ou clustering manuel)

═══════════════════════════════════════════════════════
[OK] OPÉRATIONS
═══════════════════════════════════════════════════════
[ ] /ping pour les load balancers (simple, rapide)
[ ] /health pour le monitoring détaillé
[ ] /metrics pour Prometheus
[ ] Logging structuré JSON en production
[ ] Logs rotatifs (winston-daily-rotate-file)
[ ] Sentry pour le tracking des erreurs
[ ] Alertes configurées (Sentry, Grafana...)
[ ] Graceful shutdown (SIGTERM/SIGINT)

═══════════════════════════════════════════════════════
[OK] DÉPLOIEMENT
═══════════════════════════════════════════════════════
[ ] Dockerfile multi-stage (dev, test, prod)
[ ] .dockerignore configuré
[ ] docker-compose pour le développement local
[ ] Health checks dans docker-compose
[ ] Nginx comme reverse proxy (SSL, gzip, rate limit)
[ ] CI/CD pipeline (GitHub Actions, GitLab CI...)
[ ] Backups automatiques de la base de données
[ ] Plan de rollback documenté

═══════════════════════════════════════════════════════
[OK] DOCUMENTATION
═══════════════════════════════════════════════════════
[ ] README.md avec installation, config, commandes
[ ] Documentation API (Swagger/OpenAPI ou Postman)
[ ] .env.example à jour
[ ] CHANGELOG.md (optionnel mais recommandé)
*/


// ----------------------------------------------------------------------------
// [COURS] EXERCICE FINAL : API Complète et Prête pour la Production
// ----------------------------------------------------------------------------

/*
OBJECTIF : Créer une API complète, sécurisée et déployable

CONTEXTE : API de gestion d'articles de blog

FONCTIONNALITÉS :
1. Auth JWT (register, login, refresh, logout, profil)
2. Articles (CRUD, pagination, recherche)
3. Upload d'image de couverture
4. Cache Redis sur les listes
5. Rate limiting (global + auth)
6. Tests d'intégration (>80% coverage)
7. Health check et /ping
8. Docker + PM2 config

SCHÉMA DE DONNÉES :

User : {
  name, email, password (hashé), role (user/admin),
  refreshTokens[], createdAt
}

Article : {
  title, content, slug (auto-généré), author (ref User),
  coverImage, tags[], status (draft/published),
  viewCount, createdAt, updatedAt
}

ENDPOINTS :

Auth :
  POST /api/auth/register          -> Créer compte
  POST /api/auth/login             -> Se connecter
  POST /api/auth/refresh           -> Renouveler le token
  POST /api/auth/logout            -> Se déconnecter
  GET  /api/auth/profile           -> Mon profil (auth requis)

Articles :
  GET    /api/articles             -> Liste paginée (cache 5min)
  GET    /api/articles/:slug       -> Détail + incrémenter viewCount
  POST   /api/articles             -> Créer (auth requis)
  PUT    /api/articles/:id         -> Modifier (auteur ou admin)
  DELETE /api/articles/:id         -> Supprimer (auteur ou admin)
  POST   /api/articles/:id/cover   -> Upload image de couverture

CORRIGÉ - Modèle Article avec slug automatique :
*/

// models/Article.js
const mongoose = require('mongoose');
const slugify = require('slugify');  // npm install slugify

const articleSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, 'Le titre est requis'],
    trim: true,
    maxlength: [200, 'Titre trop long']
  },

  slug: {
    type: String,
    unique: true
  },

  content: {
    type: String,
    required: [true, 'Le contenu est requis']
  },

  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },

  coverImage: {
    type: String,
    default: null
  },

  tags: [{
    type: String,
    lowercase: true,
    trim: true
  }],

  status: {
    type: String,
    enum: ['draft', 'published'],
    default: 'draft'
  },

  viewCount: {
    type: Number,
    default: 0
  }

}, {
  timestamps: true,
  toJSON: { virtuals: true }
});

// Index
articleSchema.index({ slug: 1 });
articleSchema.index({ status: 1, createdAt: -1 });
articleSchema.index({ tags: 1 });
articleSchema.index({ author: 1 });

// Générer le slug avant sauvegarde
articleSchema.pre('save', async function(next) {
  if (!this.isModified('title')) return next();

  let slug = slugify(this.title, { lower: true, strict: true });
  const existing = await this.constructor.findOne({ slug });
  if (existing && existing._id.toString() !== this._id.toString()) {
    slug = `${slug}-${Date.now()}`;
  }
  this.slug = slug;
  next();
});

// Virtual : extrait (100 premiers mots)
articleSchema.virtual('excerpt').get(function() {
  return this.content.split(' ').slice(0, 100).join(' ') +
    (this.content.split(' ').length > 100 ? '...' : '');
});

module.exports = mongoose.model('Article', articleSchema);

// controllers/article.controller.js (CRUD complet)
const Article = require('../models/Article');
const { cache } = require('../middlewares/cache');
const { paginate } = require('../utils/pagination');
const { AppError } = require('../middlewares/errorHandler');
const ApiResponse = require('../utils/ApiResponse');
const asyncHandler = require('../utils/asyncHandler');

// GET /api/articles
exports.getAll = asyncHandler(async (req, res) => {
  const { page = 1, limit = 10, tag, search } = req.query;

  const query = { status: 'published' };
  if (tag) query.tags = tag;
  if (search) query.$text = { $search: search };

  const result = await paginate(Article, query, {
    page: parseInt(page),
    limit: parseInt(limit),
    populate: { path: 'author', select: 'name' },
    select: 'title slug excerpt author tags coverImage viewCount createdAt'
  });

  ApiResponse.paginated(res, result.data, result.pagination.total, result.pagination);
});

// GET /api/articles/:slug
exports.getOne = asyncHandler(async (req, res) => {
  const article = await Article.findOneAndUpdate(
    { slug: req.params.slug, status: 'published' },
    { $inc: { viewCount: 1 } },
    { new: true }
  )
    .populate('author', 'name email')
    .lean();

  if (!article) throw new AppError('Article introuvable', 404);

  ApiResponse.success(res, article);
});

// POST /api/articles
exports.create = asyncHandler(async (req, res) => {
  const { title, content, tags, status } = req.body;

  const article = await Article.create({
    title, content, tags, status,
    author: req.user.id
  });

  await cache.invalidate('cache:/api/articles*');

  ApiResponse.created(res, article, 'Article créé');
});

// PUT /api/articles/:id
exports.update = asyncHandler(async (req, res) => {
  const article = await Article.findById(req.params.id);
  if (!article) throw new AppError('Article introuvable', 404);

  // Vérifier les droits
  if (article.author.toString() !== req.user.id && req.user.role !== 'admin') {
    throw new AppError('Non autorisé', 403);
  }

  const { title, content, tags, status } = req.body;
  Object.assign(article, { title, content, tags, status });
  await article.save();

  await cache.invalidate('cache:/api/articles*');

  ApiResponse.success(res, article, 'Article mis à jour');
});

// DELETE /api/articles/:id
exports.remove = asyncHandler(async (req, res) => {
  const article = await Article.findById(req.params.id);
  if (!article) throw new AppError('Article introuvable', 404);

  if (article.author.toString() !== req.user.id && req.user.role !== 'admin') {
    throw new AppError('Non autorisé', 403);
  }

  await article.deleteOne();
  await cache.invalidate('cache:/api/articles*');

  ApiResponse.noContent(res);
});


// ----------------------------------------------------------------------------
// [LISTE] RÉSUMÉ : DU DÉBUTANT À L'EXPERT NODE.JS
// ----------------------------------------------------------------------------

/*
ÉVOLUTION DE VOS COMPÉTENCES :

DÉBUTANT -> après Partie 1
  [OK] Comprendre l'event loop
  [OK] Lire/écrire des fichiers
  [OK] Créer des modules CommonJS

INTERMÉDIAIRE -> après Partie 2
  [OK] Créer une API REST avec Express
  [OK] Middleware pattern
  [OK] Gestion centralisée des erreurs

AVANCÉ -> après Partie 3
  [OK] MongoDB/Mongoose avec relations
  [OK] Authentification JWT complète
  [OK] Temps réel avec WebSockets
  [OK] Tests d'intégration complets

EXPERT -> après Partie 4
  [OK] Configuration multi-environnements
  [OK] Sécurité production (OWASP)
  [OK] Cache Redis + clustering
  [OK] Docker + PM2 + Nginx
  [OK] Logging structuré + Monitoring
  [OK] Best practices complètes
*/


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

/*
[OK] CE QUE VOUS AVEZ APPRIS

Chapitre 15 : Variables d'environnement
[OK] dotenv et chargement sécurisé
[OK] Module de configuration centralisé
[OK] Validation au démarrage
[OK] Gestion multi-environnements

Chapitre 16 : Sécurité
[OK] Helmet (headers HTTP)
[OK] Protection injections (NoSQL, XSS)
[OK] Rate limiting (global + auth)
[OK] Validation complète des inputs
[OK] Checklist sécurité OWASP

Chapitre 17 : Performance
[OK] Clustering (tous les CPUs)
[OK] Cache Redis avec invalidation
[OK] Worker Threads (tâches CPU)
[OK] Optimisations MongoDB (lean, index, select)

Chapitre 18 : Déploiement
[OK] Graceful shutdown
[OK] PM2 avec ecosystem.config.js
[OK] Docker multi-stage
[OK] docker-compose complet
[OK] Nginx reverse proxy
[OK] Heroku

Chapitre 19 : Logging et Monitoring
[OK] Winston (dev colorisé, prod JSON)
[OK] Logs rotatifs (daily-rotate-file)
[OK] Health checks complets
[OK] Sentry (tracking erreurs)
[OK] Prometheus + métriques custom

Chapitre 20 : Best Practices
[OK] Structure de fichiers scalable
[OK] Patterns essentiels (asyncHandler, ApiResponse, paginate)
[OK] Gestion d'erreurs centralisée
[OK] Checklist complète de production


═══════════════════════════════════════════════════════════════════════
[BRAVO] FÉLICITATIONS ! GUIDE NODE.JS TERMINÉ ! [RAPIDE]
═══════════════════════════════════════════════════════════════════════

VOUS ÊTES MAINTENANT CAPABLE DE :
-> Créer des APIs REST professionnelles avec Express
-> Authentifier les utilisateurs avec JWT
-> Gérer des bases de données MongoDB et PostgreSQL
-> Faire du temps réel avec Socket.io
-> Tester votre code (Jest + Supertest)
-> Sécuriser et optimiser vos applications
-> Déployer en production (Docker, PM2, Nginx)
-> Monitorer et déboguer en production

PROJETS POUR SOLIDIFIER VOS COMPÉTENCES :
-> Blog API complet (auth + articles + commentaires + upload)
-> Chat en temps réel (Socket.io + Redis + salles)
-> API e-commerce (produits + panier + paiement Stripe)
-> Microservices (plusieurs APIs qui communiquent)

[GUIDE] RESSOURCES :
-> nodejs.org/api            Documentation officielle Node.js
-> expressjs.com/api         Documentation Express
-> mongoosejs.com/docs       Documentation Mongoose
-> socket.io/docs            Documentation Socket.io
-> jestjs.io                 Documentation Jest
-> pm2.keymetrics.io         Documentation PM2
═══════════════════════════════════════════════════════════════════════
*/